"""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()