Files
customerTicket/tests/test_organizational_ticketing.py
T
2026-09-10 11:34:19 +03:30

233 lines
14 KiB
Python

import pytest
from django.contrib.auth.models import Permission
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from apps.accounts.models import MagicLinkAccessLog, TicketMagicLink, User
from apps.attachments.models import AttachmentPolicy
from apps.customers.models import ContactGroup, CustomerProfile
from apps.notifications.models import NotificationJob
from apps.staff.models import OrganizationalUnit, StaffProfile
from apps.tickets.models import GroupDispatch, Ticket, TicketCategory, TicketTag
from apps.tickets.policies import TicketPolicy
from apps.tickets.selectors import staff_ticket_rows
from apps.tickets.services import create_ticket, dispatch_group_tickets, refresh_group_dispatch_status
def make_staff(username, unit):
user = User.objects.create_user(username=username, account_type=User.AccountType.STAFF)
StaffProfile.objects.create(user=user, display_name=username, primary_unit=unit)
return user
def make_contact(username, mobile):
user = User.objects.create_user(username=username, mobile=mobile, account_type=User.AccountType.CUSTOMER)
CustomerProfile.objects.create(user=user, first_name=username)
return user
@pytest.mark.django_db
def test_unit_permission_isolates_ticket_queries_and_object_access():
sales = OrganizationalUnit.objects.create(name="فروش", slug="sales")
logistics = OrganizationalUnit.objects.create(name="بارگیری ایزوله", slug="isolated-logistics")
view_permission = Permission.objects.get(codename="view_ticket", content_type__app_label="tickets")
sales.permissions.add(view_permission)
logistics.permissions.add(view_permission)
sales_staff = make_staff("sales-agent", sales)
logistics_staff = make_staff("logistics-agent", logistics)
customer = make_contact("unit-customer", "09120000101")
category = TicketCategory.objects.create(title="عمومی", slug="general")
sales_ticket = create_ticket(customer=customer, created_by=sales_staff, unit=sales, category=category, subject="تیکت واحد فروش", body="شرح")
assert TicketPolicy.can_view(sales_staff, sales_ticket)
assert not TicketPolicy.can_view(logistics_staff, sales_ticket)
assert list(staff_ticket_rows(logistics_staff)) == []
@pytest.mark.django_db
def test_group_dispatch_creates_one_isolated_ticket_and_sms_job_per_contact():
unit = OrganizationalUnit.objects.create(name="لوجستیک ارسال گروهی", slug="dispatch-logistics")
permission = Permission.objects.get(codename="dispatch_group_tickets", content_type__app_label="tickets")
view_permission = Permission.objects.get(codename="view_ticket", content_type__app_label="tickets")
unit.permissions.add(permission, view_permission)
staff = make_staff("dispatcher", unit)
first = make_contact("carrier-one", "09120000111")
second = make_contact("carrier-two", "09120000112")
group = ContactGroup.objects.create(name="باربری‌های گلپایگان", unit=unit)
first.customer_profile.groups.add(group)
second.customer_profile.groups.add(group)
category = TicketCategory.objects.create(unit=unit,title="استعلام حمل", slug="test-shipping-quote")
tag = TicketTag.objects.create(name="استعلام قیمت", slug="price-inquiry")
dispatch, tickets = dispatch_group_tickets(user=staff, unit=unit, contact_group=group, category=category, subject="استعلام قیمت حمل", body="لطفاً قیمت پیشنهادی را ثبت کنید.", tags=[tag])
# Creation only queues delivery; reporting promotes it after SMS succeeds.
assert dispatch.status == GroupDispatch.Status.QUEUED
assert dispatch.recipient_count == 2
assert len(tickets) == 2
assert {ticket.customer_id for ticket in tickets} == {first.pk, second.pk}
assert all(ticket.delivery_type == Ticket.DeliveryType.GROUP for ticket in tickets)
assert NotificationJob.objects.filter(ticket__in=tickets, channel="SMS", event_type="STAFF_TICKET_CREATED").count() == 2
@pytest.mark.django_db
def test_group_dispatch_copies_shared_document_into_each_private_ticket():
unit=OrganizationalUnit.objects.create(name="مالی گروهی",slug="bulk-finance")
unit.permissions.add(Permission.objects.get(codename="dispatch_group_tickets",content_type__app_label="tickets"))
staff=make_staff("bulk-finance-user",unit)
first=make_contact("bulk-supplier-one","09120000311"); second=make_contact("bulk-supplier-two","09120000312")
group=ContactGroup.objects.create(name="تامین‌کنندگان اسناد",unit=unit)
first.customer_profile.groups.add(group); second.customer_profile.groups.add(group)
category=TicketCategory.objects.create(title="سند گروهی",slug="bulk-document")
AttachmentPolicy.objects.create(attachment_type="PDF",max_file_size=1024*1024,allowed_extensions=[".pdf"])
upload=SimpleUploadedFile("receipt.pdf",b"%PDF-1.4\n%%EOF",content_type="application/pdf")
_,tickets=dispatch_group_tickets(user=staff,unit=unit,contact_group=group,category=category,subject="ارسال سند مالی مشترک",body="سند را بررسی کنید.",files=[upload])
assert len(tickets)==2
assert all(ticket.ticket_messages.get().attachments.filter(original_name="receipt.pdf").count()==1 for ticket in tickets)
paths=[ticket.ticket_messages.get().attachments.get().file.name for ticket in tickets]
assert len(set(paths))==2
@pytest.mark.django_db
def test_magic_link_records_every_open_with_ip_and_user_agent(client):
unit = OrganizationalUnit.objects.create(name="حسابداری ممیزی", slug="audit-accounting")
staff = make_staff("accountant", unit)
customer = make_contact("supplier", "09120000121")
category = TicketCategory.objects.create(title="سند مالی", slug="financial-document")
ticket = create_ticket(customer=customer, created_by=staff, unit=unit, category=category, subject="ارسال رسید مالی", body="رسید پیوست شد.")
token, link = TicketMagicLink.issue_with_record(ticket, created_by=staff)
response = client.get(reverse("accounts:magic", args=[token]), HTTP_USER_AGENT="Audit Browser/1.0", REMOTE_ADDR="203.0.113.7")
assert response.status_code == 302
link.refresh_from_db()
assert link.view_count == 1
assert link.last_used_ip == "203.0.113.7"
assert link.last_user_agent == "Audit Browser/1.0"
assert link.created_by == staff
assert link.created_unit == unit
assert MagicLinkAccessLog.objects.filter(magic_link=link, ip_address="203.0.113.7").count() == 1
@pytest.mark.django_db
def test_dispatch_pages_are_available_only_to_the_dispatch_unit(client):
unit = OrganizationalUnit.objects.create(name="انبار دسترسی", slug="access-warehouse")
other_unit = OrganizationalUnit.objects.create(name="فروش", slug="sales-2")
for codename in ("view_ticket", "dispatch_group_tickets"):
permission = Permission.objects.get(codename=codename, content_type__app_label="tickets")
unit.permissions.add(permission)
staff = make_staff("warehouse-user", unit)
outsider = make_staff("sales-user", other_unit)
customer = make_contact("warehouse-contact", "09120000131")
group = ContactGroup.objects.create(name="تامین‌کنندگان", unit=unit)
customer.customer_profile.groups.add(group)
category = TicketCategory.objects.create(title="موجودی", slug="inventory")
client.force_login(staff)
assert client.get(reverse("staff_tickets:dispatch_create")).status_code == 200
response = client.post(reverse("staff_tickets:dispatch_create"), {"unit": unit.pk, "contact_group": group.pk, "category": category.pk, "subject": "استعلام موجودی کالا", "body": "موجودی را اعلام کنید."})
assert response.status_code == 302
dispatch = GroupDispatch.objects.get()
assert response.url == reverse("staff_tickets:dispatch_detail", args=[dispatch.public_id])
client.force_login(outsider)
assert client.get(reverse("staff_tickets:dispatch_detail", args=[dispatch.public_id])).status_code == 403
@pytest.mark.django_db
def test_staff_operations_pages_respect_unit_scoped_permissions(client):
unit = OrganizationalUnit.objects.create(name="عملیات", slug="operations")
other = OrganizationalUnit.objects.create(name="واحد محرمانه", slug="private-unit")
codenames = [
("customers", "view_customerprofile"), ("customers", "add_customerprofile"),
("customers", "view_contactgroup"), ("customers", "add_contactgroup"),
("accounts", "view_ticketmagiclink"), ("notifications", "view_smstemplate"),
("notifications", "add_smstemplate"),
]
unit.permissions.add(*[Permission.objects.get(content_type__app_label=app, codename=code) for app, code in codenames])
staff = make_staff("operations-agent", unit)
visible = make_contact("visible-contact", "09120000201")
hidden = make_contact("hidden-contact", "09120000202")
visible_group = ContactGroup.objects.create(name="گروه مجاز", unit=unit)
hidden_group = ContactGroup.objects.create(name="گروه محرمانه", unit=other)
visible.customer_profile.groups.add(visible_group)
hidden.customer_profile.groups.add(hidden_group)
client.force_login(staff)
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 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
@pytest.mark.django_db
def test_unit_scoped_add_ticket_preserves_contact_ownership_and_stakeholder(client):
unit=OrganizationalUnit.objects.create(name="حسابداری عملیاتی",slug="accounting-ops")
unit.permissions.add(Permission.objects.get(content_type__app_label="tickets",codename="add_ticket"))
staff=make_staff("unit-accountant",unit)
group=ContactGroup.objects.create(name="تامین‌کنندگان مالی",unit=unit)
category=TicketCategory.objects.create(title="رسید مالی",slug="receipt-ops")
client.force_login(staff)
response=client.post(reverse("staff_tickets:create"),{
"unit":unit.pk,"customer_mobile":"09120000301","first_name":"تامین‌کننده","last_name":"نمونه",
"organization_name":"شرکت نمونه","stakeholder_type":CustomerProfile.StakeholderType.SUPPLIER,
"groups":[group.pk],"category":category.pk,"subject":"ارسال رسید پرداخت","body":"رسید مالی پیوست خواهد شد.",
})
assert response.status_code==302
ticket=Ticket.objects.get(customer__mobile="+989120000301")
assert ticket.owning_unit==unit and ticket.created_by==staff
assert ticket.customer.customer_profile.stakeholder_type==CustomerProfile.StakeholderType.SUPPLIER
assert ticket.customer.customer_profile.groups.filter(pk=group.pk).exists()
@pytest.mark.django_db
def test_dispatch_retry_is_unit_scoped_and_refreshes_persistent_status(client):
unit=OrganizationalUnit.objects.create(name="لوجستیک retry",slug="logistics-retry")
other=OrganizationalUnit.objects.create(name="فروش retry",slug="sales-retry")
permission=Permission.objects.get(content_type__app_label="tickets",codename="dispatch_group_tickets")
unit.permissions.add(permission)
staff=make_staff("retry-dispatcher",unit); outsider=make_staff("retry-outsider",other)
contact=make_contact("retry-carrier","09120000302")
group=ContactGroup.objects.create(name="باربری retry",unit=unit); contact.customer_profile.groups.add(group)
category=TicketCategory.objects.create(title="استعلام retry",slug="quote-retry")
dispatch,tickets=dispatch_group_tickets(user=staff,unit=unit,contact_group=group,category=category,subject="استعلام هزینه جدید",body="قیمت را ثبت کنید.")
job=NotificationJob.objects.get(ticket=tickets[0],channel="SMS")
job.status=NotificationJob.Status.FAILED; job.attempts=3; job.last_error="provider"; job.save()
refresh_group_dispatch_status(dispatch.pk); dispatch.refresh_from_db()
assert dispatch.status==GroupDispatch.Status.PARTIAL
client.force_login(outsider)
assert client.post(reverse("staff_tickets:dispatch_retry",args=[dispatch.public_id])).status_code==403
client.force_login(staff)
assert client.post(reverse("staff_tickets:dispatch_retry",args=[dispatch.public_id])).status_code==302
job.refresh_from_db(); dispatch.refresh_from_db()
assert (job.status,job.attempts,job.last_error)==(NotificationJob.Status.PENDING,0,"")
assert dispatch.status==GroupDispatch.Status.QUEUED
@pytest.mark.django_db
def test_magic_link_can_be_revoked_only_with_unit_permission(client):
unit=OrganizationalUnit.objects.create(name="حسابرسی",slug="audit-unit")
other=OrganizationalUnit.objects.create(name="واحد دیگر audit",slug="audit-other")
view=Permission.objects.get(content_type__app_label="accounts",codename="view_ticketmagiclink")
change=Permission.objects.get(content_type__app_label="accounts",codename="change_ticketmagiclink")
unit.permissions.add(view,change)
auditor=make_staff("unit-auditor",unit); outsider=make_staff("audit-outsider",other)
customer=make_contact("audit-contact","09120000303")
category=TicketCategory.objects.create(title="ممیزی",slug="audit-revoke")
ticket=create_ticket(customer=customer,created_by=auditor,unit=unit,category=category,subject="سند قابل ممیزی",body="سند")
_,link=TicketMagicLink.issue_with_record(ticket,created_by=auditor)
client.force_login(outsider)
assert client.post(reverse("staff_tickets:magic_revoke",args=[link.pk])).status_code==403
client.force_login(auditor)
assert client.post(reverse("staff_tickets:magic_revoke",args=[link.pk])).status_code==302
link.refresh_from_db(); assert link.revoked_at is not None