Files
WealthySmart/backend/app/services/savings_accrual.py
Carlos Escalante 74886fbf98 Migrate all 29 money columns from float to NUMERIC/Decimal (BE-02)
Models use Money = Annotated[Decimal, PlainSerializer(float)] so storage
is exact NUMERIC(15,2) (rates 15,6) while JSON keeps emitting plain
numbers — the frontend contract is unchanged, pinned by
test_models_serialization.py. Service layers coerce to float at their
boundaries (projection math, rate multipliers, agent tool output, the
savings constants become Decimal) so no naive Decimal/float mixing can
raise. Migration 7f567c8bafdb applied to dev: per-row values and the
table sum byte-identical before/after; endpoints verified live on the
new schema. 59/59 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:38:28 -06:00

65 lines
1.6 KiB
Python

from decimal import Decimal
from sqlmodel import Session, select
from app.models.models import (
Account,
AccountType,
Bank,
SavingsAccrual,
Transaction,
)
MEMP_MONTHLY = Decimal("200000")
MPAT_MONTHLY = Decimal("200000")
def _get_savings_account(session: Session, bank: Bank) -> Account | None:
return session.exec(
select(Account).where(
Account.account_type == AccountType.SAVINGS,
Account.bank == bank,
)
).first()
def maybe_apply_monthly_savings(session: Session, tx: Transaction) -> SavingsAccrual | None:
"""Apply monthly savings contribution if this is the first salary of the month.
Idempotent: if a SavingsAccrual row already exists for (year, month), do nothing.
Bumps MEMP and MPAT savings account balances and records the accrual.
"""
year = tx.date.year
month = tx.date.month
existing = session.exec(
select(SavingsAccrual).where(
SavingsAccrual.year == year,
SavingsAccrual.month == month,
)
).first()
if existing:
return None
memp = _get_savings_account(session, Bank.MEMP)
mpat = _get_savings_account(session, Bank.MPAT)
if memp is None or mpat is None:
return None
memp.balance += MEMP_MONTHLY
mpat.balance += MPAT_MONTHLY
session.add(memp)
session.add(mpat)
accrual = SavingsAccrual(
year=year,
month=month,
memp_amount=MEMP_MONTHLY,
mpat_amount=MPAT_MONTHLY,
trigger_transaction_id=tx.id,
)
session.add(accrual)
session.commit()
session.refresh(accrual)
return accrual