Skip to content

fix(invoices): compute invoice money in Decimal (money migration phase 2)#341

Open
snowsky wants to merge 2 commits into
mainfrom
feat/invoice-money-decimal-phase2
Open

fix(invoices): compute invoice money in Decimal (money migration phase 2)#341
snowsky wants to merge 2 commits into
mainfrom
feat/invoice-money-decimal-phase2

Conversation

@snowsky
Copy link
Copy Markdown
Owner

@snowsky snowsky commented Jun 2, 2026

Phase 2 of the invoice money migration (plan: docs/todos/invoice-money-decimal-migration-plan.md). Phase 1 (#340) moved storage to NUMERIC(15,4); this phase fixes the actual float-drift correctness bug in the calculations.

Course-correction from the original plan

Phase 1 said Phase 2 would flip the columns to asdecimal=True. An audit of the codebase found 18 Decimal × float arithmetic sites across 7 files (payments, discounts, cashflow, subscriptions, inventory) that would raise TypeError under that flip — HIGH risk. So instead this PR does "Decimal math at the edges": compute totals in Decimal and round (ROUND_HALF_UP, 2dp) at the calculation/storage boundaries, returning float for the Numeric(asdecimal=False) columns. This fixes the drift where it actually happens without destabilizing the rest of the codebase. (A future asdecimal=True hardening pass would first need those 18 sites wrapped.)

Changes

  • core/utils/money.py — extended the existing Decimal helper (reused, not duplicated) with line_amount, subtotal_from_items, compute_discount, invoice_total. Each computes in Decimal and returns float.
  • core/routers/invoices/crud.py:
    • create / clone / update now compute subtotal, discount, total and per-line amounts via the helpers (per-line rounding, consistent across all three paths).
    • paid_amount adjustment: current_paid via sum_money, and the incremental + payment-removal subtractions are Decimal-rounded — fixes the residual-penny bug (e.g. removing $33.33 + $33.33 + $33.34).

Tests

api/tests/test_money.py — extended with TestInvoiceTotals (line amounts, per-line-rounded subtotal, percentage/fixed/capped discounts, totals floored at zero, float-drift elimination). 18 money tests pass locally.

Not in this PR

  • API contract unchanged — Pydantic schemas stay float (ORM returns float via asdecimal=False), so no UI break.
  • Phase 3 (UI): round intermediates and send the rounded amount so the displayed total matches the backend to the cent.
  • Discount preview endpoints (calculate-discount, discount_rule_service) still use float; they should be aligned with compute_discount in Phase 3 for display parity.
  • No asdecimal=True flip (see above).

Verification

Helpers covered by unit tests; crud.py compiles. Full create/update/clone round-trip needs the stack up — flagging for review.

snowsky added 2 commits June 2, 2026 10:31
Extend the existing money helpers with line_amount, subtotal_from_items,
compute_discount and invoice_total. They compute in Decimal (ROUND_HALF_UP, 2dp)
and return float for the Numeric(asdecimal=False) columns, so invoice totals no
longer accumulate binary-float drift. Adds unit tests (TestInvoiceTotals).
Route subtotal/discount/total and per-line amounts through the Decimal money
helpers in create, clone and update, and round the paid_amount increment/decrease
math (current_paid sum, incremental, and the payment-removal subtractions) so a
$33.33+$33.33+$33.34 reversal no longer leaves a residual penny. The ORM still
returns float (Numeric asdecimal=False), so no other arithmetic is affected.
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
yourfinanceworks Ready Ready Preview, Comment, Open in v0 Jun 2, 2026 2:34pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant