finance_balance.py 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. """Canonical definition and synchronization of a user's personal balance."""
  2. from sqlalchemy import and_, func, or_, select
  3. from sqlalchemy.ext.asyncio import AsyncSession
  4. from backend.app.models.finance import CostCenter, UserWallet, WalletTransaction
  5. def transaction_affects_personal_balance(
  6. user_id: int,
  7. cost_center_id: int | None,
  8. *,
  9. is_private: bool = False,
  10. owner_user_id: int | None = None,
  11. ) -> bool:
  12. """Apply the canonical definition to already-loaded transaction data."""
  13. return cost_center_id is None or (is_private and owner_user_id == user_id)
  14. def personal_balance_condition(user_id: int):
  15. """Return the SQL condition for transactions in a personal wallet.
  16. Unassigned transactions and transactions assigned to the user's own
  17. private cost center are personal. Shared cost centers are not.
  18. """
  19. return or_(
  20. WalletTransaction.cost_center_id.is_(None),
  21. and_(CostCenter.is_private.is_(True), CostCenter.owner_user_id == user_id),
  22. )
  23. async def calculate_personal_balance(db: AsyncSession, user_id: int) -> float:
  24. result = await db.execute(
  25. select(func.coalesce(func.sum(WalletTransaction.amount), 0.0))
  26. .select_from(WalletTransaction)
  27. .outerjoin(CostCenter, WalletTransaction.cost_center_id == CostCenter.id)
  28. .where(
  29. WalletTransaction.user_id == user_id,
  30. WalletTransaction.is_voided.is_(False),
  31. personal_balance_condition(user_id),
  32. )
  33. )
  34. return round(float(result.scalar_one() or 0.0), 2)
  35. async def is_personal_transaction(db: AsyncSession, user_id: int, cost_center_id: int | None) -> bool:
  36. if cost_center_id is None:
  37. return True
  38. result = await db.execute(
  39. select(CostCenter.is_private, CostCenter.owner_user_id).where(CostCenter.id == cost_center_id)
  40. )
  41. center = result.one_or_none()
  42. if center is None:
  43. return False
  44. return transaction_affects_personal_balance(
  45. user_id,
  46. cost_center_id,
  47. is_private=bool(center.is_private),
  48. owner_user_id=center.owner_user_id,
  49. )
  50. async def sync_personal_wallet_balance(db: AsyncSession, wallet: UserWallet) -> float:
  51. balance = await calculate_personal_balance(db, wallet.user_id)
  52. wallet.balance = balance
  53. db.add(wallet)
  54. return balance