import base64 import uuid import jdatetime import pytest from django.contrib.auth.models import Permission from django.core.exceptions import PermissionDenied, ValidationError from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse from django.utils import timezone from apps.accounts.models import User from apps.staff.models import StaffProfile, OrganizationalUnit from apps.attachments.models import AttachmentPolicy from apps.notifications.models import NotificationJob, SMSTemplate from apps.integrations.sms.providers import SMSResult from apps.payments import services, policies from apps.payments.forms import ReceiptForm, NoticeForm from apps.payments.models import (BankInstitution, CompanyBankAccount, PaymentApprovalPolicy, PaymentReceipt, PaymentAccessConfiguration, PaymentAuditEvent, ReceiptDecision, PaymentNotificationAttempt) pytestmark = pytest.mark.django_db PNG = base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=') def upload(): return SimpleUploadedFile('receipt.png', PNG, content_type='image/png') @pytest.mark.parametrize('staff', [False, True]) def test_receipt_upload_json_success_keeps_flash_and_deduplicates(case, client, staff): client.force_login(case['creator'] if staff else case['customer']) endpoint = reverse('staff_payments:receipt_new', args=[case['customer'].public_id]) if staff else reverse('payments:receipt_new') values = data(case) first = client.post(endpoint, {**values, 'receipt_image': upload()}, HTTP_ACCEPT='application/json') assert first.status_code == 200 receipt = PaymentReceipt.objects.get(customer=case['customer']) expected = reverse('staff_payments:receipt_detail' if staff else 'payments:receipt_detail', args=[receipt.public_id]) assert first.json() == {'redirect_url': expected} repeated = client.post(endpoint, {**values, 'receipt_image': upload()}, HTTP_ACCEPT='application/json') assert repeated.json() == first.json() assert PaymentReceipt.objects.filter(customer=case['customer']).count() == 1 assert 'فیش ثبت شد و در انتظار بررسی است.' in client.get(expected).content.decode() def test_receipt_upload_json_field_and_service_errors(case, client, monkeypatch): client.force_login(case['customer']) endpoint = reverse('payments:receipt_new') invalid = client.post(endpoint, {**data(case), 'paid_amount': '0', 'receipt_image': upload()}, HTTP_ACCEPT='application/json') assert invalid.status_code == 422 assert invalid.json()['errors']['paid_amount'][0]['message'] def unavailable(*args, **kwargs): raise ValidationError('اعلامیه دیگر قابل پرداخت نیست.') monkeypatch.setattr(services, 'submit_receipt', unavailable) response = client.post(endpoint, {**data(case), 'receipt_image': upload()}, HTTP_ACCEPT='application/json') assert response.status_code == 422 assert response.json()['errors']['__all__'][0]['message'] == 'اعلامیه دیگر قابل پرداخت نیست.' assert not PaymentReceipt.objects.filter(customer=case['customer']).exists() def test_receipt_revision_upload_json_errors_and_success(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]) stale = client.post(endpoint, {**data(case), 'version': '0', 'receipt_image': upload()}, HTTP_ACCEPT='application/json') assert stale.status_code == 422 and stale.json()['errors']['__all__'] valid = client.post(endpoint, {**data(case), 'version': '1', 'receipt_image': upload()}, HTTP_ACCEPT='application/json') assert valid.json() == {'redirect_url': reverse('payments:receipt_detail', args=[receipt.public_id])} receipt.refresh_from_db() assert receipt.current_version == 2 and receipt.revisions.count() == 2 assert 'نسخه اصلاحی ثبت شد' in client.get(valid.json()['redirect_url']).content.decode() def grant(user, code): user.user_permissions.add(Permission.objects.get(content_type__app_label='payments',codename=code)) for attr in ['_perm_cache','_user_perm_cache','_group_perm_cache']: user.__dict__.pop(attr,None) @pytest.fixture def case(settings, tmp_path): settings.MEDIA_ROOT = tmp_path unit = OrganizationalUnit.objects.create(name='فروش مالی',slug='payment-sales') users = {} for key in ['creator','first','second','senior','outsider']: user = User.objects.create_user(username='pay-'+key,account_type='STAFF',password='TestPayment123!') StaffProfile.objects.create(user=user,display_name=key,primary_unit=unit) users[key] = user customer = User.objects.create_user(username='pay-customer',mobile='09120009999',account_type='CUSTOMER') # Transactional PostgreSQL tests flush seed data between cases. bank, _ = BankInstitution.objects.get_or_create(code='melli',defaults={'name':'ملی ایران'}) account = CompanyBankAccount.objects.create(name='حساب فروش ملی',holder='شرکت نمونه',bank=bank,account_number='000123456',requires_deposit_id=True) AttachmentPolicy.objects.update_or_create(attachment_type='IMAGE',defaults={'max_file_size':10*1024*1024,'allowed_extensions':['.png'],'allowed_mime_types':['image/png']}) policy = PaymentApprovalPolicy.objects.create(name='دو تأیید',unit=unit) policy.reviewers.add(users['first'],users['second']);policy.seniors.add(users['senior']) for key in ['first','second']: grant(users[key],'approve_paymentreceipt') grant(users['senior'],'senior_approve_paymentreceipt') notice = services.issue_notice(users['creator'],{'customer':customer,'account':account,'subject':'پرداخت فاکتور','amount':'۲۸۰٬۰۰۰٬۰۰۰','deposit_id':'۰۰۰۱۲۳','invoice_number':'INV-1','description':'توضیح همیشه نمایان'}) return {**users,'customer':customer,'account':account,'notice':notice,'policy':policy,'unit':unit} def data(case, linked=True, **extra): today = jdatetime.date.fromgregorian(date=timezone.localdate()) 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} def submit(case, linked=True): form = ReceiptForm(data(case,linked),{'receipt_image':upload()},customer=case['customer']) assert form.is_valid(), form.errors return services.submit_receipt(case['customer'],case['customer'],form.cleaned_data) def test_identifier_required_in_form_and_service(case): values = {'customer':case['customer'].pk,'account':case['account'].pk,'subject':'فاکتور','amount':'100'} form = NoticeForm(values) assert not form.is_valid() and 'deposit_id' in form.errors with pytest.raises(ValidationError): services.issue_notice(case['creator'],{'customer':case['customer'],'account':case['account'],'subject':'فاکتور','amount':100}) assert NotificationJob.objects.filter(event_type='PAYMENT_NOTICE_CREATED').count() == 1 def test_multireviewer_and_final_sms_are_exactly_one(case): receipt = submit(case) services.decide_receipt(case['first'],receipt.public_id,1,'APPROVE') receipt.refresh_from_db();assert receipt.status == 'PARTIAL' assert not NotificationJob.objects.filter(event_type='PAYMENT_RECEIPT_APPROVED').exists() services.decide_receipt(case['first'],receipt.public_id,1,'APPROVE') assert ReceiptDecision.objects.count() == 1 services.decide_receipt(case['second'],receipt.public_id,1,'APPROVE') receipt.refresh_from_db();assert receipt.status == 'APPROVED' services.decide_receipt(case['second'],receipt.public_id,1,'APPROVE') assert NotificationJob.objects.filter(event_type='PAYMENT_RECEIPT_APPROVED').count() == 1 assert receipt.revisions.get().cycle.finalized_by == case['second'] def test_senior_explicit_override_and_reason(case): receipt = submit(case) with pytest.raises(ValidationError): services.decide_receipt(case['senior'],receipt.public_id,1,'SENIOR_APPROVE') services.decide_receipt(case['senior'],receipt.public_id,1,'SENIOR_APPROVE','بررسی تطبیق انجام شد') receipt.refresh_from_db();assert receipt.status == 'APPROVED' assert ReceiptDecision.objects.get().actor_name == 'senior' assert PaymentAuditEvent.objects.filter(action='SENIOR_APPROVE').exists() @pytest.mark.parametrize('reviewer_count', [1, 2]) def test_policy_reviewers_can_approve_without_extra_permissions(case, client, reviewer_count): case['policy'].reviewers.set([case['first'], case['second']][:reviewer_count]) for key in ['first', 'second']: case[key].user_permissions.clear() receipt = submit(case) endpoint = reverse('staff_payments:decision', args=[receipt.public_id]) detail = reverse('staff_payments:receipt_detail', args=[receipt.public_id]) for index, key in enumerate(['first', 'second'][:reviewer_count]): client.force_login(case[key]) page = client.get(detail) assert page.context['can_approve'] assert not page.context['can_senior'] assert 'ثبت تأیید من' in page.content.decode() assert 'رد فیش' in page.content.decode() response = client.post(endpoint, {'version': 1, 'action': 'APPROVE'}) assert response.status_code == 302 receipt.refresh_from_db() final = index == reviewer_count - 1 assert receipt.status == ('APPROVED' if final else 'PARTIAL') assert NotificationJob.objects.filter(event_type='PAYMENT_RECEIPT_APPROVED').count() == int(final) assert ReceiptDecision.objects.filter(cycle__revision__receipt=receipt).count() == reviewer_count @pytest.mark.parametrize('also_required', [False, True]) def test_policy_senior_can_finalize_alone_without_extra_permissions(case, client, also_required): case['senior'].user_permissions.clear() if also_required: case['policy'].reviewers.add(case['senior']) receipt = submit(case) client.force_login(case['senior']) page = client.get(reverse('staff_payments:receipt_detail', args=[receipt.public_id])) assert page.context['can_senior'] assert page.context['can_approve'] == also_required assert 'تأیید نهایی ارشد' in page.content.decode() endpoint = reverse('staff_payments:decision', args=[receipt.public_id]) client.post(endpoint, {'version': 1, 'action': 'SENIOR_APPROVE'}) assert not ReceiptDecision.objects.exists() response = client.post(endpoint, {'version': 1, 'action': 'SENIOR_APPROVE', 'reason': 'تطبیق انجام شد'}) assert response.status_code == 302 receipt.refresh_from_db() assert receipt.status == 'APPROVED' assert ReceiptDecision.objects.get().user == case['senior'] assert NotificationJob.objects.filter(event_type='PAYMENT_RECEIPT_APPROVED').count() == 1 @pytest.mark.parametrize('key', ['first', 'senior']) def test_policy_members_can_reject_without_extra_permissions(case, client, key): case[key].user_permissions.clear() receipt = submit(case) client.force_login(case[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': 'تصویر ناخوانا است', 'confirm_rejection': '1'}) assert response.status_code == 302 receipt.refresh_from_db() assert receipt.status == 'REJECTED' assert ReceiptDecision.objects.get().action == 'REJECT' assert not NotificationJob.objects.filter(event_type='PAYMENT_RECEIPT_APPROVED').exists() @pytest.mark.parametrize('who',['customer','creator','outsider']) def test_view_and_create_never_imply_approval(case,who): receipt = submit(case) with pytest.raises(PermissionDenied): services.decide_receipt(case[who],receipt.public_id,1,'APPROVE') @pytest.mark.parametrize('action', ['APPROVE', 'SENIOR_APPROVE', 'REJECT']) def test_django_permissions_without_assignment_do_not_allow_decisions(case, client, action): grant(case['outsider'], 'approve_paymentreceipt') grant(case['outsider'], 'senior_approve_paymentreceipt') receipt = submit(case) client.force_login(case['outsider']) page = client.get(reverse('staff_payments:receipt_detail', args=[receipt.public_id])) assert not page.context['can_approve'] and not page.context['can_senior'] response = client.post(reverse('staff_payments:decision', args=[receipt.public_id]), {'version': 1, 'action': action, 'reason': 'بررسی انجام شد'}) assert response.status_code == 403 assert not ReceiptDecision.objects.exists() @pytest.mark.parametrize('blocked_by', ['visibility', 'own_submission', 'inactive_user', 'inactive_profile']) def test_policy_assignment_preserves_decision_restrictions(case, blocked_by): receipt = submit(case) if blocked_by == 'visibility': PaymentAccessConfiguration.objects.update_or_create(pk=1, defaults={'open_view': False}) elif blocked_by == 'own_submission': PaymentReceipt.objects.filter(pk=receipt.pk).update(submitted_by=case['first']) elif blocked_by == 'inactive_user': User.objects.filter(pk=case['first'].pk).update(is_active=False) else: StaffProfile.objects.filter(user=case['first']).update(is_active=False) with pytest.raises(PermissionDenied): services.decide_receipt(case['first'], receipt.public_id, 1, 'APPROVE') assert not ReceiptDecision.objects.exists() def test_rejection_ends_cycle_and_preserves_evidence(case): receipt = submit(case) services.decide_receipt(case['first'],receipt.public_id,1,'REJECT','تصویر پرداخت خوانا نیست') with pytest.raises(ValidationError): services.decide_receipt(case['second'],receipt.public_id,1,'APPROVE') with pytest.raises(ValidationError): services.decide_receipt(case['senior'],receipt.public_id,1,'SENIOR_APPROVE','override') assert not NotificationJob.objects.filter(event_type='PAYMENT_RECEIPT_APPROVED').exists() def test_policy_snapshot_and_stale_version(case): receipt = submit(case) case['policy'].reviewers.clear() with pytest.raises(ValidationError): services.decide_receipt(case['first'],receipt.public_id,2,'APPROVE') services.decide_receipt(case['first'],receipt.public_id,1,'APPROVE') assert receipt.revisions.get().cycle.assignments.filter(is_senior=False).count() == 2 def test_snapshot_survives_bank_change(case,client): case['account'].account_number = '9999999';case['account'].save() case['notice'].refresh_from_db();assert case['notice'].account_snapshot['account_number'] == '000123456' client.force_login(case['customer']) page = client.get(reverse('payments:notices')).content.decode() assert '000123456' in page and '9999999' not in page and 'توضیح همیشه نمایان' in page def test_independent_receipt_and_server_date(case,client): # Independent policies use global fallback, not an arbitrary unit's ticket rights. case['policy'].unit=None;case['policy'].save() receipt = submit(case,False) assert receipt.notice is None assert receipt.revisions.get().paid_date == timezone.localdate() client.force_login(case['outsider']) response = client.get(reverse('staff_payments:desk'),{'tab':'receipts','kind':'independent'}) assert receipt.number in response.content.decode() assert client.get(reverse('staff_payments:count')).json()['pending_receipts'] == 1 @pytest.mark.parametrize('changes,field',[ ({'paid_amount':'0'},'paid_amount'),({'paid_amount':'12.5'},'paid_amount'),({'paid_amount':'1,23'},'paid_amount'), ({'paid_day':''},'paid_day'),({'paid_date':'1990-01-01'},'paid_day'),({'destination_account':''},'destination_account')]) def test_validation_exact_fields(case,changes,field): form = ReceiptForm(data(case,**changes),{'receipt_image':upload()},customer=case['customer']) assert not form.is_valid() and field in form.errors def test_cross_customer_notice_and_image_protection(case,client): receipt=submit(case) other=User.objects.create_user(username='other-customer',account_type='CUSTOMER') client.force_login(other) assert client.get(reverse('payments:receipt_detail',args=[receipt.public_id])).status_code == 404 assert client.get(reverse('payments:receipt_image',args=[receipt.public_id])).status_code == 403 form=ReceiptForm(data(case),{'receipt_image':upload()},customer=other) assert not form.is_valid() and 'notice_id' in form.errors client.force_login(case['customer']) response=client.get(reverse('payments:receipt_image',args=[receipt.public_id])) assert response.status_code == 200 and 'no-store' in response['Cache-Control'] assert b''.join(response.streaming_content) == PNG def test_nonce_duplicate_is_idempotent_and_different_values_rejected(case): values=data(case) for i in range(2): form=ReceiptForm(values,{'receipt_image':upload()},customer=case['customer']);assert form.is_valid() services.submit_receipt(case['customer'],case['customer'],form.cleaned_data) assert PaymentReceipt.objects.count() == 1 values['paid_amount']='999' form=ReceiptForm(values,{'receipt_image':upload()},customer=case['customer']);assert form.is_valid() with pytest.raises(ValidationError): services.submit_receipt(case['customer'],case['customer'],form.cleaned_data) def test_destination_flags_and_bank_permission(case,client): case['account'].available_to_customers=False;case['account'].save() linked=ReceiptForm(data(case),{'receipt_image':upload()},customer=case['customer']);assert linked.is_valid() independent=ReceiptForm(data(case,False),{'receipt_image':upload()},customer=case['customer']);assert not independent.is_valid() client.force_login(case['outsider']);assert client.get(reverse('staff_payments:banks')).status_code == 403 grant(case['creator'],'manage_company_bankaccounts');client.force_login(case['creator']) assert client.get(reverse('staff_payments:banks')).status_code == 200 client.post(reverse('staff_payments:bank_bulk'),{'accounts':[case['account'].pk],'field':'is_active','value':'0'}) case['account'].refresh_from_db();assert not case['account'].is_active assert PaymentAuditEvent.objects.filter(action='BANK_FLAG_CHANGED').exists() def test_missing_policy_stays_unapproved_until_manager_assigns(case): case['policy'].is_active=False;case['policy'].save() receipt=submit(case) assert not receipt.revisions.get().cycle.assignments.exists() with pytest.raises(PermissionDenied): services.decide_receipt(case['first'],receipt.public_id,1,'APPROVE') case['policy'].is_active=True;case['policy'].save();grant(case['creator'],'manage_payment_approval_policy') services.bind_unassigned_cycle(case['creator'],receipt) assert receipt.revisions.get().cycle.assignments.count() == 3 def test_sms_uses_database_template_and_attempt_history(case,monkeypatch): from apps.notifications.management.commands import process_notifications from apps.payments.models import PaymentNotification template=SMSTemplate.objects.get(event_type='PAYMENT_NOTICE_CREATED',unit__isnull=True) template.body='متن سفارشی {notice_number} {payment_link}';template.save() captured=[] class Provider: def send_message(self,mobile,message): captured.append(message);return SMSResult(True,'123') monkeypatch.setattr(process_notifications,'get_sms_provider',lambda:Provider()) job=NotificationJob.objects.get(event_type='PAYMENT_NOTICE_CREATED') process_notifications._send_sms(job) template.body='جدید';template.save() process_notifications._send_sms(job) notification = PaymentNotification.objects.get(job=job) assert captured[0] == captured[1] and 'متن سفارشی' in captured[0] assert '/p/' in notification.context['payment_link'] assert notification.context['payment_link'] in captured[0] assert PaymentNotificationAttempt.objects.filter(success=True).count() == 2 assert job.ticket is None def test_explicit_access_and_inactive_reviewer(case): receipt=submit(case) PaymentAccessConfiguration.objects.update(open_view=False,open_create=False) assert not policies.visible(PaymentReceipt.objects.all(),case['outsider']).exists() assert not policies.can_create(case['outsider']) StaffProfile.objects.filter(user=case['first']).update(is_active=False) actor=User.objects.get(pk=case['first'].pk) with pytest.raises(PermissionDenied): services.decide_receipt(actor,receipt.public_id,1,'APPROVE') def test_login_hub_and_existing_new_route(case,client): client.force_login(case['customer']) assert client.get('/').url == reverse('payments:home') assert client.get(reverse('payments:home')).status_code == 200 assert client.get(reverse('tickets:new')).status_code == 200 assert reverse('tickets:new') == '/tickets/new/' client.logout() assert client.get(reverse('payments:home')).status_code == 302 def test_upload_spoof_and_missing_fields(case): form=ReceiptForm(data(case),{'receipt_image':SimpleUploadedFile('fake.png',b'%PDF-1.4',content_type='image/png')},customer=case['customer']) assert not form.is_valid() and 'receipt_image' in form.errors form=ReceiptForm({},customer=case['customer']) assert not form.is_valid() assert {'paid_amount','paid_day','paid_month','paid_year','receipt_image','destination_account'} <= set(form.errors) def test_revision_preserves_old_votes_and_restarts_full_policy(case): receipt=submit(case) services.decide_receipt(case['first'],receipt.public_id,1,'REJECT','نیاز به تصویر تازه') form=ReceiptForm(data(case),{'receipt_image':upload()},customer=case['customer']);assert form.is_valid() services.revise_receipt(case['customer'],receipt.public_id,1,form.cleaned_data) receipt.refresh_from_db();assert receipt.status=='PENDING' and receipt.current_version==2 assert receipt.revisions.count()==2 assert receipt.revisions.get(version=1).cycle.decisions.count()==1 assert receipt.revisions.get(version=2).cycle.decisions.count()==0 with pytest.raises(ValidationError): services.decide_receipt(case['second'],receipt.public_id,1,'APPROVE') services.decide_receipt(case['first'],receipt.public_id,2,'APPROVE') receipt.refresh_from_db();assert receipt.status=='PARTIAL' def test_rollback_cleans_uploaded_file(case,settings): case['policy'].reviewers.clear() with pytest.raises(ValidationError): submit(case) assert PaymentReceipt.objects.count()==0 assert not list(settings.MEDIA_ROOT.rglob('*.png')) def test_invalid_notice_query_is_not_server_error(case,client): client.force_login(case['customer']) assert client.get(reverse('payments:receipt_new'),{'notice':'not-a-uuid'}).status_code==404 def test_safe_login_next_and_magic_link_not_financial_session(case,client): from apps.accounts.models import TicketMagicLink from apps.tickets.models import TicketCategory from apps.tickets.services import create_ticket case['customer'].set_password('TestFinancial123!');case['customer'].save() target=reverse('payments:notices') client.get(reverse('accounts:password_login'),{'next':target}) response=client.post(reverse('accounts:password_login'),{'mobile':case['customer'].local_mobile,'password':'TestFinancial123!'}) assert response.url==target client.logout();client.get(reverse('accounts:password_login'),{'next':'https://evil.example/chaharsu/'}) response=client.post(reverse('accounts:password_login'),{'mobile':case['customer'].local_mobile,'password':'TestFinancial123!'}) assert response.url==reverse('payments:home') client.logout() category=TicketCategory.objects.create(title='magic finance guard',slug='magic-finance-guard') ticket=create_ticket(customer=case['customer'],category=category,subject='صرفاً تیکت',body='متن') client.get(reverse('accounts:magic',args=[TicketMagicLink.issue(ticket)])) assert client.get(reverse('payments:notices')).status_code==302 def test_notice_nonce_prevents_duplicate_issuance_and_sms(case): values = {'customer':case['customer'], 'account':case['account'], 'subject':'دستور تازه', 'amount':1000, 'deposit_id':'0042', 'submission_key':uuid.uuid4()} first = services.issue_notice(case['creator'], values) second = services.issue_notice(case['creator'], values) assert first.pk == second.pk assert NotificationJob.objects.filter(event_type='PAYMENT_NOTICE_CREATED').count() == 2 with pytest.raises(ValidationError): services.issue_notice(case['creator'], {**values, 'amount':2000}) def test_revision_retains_notice_snapshot_and_identifier(case): receipt = submit(case) services.decide_receipt(case['first'], receipt.public_id, 1, 'REJECT', 'مدرک تازه لازم است') case['account'].account_number = '999999';case['account'].save() form = ReceiptForm(data(case, deposit_id=''), {'receipt_image':upload()}, customer=case['customer']) assert form.is_valid(), form.errors services.revise_receipt(case['customer'], receipt.public_id, 1, form.cleaned_data) revision = receipt.revisions.get(version=2) assert revision.account_snapshot['account_number'] == '000123456' assert revision.deposit_id == '123' def test_financial_unit_sms_template_accepts_financial_tags(case): from apps.tickets.forms import UnitSMSTemplateForm form = UnitSMSTemplateForm({'unit':case['unit'].pk, 'event_type':'PAYMENT_NOTICE_CREATED', 'title':'مالی', 'body':'{notice_number} {amount_rial} {payment_link}', 'is_active':True}, user=case['creator'], unit_ids=[case['unit'].pk]) assert form.is_valid(), form.errors assert 'payment_link' in form.save().allowed_tags def test_payment_posts_enforce_csrf_and_revoked_approval(case): from django.test import Client receipt = submit(case) client = Client(enforce_csrf_checks=True) client.force_login(case['first']) endpoint = reverse('staff_payments:decision',args=[receipt.public_id]) assert client.post(endpoint, {'version':1,'action':'APPROVE'}).status_code == 403 StaffProfile.objects.filter(user=case['first']).update(is_active=False) actor = User.objects.get(pk=case['first'].pk) with pytest.raises(PermissionDenied): services.decide_receipt(actor,receipt.public_id,1,'APPROVE') receipt.refresh_from_db();assert receipt.status == 'PENDING' assert not ReceiptDecision.objects.exists() def test_receipt_identifier_is_optional_even_for_a_different_identifier_account(case): other = CompanyBankAccount.objects.create(name='حساب شناسه‌دار دیگر',holder='شرکت', bank=case['account'].bank,account_number='1111',requires_deposit_id=True) form = ReceiptForm(data(case,destination_account=other.pk,deposit_id=''), {'receipt_image':upload()},customer=case['customer']) assert form.is_valid(), form.errors receipt = services.submit_receipt(case['customer'], case['customer'], form.cleaned_data) assert receipt.revisions.get(version=1).deposit_id == '' @pytest.mark.parametrize('day,month,year,valid', [ ('30','12','1403',True),('30','12','1402',False),('31','7','1403',False), ('31','6','1403',True),('','1','1403',False), ]) def test_jalali_calendar_validation_and_gregorian_result(day,month,year,valid): from apps.payments import validators import datetime if valid: assert isinstance(validators.paid_date(day,month,year), datetime.date) else: with pytest.raises(ValidationError): validators.paid_date(day,month,year) def test_failed_sms_template_preparation_has_attempt_history(case): from apps.payments.notifications import send_payment_sms SMSTemplate.objects.filter(event_type='PAYMENT_NOTICE_CREATED').update(is_active=False) job = NotificationJob.objects.get(event_type='PAYMENT_NOTICE_CREATED') with pytest.raises(SMSTemplate.DoesNotExist): send_payment_sms(job,provider_factory=lambda:None) attempt = PaymentNotificationAttempt.objects.get() assert attempt.success is False and attempt.finished_at is not None assert 'قالب پیامک' in attempt.error def test_stale_bank_form_does_not_overwrite_new_flags(case,client): grant(case['creator'],'manage_company_bankaccounts') client.force_login(case['creator']) account = case['account'] token = account.updated_at.isoformat() account.is_active = False;account.save() response = client.post(reverse('staff_payments:bank_edit',args=[account.public_id]),{ 'revision_token':token,'name':'نام قدیمی','bank':account.bank_id,'holder':account.holder, 'account_number':account.account_number,'sort_order':0,'is_active':'on'}) assert response.status_code == 200 assert 'تغییر کرده است' in response.content.decode() account.refresh_from_db();assert not account.is_active and account.name != 'نام قدیمی' def test_customer_receipt_list_and_revised_action_queue(case,client): receipt=submit(case) services.decide_receipt(case['first'],receipt.public_id,1,'REJECT','مدرک تازه') form=ReceiptForm(data(case),{'receipt_image':upload()},customer=case['customer']);assert form.is_valid() services.revise_receipt(case['customer'],receipt.public_id,1,form.cleaned_data) client.force_login(case['first']) assert receipt.number in client.get(reverse('staff_payments:desk'),{'mine':'1'}).content.decode() client.force_login(case['customer']) assert receipt.number in client.get(reverse('payments:receipt_list')).content.decode() def test_pwa_entry_routes_each_authenticated_role(case,client): assert client.get(reverse('web_manifest')).json()['start_url'] == '/' client.force_login(case['customer']) assert client.get('/').url == reverse('payments:home') client.force_login(case['creator']) assert client.get('/').url == reverse('staff_tickets:list') def test_financial_sms_model_clean_supplies_admin_readonly_tags(case): template=SMSTemplate(unit=case['unit'],event_type='PAYMENT_RECEIPT_APPROVED',title='مالی', body='{receipt_number} {payment_link}') template.full_clean() assert 'payment_link' in template.allowed_tags template.body='{magic_link}' with pytest.raises(ValidationError): template.full_clean() def test_cached_user_cannot_vote_after_staff_deactivation(case): receipt=submit(case) actor=case['first'] assert actor.has_perm('payments.approve_paymentreceipt') assert actor.staff_profile.is_active StaffProfile.objects.filter(user=actor).update(is_active=False) with pytest.raises(PermissionDenied): services.decide_receipt(actor,receipt.public_id,1,'APPROVE') assert not ReceiptDecision.objects.exists() def test_independent_receipt_bank_numbers_are_only_in_staff_review(case,client): receipt=submit(case,False) client.force_login(case['customer']) content=client.get(reverse('payments:receipt_detail',args=[receipt.public_id])).content.decode() assert 'شماره حساب مقصد ثبت‌شده' not in content client.force_login(case['first']) content=client.get(reverse('staff_payments:receipt_detail',args=[receipt.public_id])).content.decode() assert 'شماره حساب مقصد ثبت‌شده' in content and '000123456' in content def test_audit_permission_opens_readonly_evidence_admin(case): from django.contrib import admin from django.test import RequestFactory from apps.payments.admin import EvidenceAdmin request=RequestFactory().get('/x5admin/payments/paymentauditevent/') request.user=case['outsider'] controller=EvidenceAdmin(PaymentAuditEvent,admin.site) assert not controller.has_view_permission(request) grant(request.user,'view_payment_audit') assert controller.has_view_permission(request) assert controller.get_queryset(request).exists() assert not controller.has_change_permission(request) assert not controller.has_delete_permission(request) def test_admin_bank_audit_contains_previous_and_new_flags(case): from django.contrib import admin from django.test import RequestFactory from apps.payments.admin import AccountAdmin from apps.payments.forms import BankAccountForm account=case['account'] grant(case['creator'],'manage_company_bankaccounts') values={'revision_token':account.updated_at.isoformat(),'name':'حساب ویرایش‌شده','bank':account.bank_id, 'holder':account.holder,'account_number':account.account_number,'sort_order':0, 'requires_deposit_id':'on','available_to_staff':'on','available_to_customers':'on'} form=BankAccountForm(values,instance=account) assert form.is_valid(),form.errors request=RequestFactory().post('/x5admin/payments/companybankaccount/') request.user=case['creator'] AccountAdmin(CompanyBankAccount,admin.site).save_model(request,form.save(commit=False),form,True) event=PaymentAuditEvent.objects.get(action='CONFIG_SAVED') assert event.details['before']['is_active'] is True assert event.details['after']['is_active'] is False assert event.actor_id==request.user.pk def test_customer_search_identity_history_and_permissions(case, client): from apps.customers.models import CustomerProfile from apps.tickets.models import Ticket, TicketCategory customer = case['customer'] CustomerProfile.objects.create(user=customer, first_name='محمد', last_name='رضایی', organization_name='شرکت آفتاب', city='قم') category = TicketCategory.objects.create(title='جست‌وجوی مالی', slug='payment-picker') visible = Ticket.objects.create(customer=customer, category=category, subject='سابقه خرید قابل مشاهده', owner=case['creator'], selected_expert=case['creator'], origin_visibility='SELECTED', owning_unit=case['unit'], last_activity_at=timezone.now()) Ticket.objects.create(customer=customer, category=category, subject='سابقه محرمانه دیگر', owner=case['outsider'], selected_expert=case['outsider'], origin_visibility='SELECTED', owning_unit=case['unit'], last_activity_at=timezone.now()) inactive = User.objects.create_user(username='inactive-picker', mobile='09123334444', is_active=False) CustomerProfile.objects.create(user=inactive, first_name='محمد', last_name='رضایی') client.force_login(case['creator']) url = reverse('staff_payments:customer_search') for query in ['محمد رضایی', 'شرکت آفتاب', '۰۹۱۲۰۰۰۹۹۹۹', '0912', visible.ticket_number, 'سابقه خرید قابل مشاهده']: response = client.get(url, {'q': query}) assert response.status_code == 200 assert 'no-store' in response['Cache-Control'] result = response.json()['results'][0] assert result['id'] == str(customer.public_id) assert result['name'] == 'محمد رضایی' assert result['mobile'] == customer.local_mobile assert result['ticket_count'] == 1 assert [row['subject'] for row in result['history']] == [visible.subject] assert client.get(url, {'q': 'سابقه محرمانه دیگر'}).json()['results'] == [] assert client.get(url, {'q': 'م'}).json()['results'] == [] configuration = PaymentAccessConfiguration.objects.first() configuration.open_create = False configuration.save() assert client.get(url, {'q': 'محمد'}).status_code == 403 client.force_login(customer) assert client.get(url, {'q': 'محمد'}).status_code == 403 def test_notice_customer_prefill_uses_uuid_and_no_full_directory(case, client): client.force_login(case['creator']) url = reverse('staff_payments:notice_new') response = client.get(url, {'customer': str(case['customer'].public_id)}) assert response.status_code == 200 assert response.context['form']['customer'].value() == case['customer'].pk assert response.context['customer_selection']['id'] == str(case['customer'].public_id) html = response.content.decode() assert 'type="hidden" name="customer"' in html assert '