"""Treasury API — Phase 5.4.""" from __future__ import annotations from datetime import date from decimal import Decimal from uuid import UUID from fastapi import APIRouter, Depends, status from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_db, require_tenant from app.core.security import get_current_user from app.models.treasury import Bank, BankAccount, CashBox from app.repositories.base import TenantBaseRepository from app.services.treasury_service import TreasuryService from shared.security import CurrentUser router = APIRouter() class CashBoxCreate(BaseModel): code: str name: str account_id: UUID | None = None opening_balance: Decimal = Decimal("0") class CashBoxRead(BaseModel): id: UUID tenant_id: UUID code: str name: str current_balance: Decimal is_active: bool model_config = {"from_attributes": True} class CashReceiptRequest(BaseModel): cash_box_id: UUID amount: Decimal debit_account_id: UUID credit_account_id: UUID description: str | None = None transaction_date: date | None = None class BankCreate(BaseModel): code: str name: str class BankAccountCreate(BaseModel): bank_id: UUID account_number: str iban: str | None = None class CashBoxRepo(TenantBaseRepository[CashBox]): model = CashBox class BankRepo(TenantBaseRepository[Bank]): model = Bank class BankAccountRepo(TenantBaseRepository[BankAccount]): model = BankAccount @router.post("/cash-boxes", response_model=CashBoxRead, status_code=status.HTTP_201_CREATED) async def create_cash_box( body: CashBoxCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CashBoxRepo(db) entity = CashBox( tenant_id=tenant_id, code=body.code, name=body.name, account_id=body.account_id, opening_balance=body.opening_balance, current_balance=body.opening_balance, ) await repo.add(entity) await db.commit() await db.refresh(entity) return entity @router.get("/cash-boxes", response_model=list[CashBoxRead]) async def list_cash_boxes( tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CashBoxRepo(db) return await repo.list_by_tenant(tenant_id, limit=100) @router.post("/cash-receipts") async def record_cash_receipt( body: CashReceiptRequest, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(get_current_user), ): svc = TreasuryService(db) tx = await svc.record_cash_receipt( tenant_id, body.cash_box_id, body.amount, debit_account_id=body.debit_account_id, credit_account_id=body.credit_account_id, actor_user_id=user.user_id, description=body.description, transaction_date=body.transaction_date, ) await db.commit() return {"id": str(tx.id), "voucher_id": str(tx.voucher_id)} @router.post("/banks", status_code=status.HTTP_201_CREATED) async def create_bank( body: BankCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = BankRepo(db) entity = Bank(tenant_id=tenant_id, code=body.code, name=body.name) await repo.add(entity) await db.commit() return {"id": str(entity.id)} @router.post("/bank-accounts", status_code=status.HTTP_201_CREATED) async def create_bank_account( body: BankAccountCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = BankAccountRepo(db) entity = BankAccount( tenant_id=tenant_id, bank_id=body.bank_id, account_number=body.account_number, iban=body.iban, ) await repo.add(entity) await db.commit() return {"id": str(entity.id)} @router.get("/banks") async def list_banks( tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): banks = await BankRepo(db).list_by_tenant(tenant_id, limit=100) return [{"id": str(b.id), "code": b.code, "name": b.name, "is_active": b.is_active} for b in banks] @router.get("/bank-accounts") async def list_bank_accounts( tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): accounts = await BankAccountRepo(db).list_by_tenant(tenant_id, limit=100) return [ { "id": str(a.id), "bank_id": str(a.bank_id), "account_number": a.account_number, "iban": a.iban, "is_active": a.is_active, } for a in accounts ] @router.patch("/cash-boxes/{cash_box_id}") async def update_cash_box( cash_box_id: UUID, body: dict, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): from shared.exceptions import NotFoundError repo = CashBoxRepo(db) entity = await repo.get(tenant_id, cash_box_id) if entity is None: raise NotFoundError("صندوق یافت نشد", error_code="cash_box_not_found") if "name" in body and body["name"] is not None: entity.name = body["name"] if "is_active" in body and body["is_active"] is not None: entity.is_active = bool(body["is_active"]) await db.commit() await db.refresh(entity) return CashBoxRead.model_validate(entity)