fix payment

This commit is contained in:
javad committed 2026-10-07 13:39:29 +03:30
1 parent 4f7486152c
commit bb3ddd1610
47 files changed
+2137 -51

No files matched your search

+25
View File
@@ -4,6 +4,31 @@ from .payments import PaymentsPage
class PaymentLinkPage(PaymentsPage):
def prepare_copy(self, manual=False):
self.manual_copy = manual
if manual:
self.page.add_init_script("Object.defineProperty(navigator, 'clipboard', {value: {writeText: async () => {throw new Error('Clipboard unavailable');}}});")
else:
self.page.context.grant_permissions(['clipboard-read', 'clipboard-write'])
def copied_text(self, button_name):
self.page.get_by_role('button', name=button_name, exact=True).click()
if button_name == 'کپی لینک کوتاه این صفحه برای ارسال به دیگران':
expect(self.page.locator('.pay-share-feedback')).to_contain_text('لینک آماده است')
if self.manual_copy:
dialog = self.page.get_by_role('dialog')
expect(dialog).to_be_visible()
value = dialog.locator('textarea').input_value()
dialog.get_by_role('button', name='بستن', exact=True).click()
return value
expect(self.page.get_by_text('کپی شد', exact=True)).to_be_visible()
return self.page.evaluate('navigator.clipboard.readText()')
def copy_short_link(self):
value = self.copied_text('کپی لینک کوتاه این صفحه برای ارسال به دیگران')
expect(self.page.locator('.pay-share-feedback')).to_contain_text('لینک آماده است')
return value
def open_link(self, path):
self.goto(path)
expect(self.page.get_by_role('heading', name='جزئیات واریز', exact=True)).to_be_visible()
+79
View File
@@ -0,0 +1,79 @@
from playwright.sync_api import expect
from .base import BasePage
class ReceiptUploadPage(BasePage):
def control_transport(self):
"""Pause the browser transport while keeping the final POST real."""
self.page.add_init_script("""(() => {
const Native = window.XMLHttpRequest;
window.receiptTransport = {count: 0};
window.XMLHttpRequest = class extends Native {
open(method, url, ...rest) {
this.receiptRequest = method === 'POST' && /payment-receipt|receipts/.test(url);
super.open(method, url, ...rest);
}
send(payload) {
if (!this.receiptRequest) return super.send(payload);
window.receiptTransport.count++;
window.receiptTransport.current = this;
this.release = () => super.send(payload);
this.upload.dispatchEvent(new ProgressEvent('progress', {
lengthComputable: true, loaded: 524288, total: 1048576
}));
}
};
})();""")
def fill_receipt(self, account_id, png):
self.page.locator('#id_paid_amount').fill('۱۲۳۴۵')
self.page.locator('#id_destination_account').select_option(str(account_id))
self.page.get_by_role('button', name='امروز', exact=True).click()
self.page.locator('#id_receipt_image').set_input_files({'name': 'receipt.png', 'mimeType': 'image/png', 'buffer': png})
def submit(self):
self.page.get_by_role('button', name='ثبت و ارسال فیش', exact=True).click()
def assert_uploading(self):
expect(self.page.locator('#receipt-upload')).to_be_visible()
expect(self.page.locator('#receipt-upload-percent')).to_have_text('۵۰٪')
expect(self.page.locator('#receipt-upload-bytes')).to_have_text('ارسال‌شده: ۵۱۲ کیلوبایت از ۱ مگابایت')
expect(self.page.locator('#receipt-upload-time')).to_contain_text('دیگر برای ارسال')
expect(self.page.locator('#submit-receipt')).to_be_disabled()
expect(self.page.locator('#id_receipt_image')).to_be_disabled()
def repeat_submission(self):
self.page.locator('#receipt-form').evaluate("form => {form.requestSubmit(); form.requestSubmit();}")
assert self.page.evaluate('window.receiptTransport.count') == 1
def processing(self):
self.page.evaluate("window.receiptTransport.current.upload.dispatchEvent(new Event('load'))")
expect(self.page.locator('#receipt-upload-percent')).to_have_text('۱۰۰٪')
expect(self.page.locator('#receipt-upload-title')).to_contain_text('در حال بررسی و ثبت فیش')
expect(self.page.locator('#submit-receipt')).to_be_disabled()
def fail_network(self):
self.page.evaluate("window.receiptTransport.current.dispatchEvent(new Event('error'))")
expect(self.page.locator('#receipt-errors')).to_contain_text('ارتباط با سرور قطع شد')
expect(self.page.locator('#submit-receipt')).to_be_enabled()
expect(self.page.locator('#id_receipt_image')).to_be_enabled()
assert self.page.locator('#id_receipt_image').evaluate('input => input.files.length') == 1
def reject_fields(self):
self.page.evaluate("""() => {
const request = window.receiptTransport.current;
Object.defineProperty(request, 'status', {value: 422});
Object.defineProperty(request, 'response', {value: {errors: {
paid_amount: [{message: 'مبلغ با پرداخت مطابقت ندارد.'}],
__all__: [{message: 'اطلاعات بانکی را دوباره بررسی کنید.'}]
}}});
request.dispatchEvent(new Event('load'));
}""")
expect(self.page.locator('#receipt-errors')).to_contain_text('اطلاعات بانکی را دوباره بررسی کنید.')
expect(self.page.locator('#error-paid_amount')).to_have_text('مبلغ با پرداخت مطابقت ندارد.')
expect(self.page.locator('#submit-receipt')).to_be_enabled()
def finish(self, revision=False):
self.page.evaluate('window.receiptTransport.current.release()')
expect(self.page.locator('.flash')).to_contain_text('نسخه اصلاحی ثبت شد' if revision else 'فیش ثبت شد و در انتظار بررسی است.')
+44
View File
@@ -0,0 +1,44 @@
from django.urls import reverse
from playwright.sync_api import expect
from .base import BasePage
class StaffPaymentsPage(BasePage):
def open_desk(self, parameters=''):
self.goto(reverse('staff_payments:desk') + parameters)
def assert_action_tab(self):
expect(self.page.get_by_role('link', name='نیازمند اقدام من', exact=True)).to_have_attribute('aria-current', 'page')
expect(self.page.get_by_role('heading', name='فیش‌های نیازمند اقدام شما')).to_be_visible()
def search(self, query):
self.page.locator('[name=q]').fill(query)
self.page.get_by_role('button', name='جست‌وجو', exact=True).click()
def next_page(self):
pagination = self.page.get_by_role('navigation', name='صفحه‌بندی', exact=True)
expect(pagination).to_contain_text('صفحه 1 از 2')
pagination.get_by_role('link', name='بعدی', exact=True).click()
expect(pagination).to_contain_text('صفحه 2 از 2')
def open_notice(self, public_id):
self.goto(reverse('staff_payments:notice_detail', args=[public_id]))
def inspect_history(self):
panel = self.page.locator('.pay-sms-panel')
expect(panel).to_contain_text('رسیده به گوشی')
expect(panel).to_contain_text('پذیرفته‌شده توسط سرویس پیامک')
expect(panel).to_contain_text('اعتبار پنل کافی نیست')
for details in panel.locator('details').all():
details.locator('summary').click()
expect(panel).to_contain_text('old-delivered-id')
expect(panel).to_contain_text('کد پاسخ سرویس: -19')
def resend_sms(self):
button = self.page.get_by_role('button', name='ارسال مجدد پیامک اعلامیه واریز', exact=True)
expect(button).to_be_enabled()
button.click()
expect(self.page.locator('.flash')).to_contain_text('درخواست ارسال مجدد پیامک در صف ثبت شد')
expect(button).to_be_disabled()
expect(self.page.locator('.pay-sms-history .pay-sms-record')).to_have_count(3)
+37 -1
View File
@@ -7,11 +7,47 @@ from apps.core.models import SiteBranding
from apps.payments.access import issue_payment_link
from apps.payments import services
from tests.e2e.pages.payment_link import PaymentLinkPage
from tests.e2e.pages.auth import AuthenticationPage
from tests.e2e.test_payments_e2e import payment_case, PNG
from tests.e2e.test_payments_e2e import PASSWORD
from django.urls import reverse
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
@pytest.mark.parametrize('width,via_link,manual', [(320, True, True), (1440, True, False),
(320, False, False), (1440, False, True)])
def test_notice_sharing_and_full_copy(live_server, browser_page, payment_case, width, via_link, manual):
customer, account, reviewers = payment_case
notice = services.issue_notice(reviewers[0], {'customer': customer, 'account': account,
'subject': 'اشتراک اطلاعات واریز', 'amount': 12345, 'deposit_id': '۰۰۰۱۲۳۰۴'})
_, original_url = issue_payment_link(notice=notice)
with browser_page() as page:
page.set_viewport_size({'width': width, 'height': 900})
view = PaymentLinkPage(page, live_server.url)
view.prepare_copy(manual)
if via_link:
view.open_link(urlsplit(original_url).path)
else:
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
view.goto(reverse('payments:notice_detail', args=[notice.public_id]))
expect(page.locator('.pay-deposit-required dd')).to_have_text('12304')
full_copy = view.copied_text('کپی کامل اطلاعات')
assert 'شناسه واریز: 12304' in full_copy
assert 'وارد کردن شناسه واریز هنگام پرداخت الزامی و اجباری است' in full_copy
assert 'شماره حساب: 000123' in full_copy
shared_url = view.copy_short_link()
assert urlsplit(shared_url).path.startswith('/p/')
if via_link:
assert shared_url == original_url
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1')
with browser_page() as recipient_page:
recipient = PaymentLinkPage(recipient_page, live_server.url)
recipient.open_link(urlsplit(shared_url).path)
expect(recipient_page.locator('.pay-deposit-required dd')).to_have_text('12304')
expect(recipient_page.get_by_role('link', name='پروفایل من')).to_have_count(0)
@pytest.mark.parametrize('width,linked', [(1440, True), (320, True), (390, False)])
def test_sms_payment_without_login_preserves_copy_and_submission(live_server, browser_page, payment_case, width, linked):
customer, account, reviewers = payment_case
@@ -25,7 +61,7 @@ def test_sms_payment_without_login_preserves_copy_and_submission(live_server, br
page.set_viewport_size({'width': width, 'height': 900})
view = PaymentLinkPage(page, live_server.url)
view.open_link(urlsplit(url).path)
expect(page.locator('.pay-deposit-required dd')).to_have_text('000123')
expect(page.locator('.pay-deposit-required dd')).to_have_text('123')
expect(page.get_by_text('واریز بدون این شناسه معتبر نخواهد بود.', exact=False)).to_be_visible()
expect(page.locator('.cs-footer')).to_have_text(branding.developer_credit_text)
expect(page.get_by_role('link', name='پروفایل من')).to_have_count(0)
+4 -4
View File
@@ -150,7 +150,7 @@ def test_staff_issues_identifier_notice_and_customer_sends_linked_receipt(live_s
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')
expect(page.locator('#id_deposit_id')).to_have_value('123')
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})
@@ -362,13 +362,13 @@ def test_notice_identifier_generation_preserves_manual_value_and_customer_contex
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')
expect(page.locator('#id_deposit_id')).to_have_value('912000902169')
page.locator('#id_deposit_id').fill('manual-123')
generate.click()
expect(page.locator('#identifier-candidate')).to_have_text('0912000902169')
expect(page.locator('#identifier-candidate')).to_have_text('912000902169')
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')
expect(page.locator('#id_deposit_id')).to_have_value('912000902169')
page.get_by_role('button', name='تغییر مشتری', exact=True).click()
expect(page.locator('#id_deposit_id')).to_have_value('')
expect(generate).to_be_disabled()
+101
View File
@@ -0,0 +1,101 @@
from urllib.parse import urlsplit
import uuid
import jdatetime
import pytest
from django.contrib.auth.models import Permission
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from django.utils import timezone
from playwright.sync_api import expect
from apps.payments import services
from apps.payments.access import issue_payment_link
from apps.payments.models import PaymentReceipt
from tests.e2e.pages.auth import AuthenticationPage
from tests.e2e.pages.receipt_upload import ReceiptUploadPage
from tests.e2e.test_payments_e2e import payment_case, PNG, PASSWORD
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
@pytest.mark.parametrize('mode,width', [('independent', 320), ('linked', 1440), ('staff', 320), ('link', 320)])
def test_receipt_upload_progress_processing_and_single_submission(live_server, browser_page, payment_case, mode, width):
customer, account, reviewers = payment_case
creator = reviewers[0]
creator.user_permissions.add(Permission.objects.get(content_type__app_label='payments', codename='add_paymentnotice'))
notice = services.issue_notice(creator, {'customer': customer, 'account': account, 'subject': 'تست پیشرفت ارسال', 'amount': 12345})
_, link_url = issue_payment_link(notice=notice)
with browser_page() as page:
page.set_viewport_size({'width': width, 'height': 900})
view = ReceiptUploadPage(page, live_server.url)
view.control_transport()
if mode == 'staff':
AuthenticationPage(page, live_server.url).login_staff(creator, PASSWORD)
view.goto(reverse('staff_payments:receipt_new', args=[customer.public_id]))
elif mode == 'link':
view.goto(urlsplit(link_url).path)
page.get_by_role('link', name='ارسال فیش این واریز', exact=True).click()
else:
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
view.goto(reverse('payments:receipt_new') + ('?notice=' + str(notice.public_id) if mode == 'linked' else ''))
if mode in ('independent', 'staff'):
page.get_by_label('واریز مستقل', exact=True).check()
view.fill_receipt(account.pk, PNG)
view.submit()
view.assert_uploading()
view.repeat_submission()
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1')
view.processing()
view.finish()
assert PaymentReceipt.objects.filter(customer=customer).count() == 1
@pytest.mark.parametrize('failure', ['network', 'validation'])
def test_failed_upload_retains_image_and_can_retry(live_server, browser_page, payment_case, failure):
customer, account, _ = payment_case
with browser_page() as page:
view = ReceiptUploadPage(page, live_server.url)
view.control_transport()
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
view.goto(reverse('payments:receipt_new'))
page.get_by_label('واریز مستقل', exact=True).check()
view.fill_receipt(account.pk, PNG)
key = page.locator('[name=submission_key]').input_value()
view.submit()
view.assert_uploading()
view.fail_network() if failure == 'network' else view.reject_fields()
expect(page.locator('#id_paid_amount')).to_have_value('۱۲٬۳۴۵')
assert page.locator('[name=submission_key]').input_value() == key
view.submit()
expect(page.locator('#receipt-upload-title')).to_have_text('در حال ارسال فیش…')
view.finish()
assert PaymentReceipt.objects.filter(customer=customer).count() == 1
def test_revised_receipt_uses_upload_progress(live_server, browser_page, payment_case, client):
customer, account, reviewers = payment_case
today = jdatetime.date.fromgregorian(date=timezone.localdate())
client.force_login(customer)
response = client.post(reverse('payments:receipt_new'), {
'receipt_mode': 'independent', 'paid_amount': '12345', 'destination_account': account.pk,
'paid_day': str(today.day), 'paid_month': str(today.month), 'paid_year': str(today.year),
'submission_key': str(uuid.uuid4()),
'receipt_image': SimpleUploadedFile('receipt.png', PNG, content_type='image/png'),
})
assert response.status_code == 302
receipt = PaymentReceipt.objects.get(customer=customer)
services.decide_receipt(reviewers[0], receipt.public_id, 1, 'REJECT', 'تصویر واضح‌تر لازم است')
with browser_page() as page:
view = ReceiptUploadPage(page, live_server.url)
view.control_transport()
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
view.goto(reverse('payments:receipt_revise', args=[receipt.public_id]))
view.fill_receipt(account.pk, PNG)
view.submit()
view.assert_uploading()
view.repeat_submission()
view.processing()
view.finish(revision=True)
receipt.refresh_from_db()
assert receipt.current_version == 2 and receipt.revisions.count() == 2
+76
View File
@@ -0,0 +1,76 @@
import uuid
from urllib.parse import parse_qs, urlsplit
import jdatetime
import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from django.utils import timezone
from apps.notifications.models import NotificationJob
from apps.payments import services
from apps.payments.models import PaymentNotification, PaymentNotificationAttempt
from tests.e2e.pages.auth import AuthenticationPage
from tests.e2e.pages.staff_payments import StaffPaymentsPage
from tests.e2e.test_payments_e2e import payment_case, PASSWORD, PNG
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
@pytest.mark.parametrize('notices', [False, True])
def test_staff_default_action_tab_and_pagination(live_server, browser_page, payment_case, client, notices):
customer, account, reviewers = payment_case
if notices:
for index in range(26):
services.issue_notice(reviewers[0], {'customer': customer, 'account': account, 'subject': f'اعلامیه صفحه بندی {index}', 'amount': 12345})
else:
client.force_login(customer)
today = jdatetime.date.fromgregorian(date=timezone.localdate())
for _ in range(26):
response = client.post(reverse('payments:receipt_new'), {'receipt_mode': 'independent',
'paid_amount': '12345', 'destination_account': account.pk, 'submission_key': str(uuid.uuid4()),
'paid_day': str(today.day), 'paid_month': str(today.month), 'paid_year': str(today.year),
'receipt_image': SimpleUploadedFile('receipt.png', PNG, content_type='image/png')})
assert response.status_code == 302
with browser_page() as page:
page.set_viewport_size({'width': 320, 'height': 900})
AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD)
view = StaffPaymentsPage(page, live_server.url)
view.open_desk()
view.assert_action_tab()
if notices:
page.get_by_role('link', name='اعلامیه‌های واریز', exact=True).click()
view.search(customer.username)
view.next_page()
query = parse_qs(urlsplit(page.url).query)
assert query['q'] == [customer.username]
assert query['tab'] == ['notices' if notices else 'receipts']
if not notices:
assert query['mine'] == ['1']
view.assert_action_tab()
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1')
@pytest.mark.parametrize('width', [320, 1440])
def test_staff_sms_history_and_resend(live_server, browser_page, payment_case, width):
customer, account, reviewers = payment_case
notice = services.issue_notice(reviewers[0], {'customer': customer, 'account': account, 'subject': 'اعلامیه با سوابق پیامک', 'amount': 12345})
first = PaymentNotification.objects.get(notice=notice)
NotificationJob.objects.filter(pk=first.job_id).update(status='FAILED', attempts=1, last_error='اعتبار پنل کافی نیست')
PaymentNotificationAttempt.objects.create(notification=first, finished_at=timezone.now(), success=False,
provider_code='-19', error='اعتبار پنل کافی نیست')
delivered = services.resend_notice_sms(reviewers[0], notice, str(uuid.uuid4()))
NotificationJob.objects.filter(pk=delivered.pk).update(status='SENT', provider_message_id='old-delivered-id',
sent_at=timezone.now(), delivery_status='DELIVERED', delivery_is_final=True, delivery_checked_at=timezone.now())
PaymentNotificationAttempt.objects.create(notification=delivered.payment_notification, finished_at=timezone.now(),
success=True, provider_code='2', provider_id='old-delivered-id')
with browser_page() as page:
page.set_viewport_size({'width': width, 'height': 900})
AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD)
view = StaffPaymentsPage(page, live_server.url)
view.open_notice(notice.public_id)
view.inspect_history()
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1')
view.resend_sms()
assert PaymentNotification.objects.filter(notice=notice).count() == 3
assert PaymentNotification.objects.filter(notice=notice, job__status='PENDING').count() == 1