fix fish variz name, dalilrad, sms, bale

This commit is contained in:
javad committed 2026-10-11 15:19:58 +03:30
1 parent 90931a5a64
commit 35a1ec1794
30 files changed
+674 -39

No files matched your search

@@ -0,0 +1,42 @@
from playwright.sync_api import expect
from .receipt_upload import ReceiptUploadPage
class ReceiptCustomerDetailsPage(ReceiptUploadPage):
def expect_payer(self, value):
expect(self.page.get_by_label('نام واریزکننده')).to_have_value(value)
def enter_payer(self, name):
self.page.get_by_label('نام واریزکننده').fill(name)
def expect_missing_payer(self):
expect(self.page.locator('#error-depositor_name')).to_contain_text('نام واریزکننده را وارد کنید')
def select_destination(self, account_id, bank=None, identifier=None):
self.page.locator('#id_destination_account').select_option(str(account_id))
warning = self.page.locator('#destination-deposit-warning')
if bank:
expect(warning).to_be_visible()
expect(warning).to_contain_text(bank)
expect(warning).to_contain_text(identifier)
expect(warning).to_contain_text('برگشت بخورد')
assert warning.evaluate('el => getComputedStyle(el).color') == 'rgb(180, 35, 24)'
else:
expect(warning).to_be_hidden()
def reject(self, reason, *, confirm):
expect(self.page.locator('#decision-help')).to_contain_text('با پیامک برای مشتری ارسال می‌شود')
self.page.locator('#decision-reason').fill(reason)
previews = []
def answer(dialog):
previews.append(dialog.message)
dialog.accept() if confirm else dialog.dismiss()
self.page.once('dialog', answer)
self.page.get_by_role('button', name='رد فیش', exact=True).click()
assert len(previews) == 1 and reason in previews[0] and 'برای مشتری ارسال' in previews[0]
if confirm:
expect(self.page.locator('.pay-status').first).to_have_text('رد شده')
else:
expect(self.page.get_by_role('button', name='رد فیش', exact=True)).to_be_visible()
expect(self.page.locator('#decision-reason')).to_have_value(reason)
+2 -1
View File
@@ -21,7 +21,7 @@ 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')
customer=User.objects.create_user(username='payment-e2e-customer',mobile='09120009021',first_name='مشتری',last_name='آزمایشی',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')
@@ -308,6 +308,7 @@ def test_customer_linked_receipt_status_and_reason_without_access_roster(live_se
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.once('dialog', lambda dialog: dialog.accept())
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()
@@ -0,0 +1,85 @@
from urllib.parse import urlsplit
import pytest
from django.urls import reverse
from playwright.sync_api import expect
from apps.payments import services
from apps.payments.access import issue_payment_link
from apps.payments.deposit_identifier import generate_id_algorithm
from apps.payments.models import CompanyBankAccount
from tests.e2e.pages.auth import AuthenticationPage
from tests.e2e.pages.receipt_customer_details import ReceiptCustomerDetailsPage
from tests.e2e.test_payments_e2e import payment_case, PASSWORD, PNG
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
@pytest.mark.parametrize('mode', ['independent', 'linked', 'staff', 'link'])
def test_required_payer_on_every_receipt_entry_point(live_server, browser_page, payment_case, mode):
customer, account, reviewers = payment_case
customer.first_name = customer.last_name = ''
customer.save()
notice = services.issue_notice(reviewers[0], {'customer': customer, 'account': account, 'subject': 'ثبت فیش با نام', 'amount': 12345})
link_path = urlsplit(issue_payment_link(notice=notice)[1]).path if mode == 'link' else ''
with browser_page() as page:
page.set_viewport_size({'width': 320, 'height': 900})
view = ReceiptCustomerDetailsPage(page, live_server.url)
if mode == 'link':
view.goto(link_path)
page.get_by_role('link', name='ارسال فیش این واریز', exact=True).click()
elif mode == 'staff':
AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD)
view.goto(reverse('staff_payments:receipt_new', args=[customer.public_id]))
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.expect_payer('')
view.fill_receipt(account.pk, PNG)
view.submit()
view.expect_missing_payer()
view.enter_payer('رضا احمدی')
view.submit()
expect(page.locator('.flash')).to_contain_text('فیش ثبت شد')
expect(page.get_by_text('رضا احمدی', exact=True)).to_be_visible()
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1')
def test_identifier_warning_follows_destination_and_mode(live_server, browser_page, payment_case):
customer, account, reviewers = payment_case
account.requires_deposit_id = True
account.save()
other = CompanyBankAccount.objects.create(name='حساب بدون شناسه', holder='شرکت', bank=account.bank, account_number='1234')
identifier = generate_id_algorithm(customer.local_mobile)
notice = services.issue_notice(reviewers[0], {'customer': customer, 'account': account, 'subject': 'شناسه اختصاصی', 'amount': 12345, 'deposit_id': identifier})
with browser_page() as page:
view = ReceiptCustomerDetailsPage(page, live_server.url)
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
view.goto(reverse('payments:receipt_new') + '?notice=' + str(notice.public_id))
view.expect_payer('مشتری آزمایشی')
view.select_destination(account.pk, account.bank.name, identifier)
page.get_by_label('واریز مستقل', exact=True).check()
view.select_destination(other.pk)
view.select_destination(account.pk, account.bank.name, identifier)
def test_staff_can_cancel_then_confirm_rejection_sms(live_server, browser_page, payment_case):
customer, account, reviewers = payment_case
with browser_page() as page:
view = ReceiptCustomerDetailsPage(page, live_server.url)
auth = AuthenticationPage(page, live_server.url)
auth.login_customer(customer, PASSWORD)
view.goto(reverse('payments:receipt_new'))
page.get_by_label('واریز مستقل', exact=True).check()
view.fill_receipt(account.pk, PNG)
view.submit()
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()
auth.login_staff(reviewers[0], PASSWORD)
view.goto(reverse('staff_payments:receipt_detail', args=[receipt_id]))
view.reject('تصویر فیش ناخوانا است.', confirm=False)
view.reject('تصویر فیش ناخوانا است.', confirm=True)
expect(page.get_by_text('تصویر فیش ناخوانا است.', exact=True)).to_be_visible()
+1 -1
View File
@@ -78,7 +78,7 @@ def test_revised_receipt_uses_upload_progress(live_server, browser_page, payment
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,
'depositor_name': 'مشتری آزمایشی', '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'),
+1 -1
View File
@@ -27,7 +27,7 @@ def test_staff_default_action_tab_and_pagination(live_server, browser_page, paym
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',
response = client.post(reverse('payments:receipt_new'), {'depositor_name': 'مشتری آزمایشی', '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')})
+2 -2
View File
@@ -77,7 +77,7 @@ def test_all_customer_entry_points_enqueue_once_without_network(case, enabled, c
alerts = list(PaymentReceiptAlert.objects.filter(revision__receipt=receipt))
assert len(alerts) == 3
assert {a.recipient_mobile for a in alerts} == {'', '+989121110001', '+989121110002'}
assert alerts[0].context['customer_name'] == 'مشتری آزمایشی'
assert alerts[0].context['customer_name'] == 'مشتری آزمایشی (واریزکننده: واریزکننده آزمایشی)'
assert alerts[0].context['customer_mobile'] == '09120009999'
assert alerts[0].context['received_at'] == jalali_datetime(receipt.revisions.get().created_at)
assert alerts[0].context['amount_rial'] == '12,345'
@@ -150,7 +150,7 @@ def test_worker_sends_private_photo_with_database_token_and_sms_to_managers(case
assert run(alert.job).status == 'SENT'
assert len(posted) == 1 and posted[0][0].endswith('/botprivate-payment-token/sendPhoto')
assert posted[0][1]['chat_id'] == 'receipt-group' and posted[0][2] == PNG
assert 'نام ثبت نشده' in posted[0][1]['caption'] and '09120009999' in posted[0][1]['caption']
assert 'واریزکننده آزمایشی' in posted[0][1]['caption'] and '09120009999' in posted[0][1]['caption']
assert jalali_datetime(receipt.revisions.get().created_at) in posted[0][1]['caption']
assert len(sent) == 2 and {mobile for mobile, _ in sent} == {'+989121110001', '+989121110002'}
assert all('12,345' in body and '09120009999' in body for _, body in sent)
+2 -2
View File
@@ -101,7 +101,7 @@ def case(settings, tmp_path):
def data(case, linked=True, **extra):
today = jdatetime.date.fromgregorian(date=timezone.localdate())
return {'receipt_mode':'linked' if linked else 'independent','notice_id':case['notice'].pk if linked else '',
return {'depositor_name':'واریزکننده آزمایشی','receipt_mode':'linked' if linked else 'independent','notice_id':case['notice'].pk if linked else '',
'paid_amount':'۱۲۳۴۵','destination_account':case['account'].pk,'deposit_id':'000123',
'paid_day':str(today.day),'paid_month':str(today.month),'paid_year':str(today.year),
'submission_key':str(uuid.uuid4()), **extra}
@@ -200,7 +200,7 @@ def test_policy_members_can_reject_without_extra_permissions(case, client, key):
detail = client.get(reverse('staff_payments:receipt_detail', args=[receipt.public_id]))
assert 'رد فیش' in detail.content.decode()
response = client.post(reverse('staff_payments:decision', args=[receipt.public_id]),
{'version': 1, 'action': 'REJECT', 'reason': 'تصویر ناخوانا است'})
{'version': 1, 'action': 'REJECT', 'reason': 'تصویر ناخوانا است', 'confirm_rejection': '1'})
assert response.status_code == 302
receipt.refresh_from_db()
assert receipt.status == 'REJECTED'
+167
View File
@@ -0,0 +1,167 @@
"""Receipt identity, rejection outbox and destination warning regressions."""
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from django.core.exceptions import ValidationError
from django.db import transaction
from django.urls import reverse
from apps.customers.models import CustomerProfile
from apps.integrations.sms.base import SMSResult
from apps.notifications.management.commands import process_notifications as worker
from apps.notifications.models import NotificationJob, SMSTemplate
from apps.payments import services
from apps.payments.deposit_identifier import generate_id_algorithm
from apps.payments.forms import ReceiptForm
from apps.payments.models import PaymentNotification, PaymentReceiptAlert, ReceiptDecision
from tests.test_payment_links import open_notice
from tests.test_payment_receipt_alerts import enabled, run
from tests.test_payments import case, data, submit, upload
pytestmark = pytest.mark.django_db
@pytest.mark.parametrize('via_link', [False, True])
def test_payer_prefill_uses_profile_and_stays_editable(case, client, via_link):
customer = case['customer']
customer.first_name = 'نام حساب'
customer.save()
CustomerProfile.objects.update_or_create(user=customer, defaults={'first_name': 'علی', 'last_name': 'کریمی'})
if via_link:
open_notice(client, case)
else:
client.force_login(customer)
page = client.get(reverse('payments:receipt_new'), {'notice': case['notice'].public_id})
assert page.status_code == 200
assert page.context['form']['depositor_name'].value() == 'علی کریمی'
assert not page.context['form'].fields['depositor_name'].disabled
@pytest.mark.parametrize('payer', ['', ' ', '\u200c\u200b', 'الف' * 151])
def test_empty_or_invalid_payer_rejected_in_form_and_service(case, payer):
form = ReceiptForm(data(case, depositor_name=payer), {'receipt_image': upload()}, customer=case['customer'])
assert not form.is_valid() and 'depositor_name' in form.errors
valid = ReceiptForm(data(case), {'receipt_image': upload()}, customer=case['customer'])
assert valid.is_valid(), valid.errors
with pytest.raises(ValidationError):
services.submit_receipt(case['customer'], case['customer'], {**valid.cleaned_data, 'depositor_name': payer})
def test_name_missing_from_post_is_not_silently_prefilled(case, client):
customer = case['customer']
customer.first_name = 'نام موجود'
customer.save()
client.force_login(customer)
values = data(case)
values.pop('depositor_name')
response = client.post(reverse('payments:receipt_new'), {**values, 'receipt_image': upload()}, HTTP_ACCEPT='application/json')
assert response.status_code == 422
assert response.json()['errors']['depositor_name']
def test_receipt_payer_shown_to_staff_and_nonce_cannot_change_identity(case, client):
customer = case['customer']
customer.first_name = 'نام مشتری'
customer.save()
values = data(case, depositor_name='شخص واریزکننده')
form = ReceiptForm(values, {'receipt_image': upload()}, customer=customer)
assert form.is_valid(), form.errors
receipt = services.submit_receipt(customer, customer, form.cleaned_data)
assert receipt.revisions.get().depositor_name == 'شخص واریزکننده'
with pytest.raises(ValidationError):
services.submit_receipt(customer, customer, {**form.cleaned_data, 'depositor_name': 'شخص دیگر'})
client.force_login(case['first'])
for endpoint in [reverse('staff_payments:desk'), reverse('staff_payments:receipt_detail', args=[receipt.public_id])]:
content = client.get(endpoint).content.decode()
assert 'نام مشتری' in content and 'شخص واریزکننده' in content
def test_revision_keeps_payer_history_and_prefill(case, client):
receipt = submit(case)
services.decide_receipt(case['first'], receipt.public_id, 1, 'REJECT', 'اصلاح کنید')
client.force_login(case['customer'])
endpoint = reverse('payments:receipt_revise', args=[receipt.public_id])
assert client.get(endpoint).context['form']['depositor_name'].value() == 'واریزکننده آزمایشی'
response = client.post(endpoint, {**data(case, depositor_name='واریزکننده اصلاح‌شده'), 'version': 1, 'receipt_image': upload()})
assert response.status_code == 302
assert receipt.revisions.get(version=1).depositor_name == 'واریزکننده آزمایشی'
assert receipt.revisions.get(version=2).depositor_name == 'واریزکننده اصلاح‌شده'
@pytest.mark.parametrize('payer,expected', [('علی کریمی', 'علی کریمی'), ('علي كريمي', 'علی کریمی'), ('رضا احمدی', 'علی کریمی (واریزکننده: رضا احمدی)')])
def test_sms_and_bale_include_payer_without_duplicate_name(case, enabled, monkeypatch, payer, expected):
customer = case['customer']
customer.first_name, customer.last_name = 'علی', 'کریمی'
customer.save()
form = ReceiptForm(data(case, depositor_name=payer), {'receipt_image': upload()}, customer=customer)
assert form.is_valid(), form.errors
services.submit_receipt(customer, customer, form.cleaned_data)
sent = []
provider = SimpleNamespace(send_message=lambda mobile, body: sent.append(body) or SMSResult(True, 'sms-id'))
monkeypatch.setattr(worker, 'get_sms_provider', lambda: provider)
monkeypatch.setattr('apps.integrations.bale.providers.BaleBotProvider.send_photo',
lambda self, chat, image, caption, **kwargs: sent.append(caption) or SMSResult(True, 'bale-id'))
for alert in PaymentReceiptAlert.objects.select_related('job'):
assert alert.context['customer_name'] == expected
assert run(alert.job).status == 'SENT'
assert len(sent) == 3
assert all(expected in body and body.count('علی کریمی') == 1 for body in sent)
def test_rejection_requires_confirmation_and_queues_exact_reason_once(case, client, monkeypatch):
receipt = submit(case)
client.force_login(case['first'])
endpoint = reverse('staff_payments:decision', args=[receipt.public_id])
reason = 'تصویر ناخوانا است؛ لطفاً تصویر واضح ارسال کنید.'
network = Mock(side_effect=AssertionError('Must queue, not send during decision'))
monkeypatch.setattr(worker, 'get_sms_provider', network)
payload = {'version': 1, 'action': 'REJECT', 'reason': reason}
client.post(endpoint, payload)
receipt.refresh_from_db()
assert receipt.status == 'PENDING' and not ReceiptDecision.objects.exists()
for _ in range(2):
assert client.post(endpoint, {**payload, 'confirm_rejection': '1'}).status_code == 302
receipt.refresh_from_db()
assert receipt.status == 'REJECTED'
job = NotificationJob.objects.get(event_type='PAYMENT_RECEIPT_REJECTED')
notification = PaymentNotification.objects.get(job=job)
assert notification.context['rejection_reason'] == reason
assert notification.cycle.revision.receipt == receipt
network.assert_not_called()
sent = []
monkeypatch.setattr(worker, 'get_sms_provider', lambda: SimpleNamespace(
send_message=lambda mobile, body: sent.append((mobile, body)) or SMSResult(True, 'rejection-sms')))
assert run(job).status == 'SENT'
assert sent[0][0] == case['customer'].mobile and reason in sent[0][1]
assert '/p/' in sent[0][1]
notification.refresh_from_db()
assert notification.attempt_log.count() == 1
def test_rejection_reason_survives_custom_template_and_queue_rollback(case, monkeypatch):
receipt = submit(case)
with pytest.raises(RuntimeError), transaction.atomic():
services.decide_receipt(case['first'], receipt.public_id, 1, 'REJECT', 'ناخوانا')
raise RuntimeError('rollback')
assert not NotificationJob.objects.filter(event_type='PAYMENT_RECEIPT_REJECTED').exists()
assert not ReceiptDecision.objects.exists()
template = SMSTemplate.objects.get(event_type='PAYMENT_RECEIPT_REJECTED', unit__isnull=True)
template.body = 'فیش {receipt_number} رد شد.'
template.save()
services.decide_receipt(case['first'], receipt.public_id, 1, 'REJECT', 'ناخوانا')
job = NotificationJob.objects.get(event_type='PAYMENT_RECEIPT_REJECTED')
messages = []
monkeypatch.setattr(worker, 'get_sms_provider', lambda: SimpleNamespace(
send_message=lambda mobile, body: messages.append(body) or SMSResult(True, 'id')))
assert run(job).status == 'SENT' and 'ناخوانا' in messages[0]
@pytest.mark.parametrize('linked', [False, True])
def test_warning_uses_selected_bank_and_generated_customer_identifier(case, client, linked):
client.force_login(case['customer'])
params = {'notice': case['notice'].public_id} if linked else {}
page = client.get(reverse('payments:receipt_new'), params)
assert page.context['customer_deposit_id'] == generate_id_algorithm(case['customer'].local_mobile)
assert page.context['account_requirements'][str(case['account'].pk)] == {'required': True, 'bank': case['account'].bank.name}
assert 'ممکن است مبلغ واریزی به حساب شما برگشت بخورد' in page.content.decode()