168 lines
9.1 KiB
Python
168 lines
9.1 KiB
Python
"""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()
|