init commit
This commit is contained in:
275
.history/app/api/transactions_20251210210425.py
Normal file
275
.history/app/api/transactions_20251210210425.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
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 <jwt_token>
|
||||
- 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)
|
||||
Reference in New Issue
Block a user