"""Run against an isolated PostgreSQL test DB; SQLite cannot prove row-lock semantics. USE_SQLITE=false POSTGRES_* ... pytest tests/test_payment_concurrency.py Never point POSTGRES_DB at a production database. """ from concurrent.futures import ThreadPoolExecutor from threading import Barrier import pytest from django.db import connection, close_old_connections from django.core.exceptions import ValidationError from apps.accounts.models import User from apps.notifications.models import NotificationJob from apps.payments import services from apps.payments.models import PaymentReceipt, ReceiptDecision from tests.test_payments import case, submit pytestmark = [pytest.mark.django_db(transaction=True), pytest.mark.skipif(connection.vendor != 'postgresql', reason='Requires PostgreSQL row locks')] @pytest.mark.parametrize('actors,actions,final', [ (('first','second'), ('APPROVE','APPROVE'), 'APPROVED'), (('first','first'), ('APPROVE','APPROVE'), 'PARTIAL'), (('first','second'), ('APPROVE','REJECT'), 'REJECTED'), (('first','senior'), ('APPROVE','SENIOR_APPROVE'), 'APPROVED'), ]) def test_concurrent_decisions_have_one_consistent_final_result(case, actors, actions, final): receipt = submit(case) barrier = Barrier(2) def decide(index): close_old_connections() try: actor = User.objects.get(pk=case[actors[index]].pk) barrier.wait(timeout=10) try: services.decide_receipt(actor, receipt.public_id, 1, actions[index], 'بررسی پرداخت') return 'recorded' except ValidationError: return 'already finalized' finally: connection.close() with ThreadPoolExecutor(max_workers=2) as pool: outcomes = list(pool.map(decide, range(2))) receipt.refresh_from_db() assert receipt.status == final assert receipt.revisions.get().cycle.status == final assert outcomes.count('recorded') >= 1 votes = ReceiptDecision.objects.filter(cycle__revision__receipt=receipt) assert votes.count() == (1 if actors[0] == actors[1] else outcomes.count('recorded')) assert NotificationJob.objects.filter(event_type='PAYMENT_RECEIPT_APPROVED').count() == (1 if final == 'APPROVED' else 0)