What
Introduce a shared fixed-point/decimal money type (or a well-tested decimal library) for payroll-related financial math, and migrate the repeated parseFloat/toFixed pattern across tax, benefits, and bonus calculations to use it.
Why
Three separate services repeat the same anti-pattern of rounding each intermediate value to a fixed number of decimals via .toFixed() and then accumulating results with native JS floating-point addition:
backend/src/services/taxService.ts — calculateDeductionsRuleBased (L242-278): deductedAmount = parseFloat((grossAmount * (Number(rule.value) / 100)).toFixed(7)), then totalTax += deductedAmount accumulated as a JS number
backend/src/services/benefitsService.ts — L64: parseFloat(n.toFixed(7))
backend/src/services/payrollBonusService.ts — L222-227: .reduce((sum, item) => sum + parseFloat(item.amount), 0)
JS floats can't exactly represent most decimals. Rounding each line item to 7 decimals (matching Stellar's stroop precision) and then summing with float addition across an arbitrary number of stacked deduction/tax rules lets rounding error accumulate — net payroll amounts submitted on-chain can silently diverge from gross minus deductions by fractions of a stroop, compounding across pay periods and audit reports.
Scope
In scope:
- Introduce (or adopt an existing) fixed-point/decimal type for all money math in these three services
- Migrate the specific call sites listed above to use it consistently
- Tests demonstrating the old code's drift on a stacked multi-rule example, and that the new code doesn't drift
Out of scope:
- Contract-side (Rust) math — that's tracked separately (
#398 covers revenue_split dust/rounding)
- Non-financial numeric calculations elsewhere in the backend
Acceptance Criteria
Technical Context
backend/src/services/taxService.ts L242-278
backend/src/services/benefitsService.ts L64
backend/src/services/payrollBonusService.ts L222-227
What
Introduce a shared fixed-point/decimal money type (or a well-tested decimal library) for payroll-related financial math, and migrate the repeated
parseFloat/toFixedpattern across tax, benefits, and bonus calculations to use it.Why
Three separate services repeat the same anti-pattern of rounding each intermediate value to a fixed number of decimals via
.toFixed()and then accumulating results with native JS floating-point addition:backend/src/services/taxService.ts—calculateDeductionsRuleBased(L242-278):deductedAmount = parseFloat((grossAmount * (Number(rule.value) / 100)).toFixed(7)), thentotalTax += deductedAmountaccumulated as a JSnumberbackend/src/services/benefitsService.ts— L64:parseFloat(n.toFixed(7))backend/src/services/payrollBonusService.ts— L222-227:.reduce((sum, item) => sum + parseFloat(item.amount), 0)JS floats can't exactly represent most decimals. Rounding each line item to 7 decimals (matching Stellar's stroop precision) and then summing with float addition across an arbitrary number of stacked deduction/tax rules lets rounding error accumulate — net payroll amounts submitted on-chain can silently diverge from gross minus deductions by fractions of a stroop, compounding across pay periods and audit reports.
Scope
In scope:
Out of scope:
#398coversrevenue_splitdust/rounding)Acceptance Criteria
taxService.ts,benefitsService.ts,payrollBonusService.tsTechnical Context
backend/src/services/taxService.tsL242-278backend/src/services/benefitsService.tsL64backend/src/services/payrollBonusService.tsL222-227