"""Posting Engine API — Phase 5.2.""" from __future__ import annotations from uuid import UUID from fastapi import APIRouter, Depends, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_db, get_pagination, require_tenant from app.core.security import get_current_user from app.models.posting import Voucher, VoucherLine from app.models.types import VoucherStatus from app.repositories.posting import AuditLogRepository, VoucherRepository from app.schemas.posting import ( AuditLogRead, PostRequest, ReverseRequest, VoucherCreate, VoucherRead, VoucherUpdate, ) from app.services.posting_engine import PostingEngine from shared.exceptions import AppError, NotFoundError from shared.pagination import PaginationParams from shared.security import CurrentUser router = APIRouter() @router.get("/vouchers", response_model=list[VoucherRead]) async def list_vouchers( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): from sqlalchemy import select from sqlalchemy.orm import selectinload stmt = ( select(Voucher) .options(selectinload(Voucher.lines)) .where(Voucher.tenant_id == tenant_id) .offset(pagination.offset) .limit(pagination.page_size) .order_by(Voucher.created_at.desc()) ) result = await db.execute(stmt) return list(result.scalars().all()) @router.post("/vouchers", response_model=VoucherRead, status_code=status.HTTP_201_CREATED) async def create_voucher( body: VoucherCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(get_current_user), ): repo = VoucherRepository(db) voucher = Voucher( tenant_id=tenant_id, fiscal_period_id=body.fiscal_period_id, voucher_number=body.voucher_number, voucher_date=body.voucher_date, status=VoucherStatus.DRAFT, description=body.description, reference_number=body.reference_number, source_module=body.source_module, created_by=user.user_id, ) await repo.add(voucher) for i, line in enumerate(body.lines, start=1): db.add(VoucherLine( tenant_id=tenant_id, voucher_id=voucher.id, line_number=i, **line.model_dump(), )) await db.commit() result = await repo.get_with_lines(tenant_id, voucher.id) return result @router.get("/vouchers/{voucher_id}", response_model=VoucherRead) async def get_voucher( voucher_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = VoucherRepository(db) voucher = await repo.get_with_lines(tenant_id, voucher_id) if voucher is None: raise NotFoundError("سند یافت نشد", error_code="voucher_not_found") return voucher @router.patch("/vouchers/{voucher_id}", response_model=VoucherRead) async def update_draft_voucher( voucher_id: UUID, body: VoucherUpdate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = VoucherRepository(db) voucher = await repo.get_with_lines(tenant_id, voucher_id) if voucher is None: raise NotFoundError("سند یافت نشد", error_code="voucher_not_found") if voucher.status != VoucherStatus.DRAFT: raise AppError( "فقط اسناد پیش‌نویس قابل ویرایش هستند", status_code=422, error_code="voucher_not_draft", ) data = body.model_dump(exclude_unset=True) lines_data = data.pop("lines", None) for key, value in data.items(): setattr(voucher, key, value) if lines_data is not None: for line in list(voucher.lines): await db.delete(line) await db.flush() for i, line in enumerate(lines_data, start=1): db.add( VoucherLine( tenant_id=tenant_id, voucher_id=voucher.id, line_number=i, **line, ) ) await db.commit() return await repo.get_with_lines(tenant_id, voucher_id) @router.post("/vouchers/{voucher_id}/validate", response_model=VoucherRead) async def validate_voucher( voucher_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): engine = PostingEngine(db) voucher = await engine.validate_voucher(tenant_id, voucher_id) await db.commit() return await VoucherRepository(db).get_with_lines(tenant_id, voucher.id) @router.post("/vouchers/{voucher_id}/post", response_model=VoucherRead) async def post_voucher( voucher_id: UUID, body: PostRequest, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(get_current_user), ): engine = PostingEngine(db) voucher = await engine.post_voucher( tenant_id, voucher_id, actor_user_id=user.user_id, source_module=body.source_module ) await db.commit() return await VoucherRepository(db).get_with_lines(tenant_id, voucher.id) @router.post("/vouchers/{voucher_id}/reverse", response_model=VoucherRead) async def reverse_voucher( voucher_id: UUID, body: ReverseRequest, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(get_current_user), ): engine = PostingEngine(db) voucher = await engine.reverse_voucher( tenant_id, voucher_id, actor_user_id=user.user_id, reversal_date=body.reversal_date ) await db.commit() return await VoucherRepository(db).get_with_lines(tenant_id, voucher.id) @router.post("/vouchers/{voucher_id}/cancel", response_model=VoucherRead) async def cancel_voucher( voucher_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(get_current_user), ): engine = PostingEngine(db) voucher = await engine.cancel_voucher(tenant_id, voucher_id, actor_user_id=user.user_id) await db.commit() return voucher @router.get("/audit-logs", response_model=list[AuditLogRead]) async def list_audit_logs( tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = AuditLogRepository(db) return await repo.list_by_tenant(tenant_id, limit=100)