Files
customerTicket/tests/test_staff_payment_sms.py
T
2026-10-07 13:39:29 +03:30

154 lines
8.7 KiB
Python

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