One cent disappears without a floating-point bug
Divide a 100-cent amount equally among three invoice lines. Rounding each exact share independently gives 33 + 33 + 33 = 99 cents. With a two-cent amount, the same rule gives 1 + 1 + 1 = 3 cents. Both discrepancies exist with exact rational arithmetic. Changing the number type does not decide where the remaining cent belongs.
The downloadable model compares independent half-up rounding with an allocation rule that starts from an authoritative integer total. Across a constructed grid of 3,750 allocations, independent rounding disagrees with that total in 1,411 cases. The allocation rule has zero sum mismatches on this grid. These are results for deliberately enumerated inputs, not an estimate of how often real SaaS invoices fail.
The useful distinction is between calculating a total and distributing one. If the billing contract says each line must be rounded first, sum those committed line amounts to form the total. If it supplies a document amount that must be distributed across lines, preserve that amount through an explicit allocation policy. Switching between these contracts during rendering creates two competing authorities.
Download the executed arithmetic model and its exact inputs and outputs. The model makes no payment requests and computes no tax, foreign exchange or jurisdiction-specific invoice rule.
Define the amount before defining the rounding rule
An amount needs a currency, a scale and a stage in the calculation. In the USD example, 100 integer minor units display as 1.00. In the JPY example, one unit displays as 1. A separate three-decimal example uses a configured scale of three. It illustrates the arithmetic and does not assert support for a particular payment method or currency.
Stripe's currency documentation describes minor-unit API amounts and currency-specific exceptions. Read the convention for the exact operation you integrate. A charge, a payout and a displayed invoice should not inherit one hardcoded "multiply by 100" assumption simply because the first supported currency used two decimals.
Keep unit prices and intermediate calculations distinct from posted amounts. A fractional unit price may need more precision than a final line amount. Preserve the input decimal text. Converting a binary float into a decimal retains the float's approximation. Python's Decimal documentation describes explicit rounding modes and fixed-exponent quantization. Those facilities help implement a selected policy. They cannot select the policy for your product.
For this experiment, the total is already an integer and the weights are nonnegative decimal strings. Exact rational shares come from those inputs using Python's Fraction type. No intermediate division is rounded. Production code still needs input-size limits and a documented precision budget for the calculation that creates the total.
Allocate the residual with a stable tie-break
The model uses largest fractional remainders. First calculate each exact share from the total and the relative weight. Take the floor of each share. Subtract their sum from the total. Give one additional minor unit to that many lines, ordered by descending fractional remainder, with a stable line ID breaking ties.
For 100 cents and three equal weights, each share is 33⅓ cents. The floors contribute 99. One cent remains, and line a wins the declared ID tie-break: a = 34, b = 33, c = 33. The result does not depend on the order in which a client supplied the lines.
This policy preserves the sum and keeps each allocated amount less than one minor unit from its exact share. It also concentrates a tied residual on the same ID. That is a consequence worth approving: stable reproducibility and rotating the favored line are different product goals. A round-robin policy would need additional persisted state and its own retry behavior.
The allocator accepts a zero total with zero weights and returns zero amounts. It rejects a positive total with zero weights, duplicate IDs, negative weights, noninteger totals, negative totals and an empty line set. Signed credits are deliberately outside this function. Mixing positive and negative shares requires a separate contract. Silently taking absolute values would change the business meaning.
Read the fixtures in minor units
The table reports integer amounts. The scale controls display only; it does not alter the allocation algorithm. "Independent" means each exact share is rounded half-up in isolation. The reversal row follows stored original amounts rather than running the allocator again.
| Fixture | Total and scale | Independent line sum | Committed allocation |
|---|---|---|---|
| Equal USD thirds | 100; scale 2 | 99 | a 34, b 33, c 33 |
| Two USD cents | 2; scale 2 | 3 | a 1, b 1, c 0 |
| Uneven weights 1:2:3 | 101; scale 2 | 102 | a 17, b 34, c 50 |
| One JPY unit | 1; scale 0 | 2 | a 1, b 0 |
| Three-decimal amount | 1001; scale 3 | 1002 | a 334, b 334, c 333 |
| Zero total and weights | 0; scale 2 | 0 | a 0, b 0, c 0 |
| Single line | 100; scale 2 | 100 | a 100 |
| Full stored reversal | −100; scale 2 | Not recalculated | a −34, b −33, c −33 |
The uneven example reveals why "put the difference on the last line" is another policy. Its exact shares are 16⅚, 33⅔ and 50½. The largest remainders belong to a and b, so they receive the two residual units. A last-line adjustment would produce a different distribution while possibly preserving the same total. Compare the allocation, not just the header sum.
Sensitivity checks have a defined denominator
The grid contains every three-line weight combination from 1 through 5 and every total from 0 through 29: 125 weight combinations times 30 totals. Every allocated sum equals its input total, and every share stays within the model's stated one-unit error bound. The script asserts both properties during execution.
The five nontrivial positive fixtures also undergo input-order checks. Four have three lines and one has two, producing 26 permutations in total. Stable IDs preserve each line's assigned amount in all 26. Sorting by current row position instead would make the same invoice change when its rows were reordered.
This sample is exhaustive only within its small grid. It excludes huge inputs, signed netting, multiple tax categories, discounts with distinct eligibility, currency conversion and provider-specific rounding. A zero failure count on the grid establishes those executed arithmetic invariants. It says nothing about database atomicity or the correctness of an actual tax invoice.
Persist the allocation that customers received
Store the policy version, authoritative total, currency convention, input weights, stable line IDs and committed amounts with the invoice calculation. A PDF renderer and an API serializer should read the same committed amounts. Recalculating from current catalog prices or an updated policy can change a document that the customer has already received.
For the modeled full reversal, negate the stored original distribution: −34, −33 and −33. For a partial reversal of line a, the relevant starting amount is its committed 34 cents. Do not infer 33 cents by independently rounding an old ideal share. The application must separately define how partial quantities, discounts and previously issued credits affect the remaining reversible amount.
Bind a reversal to the original invoice and line identity, record its own operation ID and protect it against repeat application. Those are ledger rules beyond the arithmetic model. The existing usage-metering correction walkthrough covers correction identity and explainable revisions; this article supplies the separate amount-allocation contract.
For a SaaS team deciding where custom calculation ends and subscription billing begins, Pharos Production's SaaS development services for billing and metering describe subscription infrastructure, usage-event reconciliation and invoice generation. That scope is useful when assigning ownership between a metering pipeline, a billing ledger and a payment provider. The service page does not prescribe this rounding algorithm.
Verify the handoff with the actual billing system
Start with a small accepted invoice and inspect the values at each boundary: unrounded pricing inputs, authoritative total, committed line amounts, serialized provider request and returned invoice. Record the currency convention and rounding stage for each. A comparison of the final displayed totals alone cannot identify which boundary changed a value.
Use the fixtures as integration stimuli. Reorder equal-weight lines, repeat the same operation, replay a stored document after a policy change and reverse the line that received a residual unit. Check that both PDF and API views retain the committed amounts. Then add product-specific categories and credit rules that this model excludes.
Reject a discrepancy before treating it as a payment adjustment. An incorrect currency scale, duplicated line or missing discount can produce a much larger error than rounding. Reconciliation should distinguish a declared residual-allocation decision from an unexplained difference.
The arithmetic decision is narrow: a document-level total can remain authoritative while its distribution stays deterministic and auditable. Whether that is the correct invoice policy belongs to the billing contract and the requirements of the actual invoicing system. Implement those requirements first, then test the sum, each line identity and the subsequent reversal.
Sources
Documentation checked .

Dreamtsoft Editorial
Editorial follow-up: the 3,750-case grid describes constructed arithmetic inputs. It is not a real invoice error rate. The next integration check should compare committed line amounts with the provider’s actual invoice fields.