estelam ticket, varizha , others
This commit is contained in:
1 parent
35a1ec1794
commit
6b546aad76
71 files changed
+2865
-191
No files matched your search
@@ -0,0 +1,89 @@
|
||||
from pathlib import Path
|
||||
|
||||
from django.urls import reverse
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from tests.e2e.pages.base import BasePage
|
||||
|
||||
|
||||
class DispatchWorkspacePage(BasePage):
|
||||
@property
|
||||
def review_dialog(self):
|
||||
return self.page.get_by_role("dialog", name="بررسی نهایی ارسال گروهی")
|
||||
|
||||
def _activate(self, control, destination, *, hidden=False):
|
||||
control.click()
|
||||
if hidden:
|
||||
expect(destination).to_be_hidden()
|
||||
else:
|
||||
expect(destination).to_be_visible()
|
||||
|
||||
def open_composer(self):
|
||||
self.goto(reverse("staff_tickets:dispatch_create"))
|
||||
self.page.get_by_role("tab", name="ارسال جدید").wait_for(state="visible")
|
||||
|
||||
def open_history(self):
|
||||
self.page.get_by_role("tab", name="سوابق ارسال").click()
|
||||
|
||||
def return_to_composer(self):
|
||||
self.page.get_by_role("tab", name="ارسال جدید").click()
|
||||
|
||||
def compose(self, unit, group, category, subject, body):
|
||||
self.page.get_by_label("واحد ارسالکننده").select_option(str(unit.pk))
|
||||
self.page.get_by_label("گروه مخاطبان", exact=False).select_option(str(group.pk))
|
||||
self.page.get_by_label("دستهبندی", exact=False).select_option(str(category.pk))
|
||||
self.page.get_by_label("موضوع استعلام یا پیام", exact=False).fill(subject)
|
||||
self.page.get_by_label("متن پیام", exact=False).fill(body)
|
||||
|
||||
def review(self):
|
||||
self._activate(self.page.get_by_role("button", name="بررسی و ادامه"), self.review_dialog)
|
||||
|
||||
def cancel_review(self):
|
||||
self._activate(self.review_dialog.get_by_role("button", name="بازگشت و ویرایش"), self.review_dialog, hidden=True)
|
||||
|
||||
def send(self):
|
||||
self._activate(
|
||||
self.review_dialog.get_by_role("button", name="تأیید و ارسال گروهی"),
|
||||
self.page.get_by_role("heading", name="وضعیت پیامک، مشاهده و پاسخ"),
|
||||
)
|
||||
|
||||
def open_report(self, dispatch):
|
||||
self.goto(reverse("staff_tickets:dispatch_detail", args=[dispatch.public_id]))
|
||||
|
||||
def filter_recipients(self, label):
|
||||
self.page.get_by_label("نمایش مخاطبان").select_option(label=label)
|
||||
|
||||
def search_recipient(self, text):
|
||||
self.page.get_by_role("searchbox", name="جستجوی مخاطب").fill(text)
|
||||
|
||||
def reset_recipients(self):
|
||||
self.page.get_by_role("button", name="پاک کردن فیلتر", exact=True).click()
|
||||
|
||||
def visible_recipients(self):
|
||||
return self.page.locator("[data-recipient-row]:visible")
|
||||
|
||||
def open_conversation(self):
|
||||
self.page.locator(".dispatch-reply-link").first.click()
|
||||
|
||||
def review_opening(self):
|
||||
self._activate(self.page.get_by_role("link", name="بازگشایی همه پاسخها", exact=True),
|
||||
self.page.get_by_role("heading", name="تأیید بازگشایی همه پاسخها"))
|
||||
|
||||
def confirm_opening(self):
|
||||
self._activate(self.page.get_by_role("button", name="تأیید و بازگشایی همه پاسخها"),
|
||||
self.page.get_by_role("heading", name="پاسخها بازگشایی شدهاند"))
|
||||
|
||||
def choose_response_mode(self, label):
|
||||
self.page.get_by_role("radio", name=label, exact=True).check()
|
||||
|
||||
@property
|
||||
def opening_sms(self):
|
||||
return self.page.get_by_role("checkbox", name="اطلاعرسانی بازگشایی با پیامک", exact=True)
|
||||
|
||||
def expect_fits_viewport(self):
|
||||
assert self.page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1")
|
||||
|
||||
def save_screenshot(self, name):
|
||||
directory = Path("test-results/dispatch-design")
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
self.page.screenshot(path=str(directory / name), full_page=True)
|
||||
@@ -5,6 +5,13 @@ from .base import BasePage
|
||||
|
||||
|
||||
class StaffPaymentsPage(BasePage):
|
||||
@property
|
||||
def daily_summary(self):
|
||||
return self.page.get_by_role('region', name='وضعیت امروز')
|
||||
|
||||
def open_summary_definitions(self):
|
||||
self.daily_summary.locator('summary').click()
|
||||
|
||||
def open_desk(self, parameters=''):
|
||||
self.goto(reverse('staff_payments:desk') + parameters)
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from apps.messaging.models import Message
|
||||
from apps.notifications.models import SMSTemplate
|
||||
from apps.tickets.services import dispatch_group_tickets
|
||||
from tests.e2e.pages.auth import AuthenticationPage
|
||||
from tests.e2e.pages.dispatches import DispatchWorkspacePage
|
||||
from tests.e2e.test_dispatch_workspace_e2e import dispatch_case, PASSWORD
|
||||
|
||||
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("width", [1440, 390, 320])
|
||||
def test_sealed_report_requires_confirmation_and_opens_answers_together(live_server, browser_page, dispatch_case, width):
|
||||
staff, unit, group, category, _, customers = dispatch_case
|
||||
# Transactional live-server tests flush data migrations between tests.
|
||||
SMSTemplate.objects.create(unit=unit, event_type="DISPATCH_RESPONSES_OPENED", title="بازگشایی", body="متن دقیق پیامک بازگشایی برای آزمون")
|
||||
dispatch, tickets = dispatch_group_tickets(user=staff, unit=unit, contact_group=group,
|
||||
category=category, subject="استعلام محرمانه مرورگر", body="پیشنهاد خود را ثبت کنید.")
|
||||
answer = "پیشنهاد محرمانه مخاطب اول " * 12 + "پایان کامل پیشنهاد"
|
||||
Message.objects.create(ticket=tickets[0], author=customers[0], body=answer)
|
||||
second_answer = Message.objects.create(ticket=tickets[0], author=customers[0], body="توضیح تکمیلی پیشنهاد اول")
|
||||
Message.objects.create(ticket=tickets[1], author=customers[1], body="پیشنهاد مخاطب دوم")
|
||||
with browser_page() as page:
|
||||
page.set_viewport_size({"width": width, "height": 900})
|
||||
AuthenticationPage(page, live_server.url).login_staff(staff, PASSWORD)
|
||||
workspace = DispatchWorkspacePage(page, live_server.url)
|
||||
workspace.open_report(dispatch)
|
||||
expect(page.get_by_role("heading", name="پاسخها هنوز بازگشایی نشدهاند")).to_be_visible()
|
||||
expect(page.locator("body")).not_to_contain_text(answer)
|
||||
expect(page.locator('[data-recipient-row] a[href*="/staff/tickets/"]')).to_have_count(0)
|
||||
workspace.filter_recipients("دارای پاسخ")
|
||||
expect(workspace.visible_recipients()).to_have_count(2)
|
||||
expect(workspace.visible_recipients().first).to_contain_text("پاسخ داده است: بله")
|
||||
workspace.expect_fits_viewport()
|
||||
workspace.save_screenshot(f"sealed-report-{width}.png")
|
||||
workspace.review_opening()
|
||||
expect(page.locator(".opening-sms-preview")).to_have_text("متن دقیق پیامک بازگشایی برای آزمون")
|
||||
expect(page.locator("body")).not_to_contain_text(answer)
|
||||
workspace.expect_fits_viewport()
|
||||
workspace.save_screenshot(f"opening-confirm-{width}.png")
|
||||
workspace.confirm_opening()
|
||||
expect(page.locator("body")).to_contain_text(answer)
|
||||
expect(page.locator("body")).to_contain_text(second_answer.body)
|
||||
expect(page.locator("body")).to_contain_text("پیشنهاد مخاطب دوم")
|
||||
expect(page.locator(".recipient-reply")).to_have_count(3)
|
||||
expect(page.locator('[data-recipient-row] a[href*="/staff/tickets/"]')).to_have_count(0)
|
||||
from apps.core.templatetags.jalali import jalali_datetime_precise
|
||||
expect(page.locator(".recipient-reply time").nth(1)).to_have_text(jalali_datetime_precise(second_answer.created_at))
|
||||
workspace.expect_fits_viewport()
|
||||
workspace.save_screenshot(f"opened-report-{width}.png")
|
||||
|
||||
|
||||
def test_composer_defaults_to_sealed_and_disables_sms_in_individual_mode(live_server, browser_page, dispatch_case):
|
||||
staff, unit, group, category, _, _ = dispatch_case
|
||||
with browser_page() as page:
|
||||
AuthenticationPage(page, live_server.url).login_staff(staff, PASSWORD)
|
||||
workspace = DispatchWorkspacePage(page, live_server.url)
|
||||
workspace.open_composer()
|
||||
workspace.compose(unit, group, category, "پیشفرض پاسخ محرمانه", "متن درخواست")
|
||||
expect(page.get_by_role("radio", name="بازگشایی یکجا", exact=True)).to_be_checked()
|
||||
expect(workspace.opening_sms).to_be_checked()
|
||||
workspace.review()
|
||||
expect(workspace.review_dialog.locator('[data-review-inquiry-recommendation]')).to_be_hidden()
|
||||
workspace.cancel_review()
|
||||
workspace.choose_response_mode("مشاهده جداگانه")
|
||||
expect(workspace.opening_sms).to_be_disabled()
|
||||
expect(workspace.opening_sms).not_to_be_checked()
|
||||
workspace.review()
|
||||
expect(workspace.review_dialog).to_contain_text("مشاهده جداگانه؛ پیامک بازگشایی: غیرفعال")
|
||||
expect(workspace.review_dialog.locator('[data-review-inquiry-recommendation]')).to_be_visible()
|
||||
expect(workspace.review_dialog).to_contain_text("برای تمامی استعلامگیریها توصیه میکنیم")
|
||||
workspace.cancel_review()
|
||||
workspace.choose_response_mode("بازگشایی یکجا")
|
||||
expect(workspace.opening_sms).to_be_checked()
|
||||
workspace.opening_sms.uncheck()
|
||||
workspace.choose_response_mode("مشاهده جداگانه")
|
||||
workspace.choose_response_mode("بازگشایی یکجا")
|
||||
expect(workspace.opening_sms).not_to_be_checked()
|
||||
workspace.review()
|
||||
expect(workspace.review_dialog.locator('[data-review-inquiry-recommendation]')).to_be_visible()
|
||||
workspace.save_screenshot("review-inquiry-recommendation.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("count", [15, 16])
|
||||
def test_composer_enforces_opening_sms_recipient_cap(live_server, browser_page, dispatch_case, count):
|
||||
from tests.test_organizational_ticketing import make_contact
|
||||
staff, unit, group, category, _, _ = dispatch_case
|
||||
for i in range(2, count):
|
||||
make_contact(f"browser-limit-{i}", f"09122100{i:03}").customer_profile.groups.add(group)
|
||||
with browser_page() as page:
|
||||
AuthenticationPage(page, live_server.url).login_staff(staff, PASSWORD)
|
||||
workspace = DispatchWorkspacePage(page, live_server.url)
|
||||
workspace.open_composer()
|
||||
workspace.compose(unit, group, category, "آزمون سقف پیامک مرورگر", "متن استعلام")
|
||||
if count == 15:
|
||||
expect(workspace.opening_sms).to_be_enabled()
|
||||
expect(workspace.opening_sms).to_be_checked()
|
||||
else:
|
||||
expect(workspace.opening_sms).to_be_disabled()
|
||||
expect(workspace.opening_sms).not_to_be_checked()
|
||||
expect(page.locator('[data-opening-sms-hint]')).to_contain_text("بیش از ۱۵")
|
||||
workspace.review()
|
||||
expect(workspace.review_dialog.locator('[data-review-inquiry-limit]')).to_be_visible()
|
||||
@@ -0,0 +1,108 @@
|
||||
import pytest
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from apps.accounts.models import TicketMagicLink, User
|
||||
from apps.customers.models import ContactGroup, CustomerProfile
|
||||
from apps.messaging.models import Message
|
||||
from apps.notifications.models import NotificationJob, SMSTemplate
|
||||
from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||
from apps.tickets.models import TicketCategory
|
||||
from apps.tickets.services import create_group_dispatch
|
||||
from tests.e2e.pages.auth import AuthenticationPage
|
||||
from tests.e2e.pages.dispatches import DispatchWorkspacePage
|
||||
|
||||
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
|
||||
PASSWORD = "Dispatch-E2E-safe-2026"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dispatch_case():
|
||||
unit = OrganizationalUnit.objects.create(name="فروش رابط گروهی", slug="dispatch-ui")
|
||||
unit.permissions.add(*Permission.objects.filter(content_type__app_label="tickets", codename__in=("view_ticket", "change_ticket", "dispatch_group_tickets")))
|
||||
staff = User.objects.create_user(username="dispatch-ui-agent", password=PASSWORD, account_type="STAFF")
|
||||
StaffProfile.objects.create(user=staff, display_name="کارشناس ارسال", primary_unit=unit)
|
||||
group = ContactGroup.objects.create(unit=unit, name="مخاطبان منتخب فروش")
|
||||
empty_group = ContactGroup.objects.create(unit=unit, name="گروه بدون مخاطب")
|
||||
category = TicketCategory.objects.create(unit=unit, title="هماهنگی فروش", slug="dispatch-ui-category")
|
||||
customers = []
|
||||
for index, name in enumerate(("مخاطب پاسخدهنده", "مخاطب بدون پاسخ")):
|
||||
customer = User.objects.create_user(username=f"dispatch-ui-customer-{index}", mobile=f"0912000980{index}", account_type="CUSTOMER")
|
||||
CustomerProfile.objects.create(user=customer, first_name=name).groups.add(group)
|
||||
customers.append(customer)
|
||||
return staff, unit, group, category, empty_group, customers
|
||||
|
||||
|
||||
@pytest.mark.parametrize("width", [1440, 390, 320])
|
||||
def test_review_preserves_draft_and_requires_confirmation_before_group_send(live_server, browser_page, dispatch_case, width):
|
||||
staff, unit, group, category, _, _ = dispatch_case
|
||||
subject = "هماهنگی زمان تحویل گروهی"
|
||||
body = "لطفاً زمان پیشنهادی تحویل را در گفتوگوی خصوصی اعلام کنید."
|
||||
with browser_page() as page:
|
||||
page.set_viewport_size({"width": width, "height": 900 if width == 1440 else 844})
|
||||
AuthenticationPage(page, live_server.url).login_staff(staff, PASSWORD)
|
||||
workspace = DispatchWorkspacePage(page, live_server.url)
|
||||
workspace.open_composer()
|
||||
workspace.compose(unit, group, category, subject, body)
|
||||
expect(page.get_by_label("خلاصه ارسال جدید")).to_contain_text("۲ نفر")
|
||||
workspace.open_history()
|
||||
expect(page.get_by_text("هنوز ارسال گروهی ثبت نشده است")).to_be_visible()
|
||||
workspace.return_to_composer()
|
||||
expect(page.get_by_label("متن پیام", exact=False)).to_have_value(body)
|
||||
workspace.review()
|
||||
dialog = page.get_by_role("dialog")
|
||||
expect(dialog).to_contain_text("۲ نفر")
|
||||
expect(dialog).to_contain_text(subject)
|
||||
workspace.cancel_review()
|
||||
expect(page.get_by_role("button", name="بررسی و ادامه")).to_be_enabled()
|
||||
workspace.save_screenshot(f"compose-{width}.png")
|
||||
workspace.review()
|
||||
workspace.save_screenshot(f"review-{width}.png")
|
||||
workspace.send()
|
||||
expect(page.locator("[data-recipient-row]")).to_have_count(2)
|
||||
expect(page.locator(".sms-state-card").first).to_contain_text("در صف ارسال")
|
||||
expect(page.get_by_text("تحویل پیامک به معنی مشاهده لینک یا دریافت پاسخ نیست.")).to_be_visible()
|
||||
workspace.expect_fits_viewport()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("width", [1440, 390, 320])
|
||||
def test_report_filters_search_and_private_reply_navigation(live_server, browser_page, dispatch_case, width):
|
||||
staff, unit, group, category, _, customers = dispatch_case
|
||||
dispatch, tickets = create_group_dispatch(user=staff, unit=unit, contact_group=group, category=category, subject="پیگیری پاسخ مخاطبان منتخب", body="پیام اصلی با پاسخهای خصوصی مستقل.", response_mode="INDIVIDUAL")
|
||||
first = tickets[0]
|
||||
Message.objects.create(ticket=first, author=customers[0], body="زمان پیشنهادی ما ساعت ده صبح است.")
|
||||
first.last_customer_message_at = timezone.now()
|
||||
first.save(update_fields=["last_customer_message_at"])
|
||||
link = TicketMagicLink.issue_with_record(first, created_by=staff)[1]
|
||||
link.view_count = 2
|
||||
link.save(update_fields=["view_count"])
|
||||
NotificationJob.objects.filter(ticket=first, event_type=SMSTemplate.Event.STAFF_TICKET_CREATED).update(status="SENT", attempts=1, delivery_status="DELIVERED")
|
||||
NotificationJob.objects.filter(ticket=tickets[1], event_type=SMSTemplate.Event.STAFF_TICKET_CREATED).update(status="FAILED", attempts=1, last_error="خطای آزمایشی درگاه")
|
||||
with browser_page() as page:
|
||||
page.set_viewport_size({"width": width, "height": 900 if width == 1440 else 844})
|
||||
AuthenticationPage(page, live_server.url).login_staff(staff, PASSWORD)
|
||||
workspace = DispatchWorkspacePage(page, live_server.url)
|
||||
workspace.open_report(dispatch)
|
||||
expect(workspace.visible_recipients()).to_have_count(2)
|
||||
workspace.filter_recipients("دارای پاسخ")
|
||||
expect(workspace.visible_recipients()).to_have_count(1)
|
||||
expect(workspace.visible_recipients()).to_contain_text("زمان پیشنهادی ما ساعت ده صبح است.")
|
||||
workspace.reset_recipients()
|
||||
workspace.filter_recipients("پیامک نیازمند اقدام")
|
||||
expect(workspace.visible_recipients()).to_have_count(1)
|
||||
expect(workspace.visible_recipients()).to_contain_text("ارسال ناموفق")
|
||||
# One failed attempt is not a successful first attempt.
|
||||
expect(workspace.visible_recipients()).not_to_contain_text("موفق در اولین تلاش")
|
||||
workspace.reset_recipients()
|
||||
workspace.search_recipient("۰۹۱۲۰۰۰۹۸۰۰")
|
||||
expect(workspace.visible_recipients()).to_have_count(1)
|
||||
workspace.search_recipient("وجود ندارد")
|
||||
expect(page.get_by_role("heading", name="مخاطبی با این شرایط پیدا نشد")).to_be_visible()
|
||||
workspace.reset_recipients()
|
||||
workspace.expect_fits_viewport()
|
||||
workspace.save_screenshot(f"report-{width}.png")
|
||||
workspace.filter_recipients("دارای پاسخ")
|
||||
workspace.open_conversation()
|
||||
expect(page).to_have_url(workspace.url(reverse("staff_tickets:detail", args=[first.public_id])))
|
||||
@@ -0,0 +1,61 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.utils import timezone
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from apps.payments import services
|
||||
from apps.payments.models import MAX_AMOUNT
|
||||
from tests.e2e.pages.auth import AuthenticationPage
|
||||
from tests.e2e.pages.staff_payments import StaffPaymentsPage
|
||||
from tests.e2e.test_payments_e2e import PASSWORD, PNG, payment_case
|
||||
|
||||
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
|
||||
|
||||
|
||||
def test_daily_summary_mobile_desktop_and_filtered_empty_results(live_server, browser_page, payment_case, tmp_path):
|
||||
customer, account, reviewers = payment_case
|
||||
services.issue_notice(reviewers[0], {'customer': customer, 'account': account,
|
||||
'amount': 100000, 'subject': 'اعلامیه آزمایشی', 'description': '', 'deposit_id': ''})
|
||||
receipt = services.submit_receipt(customer, customer, {
|
||||
'paid_amount': MAX_AMOUNT, 'paid_date': timezone.localdate(),
|
||||
'destination_account': account, 'depositor_name': 'مشتری آزمایشی',
|
||||
'submission_key': uuid.uuid4(),
|
||||
'receipt_image': SimpleUploadedFile('receipt.png', PNG, content_type='image/png'),
|
||||
})
|
||||
services.decide_receipt(reviewers[0], receipt.public_id, 1, 'APPROVE')
|
||||
services.decide_receipt(reviewers[1], receipt.public_id, 1, 'APPROVE')
|
||||
with browser_page() as page:
|
||||
AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD)
|
||||
workspace = StaffPaymentsPage(page, live_server.url)
|
||||
workspace.open_desk()
|
||||
summary = workspace.daily_summary
|
||||
expect(summary).to_be_visible()
|
||||
expect(summary.locator('[data-summary=submitted]')).to_contain_text('1 فیش')
|
||||
expect(summary.locator('[data-summary=approved]')).to_contain_text('1 فیش')
|
||||
expect(summary.locator('[data-summary=rejected]')).to_contain_text('0 فیش')
|
||||
expect(summary.locator('[data-summary=pending]')).to_contain_text('0 فیش')
|
||||
expect(summary.locator('[data-summary=notices]')).to_contain_text('1 اعلامیه')
|
||||
amount = f'{MAX_AMOUNT:,}'
|
||||
expect(summary.locator('[data-summary=approved]')).to_contain_text(amount)
|
||||
for width, height in [(320, 568), (360, 640), (390, 844), (768, 1024), (1440, 900)]:
|
||||
page.set_viewport_size({'width': width, 'height': height})
|
||||
expect(summary).to_be_visible()
|
||||
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1')
|
||||
assert summary.evaluate('(el) => el.scrollWidth <= el.clientWidth + 1')
|
||||
page.screenshot(path=tmp_path / f'payment-summary-{width}.png', full_page=True)
|
||||
page.set_viewport_size({'width': 320, 'height': 568})
|
||||
page.evaluate("document.documentElement.style.fontSize = '200%'")
|
||||
assert summary.evaluate('(el) => el.scrollWidth <= el.clientWidth + 1')
|
||||
expect(summary.locator('[data-summary=approved]')).to_contain_text(amount)
|
||||
workspace.open_summary_definitions()
|
||||
definitions = summary.locator('.pay-day-definitions')
|
||||
for title in ['ارسالی امروز:', 'تأیید امروز:', 'رد امروز:', 'در انتظار بررسی:', 'اعلامیه امروز:', 'قواعد مشترک:']:
|
||||
expect(definitions.get_by_text(title, exact=True)).to_be_visible()
|
||||
expect(definitions).not_to_contain_text('امروز بر اساس ساعت تهران است')
|
||||
page.evaluate("document.documentElement.style.fontSize = ''")
|
||||
workspace.search('no-such-customer')
|
||||
expect(page.get_by_text('موردی پیدا نشد', exact=True)).to_be_visible()
|
||||
expect(summary.locator('[data-summary=approved]')).to_contain_text('1 فیش')
|
||||
expect(summary.locator('[data-summary=approved]')).to_contain_text(amount)
|
||||
@@ -16,6 +16,7 @@ from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||
from apps.tickets.models import TicketCategory
|
||||
from apps.tickets.services import create_ticket, dispatch_group_tickets
|
||||
from tests.e2e.pages.auth import AuthenticationPage
|
||||
from tests.e2e.pages.dispatches import DispatchWorkspacePage
|
||||
from tests.e2e.pages.tickets import TicketConversationPage
|
||||
|
||||
|
||||
@@ -127,7 +128,10 @@ def test_staff_operations_pages_are_authorized_and_visually_styled(live_server,
|
||||
expect(page).to_have_title("ردیابی E2E")
|
||||
expect(page.locator(".staff-brand-copy b")).to_have_text("مرکز عملیات E2E")
|
||||
expect(page.locator(".staff-developer-credit")).to_contain_text("امضای مستقل sidebar E2E")
|
||||
workspace = DispatchWorkspacePage(page, live_server.url)
|
||||
workspace.open_history()
|
||||
expect(page.get_by_role("searchbox", name="جستجو")).to_be_visible()
|
||||
workspace.return_to_composer()
|
||||
expect(page.locator('link[href*="dispatches.css"]')).to_have_count(1)
|
||||
expect(page.get_by_text("افزودن عکس از گالری")).to_be_visible()
|
||||
expect(page.locator('label[for="dispatch-file-input"]')).to_be_visible()
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import Barrier
|
||||
|
||||
import pytest
|
||||
from django.db import connection, connections
|
||||
from django.db.migrations.executor import MigrationExecutor
|
||||
|
||||
from apps.accounts.models import User
|
||||
from apps.messaging.models import Message
|
||||
from apps.notifications.dispatch_opening import ensure_opening_template
|
||||
from apps.notifications.models import NotificationJob
|
||||
from apps.tickets.dispatch_responses import OPENING_EVENT, open_dispatch_responses, render_opening_sms
|
||||
from apps.tickets.models import DispatchResponseControl, GroupDispatch
|
||||
from apps.tickets.services import reply_ticket
|
||||
from tests.test_dispatch_response_opening import opening_case
|
||||
|
||||
pytestmark = pytest.mark.django_db(transaction=True)
|
||||
|
||||
|
||||
def _postgres_only():
|
||||
if connection.vendor != "postgresql":
|
||||
pytest.skip("Real row-lock concurrency requires PostgreSQL")
|
||||
|
||||
|
||||
def test_two_simultaneous_openings_commit_one_event_and_one_outbox_job(opening_case):
|
||||
_postgres_only()
|
||||
c = opening_case
|
||||
ensure_opening_template()
|
||||
text = render_opening_sms(c["dispatch"])
|
||||
barrier = Barrier(2)
|
||||
def open_once(user_id):
|
||||
connections.close_all()
|
||||
try:
|
||||
user = User.objects.get(pk=user_id)
|
||||
dispatch = GroupDispatch.objects.get(pk=c["dispatch"].pk)
|
||||
barrier.wait(timeout=10)
|
||||
return open_dispatch_responses(dispatch, user, text)[1]
|
||||
finally:
|
||||
connections.close_all()
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
futures = [executor.submit(open_once, c[actor].pk) for actor in ("staff", "admin")]
|
||||
assert sorted(future.result(timeout=20) for future in futures) == [False, True]
|
||||
assert NotificationJob.objects.filter(event_type=OPENING_EVENT).count() == 1
|
||||
assert DispatchResponseControl.objects.get(dispatch=c["dispatch"]).opened_at
|
||||
|
||||
|
||||
def test_reply_racing_opening_matches_the_atomic_recipient_snapshot(opening_case):
|
||||
_postgres_only()
|
||||
c = opening_case
|
||||
ensure_opening_template()
|
||||
text = render_opening_sms(c["dispatch"])
|
||||
barrier = Barrier(2)
|
||||
def run(action):
|
||||
connections.close_all()
|
||||
try:
|
||||
barrier.wait(timeout=10)
|
||||
if action == "reply":
|
||||
return reply_ticket(ticket_id=c["tickets"][1].pk, user=User.objects.get(pk=c["customers"][1].pk), body="پاسخ همزمان")
|
||||
return open_dispatch_responses(c["dispatch"], User.objects.get(pk=c["staff"].pk), text)
|
||||
finally:
|
||||
connections.close_all()
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
futures = [executor.submit(run, action) for action in ("reply", "open")]
|
||||
for future in futures:
|
||||
future.result(timeout=20)
|
||||
control = DispatchResponseControl.objects.get(dispatch=c["dispatch"])
|
||||
expected = set(Message.objects.filter(ticket__group_dispatch=c["dispatch"], author_id__in=[u.pk for u in c["customers"]], created_at__lte=control.opened_at).values_list("author_id", flat=True))
|
||||
assert set(control.respondents_snapshot) == {str(value) for value in User.objects.filter(pk__in=expected).values_list("public_id", flat=True)}
|
||||
assert set(NotificationJob.objects.filter(event_type=OPENING_EVENT).values_list("customer_id", flat=True)) == expected
|
||||
|
||||
|
||||
def test_additive_migrations_preserve_existing_rows_and_legacy_response_visibility(opening_case):
|
||||
c = opening_case
|
||||
# Turn this fixture into a pre-feature dispatch before moving the test schema back.
|
||||
DispatchResponseControl.objects.filter(dispatch=c["dispatch"]).delete()
|
||||
before_dispatch = list(GroupDispatch.objects.values())
|
||||
before_messages = list(Message.objects.values())
|
||||
before_users = list(User.objects.values())
|
||||
old_targets = [("tickets", "0017_ticketaccessgrant_first_viewed_at_and_more"), ("notifications", "0020_receipt_rejection_template")]
|
||||
executor = MigrationExecutor(connection)
|
||||
current_targets = executor.loader.graph.leaf_nodes()
|
||||
try:
|
||||
executor.migrate(old_targets)
|
||||
MigrationExecutor(connection).migrate(current_targets)
|
||||
assert list(GroupDispatch.objects.values()) == before_dispatch
|
||||
assert list(Message.objects.values()) == before_messages
|
||||
assert list(User.objects.values()) == before_users
|
||||
assert not DispatchResponseControl.objects.filter(dispatch=c["dispatch"]).exists()
|
||||
from apps.tickets.staff_conversation import visible_messages
|
||||
assert visible_messages(c["tickets"][0], c["staff"]).filter(pk=c["message"].pk).exists()
|
||||
finally:
|
||||
MigrationExecutor(connection).migrate(current_targets)
|
||||
|
||||
|
||||
def test_migration_refuses_to_drop_response_confidentiality_after_use(opening_case):
|
||||
from django.apps import apps
|
||||
from django.db.migrations.exceptions import IrreversibleError
|
||||
from types import SimpleNamespace
|
||||
import importlib
|
||||
guard = importlib.import_module("apps.tickets.migrations.0018_dispatch_response_opening").preserve_used_response_controls
|
||||
with pytest.raises(IrreversibleError):
|
||||
guard(apps, SimpleNamespace(connection=connection))
|
||||
assert DispatchResponseControl.objects.filter(dispatch=opening_case["dispatch"]).exists()
|
||||
@@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.accounts.models import User
|
||||
from apps.customers.models import ContactGroup, CustomerProfile
|
||||
from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||
from apps.tickets.dispatch_presentation import dispatch_form_options
|
||||
from apps.tickets.models import TicketCategory
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_dispatch_options_count_only_eligible_members_and_preserve_unit_scope(client):
|
||||
unit = OrganizationalUnit.objects.create(name="واحد مجاز", slug="dispatch-options")
|
||||
other = OrganizationalUnit.objects.create(name="واحد دیگر", slug="dispatch-options-other")
|
||||
unit.permissions.add(Permission.objects.get(codename="dispatch_group_tickets"))
|
||||
user = User.objects.create_user(username="dispatch-options", account_type="STAFF")
|
||||
StaffProfile.objects.create(user=user, primary_unit=unit)
|
||||
group = ContactGroup.objects.create(unit=unit, name="گروه مجاز")
|
||||
hidden_group = ContactGroup.objects.create(unit=other, name="گروه پنهان")
|
||||
category = TicketCategory.objects.create(unit=unit, title="مجاز", slug="options-category")
|
||||
hidden_category = TicketCategory.objects.create(unit=other, title="پنهان", slug="options-hidden-category")
|
||||
for index, (mobile, active) in enumerate([("09120000091", True), ("", True), (None, True), ("09120000092", False)]):
|
||||
contact = User.objects.create_user(username=f"options-customer-{index}", mobile=mobile, is_active=active, account_type="CUSTOMER")
|
||||
profile = CustomerProfile.objects.create(user=contact)
|
||||
profile.groups.add(group)
|
||||
client.force_login(user)
|
||||
response = client.get(reverse("staff_tickets:dispatch_create"))
|
||||
assert response.status_code == 200
|
||||
options = response.context["dispatch_options"]
|
||||
assert options["units"] == [{"id": unit.pk, "name": unit.name, "operations": ["BULK"]}]
|
||||
assert options["groups"] == [{"id": group.pk, "name": group.name, "unit_id": unit.pk, "recipient_count": 1}]
|
||||
assert category.pk in [item["id"] for item in options["categories"]]
|
||||
assert hidden_category.pk not in [item["id"] for item in options["categories"]]
|
||||
assert hidden_group.pk not in [item["id"] for item in options["groups"]]
|
||||
assert "?v=" in response.content.decode().split("dispatch-workspace.js", 1)[1]
|
||||
|
||||
|
||||
def test_dispatch_options_include_global_category_fallback_for_units_without_categories():
|
||||
unit = OrganizationalUnit.objects.create(name="بدون دستهبندی", slug="dispatch-options-fallback")
|
||||
user = User.objects.create_superuser(username="options-admin", account_type="STAFF")
|
||||
global_category = TicketCategory.objects.create(title="عمومی", slug="options-global", unit=None)
|
||||
options = dispatch_form_options(user, [unit], ContactGroup.objects.none())
|
||||
assert {"id": global_category.pk, "title": global_category.title, "unit_id": None} in options["categories"]
|
||||
assert options["groups"] == []
|
||||
@@ -0,0 +1,368 @@
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.accounts.models import User
|
||||
from apps.attachments.models import Attachment, AttachmentPolicy
|
||||
from apps.messaging.models import Message, MessageReadReceipt, MessageRevision
|
||||
from apps.messaging.services import edit_message
|
||||
from apps.notifications.models import NotificationJob, SMSTemplate
|
||||
from apps.notifications.management.commands.process_notifications import _send_bale, _send_sms
|
||||
from apps.staff.models import OrganizationalUnit
|
||||
from apps.tickets.dispatch_responses import OPENING_EVENT, open_dispatch_responses, render_opening_sms
|
||||
from apps.tickets.models import DispatchResponseControl, GroupDispatch, MessageShare, TicketCategory
|
||||
from apps.tickets.policies import TicketPolicy
|
||||
from apps.tickets.services import create_group_dispatch, dispatch_group_tickets, reply_ticket
|
||||
from apps.tickets.sharing import share_staff_message
|
||||
from tests.test_organizational_ticketing import make_contact, make_staff
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def opening_case():
|
||||
unit = OrganizationalUnit.objects.create(name="واحد بازگشایی", slug="opening-unit", bale_chat_id="opening-room")
|
||||
unit.permissions.add(*Permission.objects.filter(content_type__app_label="tickets", codename__in=["view_ticket", "create_inquiry", "create_bulk_dispatch"]))
|
||||
staff = make_staff("opening-owner", unit)
|
||||
other = make_staff("opening-peer", unit)
|
||||
viewer = make_staff("opening-viewer", unit)
|
||||
viewer.user_permissions.add(Permission.objects.get(codename="view_all_bulk_dispatches"))
|
||||
admin = User.objects.create_superuser(username="opening-admin", password="Admin-test-91", account_type="STAFF")
|
||||
from apps.customers.models import ContactGroup
|
||||
group = ContactGroup.objects.create(name="گروه استعلام", unit=unit)
|
||||
customers = [make_contact(f"opening-contact-{i}", f"0912200010{i}") for i in range(2)]
|
||||
for customer in customers:
|
||||
customer.customer_profile.groups.add(group)
|
||||
category = TicketCategory.objects.create(unit=unit, title="استعلام", slug="opening-category")
|
||||
dispatch, tickets = dispatch_group_tickets(user=staff, unit=unit, contact_group=group, category=category,
|
||||
subject="استعلام قیمت محرمانه", body="لطفاً پیشنهاد خود را ثبت کنید.")
|
||||
message = reply_ticket(ticket_id=tickets[0].pk, user=customers[0], body="SECRET_BID_84723")
|
||||
return dict(unit=unit, staff=staff, other=other, viewer=viewer, admin=admin, group=group,
|
||||
customers=customers, category=category, dispatch=dispatch, tickets=tickets, message=message)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("actor", ["staff", "viewer", "admin"])
|
||||
def test_seal_blocks_report_ticket_poll_revisions_and_receipts(client, opening_case, actor):
|
||||
c = opening_case
|
||||
client.force_login(c[actor])
|
||||
for route, public_id in [("dispatch_detail", c["dispatch"].public_id), ("detail", c["tickets"][0].public_id),
|
||||
("messages", c["tickets"][0].public_id), ("poll", c["tickets"][0].public_id)]:
|
||||
response = client.get(reverse("staff_tickets:" + route, args=[public_id]))
|
||||
assert response.status_code == 200
|
||||
assert c["message"].body not in response.content.decode()
|
||||
report = client.get(reverse("staff_tickets:dispatch_detail", args=[c["dispatch"].public_id]))
|
||||
assert report.context["metrics"]["replied"] == 1
|
||||
assert 'data-answered="true"' in report.content.decode()
|
||||
assert 'پاسخ داده است: بله' in report.content.decode()
|
||||
assert client.get(reverse("messages:revisions", args=[c["message"].public_id])).status_code == 403
|
||||
client.post(reverse("staff_tickets:read_shares", args=[c["tickets"][0].public_id]), {"message_ids": [str(c["message"].public_id)]})
|
||||
assert not MessageReadReceipt.objects.filter(message=c["message"]).exists()
|
||||
|
||||
|
||||
def test_unit_wide_access_does_not_grant_unrelated_managed_dispatch_tickets(client, opening_case):
|
||||
c = opening_case
|
||||
assert not TicketPolicy.can_view(c["other"], c["tickets"][0])
|
||||
assert not c["tickets"][0].__class__.objects.filter(TicketPolicy.viewable_q(c["other"]), pk=c["tickets"][0].pk).exists()
|
||||
client.force_login(c["other"])
|
||||
assert client.get(reverse("staff_tickets:detail", args=[c["tickets"][0].public_id])).status_code == 403
|
||||
c["tickets"][0].owner = c["other"]
|
||||
c["tickets"][0].save()
|
||||
assert TicketPolicy.can_view(c["other"], c["tickets"][0])
|
||||
response = client.get(reverse("staff_tickets:detail", args=[c["tickets"][0].public_id]))
|
||||
assert response.status_code == 200 and c["message"].body not in response.content.decode()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["SEALED", "INDIVIDUAL", "LEGACY"])
|
||||
def test_report_shows_full_response_history_and_files_without_ticket_navigation(client, opening_case, mode):
|
||||
from apps.core.templatetags.jalali import jalali_datetime_precise
|
||||
c = opening_case
|
||||
control = c["dispatch"].response_control
|
||||
if mode == "LEGACY":
|
||||
control.delete()
|
||||
elif mode == "INDIVIDUAL":
|
||||
control.mode, control.notify_sms = mode, False
|
||||
control.save()
|
||||
body = "متن کامل پیشنهاد " * 30 + "پایان متن بلند"
|
||||
answer = reply_ticket(ticket_id=c["tickets"][0].pk, user=c["customers"][0], body=body)
|
||||
AttachmentPolicy.objects.create(attachment_type="PDF", max_file_size=1024 * 1024,
|
||||
allowed_extensions=[".pdf"], allowed_mime_types=["application/pdf"])
|
||||
from apps.attachments.services import save_attachments
|
||||
save_attachments(answer, [SimpleUploadedFile("offer.pdf", b"%PDF-1.4\n%%EOF", content_type="application/pdf")], c["customers"][0])
|
||||
file = answer.attachments.get()
|
||||
deleted = reply_ticket(ticket_id=c["tickets"][0].pk, user=c["customers"][0], body="DELETED_OFFER")
|
||||
from django.utils import timezone
|
||||
deleted.deleted_at = timezone.now()
|
||||
deleted.save(update_fields=["deleted_at"])
|
||||
client.force_login(c["staff"])
|
||||
url = reverse("staff_tickets:dispatch_detail", args=[c["dispatch"].public_id])
|
||||
ticket_url = reverse("staff_tickets:detail", args=[c["tickets"][0].public_id])
|
||||
attachment_url = reverse("attachments:view", args=[file.public_id])
|
||||
if mode == "SEALED":
|
||||
locked = client.get(url)
|
||||
assert body not in locked.content.decode() and attachment_url not in locked.content.decode()
|
||||
assert not locked.context["tickets"][0].report_customer_responses
|
||||
open_dispatch_responses(c["dispatch"], c["staff"], render_opening_sms(c["dispatch"]))
|
||||
report = client.get(url)
|
||||
html = report.content.decode()
|
||||
assert body in html and c["message"].body in html and deleted.body not in html
|
||||
assert attachment_url in html and jalali_datetime_precise(answer.created_at) in html
|
||||
assert (f'href="{ticket_url}"' in html) == (mode != "SEALED")
|
||||
|
||||
|
||||
def test_customer_can_read_and_edit_own_sealed_answer(client, opening_case):
|
||||
c = opening_case
|
||||
client.force_login(c["customers"][0])
|
||||
response = client.get(reverse("tickets:detail", args=[c["tickets"][0].public_id]))
|
||||
assert c["message"].body in response.content.decode()
|
||||
edit_message(c["message"].pk, c["customers"][0], "SECRET_EDITED_BID")
|
||||
client.force_login(c["admin"])
|
||||
assert client.get(reverse("messages:revisions", args=[c["message"].public_id])).status_code == 403
|
||||
|
||||
|
||||
def test_opening_is_once_and_uses_confirmed_template_snapshot(opening_case, monkeypatch):
|
||||
c = opening_case
|
||||
text = render_opening_sms(c["dispatch"])
|
||||
control, opened = open_dispatch_responses(c["dispatch"], c["staff"], text)
|
||||
assert opened and control.opened_by == c["staff"] and control.opened_at
|
||||
assert control.respondents_snapshot == [str(c["customers"][0].public_id)]
|
||||
job = NotificationJob.objects.get(event_type=OPENING_EVENT)
|
||||
SMSTemplate.objects.filter(event_type=OPENING_EVENT).update(body="متن تغییر یافته")
|
||||
assert open_dispatch_responses(c["dispatch"], c["admin"], text)[1] is False
|
||||
assert NotificationJob.objects.filter(event_type=OPENING_EVENT).count() == 1
|
||||
sent = []
|
||||
class Provider:
|
||||
def send_message(self, mobile, body):
|
||||
sent.append((mobile, body))
|
||||
return "test-provider-id"
|
||||
monkeypatch.setattr("apps.notifications.management.commands.process_notifications.get_sms_provider", lambda: Provider())
|
||||
assert _send_sms(job) == ("test-provider-id", None)
|
||||
assert sent == [(c["customers"][0].mobile, text)]
|
||||
reply_ticket(ticket_id=c["tickets"][1].pk, user=c["customers"][1], body="پاسخ پس از بازگشایی")
|
||||
assert NotificationJob.objects.filter(event_type=OPENING_EVENT).count() == 1
|
||||
|
||||
|
||||
def test_template_change_requires_new_confirmation_and_view_permission_is_not_open_permission(client, opening_case):
|
||||
c = opening_case
|
||||
with pytest.raises(PermissionDenied):
|
||||
open_dispatch_responses(c["dispatch"], c["viewer"], render_opening_sms(c["dispatch"]))
|
||||
c["viewer"].user_permissions.add(Permission.objects.get(codename="open_all_dispatch_responses"))
|
||||
viewer = User.objects.get(pk=c["viewer"].pk)
|
||||
client.force_login(viewer)
|
||||
url = reverse("staff_tickets:dispatch_open_responses", args=[c["dispatch"].public_id])
|
||||
assert client.post(url).status_code == 200
|
||||
proof = client.get(url).context["confirmation"]
|
||||
SMSTemplate.objects.filter(event_type=OPENING_EVENT).update(body="متن جدید بازگشایی")
|
||||
client.post(url, {"confirmation": proof})
|
||||
assert not DispatchResponseControl.objects.get(dispatch=c["dispatch"]).opened_at
|
||||
proof = client.get(url).context["confirmation"]
|
||||
assert client.post(url, {"confirmation": proof}).status_code == 302
|
||||
assert DispatchResponseControl.objects.get(dispatch=c["dispatch"]).opened_by == viewer
|
||||
|
||||
|
||||
@pytest.mark.parametrize("count,mode,allowed", [(15, "SEALED", True), (16, "SEALED", False), (2, "INDIVIDUAL", False)])
|
||||
def test_sms_recipient_limit_is_frozen_and_individual_mode_disables_it(opening_case, count, mode, allowed):
|
||||
c = opening_case
|
||||
for i in range(2, count):
|
||||
make_contact(f"limit-{i}", f"09122000{i:03}").customer_profile.groups.add(c["group"])
|
||||
dispatch, tickets = create_group_dispatch(user=c["staff"], unit=c["unit"], contact_group=c["group"], category=c["category"],
|
||||
subject="آزمون سقف مخاطبان", body="متن استعلام", response_mode=mode, notify_opening_sms=True)
|
||||
control = DispatchResponseControl.objects.get(dispatch=dispatch)
|
||||
assert control.recipient_count_snapshot == count and control.notify_sms is allowed
|
||||
if count == 16:
|
||||
c["group"].contacts.clear()
|
||||
with transaction.atomic(), pytest.raises(IntegrityError):
|
||||
DispatchResponseControl.objects.filter(pk=control.pk).update(notify_sms=True)
|
||||
if mode == "INDIVIDUAL":
|
||||
message = reply_ticket(ticket_id=tickets[0].pk, user=c["customers"][0], body="پاسخ جداگانه")
|
||||
from apps.tickets.staff_conversation import visible_messages
|
||||
assert visible_messages(tickets[0], c["staff"]).filter(pk=message.pk).exists()
|
||||
|
||||
|
||||
def test_sealed_attachment_shared_routes_admin_and_direct_media_are_blocked(client, opening_case, settings, tmp_path):
|
||||
c = opening_case
|
||||
settings.MEDIA_ROOT = tmp_path
|
||||
settings.USE_NGINX_MEDIA = True
|
||||
from apps.attachments.services import save_attachments
|
||||
AttachmentPolicy.objects.create(attachment_type="PDF", max_file_size=1024 * 1024,
|
||||
allowed_extensions=[".pdf"], allowed_mime_types=["application/pdf"])
|
||||
save_attachments(c["message"], [SimpleUploadedFile("secret.pdf", b"%PDF-1.4\n%%EOF", content_type="application/pdf")], c["customers"][0])
|
||||
attachment = Attachment.objects.get(message=c["message"])
|
||||
assert attachment.relative_path.startswith("dispatch-responses/")
|
||||
MessageRevision.objects.create(message=c["message"], body="OLD_SECRET_BID", edited_by=c["customers"][0])
|
||||
with pytest.raises(PermissionDenied):
|
||||
share_staff_message(c["message"], c["staff"], c["other"])
|
||||
grant = MessageShare.objects.create(message=c["message"], user=c["other"], granted_by=c["staff"])
|
||||
client.force_login(c["other"])
|
||||
assert client.get(reverse("staff_tickets:shared_message_attachment", args=[grant.public_id, attachment.public_id])).status_code == 403
|
||||
client.force_login(c["admin"])
|
||||
for route in ("view", "download"):
|
||||
assert client.get(reverse("attachments:" + route, args=[attachment.public_id])).status_code == 403
|
||||
for name, obj in [("messaging_message", c["message"]), ("messaging_messagerevision", c["message"].revisions.get()), ("attachments_attachment", attachment)]:
|
||||
response = client.get(reverse("admin:" + name + "_change", args=[obj.pk]), follow=True)
|
||||
assert response.redirect_chain and c["message"].body not in response.content.decode()
|
||||
open_dispatch_responses(c["dispatch"], c["admin"], render_opening_sms(c["dispatch"]))
|
||||
assert client.get(reverse("admin:messaging_message_change", args=[c["message"].pk])).status_code == 200
|
||||
client.force_login(c["customers"][0])
|
||||
response = client.get(reverse("attachments:view", args=[attachment.public_id]))
|
||||
assert response.status_code == 200 and "no-store" in response["Cache-Control"]
|
||||
assert b"".join(response.streaming_content).startswith(b"%PDF")
|
||||
from config.urls import debug_media
|
||||
from django.http import Http404
|
||||
with pytest.raises(Http404):
|
||||
debug_media(RequestFactory().get("/media/"), attachment.relative_path)
|
||||
|
||||
|
||||
def test_bale_does_not_disclose_managed_response_text(opening_case, monkeypatch):
|
||||
c = opening_case
|
||||
profile = c["staff"].staff_profile
|
||||
profile.bale_chat_id = "private-dispatch-owner"
|
||||
profile.save(update_fields=["bale_chat_id"])
|
||||
sent = []
|
||||
class Provider:
|
||||
def send_message(self, chat_id, body):
|
||||
sent.append((chat_id, body))
|
||||
monkeypatch.setattr("apps.notifications.management.commands.process_notifications.get_bale_provider", lambda: Provider())
|
||||
job = NotificationJob.objects.get(event_type="CUSTOMER_TICKET_REPLY", ticket=c["tickets"][0])
|
||||
_send_bale(job)
|
||||
assert sent and sent[0][0] == "private-dispatch-owner" and c["message"].body not in sent[0][1]
|
||||
|
||||
|
||||
def test_seed_only_adds_missing_opening_template_and_preserves_customizations():
|
||||
from django.apps import apps
|
||||
from django.db import connection
|
||||
migration = importlib.import_module("apps.notifications.migrations.0021_dispatch_response_opening")
|
||||
original = list(SMSTemplate.objects.exclude(event_type=OPENING_EVENT).values())
|
||||
SMSTemplate.objects.filter(event_type=OPENING_EVENT).update(body="قالب اختصاصی حفظ شود")
|
||||
from types import SimpleNamespace
|
||||
schema_editor = SimpleNamespace(connection=connection)
|
||||
migration.seed_opening_template(apps, schema_editor)
|
||||
migration.seed_opening_template(apps, schema_editor)
|
||||
assert SMSTemplate.objects.get(event_type=OPENING_EVENT).body == "قالب اختصاصی حفظ شود"
|
||||
assert list(SMSTemplate.objects.exclude(event_type=OPENING_EVENT).values()) == original
|
||||
|
||||
|
||||
def test_view_only_staff_can_list_reports_without_creation_permission(client, opening_case):
|
||||
c = opening_case
|
||||
unit = OrganizationalUnit.objects.create(name="واحد مشاهده", slug="opening-observer")
|
||||
viewer = make_staff("only-view-dispatches", unit)
|
||||
viewer.user_permissions.add(Permission.objects.get(codename="view_all_bulk_dispatches"))
|
||||
client.force_login(viewer)
|
||||
response = client.get(reverse("staff_tickets:dispatch_create"))
|
||||
assert response.status_code == 200 and c["dispatch"].subject in response.content.decode()
|
||||
assert response.context["staff_operations_access"]["dispatches"] is True
|
||||
assert 'id="dispatch-compose-form"' not in response.content.decode()
|
||||
assert client.post(reverse("staff_tickets:dispatch_create"), {}).status_code == 403
|
||||
assert client.get(reverse("staff_tickets:dispatch_open_responses", args=[c["dispatch"].public_id])).status_code == 403
|
||||
|
||||
|
||||
def test_opening_rolls_back_if_outbox_cannot_be_saved(opening_case, monkeypatch):
|
||||
c = opening_case
|
||||
def fail(*args, **kwargs):
|
||||
raise RuntimeError("outbox unavailable")
|
||||
monkeypatch.setattr("apps.notifications.services.enqueue", fail)
|
||||
with pytest.raises(RuntimeError):
|
||||
open_dispatch_responses(c["dispatch"], c["staff"], render_opening_sms(c["dispatch"]))
|
||||
assert not DispatchResponseControl.objects.get(dispatch=c["dispatch"]).opened_at
|
||||
assert not NotificationJob.objects.filter(event_type=OPENING_EVENT).exists()
|
||||
|
||||
|
||||
def test_existing_worker_sends_opening_job_once_without_issuing_magic_link(opening_case, monkeypatch):
|
||||
from django.core.management import call_command
|
||||
from apps.integrations.sms.base import SMSResult
|
||||
from apps.accounts.models import TicketMagicLink
|
||||
c = opening_case
|
||||
text = render_opening_sms(c["dispatch"])
|
||||
open_dispatch_responses(c["dispatch"], c["staff"], text)
|
||||
job = NotificationJob.objects.get(event_type=OPENING_EVENT)
|
||||
sent = []
|
||||
class Provider:
|
||||
def send_message(self, mobile, body):
|
||||
sent.append((mobile, body))
|
||||
return SMSResult(True, "opening-test-provider")
|
||||
monkeypatch.setattr("apps.notifications.management.commands.process_notifications.get_sms_provider", lambda: Provider())
|
||||
links_before = TicketMagicLink.objects.count()
|
||||
call_command("process_notifications", once=True, job_id=job.pk)
|
||||
call_command("process_notifications", once=True, job_id=job.pk)
|
||||
job.refresh_from_db()
|
||||
assert job.status == "SENT" and sent == [(c["customers"][0].mobile, text)]
|
||||
assert TicketMagicLink.objects.count() == links_before
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_seed_round_trip_preserves_opening_with_user_primary_key_gaps(opening_case, settings, tmp_path, monkeypatch):
|
||||
from django.core.management import call_command
|
||||
from django.db import connection
|
||||
if connection.vendor != "sqlite":
|
||||
pytest.skip("Seed export is deliberately restricted to SQLite sources")
|
||||
c = opening_case
|
||||
settings.MEDIA_ROOT = tmp_path / "source-media"
|
||||
User.objects.create_user(username="discarded-seed-contact").delete()
|
||||
customer = make_contact("stable-seed-respondent", "09122000888")
|
||||
customer.customer_profile.groups.add(c["group"])
|
||||
dispatch, tickets = create_group_dispatch(user=c["staff"], unit=c["unit"], contact_group=c["group"],
|
||||
category=c["category"], subject="بازگشایی با انتقال داده", body="متن", notify_opening_sms=False)
|
||||
ticket = next(ticket for ticket in tickets if ticket.customer_id == customer.pk)
|
||||
reply_ticket(ticket_id=ticket.pk, user=customer, body="پاسخ منتقلشونده")
|
||||
control, _ = open_dispatch_responses(dispatch, c["staff"])
|
||||
snapshot = control.respondents_snapshot
|
||||
opened_at = control.opened_at
|
||||
assert snapshot == [str(customer.public_id)]
|
||||
bundle = tmp_path / "bundle"
|
||||
call_command("export_production_seed", output=str(bundle))
|
||||
call_command("flush", interactive=False, verbosity=0)
|
||||
settings.MEDIA_ROOT = tmp_path / "destination-media"
|
||||
monkeypatch.setattr(connection, "vendor", "postgresql")
|
||||
call_command("import_production_seed", bundle_dir=str(bundle))
|
||||
restored = DispatchResponseControl.objects.get(dispatch_id=dispatch.pk)
|
||||
# Django's existing JSON fixture format retains millisecond precision.
|
||||
assert 0 <= (opened_at - restored.opened_at).total_seconds() < 0.001
|
||||
assert restored.respondents_snapshot == snapshot
|
||||
assert restored.opened_by.username == c["staff"].username
|
||||
assert str(User.objects.get(username=customer.username).public_id) in restored.respondents_snapshot
|
||||
|
||||
|
||||
def test_managed_reply_bale_never_falls_back_to_a_broad_unit_room(opening_case, monkeypatch):
|
||||
c = opening_case
|
||||
sent = []
|
||||
class Provider:
|
||||
def send_message(self, chat_id, text):
|
||||
sent.append((chat_id, text))
|
||||
monkeypatch.setattr("apps.notifications.management.commands.process_notifications.get_bale_provider", lambda: Provider())
|
||||
job = NotificationJob.objects.get(event_type="CUSTOMER_TICKET_REPLY", ticket=c["tickets"][0])
|
||||
assert _send_bale(job) is None
|
||||
assert not sent
|
||||
|
||||
|
||||
def test_acknowledging_question_does_not_mark_a_sealed_response_ticket_read(client, opening_case):
|
||||
from apps.tickets.models import TicketReadState
|
||||
c = opening_case
|
||||
ticket = c["tickets"][0]
|
||||
question = ticket.ticket_messages.get(author=c["staff"])
|
||||
client.force_login(c["admin"])
|
||||
url = reverse("staff_tickets:read_shares", args=[ticket.public_id])
|
||||
client.post(url, {"message_ids": [str(question.public_id)]})
|
||||
assert MessageReadReceipt.objects.filter(message=question, user=c["admin"]).exists()
|
||||
assert not TicketReadState.objects.filter(ticket=ticket, user=c["admin"]).exists()
|
||||
open_dispatch_responses(c["dispatch"], c["admin"], render_opening_sms(c["dispatch"]))
|
||||
assert not MessageReadReceipt.objects.filter(message=c["message"], user=c["admin"]).exists()
|
||||
client.post(url, {"message_ids": [str(c["message"].public_id)]})
|
||||
assert TicketReadState.objects.filter(ticket=ticket, user=c["admin"]).exists()
|
||||
|
||||
|
||||
def test_existing_share_audit_cannot_disclose_sealed_body_to_creator_or_admin(client, opening_case):
|
||||
c = opening_case
|
||||
MessageShare.objects.create(message=c["message"], user=c["other"], granted_by=c["staff"])
|
||||
for actor in ("staff", "admin"):
|
||||
client.force_login(c[actor])
|
||||
response = client.get(reverse("staff_tickets:detail", args=[c["tickets"][0].public_id]))
|
||||
assert response.status_code == 200 and c["message"].body not in response.content.decode()
|
||||
assert not response.context["message_shares"].filter(message=c["message"]).exists()
|
||||
open_dispatch_responses(c["dispatch"], c["staff"], render_opening_sms(c["dispatch"]))
|
||||
response = client.get(reverse("staff_tickets:detail", args=[c["tickets"][0].public_id]))
|
||||
assert c["message"].body in response.content.decode()
|
||||
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime, timezone as dt_timezone
|
||||
import pytest
|
||||
|
||||
from apps.core.templatetags.jalali import jalali_date, jalali_datetime, persian_time
|
||||
from apps.core.templatetags.jalali import jalali_date, jalali_datetime, jalali_datetime_precise, persian_time
|
||||
|
||||
|
||||
def test_jalali_datetime_uses_iran_time_persian_month_and_digits():
|
||||
@@ -14,3 +15,11 @@ def test_jalali_filters_fail_safely_for_empty_values():
|
||||
assert jalali_datetime(None) == ""
|
||||
assert jalali_date(None) == ""
|
||||
assert persian_time(None) == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value,expected", [
|
||||
(datetime(2026, 9, 5, 22, 31, 17, tzinfo=dt_timezone.utc), "۱۵ شهریور ۱۴۰۵، ساعت ۰۲:۰۱:۱۷"),
|
||||
(None, ""),
|
||||
])
|
||||
def test_opening_timestamp_includes_seconds_in_iran_time(value, expected):
|
||||
assert jalali_datetime_precise(value) == expected
|
||||
@@ -0,0 +1,114 @@
|
||||
from datetime import datetime, time, timedelta, timezone as datetime_timezone
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.payments import policies, selectors, services
|
||||
from apps.payments.forms import ReceiptForm
|
||||
from apps.payments.models import (
|
||||
PaymentAccessConfiguration, PaymentApprovalPolicy, PaymentNotice, PaymentReceipt,
|
||||
PaymentReceiptRevision, ReceiptApprovalCycle,
|
||||
)
|
||||
from tests.test_payments import case, data, grant, submit, upload
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_daily_summary_uses_current_amounts_final_outcomes_and_all_pending(case, django_assert_num_queries):
|
||||
independent_policy = PaymentApprovalPolicy.objects.create(name='بررسی فیش مستقل')
|
||||
independent_policy.reviewers.add(case['first'], case['second'])
|
||||
approved = submit(case)
|
||||
services.decide_receipt(case['first'], approved.public_id, 1, 'APPROVE')
|
||||
services.decide_receipt(case['second'], approved.public_id, 1, 'APPROVE')
|
||||
rejected = submit(case, linked=False)
|
||||
services.decide_receipt(case['first'], rejected.public_id, 1, 'REJECT', 'ناخوانا')
|
||||
partial = submit(case)
|
||||
services.decide_receipt(case['first'], partial.public_id, 1, 'APPROVE')
|
||||
pending = submit(case, linked=False)
|
||||
revised = submit(case)
|
||||
services.decide_receipt(case['first'], revised.public_id, 1, 'REJECT', 'اصلاح مبلغ')
|
||||
form = ReceiptForm(data(case, paid_amount='9000'), {'receipt_image': upload()}, customer=case['customer'])
|
||||
assert form.is_valid(), form.errors
|
||||
services.revise_receipt(case['customer'], revised.public_id, 1, form.cleaned_data)
|
||||
PaymentReceiptRevision.objects.filter(version=1).update(amount=100)
|
||||
yesterday = timezone.now() - timedelta(days=1)
|
||||
PaymentReceipt.objects.filter(pk__in=[approved.pk, pending.pk]).update(created_at=yesterday)
|
||||
# The written bank date is unrelated to when evidence arrived or was reviewed.
|
||||
PaymentReceiptRevision.objects.update(paid_date=yesterday.date())
|
||||
with django_assert_num_queries(2):
|
||||
summary = selectors.daily_payment_summary(
|
||||
PaymentReceipt.objects.all(), PaymentNotice.objects.filter(deleted_at__isnull=True))
|
||||
assert summary == {
|
||||
'date': timezone.localdate(), 'submitted_count': 3, 'submitted_amount': 9200,
|
||||
'approved_count': 1, 'approved_amount': 100, 'rejected_count': 1, 'rejected_amount': 100,
|
||||
'pending_count': 3, 'pending_amount': 9200, 'notice_count': 1,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('offset,included', [
|
||||
(-1, False), (0, True), (24 * 60 * 60 - 1, True), (24 * 60 * 60, False),
|
||||
])
|
||||
def test_daily_summary_tehran_midnight_boundaries(case, monkeypatch, offset, included):
|
||||
receipt = submit(case)
|
||||
services.decide_receipt(case['senior'], receipt.public_id, 1, 'SENIOR_APPROVE', 'بررسی نهایی')
|
||||
today = timezone.localdate()
|
||||
start = timezone.make_aware(datetime.combine(today, time.min), timezone.get_default_timezone())
|
||||
stamp = start + timedelta(seconds=offset)
|
||||
PaymentReceipt.objects.filter(pk=receipt.pk).update(created_at=stamp)
|
||||
PaymentNotice.objects.filter(pk=case['notice'].pk).update(created_at=stamp)
|
||||
ReceiptApprovalCycle.objects.filter(revision__receipt=receipt).update(finalized_at=stamp)
|
||||
monkeypatch.setattr(timezone, 'now', lambda: start + timedelta(hours=1))
|
||||
# An active display timezone must not move the Tehran business-day boundary.
|
||||
with timezone.override(datetime_timezone.utc):
|
||||
summary = selectors.daily_payment_summary(PaymentReceipt.objects.all(), PaymentNotice.objects.all())
|
||||
assert summary['date'] == today
|
||||
assert summary['submitted_count'] == summary['approved_count'] == summary['notice_count'] == int(included)
|
||||
assert summary['submitted_amount'] == summary['approved_amount']
|
||||
assert bool(summary['submitted_amount']) == included
|
||||
|
||||
|
||||
def test_daily_summary_excludes_old_finalizations_and_deleted_notices(case):
|
||||
receipt = submit(case)
|
||||
services.decide_receipt(case['first'], receipt.public_id, 1, 'REJECT', 'ناخوانا')
|
||||
ReceiptApprovalCycle.objects.filter(revision__receipt=receipt).update(
|
||||
finalized_at=timezone.now() - timedelta(days=1))
|
||||
PaymentNotice.objects.filter(pk=case['notice'].pk).update(deleted_at=timezone.now())
|
||||
summary = selectors.daily_payment_summary(
|
||||
policies.visible(PaymentReceipt.objects.all(), case['creator']),
|
||||
policies.visible(PaymentNotice.objects.all(), case['creator']))
|
||||
assert summary['submitted_count'] == 1
|
||||
assert summary['rejected_count'] == summary['rejected_amount'] == summary['notice_count'] == 0
|
||||
|
||||
|
||||
def test_daily_summary_permission_scope_and_zero_state(case, client):
|
||||
submit(case)
|
||||
PaymentAccessConfiguration.objects.update_or_create(pk=1, defaults={'open_view': False})
|
||||
client.force_login(case['outsider'])
|
||||
response = client.get(reverse('staff_payments:desk'))
|
||||
assert response.status_code == 200
|
||||
summary = response.context['daily_summary']
|
||||
assert all(value == 0 for key, value in summary.items() if key != 'date')
|
||||
grant(case['outsider'], 'view_paymentreceipt')
|
||||
response = client.get(reverse('staff_payments:desk'))
|
||||
assert response.context['daily_summary']['submitted_count'] == 1
|
||||
assert response.context['daily_summary']['notice_count'] == 0
|
||||
grant(case['outsider'], 'view_paymentnotice')
|
||||
assert client.get(reverse('staff_payments:desk')).context['daily_summary']['notice_count'] == 1
|
||||
|
||||
|
||||
def test_daily_summary_stays_independent_of_tabs_search_and_pagination(case, client):
|
||||
submit(case)
|
||||
client.force_login(case['creator'])
|
||||
endpoint = reverse('staff_payments:desk')
|
||||
baseline = client.get(endpoint).context['daily_summary']
|
||||
for parameters in [
|
||||
{'tab': 'notices', 'notice_state': 'cancelled'},
|
||||
{'tab': 'receipts', 'mine': '1', 'q': 'no-such-customer', 'page': '99'},
|
||||
{'tab': 'receipts', 'status': 'APPROVED', 'kind': 'independent', 'from': '2000-01-01', 'to': '2000-01-02'},
|
||||
]:
|
||||
response = client.get(endpoint, parameters)
|
||||
assert response.status_code == 200
|
||||
assert response.context['daily_summary'] == baseline
|
||||
assert response.context['page_obj'].paginator.count == 0
|
||||
assert 'مبنای آمار' in response.content.decode()
|
||||
@@ -42,14 +42,12 @@ def run(job):
|
||||
return job
|
||||
|
||||
|
||||
def test_initially_disabled_and_staff_uploads_do_not_notify(case, enabled, monkeypatch):
|
||||
def test_disabled_channels_do_not_notify_customer_or_staff_uploads(case, enabled, monkeypatch):
|
||||
network = Mock(side_effect=AssertionError('Network must not run in registration'))
|
||||
monkeypatch.setattr(requests, 'post', network)
|
||||
enabled.bale_enabled = enabled.sms_enabled = False
|
||||
enabled.save()
|
||||
submit(case)
|
||||
enabled.bale_enabled = enabled.sms_enabled = True
|
||||
enabled.save()
|
||||
form = ReceiptForm(data(case, linked=False), {'receipt_image': upload()}, customer=case['customer'])
|
||||
assert form.is_valid(), form.errors
|
||||
services.submit_receipt(case['creator'], case['customer'], form.cleaned_data)
|
||||
@@ -57,6 +55,38 @@ def test_initially_disabled_and_staff_uploads_do_not_notify(case, enabled, monke
|
||||
network.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('picker,linked', [(False, True), (False, False), (True, True), (True, False)])
|
||||
def test_staff_entry_points_enqueue_once_with_customer_identity(case, enabled, client, monkeypatch, picker, linked):
|
||||
network = Mock(side_effect=AssertionError('Network must not run in registration'))
|
||||
monkeypatch.setattr(requests, 'post', network)
|
||||
client.force_login(case['creator'])
|
||||
endpoint = reverse('staff_payments:receipt_new', args=[case['customer'].public_id])
|
||||
if picker:
|
||||
response = client.post(reverse('staff_payments:receipt_customer'), {'customer': case['customer'].pk})
|
||||
assert response.status_code == 302 and response.url == endpoint
|
||||
assert client.get(endpoint).status_code == 200
|
||||
values = data(case, linked=linked)
|
||||
invalid = client.post(endpoint, {**values, 'paid_amount': '0', 'receipt_image': upload()},
|
||||
HTTP_ACCEPT='application/json')
|
||||
assert invalid.status_code == 422
|
||||
assert not PaymentReceiptAlert.objects.exists()
|
||||
for _ in range(2):
|
||||
response = client.post(endpoint, {**values, 'receipt_image': upload()}, HTTP_ACCEPT='application/json')
|
||||
assert response.status_code == 200, response.content
|
||||
receipt = PaymentReceipt.objects.get(customer=case['customer'])
|
||||
assert receipt.submitted_by_id == case['creator'].pk
|
||||
assert receipt.notice_id == (case['notice'].pk if linked else None)
|
||||
alerts = list(receipt.revisions.get().alerts.select_related('job'))
|
||||
assert len(alerts) == 3
|
||||
assert {a.job.channel for a in alerts} == {'SMS', 'BALE'}
|
||||
assert {a.recipient_mobile for a in alerts} == {'', '+989121110001', '+989121110002'}
|
||||
assert all(a.job.customer_id == case['customer'].pk for a in alerts)
|
||||
assert all(a.context['customer_mobile'] == case['customer'].local_mobile for a in alerts)
|
||||
assert all('واریزکننده آزمایشی' in a.context['customer_name'] for a in alerts)
|
||||
assert all(a.context['receipt_link'].endswith(reverse('staff_payments:receipt_detail', args=[receipt.public_id])) for a in alerts)
|
||||
network.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('anonymous,linked', [(False, True), (False, False), (True, True), (True, False)])
|
||||
def test_all_customer_entry_points_enqueue_once_without_network(case, enabled, client, monkeypatch, anonymous, linked):
|
||||
network = Mock(side_effect=AssertionError('Network must not run in registration'))
|
||||
@@ -85,27 +115,37 @@ def test_all_customer_entry_points_enqueue_once_without_network(case, enabled, c
|
||||
network.assert_not_called()
|
||||
|
||||
|
||||
def test_customer_corrections_have_separate_idempotent_alerts_and_can_be_disabled(case, enabled):
|
||||
@pytest.mark.parametrize('staff', [False, True])
|
||||
def test_customer_corrections_have_separate_idempotent_alerts_and_can_be_disabled(case, enabled, staff):
|
||||
receipt = submit(case)
|
||||
actor = case['creator'] if staff else case['customer']
|
||||
if staff:
|
||||
grant(actor, 'submit_customer_receipt')
|
||||
services.decide_receipt(case['first'], receipt.public_id, 1, 'REJECT', 'تصویر خوانا نیست')
|
||||
form = ReceiptForm(data(case), {'receipt_image': upload()}, customer=case['customer'])
|
||||
assert form.is_valid(), form.errors
|
||||
services.revise_receipt(case['customer'], receipt.public_id, 1, form.cleaned_data)
|
||||
services.revise_receipt(actor, receipt.public_id, 1, form.cleaned_data)
|
||||
assert PaymentReceiptAlert.objects.count() == 6
|
||||
receipt_alerts.safe_queue_receipt_alerts(receipt.revisions.get(version=2), case['customer'])
|
||||
receipt_alerts.safe_queue_receipt_alerts(receipt.revisions.get(version=2), actor)
|
||||
assert PaymentReceiptAlert.objects.count() == 6
|
||||
enabled.notify_revisions = False
|
||||
enabled.save()
|
||||
services.decide_receipt(case['first'], receipt.public_id, 2, 'REJECT', 'اصلاح لازم است')
|
||||
form = ReceiptForm(data(case), {'receipt_image': upload()}, customer=case['customer'])
|
||||
assert form.is_valid()
|
||||
services.revise_receipt(case['customer'], receipt.public_id, 2, form.cleaned_data)
|
||||
services.revise_receipt(actor, receipt.public_id, 2, form.cleaned_data)
|
||||
assert PaymentReceiptAlert.objects.count() == 6
|
||||
|
||||
|
||||
def test_outer_rollback_removes_receipt_and_alerts(case, enabled):
|
||||
@pytest.mark.parametrize('staff', [False, True])
|
||||
def test_outer_rollback_removes_receipt_and_alerts(case, enabled, staff):
|
||||
with pytest.raises(RuntimeError), transaction.atomic():
|
||||
submit(case)
|
||||
if staff:
|
||||
form = ReceiptForm(data(case), {'receipt_image': upload()}, customer=case['customer'])
|
||||
assert form.is_valid(), form.errors
|
||||
services.submit_receipt(case['creator'], case['customer'], form.cleaned_data)
|
||||
else:
|
||||
submit(case)
|
||||
raise RuntimeError('rollback')
|
||||
assert not PaymentReceipt.objects.exists() and not PaymentReceiptAlert.objects.exists()
|
||||
assert not NotificationJob.objects.filter(event_type=receipt_alerts.RECEIPT_ALERT_EVENT).exists()
|
||||
@@ -131,9 +171,15 @@ def test_configuration_failure_isolated_from_registration(case, enabled, monkeyp
|
||||
assert receipt.revisions.count() == 1
|
||||
|
||||
|
||||
def test_worker_sends_private_photo_with_database_token_and_sms_to_managers(case, enabled, monkeypatch, settings):
|
||||
@pytest.mark.parametrize('staff', [False, True])
|
||||
def test_worker_sends_private_photo_with_database_token_and_sms_to_managers(case, enabled, monkeypatch, settings, staff):
|
||||
settings.BALE_BOT_TOKEN = 'existing-ticket-token'
|
||||
receipt = submit(case)
|
||||
if staff:
|
||||
form = ReceiptForm(data(case), {'receipt_image': upload()}, customer=case['customer'])
|
||||
assert form.is_valid(), form.errors
|
||||
receipt = services.submit_receipt(case['creator'], case['customer'], form.cleaned_data)
|
||||
else:
|
||||
receipt = submit(case)
|
||||
posted, sent = [], []
|
||||
def post(url, **kwargs):
|
||||
posted.append((url, kwargs['data'], kwargs['files']['photo'][1].read(), kwargs['timeout']))
|
||||
|
||||
Reference in new issue
Block a user