fix customer view and edit

This commit is contained in:
javad committed 2026-09-29 10:30:54 +03:30
1 parent 81335b1439
commit d5c22a39dc
13 files changed
+329 -81

No files matched your search

+152 -21
View File
@@ -1,15 +1,19 @@
import base64
from datetime import timedelta
from importlib import import_module
from types import SimpleNamespace
import pytest
from django.apps import apps as django_apps
from django.contrib.auth.models import Permission
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db import connection
from django.urls import reverse
from django.utils import timezone
from apps.accounts.models import MagicLinkAccessLog, TicketMagicLink, User
from apps.attachments.models import AttachmentPolicy
from apps.customers.models import ContactGroup, CustomerProfile
from apps.customers.models import ContactEditLog, ContactGroup, CustomerProfile
from apps.notifications.models import NotificationJob
from apps.staff.models import OrganizationalUnit, StaffProfile
from apps.tickets.models import GroupDispatch, Ticket, TicketCategory, TicketEvent, TicketTag
@@ -314,7 +318,7 @@ def test_dispatch_pages_are_available_only_to_the_dispatch_unit(client):
@pytest.mark.django_db
def test_staff_operations_pages_respect_unit_scoped_permissions(client):
def test_staff_contacts_are_shared_while_groups_remain_unit_scoped(client):
unit = OrganizationalUnit.objects.create(name="عملیات", slug="operations")
other = OrganizationalUnit.objects.create(name="واحد محرمانه", slug="private-unit")
codenames = [
@@ -336,7 +340,9 @@ def test_staff_operations_pages_respect_unit_scoped_permissions(client):
response = client.get(reverse("staff_tickets:contacts"))
assert response.status_code == 200
assert "visible-contact" in response.content.decode()
assert "hidden-contact" not in response.content.decode()
assert "hidden-contact" in response.content.decode()
assert client.get(reverse("staff_tickets:contact_edit", args=[hidden.public_id])).status_code == 200
assert hidden_group.name not in response.content.decode()
assert client.get(reverse("staff_tickets:groups")).status_code == 200
assert client.get(reverse("staff_tickets:magic_audit")).status_code == 200
assert client.get(reverse("staff_tickets:sms_templates")).status_code == 200
@@ -348,7 +354,7 @@ def test_superuser_without_staff_profile_can_open_all_staff_contacts(client):
contact = make_contact("admin-visible-contact", "09120000503")
group = ContactGroup.objects.create(name="گروه قابل مشاهده مدیر", unit=unit)
contact.customer_profile.groups.add(group)
User.objects.create_user(
incomplete = User.objects.create_user(
username="customer-without-profile",
mobile="09120000504",
account_type=User.AccountType.CUSTOMER,
@@ -364,7 +370,12 @@ def test_superuser_without_staff_profile_can_open_all_staff_contacts(client):
assert response.status_code == 200
assert "admin-visible-contact" in response.content.decode()
assert "customer-without-profile" not in response.content.decode()
assert "09120000504" in response.content.decode()
searched = client.get(reverse("staff_tickets:contacts"), {"q": "09120000504"})
assert searched.status_code == 200
assert str(incomplete.public_id) in searched.content.decode()
assert client.get(reverse("staff_tickets:contact_edit", args=[incomplete.public_id])).status_code == 200
assert CustomerProfile.objects.filter(user=incomplete).exists()
@pytest.mark.django_db
@@ -434,8 +445,8 @@ def test_staff_can_classify_and_edit_ticket_customer_without_an_existing_group(c
"province": "",
"city": "",
})
assert missing_group.status_code == 200
assert missing_group.context["form"]["groups"].errors
assert missing_group.status_code == 302
assert not contact.customer_profile.groups.exists()
saved = client.post(url, {
"mobile": contact.local_mobile,
@@ -452,21 +463,141 @@ def test_staff_can_classify_and_edit_ticket_customer_without_an_existing_group(c
@pytest.mark.django_db
def test_staff_cannot_edit_ungrouped_customer_from_an_unrelated_ticket_unit(client):
ticket_unit = OrganizationalUnit.objects.create(name="واحد مالک مشتری", slug="ungrouped-owner-unit")
unrelated_unit = OrganizationalUnit.objects.create(name="واحد نامرتبط مشتری", slug="ungrouped-unrelated-unit")
unrelated_unit.permissions.add(*Permission.objects.filter(
content_type__app_label="customers",
codename__in=("view_customerprofile", "change_customerprofile"),
))
owner = make_staff("ungrouped-owner-agent", ticket_unit)
outsider = make_staff("ungrouped-outsider-agent", unrelated_unit)
contact = make_contact("private-ungrouped-customer", "09120000516")
category = TicketCategory.objects.create(unit=ticket_unit, title="درخواست محرمانه", slug="private-ungrouped-ticket")
create_ticket(customer=contact, created_by=owner, unit=ticket_unit, category=category, subject="محرمانه", body="شرح")
client.force_login(outsider)
def test_legacy_ungrouped_contact_is_searchable_editable_and_audited(client):
unit = OrganizationalUnit.objects.create(name="واحد جست‌وجوی مخاطب", slug="shared-contact-search")
staff = make_staff("shared-contact-editor", unit)
contact = make_contact("legacy-ungrouped-contact", "09120000561")
incomplete = User.objects.create_user(username="legacy-without-profile", mobile="09120000564", first_name="نام قدیمی ناقص", account_type=User.AccountType.CUSTOMER)
edit_url = reverse("staff_tickets:contact_edit", args=[contact.public_id])
client.force_login(staff)
assert client.get(reverse("staff_tickets:contact_edit", args=[contact.public_id])).status_code == 404
for query in ("legacy-ungrouped-contact", "09120000561"):
result = client.get(reverse("staff_tickets:contacts"), {"q": query})
assert result.status_code == 200
assert str(contact.public_id) in result.content.decode()
assert str(incomplete.public_id) in client.get(reverse("staff_tickets:contacts"), {"q": "09120000564"}).content.decode()
assert str(incomplete.public_id) in client.get(reverse("staff_tickets:contacts"), {"q": "نام قدیمی ناقص"}).content.decode()
assert client.get(reverse("staff_tickets:contact_edit", args=[incomplete.public_id])).status_code == 200
assert CustomerProfile.objects.filter(user=incomplete).exists()
assert incomplete.customer_profile.first_name == "نام قدیمی ناقص"
assert client.get(edit_url).status_code == 200
saved = client.post(edit_url, {
"mobile": "09120000562",
"first_name": "مخاطب اصلاح‌شده",
"last_name": "",
"organization_name": "",
"stakeholder_type": CustomerProfile.StakeholderType.CUSTOMER,
"groups": [],
"province": "",
"city": "",
})
assert saved.status_code == 302
contact.refresh_from_db()
contact.customer_profile.refresh_from_db()
assert contact.mobile == "+989120000562"
assert contact.customer_profile.first_name == "مخاطب اصلاح‌شده"
assert not contact.customer_profile.groups.exists()
assert User.objects.filter(pk=contact.pk).exists()
assert CustomerProfile.objects.filter(user=contact).exists()
log = ContactEditLog.objects.get(contact=contact.customer_profile)
assert log.actor == staff
assert log.actor_name == staff.username
assert log.contact_public_id == contact.public_id
assert {change["field"] for change in log.changes} == {"شماره همراه", "نام"}
assert staff.username in client.get(edit_url).content.decode()
assert ContactEditLog.objects.count() == 1
repeated = client.post(edit_url, {
"mobile": "09120000562",
"first_name": "مخاطب اصلاح‌شده",
"last_name": "",
"organization_name": "",
"stakeholder_type": CustomerProfile.StakeholderType.CUSTOMER,
"groups": [],
"province": "",
"city": "",
})
assert repeated.status_code == 302
assert ContactEditLog.objects.count() == 1
@pytest.mark.django_db
def test_customer_profile_backfill_is_repeatable_and_keeps_existing_contacts():
existing = make_contact("existing-profile-backfill", "09120000565")
incomplete = User.objects.create_user(username="missing-profile-backfill", mobile="09120000566", first_name="نام قدیمی", last_name="مشتری", account_type=User.AccountType.CUSTOMER)
migration = import_module("apps.customers.migrations.0006_backfill_customer_profiles")
editor = SimpleNamespace(connection=connection)
migration.backfill_customer_profiles(django_apps, editor)
migration.backfill_customer_profiles(django_apps, editor)
assert CustomerProfile.objects.filter(user=existing, first_name="existing-profile-backfill").count() == 1
assert CustomerProfile.objects.filter(user=incomplete).count() == 1
assert incomplete.customer_profile.first_name == "نام قدیمی"
assert not incomplete.customer_profile.groups.exists()
@pytest.mark.django_db
def test_shared_contact_edit_preserves_other_unit_groups_and_ticket_counts(client):
sales = OrganizationalUnit.objects.create(name="فروش مشترک", slug="shared-sales")
finance = OrganizationalUnit.objects.create(name="حسابداری مشترک", slug="shared-finance")
view_ticket = Permission.objects.get(content_type__app_label="tickets", codename="view_ticket")
sales.permissions.add(view_ticket)
finance.permissions.add(view_ticket)
sales_staff = make_staff("shared-sales-agent", sales)
finance_staff = make_staff("shared-finance-agent", finance)
contact = make_contact("shared-contact", "09120000563")
finance_group = ContactGroup.objects.create(name="مخاطبان حسابداری مشترک", unit=finance)
sales_category = TicketCategory.objects.create(unit=sales, title="فروش مشترک", slug="shared-sales-ticket")
finance_category = TicketCategory.objects.create(unit=finance, title="مالی مشترک", slug="shared-finance-ticket")
sales_ticket = create_ticket(customer=contact, created_by=sales_staff, unit=sales, category=sales_category, subject="درخواست فروش", body="شرح")
finance_ticket = create_ticket(customer=contact, created_by=finance_staff, unit=finance, category=finance_category, subject="درخواست مالی", body="شرح")
client.force_login(sales_staff)
assert client.get(reverse("staff_tickets:contact_edit", args=[contact.public_id])).status_code == 200
assert client.get(reverse("staff_tickets:detail", args=[finance_ticket.public_id])).status_code == 403
contact.customer_profile.groups.add(finance_group)
directory = client.get(reverse("staff_tickets:contacts"), {"q": "shared-contact"})
assert directory.status_code == 200
assert directory.context["contacts"][0].ticket_count == 1
assert finance_group.name not in directory.content.decode()
assert client.get(reverse("staff_tickets:detail", args=[sales_ticket.public_id])).status_code == 200
assert client.get(reverse("staff_tickets:detail", args=[finance_ticket.public_id])).status_code == 403
forged = client.post(reverse("staff_tickets:contact_edit", args=[contact.public_id]), {
"mobile": contact.local_mobile,
"first_name": "نام جعلی",
"last_name": "",
"organization_name": "",
"stakeholder_type": CustomerProfile.StakeholderType.CUSTOMER,
"groups": [finance_group.pk],
"province": "",
"city": "",
})
assert forged.status_code == 200
assert forged.context["form"]["groups"].errors
contact.customer_profile.refresh_from_db()
assert contact.customer_profile.first_name == "shared-contact"
assert ContactEditLog.objects.count() == 0
edited = client.post(reverse("staff_tickets:contact_edit", args=[contact.public_id]), {
"mobile": contact.local_mobile,
"first_name": "نام تازه",
"last_name": "",
"organization_name": "",
"stakeholder_type": CustomerProfile.StakeholderType.CUSTOMER,
"groups": [],
"province": "",
"city": "",
})
assert edited.status_code == 302
assert contact.customer_profile.groups.filter(pk=finance_group.pk).exists()
assert ContactEditLog.objects.filter(contact=contact.customer_profile, actor=sales_staff).exists()
client.force_login(contact)
assert client.get(reverse("staff_tickets:contacts")).status_code == 403
assert client.get(reverse("staff_tickets:contact_edit", args=[contact.public_id])).status_code == 403
@pytest.mark.django_db
+6 -3
View File
@@ -10,7 +10,7 @@ from apps.tickets.models import Ticket, TicketCategory
from apps.tickets.services import create_ticket, claim_ticket, reply_ticket
from apps.messaging.models import Message,MessageReadReceipt
from apps.notifications.models import NotificationJob
from apps.customers.models import CustomerProfile
from apps.customers.models import ContactEditLog, CustomerProfile
from apps.core.persian_text import normalize_persian_text
@pytest.mark.django_db
@@ -664,7 +664,8 @@ def test_staff_customer_directory_search_and_ticket_stats(client):
response=client.get(legacy.url)
content=response.content.decode()
assert response.status_code==200 and "زهرا محمدی" in content and "09121234567" in content
assert "کل تیکت" in content and "نیازمند اقدام" in content
assert "تیکت قابل مشاهده" in content and "نیازمند اقدام" in content
assert response.context["contacts"][0].ticket_count == 0
@pytest.mark.django_db
def test_staff_customer_live_search_returns_profile_data(client):
@@ -687,9 +688,11 @@ def test_staff_create_uses_existing_customer_selected_by_public_id(client):
StaffProfile.objects.create(user=expert,display_name="کارشناس انتخاب")
expert.user_permissions.add(Permission.objects.get(content_type__app_label="tickets",codename="add_ticket"))
client.force_login(expert)
response=client.post(reverse("staff_tickets:create"),{"customer_id":customer.public_id,"customer_mobile":"09125550000","category":category.pk,"subject":"پیگیری مشتری موجود","province":"گیلان","city":"رشت","body":"شرح کامل پیگیری"})
response=client.post(reverse("staff_tickets:create"),{"customer_id":customer.public_id,"customer_mobile":"09125550000","first_name":"مریم تازه","category":category.pk,"subject":"پیگیری مشتری موجود","province":"گیلان","city":"رشت","body":"شرح کامل پیگیری"})
assert response.status_code==302
assert customer.customer_tickets.filter(subject="پیگیری مشتری موجود").exists()
log = ContactEditLog.objects.get(contact__user=customer, actor=expert)
assert {"field": "نام", "before": "مریم", "after": "مریم تازه"} in log.changes
@pytest.mark.django_db
def test_staff_create_requires_name_for_unknown_mobile(client):