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>
This commit is contained in:
Carlos Escalante
2026-06-10 13:38:28 -06:00
parent 881d879ed1
commit 74886fbf98
7 changed files with 414 additions and 72 deletions

View File

@@ -66,9 +66,9 @@ def get_accounts() -> list[dict]:
"bank": a.bank.value,
"label": a.label,
"currency": a.currency.value,
"balance": a.balance,
"balance": float(a.balance),
"account_type": a.account_type.value,
"next_payment": a.next_payment,
"next_payment": float(a.next_payment) if a.next_payment is not None else None,
}
for a in rows
]
@@ -79,15 +79,15 @@ def get_net_worth() -> dict:
USD/EUR balances are converted at the latest exchange rate."""
accounts = _s().exec(select(Account)).all()
rate = get_current_rate(_s())
sell = rate.sell_rate if rate else 600.0
sell = float(rate.sell_rate) if rate else 600.0
assets_crc = 0.0
liabilities_crc = 0.0
for a in accounts:
amt = a.balance
amt = float(a.balance)
if a.currency.value == "USD":
amt = a.balance * sell
amt = float(a.balance) * sell
elif a.currency.value == "EUR":
amt = a.balance * sell * 1.08 # rough; real conversion is endpoint-side
amt = float(a.balance) * sell * 1.08 # rough; real conversion is endpoint-side
if a.account_type.value == "LIABILITY":
liabilities_crc += amt
else:
@@ -139,7 +139,7 @@ def get_recent_transactions(
"id": t.id,
"date": t.date.isoformat(),
"merchant": t.merchant,
"amount": t.amount,
"amount": float(t.amount),
"currency": t.currency.value,
"source": t.source.value,
"transaction_type": t.transaction_type.value,
@@ -243,7 +243,7 @@ def list_recurring_items() -> list[dict]:
{
"id": r.id,
"name": r.name,
"amount": r.amount,
"amount": float(r.amount),
"currency": r.currency.value,
"item_type": r.item_type.value,
"frequency": r.frequency.value,
@@ -281,12 +281,12 @@ def get_pension_snapshots(
"fund": r.fund.value,
"period_start": r.period_start.isoformat(),
"period_end": r.period_end.isoformat(),
"saldo_anterior": r.saldo_anterior,
"aportes": r.aportes,
"rendimientos": r.rendimientos,
"retiros": r.retiros,
"comision": r.comision,
"saldo_final": r.saldo_final,
"saldo_anterior": float(r.saldo_anterior),
"aportes": float(r.aportes),
"rendimientos": float(r.rendimientos),
"retiros": float(r.retiros),
"comision": float(r.comision),
"saldo_final": float(r.saldo_final),
}
for r in rows
]
@@ -334,11 +334,11 @@ def get_municipal_receipts(
"period": r.period,
"account": r.account,
"finca": r.finca,
"subtotal": r.subtotal,
"interests": r.interests,
"iva": r.iva,
"total": r.total,
"water_consumption_m3": sum(w.consumption_m3 for w in readings),
"subtotal": float(r.subtotal),
"interests": float(r.interests),
"iva": float(r.iva),
"total": float(r.total),
"water_consumption_m3": float(sum(w.consumption_m3 for w in readings)),
}
)
return out
@@ -436,8 +436,8 @@ def get_exchange_rate() -> dict:
if not rate:
return {"buy_rate": None, "sell_rate": None, "date": None}
return {
"buy_rate": rate.buy_rate,
"sell_rate": rate.sell_rate,
"buy_rate": float(rate.buy_rate),
"sell_rate": float(rate.sell_rate),
"date": rate.date.isoformat(),
}