import uuid from urllib.parse import parse_qs, urlsplit import pytest from django.core.exceptions import PermissionDenied, ValidationError from django.core.management import call_command from django.test import Client from django.urls import reverse from django.utils import timezone from apps.integrations.sms.base import SMSDeliveryResult, SMSResult from apps.notifications.models import NotificationJob from apps.notifications.management.commands import process_notifications from apps.payments import services from apps.payments.models import PaymentAuditEvent, PaymentAccessConfiguration, PaymentNotification, PaymentNotice from tests.test_payments import case, submit, grant pytestmark = pytest.mark.django_db def test_default_action_tab_and_explicit_all_receipts(case, client): receipt = submit(case) client.force_login(case['first']) first = client.get(reverse('staff_payments:desk')) assert first.context['mine'] is True and receipt.number in first.content.decode() assert 'صفحه 1 از 1' in first.content.decode() services.decide_receipt(case['first'], receipt.public_id, 1, 'APPROVE') assert receipt.number not in client.get(reverse('staff_payments:desk')).content.decode() assert receipt.number in client.get(reverse('staff_payments:desk'), {'tab': 'receipts'}).content.decode() client.force_login(case['outsider']) assert receipt.number not in client.get(reverse('staff_payments:desk')).content.decode() assert client.get(reverse('staff_payments:desk'), {'mine': '0'}).context['mine'] is False @pytest.mark.parametrize('tab', ['notices', 'receipts', 'mine']) def test_desk_pagination_preserves_tab_and_filters(case, client, tab): if tab == 'notices': for index in range(26): services.issue_notice(case['creator'], {'customer': case['customer'], 'account': case['account'], 'subject': f'صفحه بندی {index}', 'amount': 12345, 'deposit_id': '123'}) else: for _ in range(26): submit(case) client.force_login(case['first']) parameters = {'q': case['customer'].username, 'account': case['account'].pk} if tab != 'mine': parameters['tab'] = tab first = client.get(reverse('staff_payments:desk'), parameters) assert len(first.context['page_obj']) == 25 query = parse_qs(first.context['query_string']) assert query['tab'] == ['notices' if tab == 'notices' else 'receipts'] assert query.get('mine') == (['1'] if tab == 'mine' else None) assert query['q'] == [case['customer'].username] second = client.get(reverse('staff_payments:desk'), {**parameters, 'page': 2}) assert second.context['page_obj'].number == 2 and len(second.context['page_obj']) >= 1 assert not {item.pk for item in first.context['page_obj']} & {item.pk for item in second.context['page_obj']} def test_resend_uses_worker_keeps_attempts_and_delivery_history(case, client, monkeypatch, settings): settings.SMS_MAX_RETRIES = 0 sent = [] class Provider: def send_message(self, mobile, body): sent.append((mobile, body)) return SMSResult(False, error='اعتبار پنل کافی نیست', provider_code='-19') if len(sent) == 1 else SMSResult(True, 'new-sms-id', provider_code='2') def get_delivery_status(self, provider_id): assert provider_id == 'new-sms-id' return SMSDeliveryResult('DELIVERED', '0', 'پیامک به گوشی رسید', True) monkeypatch.setattr(process_notifications, 'get_sms_provider', lambda: Provider()) original = PaymentNotification.objects.get(notice=case['notice']) call_command('process_notifications', once=True, job_id=original.job_id) original.refresh_from_db() original.job.refresh_from_db() assert original.job.status == 'FAILED' endpoint = reverse('staff_payments:notice_resend_sms', args=[case['notice'].public_id]) detail = reverse('staff_payments:notice_detail', args=[case['notice'].public_id]) client.force_login(case['creator']) key = str(uuid.uuid4()) assert client.get(endpoint).status_code == 405 assert client.post(endpoint, {'request_key': key}).status_code == 302 retry = PaymentNotification.objects.filter(notice=case['notice']).exclude(pk=original.pk).get() assert retry.job.status == 'PENDING' client.post(endpoint, {'request_key': key}) assert PaymentNotification.objects.filter(notice=case['notice']).count() == 2 client.post(endpoint, {'request_key': str(uuid.uuid4())}) assert PaymentNotification.objects.filter(notice=case['notice']).count() == 2 call_command('process_notifications', once=True, job_id=retry.job_id) retry.refresh_from_db(); retry.job.refresh_from_db() assert retry.job.status == 'SENT' and retry.job.delivery_status == 'PENDING' assert original.access_link_id != retry.access_link_id assert retry.context['payment_link'] in sent[-1][1] awaiting = client.get(detail).content.decode() assert 'پذیرفته‌شده توسط سرویس پیامک' in awaiting and 'در انتظار استعلام دریافت توسط گوشی' in awaiting NotificationJob.objects.filter(pk=retry.job_id).update(delivery_next_check_at=timezone.now()) call_command('process_notifications', once=True, job_id=retry.job_id) completed = client.get(detail).content.decode() assert all(value in completed for value in ['رسیده به گوشی', 'پیامک به گوشی رسید', 'new-sms-id', 'اعتبار پنل کافی نیست', 'آخرین استعلام']) assert original.attempt_log.count() == 1 and retry.attempt_log.count() == 1 and len(sent) == 2 assert PaymentAuditEvent.objects.filter(action='NOTICE_SMS_RESEND_REQUESTED').count() == 1 # Replaying the same form after completion must still not issue another message. client.post(endpoint, {'request_key': key}) assert PaymentNotification.objects.filter(notice=case['notice']).count() == 2 @pytest.mark.parametrize('status,final,next_check,text', [ ('UNDELIVERED', False, True, 'نتیجه غیرقطعی'), ('UNDELIVERED', True, False, 'نرسیده به گوشی'), ('UNKNOWN', False, False, 'دریافت توسط گوشی تأیید نشده است'), ('CHECK_FAILED', False, True, 'خطای موقت استعلام'), ('EXPIRED', True, False, 'منقضی‌شده'), ]) def test_sms_history_distinguishes_final_and_provisional_delivery(case, client, status, final, next_check, text): notification = PaymentNotification.objects.get(notice=case['notice']) NotificationJob.objects.filter(pk=notification.job_id).update(status='SENT', provider_message_id='tracked-id', delivery_status=status, delivery_is_final=final, delivery_status_code='6', delivery_next_check_at=timezone.now() if next_check else None) client.force_login(case['creator']) html = client.get(reverse('staff_payments:notice_detail', args=[case['notice'].public_id])).content.decode() assert text in html assert ('نتیجه غیرقطعی' in html) is not final def test_sms_resend_permission_invalid_key_and_csrf(case, client): notice = case['notice'] notification = PaymentNotification.objects.get(notice=notice) NotificationJob.objects.filter(pk=notification.job_id).update(status='FAILED') endpoint = reverse('staff_payments:notice_resend_sms', args=[notice.public_id]) secure = Client(enforce_csrf_checks=True) secure.force_login(case['creator']) assert secure.post(endpoint, {'request_key': str(uuid.uuid4())}).status_code == 403 client.force_login(case['creator']) client.post(endpoint, {'request_key': 'invalid'}) assert PaymentNotification.objects.filter(notice=notice).count() == 1 PaymentAccessConfiguration.objects.update_or_create(pk=1, defaults={'open_create': False, 'open_view': False}) with pytest.raises(PermissionDenied): services.resend_notice_sms(case['creator'], notice, str(uuid.uuid4())) grant(case['outsider'], 'add_paymentnotice') with pytest.raises(PermissionDenied): services.resend_notice_sms(case['outsider'], notice, str(uuid.uuid4())) with pytest.raises(PermissionDenied): services.resend_notice_sms(case['customer'], notice, str(uuid.uuid4())) @pytest.mark.parametrize('reason', ['cancelled', 'inactive', 'deleted']) def test_invalid_notice_cannot_be_resent(case, reason): notice = case['notice'] if reason == 'inactive': case['customer'].is_active = False; case['customer'].save(update_fields=['is_active']) else: PaymentNotice.objects.filter(pk=notice.pk).update(**{'cancelled_at' if reason == 'cancelled' else 'deleted_at': timezone.now()}) with pytest.raises((ValidationError, PermissionDenied)): services.resend_notice_sms(case['creator'], notice, str(uuid.uuid4())) assert PaymentNotification.objects.filter(notice=notice).count() == 1