import base64 import pytest from django.contrib.auth.models import Permission from django.urls import reverse from playwright.sync_api import expect from apps.accounts.models import User from apps.staff.models import StaffProfile from apps.attachments.models import AttachmentPolicy from apps.payments.models import BankInstitution, CompanyBankAccount, PaymentApprovalPolicy from apps.payments import services from tests.e2e.pages.auth import AuthenticationPage from tests.e2e.pages.payments import PaymentsPage pytestmark=[pytest.mark.e2e,pytest.mark.django_db(transaction=True)] PASSWORD='PaymentE2e-safe-2026' PNG=base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=') @pytest.fixture def payment_case(settings,tmp_path): settings.MEDIA_ROOT=tmp_path bank,_=BankInstitution.objects.get_or_create(code='melli',defaults={'name':'ملی'}) account=CompanyBankAccount.objects.create(name='حساب فروش شرکت',holder='شرکت',bank=bank,account_number='000123') customer=User.objects.create_user(username='payment-e2e-customer',mobile='09120009021',password=PASSWORD,account_type='CUSTOMER') reviewers=[] for index in range(2): user=User.objects.create_user(username=f'payment-e2e-reviewer-{index}',password=PASSWORD,account_type='STAFF') StaffProfile.objects.create(user=user,display_name=f'تأییدکننده {index}') user.user_permissions.add(Permission.objects.get(content_type__app_label='payments',codename='approve_paymentreceipt')) reviewers.append(user) policy=PaymentApprovalPolicy.objects.create(name='تأیید هر دو نفر') policy.reviewers.add(*reviewers) AttachmentPolicy.objects.update_or_create(attachment_type='IMAGE',defaults={'max_file_size':10*1024*1024,'allowed_extensions':['.png'],'allowed_mime_types':['image/png']}) return customer,account,reviewers def test_customer_hub_no_history_and_small_mobile_services(live_server,browser_page,payment_case): customer,account,reviewers=payment_case with browser_page() as page: page.set_viewport_size({'width':320,'height':568}) AuthenticationPage(page,live_server.url).login_customer(customer,PASSWORD) PaymentsPage(page,live_server.url).open_hub() expect(page.get_by_role('heading',name='درخواست‌های اخیر شما')).to_have_count(0) for name in ['ارتباط با کارشناسان','ارسال فیش واریزی','شماره حساب‌ها']: expect(page.get_by_role('heading',name=name,exact=True)).to_be_in_viewport() assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1') page.get_by_role('link',name='ثبت درخواست',exact=True).click() expect(page).to_have_url(live_server.url+reverse('tickets:new')) def test_independent_receipt_and_two_person_approval(live_server,browser_page,payment_case): customer,account,reviewers=payment_case with browser_page() as page: AuthenticationPage(page,live_server.url).login_customer(customer,PASSWORD) PaymentsPage(page,live_server.url).send_receipt(account.pk,PNG) expect(page.get_by_text('واریز مستقل',exact=True)).to_be_visible() # Read the public identifier from real navigation; no ORM inside Playwright's loop. receipt_id=page.url.rstrip('/').rsplit('/',1)[-1] page.goto(live_server.url+reverse('accounts:staff_login')) # Explicit logout using the real page control preserves session/CSRF behavior. page.get_by_role('button',name='خروج',exact=True).first.click() AuthenticationPage(page,live_server.url).login_staff(reviewers[0],PASSWORD) PaymentsPage(page,live_server.url).approve(receipt_id) expect(page.get_by_text('تأیید ناقص',exact=True)).to_be_visible() page.get_by_role('button',name='خروج',exact=True).first.click() AuthenticationPage(page,live_server.url).login_staff(reviewers[1],PASSWORD) PaymentsPage(page,live_server.url).approve(receipt_id) expect(page.get_by_text('تأیید شده',exact=True)).to_be_visible() expect(page.get_by_role('button',name='ثبت تأیید من')).to_have_count(0) def test_form_errors_focus_and_full_copy(live_server,browser_page,payment_case): customer,account,reviewers=payment_case notice=services.issue_notice(reviewers[0],{'customer':customer,'account':account,'amount':12345,'subject':'فاکتور نمونه','description':'توضیح ثابت و کامل','deposit_id':'00099'}) with browser_page() as page: AuthenticationPage(page,live_server.url).login_customer(customer,PASSWORD) page.goto(live_server.url+reverse('payments:receipt_new')) page.get_by_role('button',name='ثبت و ارسال فیش').click() expect(page.locator('#receipt-errors')).to_be_visible() expect(page.locator('#id_paid_amount')).to_be_focused() page.goto(live_server.url+reverse('payments:notices')+'?notice='+str(notice.public_id)) expect(page.get_by_text('توضیح ثابت و کامل',exact=True)).to_be_visible() page.get_by_role('button',name='کپی کامل اطلاعات').click() # Browser may deny Clipboard on LAN HTTP; accessible manual fallback is valid. expect(page.get_by_text('کپی شد',exact=True).or_(page.get_by_role('heading',name='متن را انتخاب و کپی کنید'))).to_be_visible() def test_amount_grouping_and_jalali_leap_calendar(live_server,browser_page,payment_case): customer,account,reviewers=payment_case with browser_page() as page: assets=[] page.on('request',lambda r:assets.append(r.url) if r.resource_type in {'script','stylesheet','font','image'} else None) AuthenticationPage(page,live_server.url).login_customer(customer,PASSWORD) page.goto(live_server.url+reverse('payments:receipt_new')) amount=page.locator('#id_paid_amount') amount.fill('12345') expect(amount).to_have_value('۱۲٬۳۴۵') expect(page.locator('#amount-words')).to_contain_text('دهم تومان') amount.fill('12.5') expect(amount).to_have_value('12.5') page.get_by_role('button',name='ثبت و ارسال فیش').click() expect(amount).to_be_focused() expect(page.locator('#error-paid_amount')).to_contain_text('عدد صحیح') page.locator('#id_paid_year').select_option('1403') page.locator('#id_paid_month').select_option('12') page.locator('#id_paid_day').select_option('30') expect(page.locator('#paid-date-iso')).to_have_value('2025-03-20') page.locator('#id_paid_year').select_option('1402') expect(page.locator('#id_paid_day')).to_have_value('') expect(page.locator('#paid-date-iso')).to_have_value('') assert assets and all(url.startswith(live_server.url+'/') or url.startswith('data:') for url in assets) def test_staff_issues_identifier_notice_and_customer_sends_linked_receipt(live_server,browser_page,payment_case): customer,account,reviewers=payment_case account.requires_deposit_id=True;account.save() with browser_page() as page: AuthenticationPage(page,live_server.url).login_staff(reviewers[0],PASSWORD) page.goto(live_server.url+reverse('staff_payments:desk')) page.get_by_role('link',name='ایجاد اعلامیه واریز',exact=True).click() PaymentsPage(page,live_server.url).select_customer(customer.local_mobile) page.locator('#id_account').select_option(str(account.pk)) page.locator('#id_subject').fill('پرداخت فاکتور اختصاصی') page.locator('#id_amount').fill('100000') expect(page.locator('#id_amount')).to_have_value('۱۰۰٬۰۰۰') expect(page.locator('#id_deposit_id')).to_have_attribute('required','') page.locator('#id_deposit_id').fill('۰۰۱۲۳') page.locator('#id_description').fill('شناسه فقط برای همین واریز') page.get_by_role('button',name='صدور و اطلاع‌رسانی').click() expect(page.locator('.flash')).to_contain_text('اعلامیه صادر شد') notice_id=page.url.rstrip('/').rsplit('/',1)[-1] page.get_by_role('button',name='خروج',exact=True).first.click() AuthenticationPage(page,live_server.url).login_customer(customer,PASSWORD) page.goto(live_server.url+reverse('payments:notices')+'?notice='+notice_id) expect(page.get_by_text('شناسه فقط برای همین واریز',exact=True)).to_be_visible() page.get_by_role('link',name='ارسال فیش این واریز',exact=True).click() expect(page.get_by_label('واریز مرتبط (پیشنهادی)',exact=True)).to_be_checked() expect(page.locator('#id_destination_account')).to_have_value(str(account.pk)) expect(page.locator('#id_deposit_id')).to_have_value('00123') page.locator('#id_paid_amount').fill('100000') page.get_by_role('button',name='امروز',exact=True).click() page.locator('#id_receipt_image').set_input_files({'name':'receipt.png','mimeType':'image/png','buffer':PNG}) page.get_by_role('button',name='ثبت و ارسال فیش').click() expect(page.locator('.flash')).to_contain_text('فیش ثبت شد') expect(page.get_by_text('واریز مستقل',exact=True)).to_have_count(0) page.goto(live_server.url+reverse('payments:receipt_list')) expect(page.locator('article.pay-card').filter(has_text='پرداخت فاکتور اختصاصی')).to_contain_text('در انتظار بررسی') def test_staff_payment_filters_and_bank_selection(live_server, browser_page, payment_case): customer, account, reviewers = payment_case reviewers[0].user_permissions.add(Permission.objects.get( content_type__app_label='payments', codename='manage_company_bankaccounts')) CompanyBankAccount.objects.create(name='حساب دوم شرکت', holder='شرکت', bank=account.bank, account_number='000456', is_active=False) services.issue_notice(reviewers[0], {'customer': customer, 'account': account, 'amount': 100000, 'subject': 'پرداخت نمونه برای بررسی میز مالی', 'description': '', 'deposit_id': ''}) with browser_page() as page: AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD) page.goto(live_server.url + reverse('staff_payments:desk')) page.get_by_role('link', name='اعلامیه‌های واریز', exact=True).click() expect(page.get_by_role('link', name='پرداخت نمونه برای بررسی میز مالی')).to_be_visible() page.get_by_role('link', name='نیازمند اقدام من', exact=True).click() page.get_by_role('button', name='جست‌وجو', exact=True).click() expect(page.get_by_role('link', name='نیازمند اقدام من', exact=True)).to_have_attribute('aria-current', 'page') expect(page.get_by_text('در حال حاضر فیشی برای اقدام شما وجود ندارد.', exact=True)).to_be_visible() page.goto(live_server.url + reverse('staff_payments:banks')) page.set_viewport_size({'width': 375, 'height': 812}) submit = page.get_by_role('button', name='اعمال بر انتخاب‌شده‌ها', exact=True) expect(submit).to_be_disabled() page.get_by_role('checkbox', name='انتخاب همه حساب‌های این صفحه', exact=True).check() expect(submit).to_be_enabled() expect(page.get_by_text('۲ حساب انتخاب شده', exact=True)).to_be_visible() page.get_by_role('checkbox', name='انتخاب حساب دوم شرکت', exact=True).uncheck() expect(page.get_by_text('۱ حساب انتخاب شده', exact=True)).to_be_visible() expect(page.get_by_role('link', name='ویرایش', exact=True)).to_have_count(2) assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1') def test_staff_bank_forms_and_receipt_customer_steps(live_server, browser_page, payment_case): customer, account, reviewers = payment_case reviewers[0].user_permissions.add(Permission.objects.get( content_type__app_label='payments', codename='manage_company_bankaccounts')) with browser_page() as page: AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD) page.set_viewport_size({'width': 375, 'height': 812}) page.goto(live_server.url + reverse('staff_payments:bank_new')) page.get_by_label('نام نمایشی حساب', exact=False).fill('حساب طراحی فرم') page.locator('#id_bank').select_option(str(account.bank_id)) page.locator('#id_holder').fill('شرکت نمونه') page.locator('#id_account_number').fill('123456789') page.get_by_role('checkbox', name='شناسه واریز الزامی است', exact=False).check() assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1') page.get_by_role('button', name='ذخیره حساب', exact=True).click() page.get_by_role('link', name='حساب طراحی فرم', exact=True).click() expect(page.get_by_role('checkbox', name='شناسه واریز الزامی است', exact=False)).to_be_checked() page.get_by_role('checkbox', name='قابل انتخاب کارشناسان', exact=False).uncheck() page.get_by_role('button', name='ذخیره حساب', exact=True).click() expect(page.locator('.pay-bank-row').filter(has_text='حساب طراحی فرم').get_by_text('غیرفعال', exact=True)).to_be_visible() page.goto(live_server.url + reverse('staff_payments:receipt_customer')) PaymentsPage(page,live_server.url).select_customer(customer.local_mobile) page.get_by_role('button', name='ادامه', exact=True).click() expect(page.get_by_role('heading', name='ثبت فیش برای مشتری', exact=True)).to_be_visible() expect(page.locator('#id_paid_amount')).to_be_visible() page.get_by_role('button', name='ثبت و ارسال فیش', exact=True).click() expect(page.locator('#receipt-errors')).to_be_visible() expect(page.locator('#id_paid_amount')).to_be_focused() assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1') def test_customer_search_disambiguation_and_context_shortcuts(live_server, browser_page, payment_case): from tests.e2e.pages.ticket_notices import TicketNoticesPage from apps.customers.models import CustomerProfile from apps.tickets.models import Ticket, TicketCategory from django.utils import timezone customer, account, reviewers = payment_case CustomerProfile.objects.create(user=customer, first_name='محمد', last_name='رضایی', organization_name='شرکت اول') other = User.objects.create_user(username='other-payment-customer', mobile='09120009022', account_type='CUSTOMER') CustomerProfile.objects.create(user=other, first_name='محمد', last_name='رضایی', organization_name='شرکت دوم') ticket = Ticket.objects.create(customer=customer, category=TicketCategory.objects.create(title='پرونده مالی', slug='finance-picker-e2e'), subject='سابقه خرید برای انتخاب دقیق', owner=reviewers[0], selected_expert=reviewers[0], origin_visibility='SELECTED', last_activity_at=timezone.now()) with browser_page() as page: AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD) page.goto(live_server.url + reverse('staff_payments:notice_new')) page.get_by_label('جست‌وجو و انتخاب مشتری', exact=False).fill('محمد رضایی') expect(page.locator('.pay-customer-result')).to_have_count(2) result = page.locator('.pay-customer-result').filter(has_text=customer.local_mobile) result.locator('summary').click() expect(result.get_by_role('link', name=ticket.ticket_number + ' · ' + ticket.subject)).to_be_visible() result.get_by_role('button', name='انتخاب این مشتری', exact=True).click() expect(page.locator('[data-selected-customer]')).to_contain_text('شرکت اول') expect(page.locator('[data-selected-customer]')).to_contain_text(customer.local_mobile) page.locator('#id_account').select_option(str(account.pk)) page.locator('#id_subject').fill('انتخاب مشتری باید دوباره انجام شود') page.locator('#id_amount').fill('100000') page.get_by_label('جست‌وجو و انتخاب مشتری', exact=False).fill('0912') page.get_by_role('button', name='صدور و اطلاع‌رسانی', exact=True).click() expect(page.get_by_label('جست‌وجو و انتخاب مشتری', exact=False)).to_be_focused() page.goto(live_server.url + reverse('staff_tickets:detail', args=[ticket.public_id])) TicketNoticesPage(page, live_server.url).toggle_notices() page.locator('#customer-info').get_by_role('link', name='ایجاد اعلامیه واریز', exact=True).click() expect(page).to_have_url(live_server.url + reverse('staff_payments:notice_new') + '?customer=' + str(customer.public_id)) expect(page.locator('[data-selected-customer]')).to_contain_text(customer.local_mobile) page.goto(live_server.url + reverse('staff_tickets:contacts')) page.locator('.contact-card').filter(has_text=customer.local_mobile).get_by_role('link', name='ایجاد اعلامیه واریز', exact=True).click() expect(page.locator('[data-selected-customer]')).to_contain_text(customer.local_mobile) def test_customer_finance_preview_and_notice_context(live_server, browser_page, payment_case): customer, account, reviewers = payment_case notice = services.issue_notice(reviewers[0], {'customer': customer, 'account': account, 'amount': 120000, 'subject': 'واریز مرتبط نمونه', 'description': 'توضیحات همیشه نمایان', 'invoice_number': 'INV-120', 'deposit_id': '00055'}) with browser_page() as page: page.set_viewport_size({'width': 320, 'height': 568}) AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD) page.goto(live_server.url + reverse('payments:notices') + '?notice=' + str(notice.public_id)) expect(page.get_by_text('توضیحات همیشه نمایان', exact=True)).to_be_visible() assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1') page.get_by_role('link', name='ارسال فیش این واریز').click() expect(page.locator('[data-selected-notice-title]')).to_have_text(notice.subject) expect(page.locator('[data-selected-notice-summary]')).to_contain_text('INV-120') expect(page.locator('[data-selected-notice-link]')).to_have_attribute('href', reverse('payments:notice_detail', args=[notice.public_id])) page.locator('#id_receipt_image').set_input_files({'name': 'receipt.png', 'mimeType': 'image/png', 'buffer': PNG}) expect(page.get_by_alt_text('پیش‌نمایش تصویر فیش')).to_be_visible() expect(page.locator('#receipt-file-status')).to_contain_text('receipt.png') page.get_by_role('button', name='حذف انتخاب').click() expect(page.get_by_alt_text('پیش‌نمایش تصویر فیش')).to_be_hidden() page.locator('#id_paid_amount').fill('120000') page.get_by_role('button', name='امروز', exact=True).click() page.get_by_role('button', name='ثبت و ارسال فیش').click() expect(page.locator('#error-receipt_image')).to_be_visible() assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1') def test_customer_linked_receipt_status_and_reason_without_access_roster(live_server, browser_page, payment_case): customer, account, reviewers = payment_case notice = services.issue_notice(reviewers[0], {'customer': customer, 'account': account, 'amount': 12345, 'subject': 'بررسی فیش مرتبط', 'description': 'جزئیات واریز'}) with browser_page() as page: AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD) page.goto(live_server.url + reverse('payments:receipt_new') + '?notice=' + str(notice.public_id)) page.locator('#id_paid_amount').fill('12345') page.get_by_role('button', name='امروز', exact=True).click() page.locator('#id_receipt_image').set_input_files({'name': 'receipt.png', 'mimeType': 'image/png', 'buffer': PNG}) page.get_by_role('button', name='ثبت و ارسال فیش').click() expect(page.locator('.flash')).to_contain_text('فیش ثبت شد') receipt_id = page.url.rstrip('/').rsplit('/', 1)[-1] page.get_by_role('button', name='خروج', exact=True).first.click() AuthenticationPage(page, live_server.url).login_staff(reviewers[1], PASSWORD) page.goto(live_server.url + reverse('staff_payments:receipt_detail', args=[receipt_id])) expect(page.get_by_role('button', name='ثبت تأیید من', exact=True)).to_be_visible() expect(page.get_by_role('heading', name='افراد تعیین‌شده', exact=True)).to_have_count(0) page.locator('#decision-reason').fill('تصویر فیش خوانا نیست') page.get_by_role('button', name='رد فیش', exact=True).click() expect(page.get_by_text('تصویر فیش خوانا نیست', exact=True)).to_be_visible() page.get_by_role('button', name='خروج', exact=True).first.click() AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD) page.goto(live_server.url + reverse('payments:notices') + '?notice=' + str(notice.public_id)) expect(page.get_by_text('تصویر فیش خوانا نیست', exact=True)).to_be_visible() page.get_by_role('link', name='جزئیات فیش و سوابق بررسی', exact=True).click() expect(page.get_by_text('تصویر فیش خوانا نیست', exact=True)).to_be_visible() expect(page.get_by_role('heading', name='افراد تعیین‌شده', exact=True)).to_have_count(0) expect(page.get_by_role('heading', name='سوابق ارسال و اصلاح', exact=True)).to_be_visible() def test_notice_permalink_and_staff_receipt_preview(live_server, browser_page, payment_case): customer, account, reviewers = payment_case notice = services.issue_notice(reviewers[0], {'customer': customer, 'account': account, 'amount': 12000, 'subject': 'واریز با لینک مستقل', 'description': 'اطلاعات واریز'}) with browser_page() as page: page.set_viewport_size({'width': 375, 'height': 812}) AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD) page.goto(live_server.url + reverse('payments:notices')) page.get_by_role('link', name='جزئیات واریز و فیش‌ها', exact=True).click() expect(page).to_have_url(live_server.url + reverse('payments:notice_detail', args=[notice.public_id])) expect(page.get_by_text('اطلاعات واریز', exact=True)).to_be_visible() page.get_by_role('link', name='ارسال فیش این واریز', exact=True).click() page.locator('[data-selected-notice-link]').click() expect(page).to_have_url(live_server.url + reverse('payments:notice_detail', args=[notice.public_id])) page.get_by_role('button', name='خروج', exact=True).first.click() AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD) page.goto(live_server.url + reverse('staff_payments:receipt_new', args=[customer.public_id]) + '?notice=' + str(notice.public_id)) expect(page.get_by_role('heading', name='ثبت فیش برای مشتری', exact=True)).to_be_visible() expect(page.locator('[data-selected-notice-link]')).to_have_attribute('href', reverse('staff_payments:notice_detail', args=[notice.public_id])) page.locator('#id_paid_amount').fill('12000') page.get_by_role('button', name='امروز', exact=True).click() page.locator('#id_receipt_image').set_input_files({'name': 'receipt.png', 'mimeType': 'image/png', 'buffer': PNG}) expect(page.get_by_alt_text('پیش‌نمایش تصویر فیش')).to_be_visible() assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1') page.get_by_role('button', name='ثبت و ارسال فیش', exact=True).click() expect(page.locator('.flash')).to_contain_text('فیش ثبت شد') def test_notice_identifier_generation_preserves_manual_value_and_customer_context(live_server, browser_page, payment_case): customer, account, reviewers = payment_case account.requires_deposit_id = True account.save() with browser_page() as page: page.set_viewport_size({'width': 375, 'height': 812}) AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD) page.goto(live_server.url + reverse('staff_payments:notice_new')) generate = page.get_by_role('button', name='تولید خودکار شناسه', exact=True) expect(generate).to_be_disabled() PaymentsPage(page, live_server.url).select_customer(customer.local_mobile) page.locator('#id_account').select_option(str(account.pk)) expect(page.locator('#id_deposit_id')).to_have_attribute('required', '') generate.click() expect(page.locator('#id_deposit_id')).to_have_value('0912000902169') page.locator('#id_deposit_id').fill('manual-123') generate.click() expect(page.locator('#identifier-candidate')).to_have_text('0912000902169') expect(page.locator('#id_deposit_id')).to_have_value('manual-123') page.get_by_role('button', name='جایگزینی شناسه', exact=True).click() expect(page.locator('#id_deposit_id')).to_have_value('0912000902169') page.get_by_role('button', name='تغییر مشتری', exact=True).click() expect(page.locator('#id_deposit_id')).to_have_value('') expect(generate).to_be_disabled() assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1')