"""Treasury service — integrates with Posting Engine only.""" from __future__ import annotations from datetime import date from decimal import Decimal from uuid import UUID from sqlalchemy.ext.asyncio import AsyncSession from app.models.posting import Voucher, VoucherLine from app.models.treasury import CashBox, CashTransaction, BankAccount, BankTransaction from app.models.types import TreasuryTransactionType, VoucherStatus from app.repositories.base import TenantBaseRepository from app.repositories.foundation import FiscalPeriodRepository from app.repositories.posting import VoucherRepository from app.services.posting_engine import PostingEngine from shared.exceptions import AppError, NotFoundError class TreasuryError(AppError): status_code = 422 error_code = "treasury_error" class CashBoxRepository(TenantBaseRepository[CashBox]): model = CashBox class CashTransactionRepository(TenantBaseRepository[CashTransaction]): model = CashTransaction class BankAccountRepository(TenantBaseRepository[BankAccount]): model = BankAccount class TreasuryService: def __init__(self, session: AsyncSession) -> None: self.session = session self.cash_box_repo = CashBoxRepository(session) self.cash_tx_repo = CashTransactionRepository(session) self.bank_account_repo = BankAccountRepository(session) self.voucher_repo = VoucherRepository(session) self.period_repo = FiscalPeriodRepository(session) self.posting_engine = PostingEngine(session) async def record_cash_receipt( self, tenant_id: UUID, cash_box_id: UUID, amount: Decimal, *, debit_account_id: UUID, credit_account_id: UUID, actor_user_id: str, description: str | None = None, transaction_date: date | None = None, ) -> CashTransaction: cash_box = await self.cash_box_repo.get(tenant_id, cash_box_id) if cash_box is None: raise NotFoundError("صندوق یافت نشد", error_code="cash_box_not_found") period = await self.period_repo.get_current(tenant_id) if period is None: raise NotFoundError("دوره مالی جاری یافت نشد", error_code="period_not_found") tx_date = transaction_date or date.today() ref = f"CASH-R-{tx_date.isoformat()}-{cash_box.code}" voucher = await self._create_treasury_voucher( tenant_id, period.id, ref, tx_date, debit_account_id, credit_account_id, amount, description or "دریافت نقدی", actor_user_id, "treasury", ) tx = CashTransaction( tenant_id=tenant_id, cash_box_id=cash_box_id, transaction_type=TreasuryTransactionType.CASH_RECEIPT, transaction_date=tx_date, amount=amount, description=description, voucher_id=voucher.id, reference_number=ref, ) await self.cash_tx_repo.add(tx) cash_box.current_balance += amount return tx async def record_cash_payment( self, tenant_id: UUID, cash_box_id: UUID, amount: Decimal, *, debit_account_id: UUID, credit_account_id: UUID, actor_user_id: str, description: str | None = None, transaction_date: date | None = None, ) -> CashTransaction: cash_box = await self.cash_box_repo.get(tenant_id, cash_box_id) if cash_box is None: raise NotFoundError("صندوق یافت نشد", error_code="cash_box_not_found") if cash_box.current_balance < amount: raise TreasuryError("موجودی صندوق کافی نیست") period = await self.period_repo.get_current(tenant_id) if period is None: raise NotFoundError("دوره مالی جاری یافت نشد", error_code="period_not_found") tx_date = transaction_date or date.today() ref = f"CASH-P-{tx_date.isoformat()}-{cash_box.code}" voucher = await self._create_treasury_voucher( tenant_id, period.id, ref, tx_date, debit_account_id, credit_account_id, amount, description or "پرداخت نقدی", actor_user_id, "treasury", ) tx = CashTransaction( tenant_id=tenant_id, cash_box_id=cash_box_id, transaction_type=TreasuryTransactionType.CASH_PAYMENT, transaction_date=tx_date, amount=amount, description=description, voucher_id=voucher.id, reference_number=ref, ) await self.cash_tx_repo.add(tx) cash_box.current_balance -= amount return tx async def post_receipt_payment_voucher( self, tenant_id: UUID, *, amount: Decimal, treasury_account_id: UUID, counter_account_id: UUID, is_receipt: bool, actor_user_id: str, description: str, transaction_date: date | None = None, reference: str | None = None, cash_box_id: UUID | None = None, ) -> Voucher: """Post GL for treasury receipt (Dr treasury / Cr counter) or payment (Dr counter / Cr treasury).""" from app.services.document_number_service import DocumentNumberService from app.services.source_posting_guard import SourcePostingGuard period = await self.period_repo.get_current(tenant_id) if period is None: raise NotFoundError("دوره مالی جاری یافت نشد", error_code="period_not_found") tx_date = transaction_date or date.today() number = await DocumentNumberService(self.session).allocate(tenant_id, "voucher") if is_receipt: debit_id, credit_id = treasury_account_id, counter_account_id else: debit_id, credit_id = counter_account_id, treasury_account_id if cash_box_id is not None: cash_box = await self.cash_box_repo.get(tenant_id, cash_box_id) if cash_box is not None and cash_box.current_balance < amount: raise TreasuryError("موجودی صندوق کافی نیست") voucher = await self._create_treasury_voucher( tenant_id, period.id, number, tx_date, debit_id, credit_id, amount, description, actor_user_id, "treasury", post_immediately=await SourcePostingGuard(self.session).should_post_immediately(tenant_id), reference_number=reference, ) if cash_box_id is not None: cash_box = await self.cash_box_repo.get(tenant_id, cash_box_id) if cash_box is not None: if is_receipt: cash_box.current_balance += amount else: cash_box.current_balance -= amount return voucher async def _create_treasury_voucher( self, tenant_id: UUID, period_id: UUID, voucher_number: str, voucher_date: date, debit_account_id: UUID, credit_account_id: UUID, amount: Decimal, description: str, actor_user_id: str, source_module: str, *, post_immediately: bool = True, reference_number: str | None = None, ) -> Voucher: voucher = Voucher( tenant_id=tenant_id, fiscal_period_id=period_id, voucher_number=voucher_number, voucher_date=voucher_date, status=VoucherStatus.DRAFT, description=description, source_module=source_module, reference_number=reference_number, created_by=actor_user_id, ) await self.voucher_repo.add(voucher) self.session.add(VoucherLine( tenant_id=tenant_id, voucher_id=voucher.id, line_number=1, account_id=debit_account_id, debit=amount, credit=Decimal("0"), description=description, )) self.session.add(VoucherLine( tenant_id=tenant_id, voucher_id=voucher.id, line_number=2, account_id=credit_account_id, debit=Decimal("0"), credit=amount, description=description, )) await self.session.flush() if not post_immediately: return voucher return await self.posting_engine.post_voucher( tenant_id, voucher.id, actor_user_id=actor_user_id, source_module=source_module )