Files
finance_bot/app/services/transaction_service.py
2025-12-10 22:09:31 +09:00

146 lines
4.6 KiB
Python

"""
Transaction Service - Core business logic
Handles transaction creation, approval, reversal with audit trail
"""
from datetime import datetime
from typing import Optional, Dict, Any
from decimal import Decimal
import logging
from sqlalchemy.orm import Session
from app.db.models import Transaction, Account, Family, User
from app.security.rbac import RBACEngine, Permission, UserContext
logger = logging.getLogger(__name__)
class TransactionService:
"""Manages financial transactions with approval workflow"""
APPROVAL_THRESHOLD = 500.0
def __init__(self, db: Session):
self.db = db
async def create_transaction(
self,
user_context: UserContext,
family_id: int,
from_account_id: Optional[int],
to_account_id: Optional[int],
amount: Decimal,
category_id: Optional[int] = None,
description: str = "",
requires_approval: bool = False,
) -> Dict[str, Any]:
"""Create new transaction"""
RBACEngine.check_permission(user_context, Permission.CREATE_TRANSACTION)
RBACEngine.check_family_access(user_context, family_id)
if amount <= 0:
raise ValueError("Amount must be positive")
needs_approval = requires_approval or (float(amount) > self.APPROVAL_THRESHOLD and user_context.role.value != "owner")
tx_status = "pending_approval" if needs_approval else "executed"
transaction = Transaction(
family_id=family_id,
created_by_id=user_context.user_id,
from_account_id=from_account_id,
to_account_id=to_account_id,
amount=float(amount),
category_id=category_id,
description=description,
created_at=datetime.utcnow(),
)
self.db.add(transaction)
self.db.commit()
logger.info(f"Transaction created: {transaction.id}")
return {
"id": transaction.id,
"status": tx_status,
"amount": float(amount),
"requires_approval": needs_approval,
}
async def confirm_transaction(
self,
user_context: UserContext,
transaction_id: int,
family_id: int,
) -> Dict[str, Any]:
"""Approve pending transaction"""
RBACEngine.check_permission(user_context, Permission.APPROVE_TRANSACTION)
RBACEngine.check_family_access(user_context, family_id)
tx = self.db.query(Transaction).filter_by(
id=transaction_id,
family_id=family_id,
).first()
if not tx:
raise ValueError(f"Transaction {transaction_id} not found")
tx.status = "executed"
tx.approved_by_id = user_context.user_id
tx.approved_at = datetime.utcnow()
self.db.commit()
logger.info(f"Transaction {transaction_id} approved")
return {
"id": tx.id,
"status": "executed",
}
async def reverse_transaction(
self,
user_context: UserContext,
transaction_id: int,
family_id: int,
) -> Dict[str, Any]:
"""Reverse transaction by creating compensation"""
RBACEngine.check_permission(user_context, Permission.REVERSE_TRANSACTION)
RBACEngine.check_family_access(user_context, family_id)
original = self.db.query(Transaction).filter_by(
id=transaction_id,
family_id=family_id,
).first()
if not original:
raise ValueError(f"Transaction {transaction_id} not found")
if original.status == "reversed":
raise ValueError("Transaction already reversed")
reversal = Transaction(
family_id=family_id,
created_by_id=user_context.user_id,
from_account_id=original.to_account_id,
to_account_id=original.from_account_id,
amount=original.amount,
category_id=original.category_id,
description=f"Reversal of transaction #{original.id}",
status="executed",
created_at=datetime.utcnow(),
)
original.status = "reversed"
original.reversed_at = datetime.utcnow()
original.reversed_by_id = user_context.user_id
self.db.add(reversal)
self.db.commit()
logger.info(f"Transaction {transaction_id} reversed, created {reversal.id}")
return {
"original_id": original.id,
"reversal_id": reversal.id,
"status": "reversed",
}