87 lines
4.2 KiB
Python
87 lines
4.2 KiB
Python
# [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]
|
||
#
|
||
# @INVARIANT: Total system balance must remain constant (Double-Entry Bookkeeping).
|
||
# @INVARIANT: Negative transfers are strictly forbidden.
|
||
|
||
# --- Test Specifications ---
|
||
# @TEST_CONTRACT: TransferRequestDTO -> TransferResultDTO
|
||
# @TEST_SCENARIO: sufficient_funds -> Returns COMPLETED, balances updated.
|
||
# @TEST_FIXTURE: sufficient_funds -> file:./__tests__/fixtures/transfers.json#happy_path
|
||
# @TEST_EDGE: insufficient_funds -> Throws BusinessRuleViolation("INSUFFICIENT_FUNDS").
|
||
# @TEST_EDGE: negative_amount -> Throws BusinessRuleViolation("Transfer amount must be positive.").
|
||
# @TEST_EDGE: concurrency_conflict -> Throws DBTransactionError.
|
||
#
|
||
# @TEST_INVARIANT: total_balance_constant -> VERIFIED_BY: [sufficient_funds, concurrency_conflict]
|
||
# @TEST_INVARIANT: negative_transfer_forbidden -> VERIFIED_BY: [negative_amount]
|
||
|
||
from decimal import Decimal
|
||
from typing import NamedTuple
|
||
# GRACE: Импорт глобального логгера с семантическими методами
|
||
from ...core.logger import logger, belief_scope
|
||
from ...core.db import atomic_transaction, get_balance, update_balance
|
||
from ...core.audit import log_audit_trail
|
||
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.
|
||
# @DATA_CONTRACT: Input -> (sender_id: str, receiver_id: str, amount: Decimal), Output -> TransferResult
|
||
# @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.
|
||
def execute_transfer(sender_id: str, receiver_id: str, amount: Decimal) -> TransferResult:
|
||
# Guard: Input Validation (Вне belief_scope, так как это trivial проверка)
|
||
if amount <= Decimal("0.00"):
|
||
raise BusinessRuleViolation("Transfer amount must be positive.")
|
||
if sender_id == receiver_id:
|
||
raise BusinessRuleViolation("Cannot transfer to self.")
|
||
|
||
# GRACE: Используем strict Context Manager без 'as context'
|
||
with belief_scope("execute_transfer"):
|
||
# GRACE: [REASON] - Жесткая дедукция, начало алгоритма
|
||
logger.reason("Initiating transfer", extra={"from": sender_id, "to": receiver_id, "amount": amount})
|
||
|
||
try:
|
||
# @RELATION: CALLS -> atomic_transaction
|
||
with atomic_transaction():
|
||
current_balance = get_balance(sender_id, for_update=True)
|
||
|
||
if current_balance < amount:
|
||
# GRACE: [EXPLORE] - Отклонение от Happy Path (фолбэк/ошибка)
|
||
logger.explore("Insufficient funds validation hit", extra={"balance": current_balance})
|
||
raise BusinessRuleViolation("INSUFFICIENT_FUNDS")
|
||
|
||
# Mutation
|
||
new_src_bal = update_balance(sender_id, -amount)
|
||
new_dst_bal = update_balance(receiver_id, +amount)
|
||
|
||
# Audit
|
||
tx_id = log_audit_trail("TRANSFER", sender_id, receiver_id, amount)
|
||
|
||
# GRACE:[REFLECT] - Сверка с @POST перед возвратом
|
||
logger.reflect("Transfer committed successfully", extra={"tx_id": tx_id, "new_balance": new_src_bal})
|
||
|
||
return TransferResult(tx_id, "COMPLETED", new_src_bal)
|
||
|
||
except BusinessRuleViolation as e:
|
||
# Explicit re-raise for UI mapping
|
||
raise e
|
||
except Exception as e:
|
||
# GRACE: [EXPLORE] - Неожиданный сбой
|
||
logger.explore("Critical Transfer Failure", exc_info=e)
|
||
raise RuntimeError("TRANSACTION_ABORTED") from e
|
||
#[/DEF:execute_transfer:Function]
|
||
|
||
# [/DEF:TransactionCore:Module] |