from __future__ import annotations import threading import uuid from concurrent.futures import ThreadPoolExecutor from datetime import UTC, datetime from decimal import Decimal from savings_postgres_testkit import ( # noqa: F401 - 注册 pg_factory fixture _pg_factory_fixture, ) from sqlalchemy import func, select, text from sqlalchemy.exc import DBAPIError from sqlalchemy.orm import Session, sessionmaker from app.models.expense_case import BusinessEvent from app.models.financial_connector import ( FinancialConnectorConfig, FinancialConnectorConfigEvent, FinancialConnectorEvent, FinancialConnectorOperationalEvent, ) from app.models.financial_record import ExpenseClaim from app.models.tenant import Tenant from app.schemas.financial_connector import ( FinancialConnectorConfigLifecycleAction, FinancialEventEnvelope, ) from app.services.expense_cases import ExpenseCaseService from app.services.financial_connector_auth import ( FinancialConnectorSecretResolver, sign_financial_event, ) from app.services.financial_connector_config_lifecycle import ( FinancialConnectorConfigConflictError, FinancialConnectorConfigLifecycleService, ) from app.services.financial_connector_ingestion import ( FinancialConnectorConflictError, FinancialConnectorIngestionService, ) from app.services.financial_connector_operational_events import ( FinancialConnectorOperationalContext, FinancialConnectorOperationalEventService, operational_event_candidate, ) def test_concurrent_identical_settlement_has_one_fact_and_one_payment( pg_factory: sessionmaker[Session], ) -> None: seeded = _seed(pg_factory) timestamp = 1_800_000_000 envelope = _envelope(seeded, event_id=f"settled-{uuid.uuid4().hex}") ready = threading.Barrier(2) def ingest_once() -> bool: with pg_factory() as db: ready.wait(timeout=5) result = _ingest(db, envelope, timestamp) db.commit() return result.replayed with ThreadPoolExecutor(max_workers=2) as pool: outcomes = [ future.result(timeout=10) for future in (pool.submit(ingest_once), pool.submit(ingest_once)) ] assert sorted(outcomes) == [False, True] with pg_factory() as db: claim = db.get(ExpenseClaim, seeded[1]) assert claim is not None and claim.status == "paid" assert ( db.scalar( select(func.count(FinancialConnectorEvent.id)).where( FinancialConnectorEvent.tenant_id == seeded[0], FinancialConnectorEvent.external_event_id == envelope.external_event_id, ) ) == 1 ) assert ( db.scalar( select(func.count(BusinessEvent.id)).where( BusinessEvent.tenant_id == seeded[0], BusinessEvent.aggregate_id == seeded[1], BusinessEvent.event_type == "payment_completed", ) ) == 1 ) refund = _envelope( seeded, event_id=f"refund-{uuid.uuid4().hex}", event_type="payment_refunded", origin_external_event_id=envelope.external_event_id, ) with pg_factory() as db: _ingest(db, refund, timestamp + 1) db.commit() claim = db.get(ExpenseClaim, seeded[1]) assert claim is not None and claim.status == "pending_payment" second_settlement = _envelope( seeded, event_id=f"settled-{uuid.uuid4().hex}", ) with pg_factory() as db: _ingest(db, second_settlement, timestamp + 2) db.commit() claim = db.get(ExpenseClaim, seeded[1]) assert claim is not None and claim.status == "paid" def test_concurrent_conflicting_payload_has_one_winner_and_facts_are_append_only( pg_factory: sessionmaker[Session], ) -> None: seeded = _seed(pg_factory) timestamp = 1_800_000_000 event_id = f"failed-{uuid.uuid4().hex}" first = _envelope(seeded, event_id=event_id, event_type="payment_failed") second = first.model_copy( update={"payload": {**first.payload, "failure_code": "different_failure"}} ) ready = threading.Barrier(2) def ingest_once(envelope: FinancialEventEnvelope) -> str: with pg_factory() as db: ready.wait(timeout=5) try: _ingest(db, envelope, timestamp) db.commit() return "created" except FinancialConnectorConflictError as error: db.rollback() FinancialConnectorOperationalEventService(db).record(error.operational_event) db.commit() return "conflict" with ThreadPoolExecutor(max_workers=2) as pool: outcomes = [ future.result(timeout=10) for future in (pool.submit(ingest_once, first), pool.submit(ingest_once, second)) ] assert sorted(outcomes) == ["conflict", "created"] with pg_factory() as db: event = db.scalar( select(FinancialConnectorEvent).where( FinancialConnectorEvent.tenant_id == seeded[0], FinancialConnectorEvent.external_event_id == event_id, ) ) assert event is not None assert ( db.scalar( select(func.count(FinancialConnectorOperationalEvent.id)).where( FinancialConnectorOperationalEvent.tenant_id == seeded[0], FinancialConnectorOperationalEvent.event_type == "payload_conflict", ) ) == 1 ) savepoint = db.begin_nested() try: try: db.execute( text( "UPDATE financial_connector_events SET error_code = 'tampered' " "WHERE id = :event_id" ), {"event_id": event.id}, ) except DBAPIError: pass else: # pragma: no cover - PostgreSQL trigger must reject raise AssertionError("append-only event unexpectedly accepted UPDATE") finally: savepoint.rollback() def test_concurrent_operational_candidate_has_one_append_only_fact( pg_factory: sessionmaker[Session], ) -> None: tenant_id = f"tenant-operational-{uuid.uuid4().hex}" config_id = str(uuid.uuid4()) with pg_factory() as db: db.add( FinancialConnectorConfig( id=config_id, tenant_id=tenant_id, provider="operational-bank", environment="production", key_version="v1", secret_ref=f"connector/{tenant_id}", allowed_event_types_json=["payment_settled"], clock_skew_seconds=300, status="active", created_by="postgres-test", ) ) db.commit() context = FinancialConnectorOperationalContext( tenant_id=tenant_id, config_id=config_id, provider="operational-bank", environment="production", request_fingerprint="hmac-sha256:" + "a" * 64, external_event_fingerprint="hmac-sha256:" + "b" * 64, ) occurred_at = datetime.now(UTC) candidate = operational_event_candidate( context, event_type="replay", reason_code="duplicate_external_event", occurred_at=occurred_at, ) ready = threading.Barrier(2) def record_once() -> None: with pg_factory() as db: ready.wait(timeout=5) FinancialConnectorOperationalEventService(db).record(candidate) db.commit() with ThreadPoolExecutor(max_workers=2) as pool: for future in (pool.submit(record_once), pool.submit(record_once)): future.result(timeout=10) with pg_factory() as db: rows = list( db.scalars( select(FinancialConnectorOperationalEvent).where( FinancialConnectorOperationalEvent.tenant_id == tenant_id ) ).all() ) assert len(rows) == 1 savepoint = db.begin_nested() try: try: db.execute( text("DELETE FROM financial_connector_operational_events WHERE id = :event_id"), {"event_id": rows[0].id}, ) except DBAPIError: pass else: # pragma: no cover - PostgreSQL trigger must reject raise AssertionError("append-only operational event accepted DELETE") finally: savepoint.rollback() def test_concurrent_config_activation_has_one_version_winner( pg_factory: sessionmaker[Session], ) -> None: tenant_id = f"tenant-config-{uuid.uuid4().hex}" config_id = str(uuid.uuid4()) secret_ref = f"connector/{tenant_id}" secret = f"strong-secret-{tenant_id}" with pg_factory() as db: db.add( FinancialConnectorConfig( id=config_id, tenant_id=tenant_id, provider="versioned-bank", environment="production", key_version="v1", secret_ref=secret_ref, allowed_event_types_json=["payment_settled"], clock_skew_seconds=300, status="disabled", version=1, created_by="postgres-test", ) ) db.commit() ready = threading.Barrier(2) def activate_once(index: int) -> str: with pg_factory() as db: ready.wait(timeout=5) try: FinancialConnectorConfigLifecycleService( db, secrets=FinancialConnectorSecretResolver({secret_ref: secret}), ).activate( tenant_id=tenant_id, config_id=config_id, payload=FinancialConnectorConfigLifecycleAction( expected_version=1, request_id=f"config-activate-{index:03d}", reason="并发激活只能有一个版本胜者。", ), actor_id=f"postgres-operator-{index}", ) db.commit() return "activated" except FinancialConnectorConfigConflictError: db.rollback() return "conflict" with ThreadPoolExecutor(max_workers=2) as pool: outcomes = [ future.result(timeout=10) for future in (pool.submit(activate_once, 1), pool.submit(activate_once, 2)) ] assert sorted(outcomes) == ["activated", "conflict"] with pg_factory() as db: config = db.get(FinancialConnectorConfig, config_id) assert config is not None assert config.status == "active" and config.version == 2 assert ( db.scalar( select(func.count(FinancialConnectorConfigEvent.id)).where( FinancialConnectorConfigEvent.tenant_id == tenant_id, FinancialConnectorConfigEvent.config_id == config_id, FinancialConnectorConfigEvent.action == "activated", ) ) == 1 ) def _seed(factory: sessionmaker[Session]) -> tuple[str, str, str, str]: tenant_id = f"tenant-connector-{uuid.uuid4().hex}" claim_id = str(uuid.uuid4()) config_id = str(uuid.uuid4()) with factory() as db: db.add( Tenant( tenant_id=tenant_id, tenant_code=tenant_id, name="连接器并发探针租户", status="active", ) ) db.flush() claim = ExpenseClaim( id=claim_id, tenant_id=tenant_id, claim_no=f"BX-PG-{uuid.uuid4().hex[:12]}", employee_name="PostgreSQL 测试员工", department_name="测试部", expense_type="travel", reason="连接器并发验证", location="上海", amount=Decimal("66.00"), currency="CNY", invoice_count=1, occurred_at=datetime.now(UTC), submitted_at=datetime.now(UTC), status="pending_payment", approval_stage="待付款", risk_flags_json=[], ) db.add(claim) db.flush() ExpenseCaseService(db).ensure_case_for_claim(claim, tenant_id=tenant_id) db.add( FinancialConnectorConfig( id=config_id, tenant_id=tenant_id, provider="postgres-bank", environment="production", key_version="v1", secret_ref=f"connector/{tenant_id}", allowed_event_types_json=[ "payment_settled", "payment_failed", "payment_refunded", ], clock_skew_seconds=300, status="active", created_by="postgres-test", ) ) db.commit() return tenant_id, claim_id, config_id, claim.claim_no def _envelope( seeded: tuple[str, str, str, str], *, event_id: str, event_type: str = "payment_settled", origin_external_event_id: str | None = None, ) -> FinancialEventEnvelope: tenant_id, claim_id, _, claim_no = seeded return FinancialEventEnvelope( tenant_id=tenant_id, external_event_id=event_id, event_type=event_type, occurred_at=datetime.now(UTC), correlation_id=f"correlation-{event_id}"[:64], payload={ "claim_id": claim_id, "claim_reference": claim_no, "amount": "66.00", "currency": "CNY", "external_payment_reference": f"PAY-{event_id}", "failure_code": "provider_rejected", **( {"origin_external_event_id": origin_external_event_id} if origin_external_event_id else {} ), }, ) def _ingest( db: Session, envelope: FinancialEventEnvelope, timestamp: int, ): claim = db.get(ExpenseClaim, str(envelope.payload["claim_id"])) assert claim is not None envelope = envelope.model_copy( update={"payload": {**envelope.payload, "claim_reference": claim.claim_no}} ) secret_ref = f"connector/{envelope.tenant_id}" secret = f"secret-{envelope.tenant_id}" signature = sign_financial_event( envelope, timestamp=timestamp, secret=secret, tenant_id=envelope.tenant_id, provider="postgres-bank", key_version="v1", ) return FinancialConnectorIngestionService( db, secrets=FinancialConnectorSecretResolver({secret_ref: secret}), now_epoch=timestamp, ).ingest( envelope, tenant_header=envelope.tenant_id, provider_header="postgres-bank", key_version_header="v1", timestamp_header=str(timestamp), signature_header=signature, )