# [DEF:TransactionCore:Module] # @TIER: CRITICAL # @SEMANTICS: Finance, ACID, Transfer, Ledger # @PURPOSE: Core banking transaction processor with ACID guarantees. # @LAYER: Domain (Core) # @RELATION: DEPENDS_ON -> [DEF:Infra:PostgresDB] # @RELATION: DEPENDS_ON -> [DEF:Infra:AuditLog] # @INVARIANT: Total system balance must remain constant (Double-Entry Bookkeeping). # @INVARIANT: Negative transfers are strictly forbidden. # @TEST_DATA: sufficient_funds -> {"from": "acc_A", "to": "acc_B", "amt": 100.00} # @TEST_DATA: insufficient_funds -> {"from": "acc_empty", "to": "acc_B", "amt": 1000.00} # @TEST_DATA: concurrency_lock -> {./fixtures/transactions.json#race_condition} from decimal import Decimal from typing import NamedTuple from ...core.logger import belief_scope from ...core.db import atomic_transaction, get_balance, update_balance from ...core.exceptions import BusinessRuleViolation class TransferResult(NamedTuple): tx_id: str status: str new_balance: Decimal # [DEF:execute_transfer:Function] # @PURPOSE: Atomically move funds between accounts with audit trails. # @PARAM: sender_id (str) - Source account. # @PARAM: receiver_id (str) - Destination account. # @PARAM: amount (Decimal) - Positive amount to transfer. # @PRE: amount > 0; sender != receiver; sender_balance >= amount. # @POST: sender_balance -= amount; receiver_balance += amount; Audit Record Created. # @SIDE_EFFECT: Database mutation (Rows locked), Audit IO. # # @UX_STATE: Success -> Returns 200 OK + Transaction Receipt. # @UX_STATE: Error(LowBalance) -> 422 Unprocessable -> UI shows "Top-up needed" modal. # @UX_STATE: Error(System) -> 500 Internal -> UI shows "Retry later" toast. def execute_transfer(sender_id: str, receiver_id: str, amount: Decimal) -> TransferResult: # Guard: Input Validation if amount <= Decimal("0.00"): raise BusinessRuleViolation("Transfer amount must be positive.") if sender_id == receiver_id: raise BusinessRuleViolation("Cannot transfer to self.") with belief_scope("execute_transfer") as context: context.logger.info("Initiating transfer", data={"from": sender_id, "to": receiver_id}) try: # 1. Action: Atomic DB Transaction # @RELATION: CALLS -> atomic_transaction with atomic_transaction(): # Guard: State Validation (Strict) current_balance = get_balance(sender_id, for_update=True) if current_balance < amount: # @UX_FEEDBACK: Triggers specific UI flow for insufficient funds context.logger.warn("Insufficient funds", data={"balance": current_balance}) raise BusinessRuleViolation("INSUFFICIENT_FUNDS") # 2. Action: Mutation new_src_bal = update_balance(sender_id, -amount) new_dst_bal = update_balance(receiver_id, +amount) # 3. Action: Audit tx_id = context.audit.log_transfer(sender_id, receiver_id, amount) context.logger.info("Transfer committed", data={"tx_id": tx_id}) return TransferResult(tx_id, "COMPLETED", new_src_bal) except BusinessRuleViolation as e: # Logic: Explicit re-raise for UI mapping raise e except Exception as e: # Logic: Catch-all safety net context.logger.error("Critical Transfer Failure", error=e) raise RuntimeError("TRANSACTION_ABORTED") from e # [/DEF:execute_transfer:Function] # [/DEF:TransactionCore:Module]