""" Transaction API Endpoints - CRUD + Approval Workflow """ from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from typing import Optional, List from decimal import Decimal from datetime import datetime from sqlalchemy.orm import Session from app.db.database import get_db from app.services.transaction_service import TransactionService from app.security.rbac import UserContext, RBACEngine, MemberRole, Permission from app.core.config import settings import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/transactions", tags=["transactions"]) # Request/Response Models class TransactionCreateRequest(BaseModel): family_id: int from_wallet_id: Optional[int] = None to_wallet_id: Optional[int] = None category_id: Optional[int] = None amount: Decimal description: str notes: Optional[str] = None class Config: schema_extra = { "example": { "family_id": 1, "from_wallet_id": 10, "to_wallet_id": 11, "category_id": 5, "amount": 50.00, "description": "Rent payment", } } class TransactionResponse(BaseModel): id: int status: str # draft, pending_approval, executed, reversed amount: Decimal description: str confirmation_required: bool created_at: datetime class Config: from_attributes = True class TransactionConfirmRequest(BaseModel): confirmation_token: Optional[str] = None class TransactionReverseRequest(BaseModel): reason: Optional[str] = None # Dependency to extract user context async def get_user_context(request: Request) -> UserContext: """Extract user context from JWT""" user_id = getattr(request.state, "user_id", None) family_id = getattr(request.state, "family_id", None) if not user_id or not family_id: raise HTTPException(status_code=401, detail="Invalid authentication") # Load user role from DB (simplified for MVP) # In production: Load from users->family_members join role = MemberRole.OWNER # TODO: Load from DB permissions = RBACEngine.get_permissions(role) return UserContext( user_id=user_id, family_id=family_id, role=role, permissions=permissions, family_ids=[family_id], device_id=getattr(request.state, "device_id", None), client_id=getattr(request.state, "client_id", None), ) @router.post( "", response_model=TransactionResponse, status_code=201, summary="Create new transaction", ) async def create_transaction( request: TransactionCreateRequest, user_context: UserContext = Depends(get_user_context), db: Session = Depends(get_db), ) -> TransactionResponse: """ Create a new financial transaction. **Request Headers Required:** - Authorization: Bearer - X-Client-Id: telegram_bot | web_frontend | ios_app - X-Signature: HMAC_SHA256(...) - X-Timestamp: unix timestamp **Response:** - If amount ≤ threshold: status="executed" immediately - If amount > threshold: status="pending_approval", requires confirmation **Events Emitted:** - transaction.created """ try: service = TransactionService(db) result = await service.create_transaction( user_context=user_context, family_id=request.family_id, from_wallet_id=request.from_wallet_id, to_wallet_id=request.to_wallet_id, amount=request.amount, category_id=request.category_id, description=request.description, ) return TransactionResponse(**result) except PermissionError as e: logger.warning(f"Permission denied: {e} (user: {user_context.user_id})") raise HTTPException(status_code=403, detail=str(e)) except ValueError as e: logger.warning(f"Validation error: {e}") raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Error creating transaction: {e}", exc_info=True) raise HTTPException(status_code=500, detail="Internal server error") @router.post( "/{transaction_id}/confirm", response_model=TransactionResponse, summary="Confirm pending transaction", ) async def confirm_transaction( transaction_id: int, request: TransactionConfirmRequest, user_context: UserContext = Depends(get_user_context), db: Session = Depends(get_db), ): """ Approve a pending transaction for execution. Only owner or designated approver can confirm. **Events Emitted:** - transaction.confirmed - transaction.executed """ try: service = TransactionService(db) result = await service.confirm_transaction( user_context=user_context, transaction_id=transaction_id, confirmation_token=request.confirmation_token, ) return TransactionResponse(**result) except (PermissionError, ValueError) as e: raise HTTPException(status_code=400, detail=str(e)) @router.delete( "/{transaction_id}", response_model=dict, summary="Reverse (cancel) transaction", ) async def reverse_transaction( transaction_id: int, request: TransactionReverseRequest, user_context: UserContext = Depends(get_user_context), db: Session = Depends(get_db), ): """ Reverse (cancel) executed transaction. Creates a compensation (reverse) transaction instead of deletion. Original transaction status changes to "reversed". **Events Emitted:** - transaction.reversed - transaction.created (compensation) """ try: service = TransactionService(db) result = await service.reverse_transaction( user_context=user_context, transaction_id=transaction_id, reason=request.reason, ) return result except (PermissionError, ValueError) as e: raise HTTPException(status_code=400, detail=str(e)) @router.get( "", response_model=List[TransactionResponse], summary="List transactions", ) async def list_transactions( family_id: int, skip: int = 0, limit: int = 20, user_context: UserContext = Depends(get_user_context), db: Session = Depends(get_db), ): """ List all transactions for family. **Filtering:** - ?family_id=1 - ?wallet_id=10 - ?category_id=5 - ?status=executed - ?from_date=2023-12-01&to_date=2023-12-31 **Pagination:** - ?skip=0&limit=20 """ # Verify family access RBACEngine.check_family_access(user_context, family_id) from app.db.models import Transaction transactions = db.query(Transaction).filter( Transaction.family_id == family_id, ).offset(skip).limit(limit).all() return [TransactionResponse.from_orm(t) for t in transactions] @router.get( "/{transaction_id}", response_model=TransactionResponse, summary="Get transaction details", ) async def get_transaction( transaction_id: int, user_context: UserContext = Depends(get_user_context), db: Session = Depends(get_db), ): """Get detailed transaction information""" from app.db.models import Transaction transaction = db.query(Transaction).filter( Transaction.id == transaction_id, Transaction.family_id == user_context.family_id, ).first() if not transaction: raise HTTPException(status_code=404, detail="Transaction not found") return TransactionResponse.from_orm(transaction)