Installments
Splitting a monetary amount into equal installment payments — ensuring the sum is always exact and no cent is lost or duplicated.
The scenario
A checkout that offers installment plans. The total must be split into n equal payments, with any indivisible remainder distributed fairly across the first installments.
Why not divide?
The naive approach — dividing the total by the number of installments — produces rounding errors that cause the sum of parts to differ from the original:
ts
import { from } from '@eriveltondasilva/currency'
const price = from(100, 'BR')
// ❌ divide — loses R$ 0,01
const installment = price.divide(3)
installment.format() // => 'R$ 33,33'
// 33.33 × 3 = 99.99 ≠ 100.00allocate() uses the largest-remainder method to distribute the indivisible cent to the first slot, guaranteeing the sum is exact:
ts
// ✅ allocate — exact
price.allocate(3).map(m => m.format())
// => ['R$ 33,34', 'R$ 33,33', 'R$ 33,33']
// 33.34 + 33.33 + 33.33 = 100.00 ✓Implementation
ts
import { from } from '@eriveltondasilva/currency'
import type { MoneyContract } from '@eriveltondasilva/currency'
interface InstallmentPlan {
count: number
installments: MoneyContract[]
total: MoneyContract
}
function buildInstallmentPlan(
amount: MoneyContract,
count: number,
): InstallmentPlan {
const installments = amount.allocate(count)
// Verify sum is exact (defensive — allocate guarantees this)
const total = installments.reduce((acc, p) => acc.plus(p), from(0, 'BR'))
return { count, installments, total }
}Usage
ts
const price = from(299.90, 'BR')
const plan3 = buildInstallmentPlan(price, 3)
const plan6 = buildInstallmentPlan(price, 6)
const plan12 = buildInstallmentPlan(price, 12)
// 3 installments
plan3.installments.map(m => m.format())
// => ['R$ 99,97', 'R$ 99,97', 'R$ 99,96']
plan3.total.equals(price) // => true ✓
// 12 installments
plan12.installments.map(m => m.format())
// => ['R$ 25,00', 'R$ 25,00', 'R$ 24,99', ...(×10)]
plan12.total.equals(price) // => true ✓Installment plan with interest
When installments carry interest (a common pattern in Brazilian e-commerce), apply the surcharge before allocating:
ts
interface InterestRate {
installments: number
rate: number // percentage surcharge per plan
}
const INTEREST_TABLE: InterestRate[] = [
{ installments: 1, rate: 0 }, // no interest
{ installments: 2, rate: 0 },
{ installments: 3, rate: 0 },
{ installments: 6, rate: 4.5 },
{ installments: 12, rate: 10.2 },
]
function buildPlanWithInterest(
price: MoneyContract,
count: number,
): InstallmentPlan {
const rateEntry = INTEREST_TABLE.find(e => e.installments === count)
if (!rateEntry) {
throw new Error(`No interest rate configured for ${count} installments.`)
}
const total = rateEntry.rate > 0
? price.applySurcharge(rateEntry.rate)
: price
return buildInstallmentPlan(total, count)
}ts
const price = from(1000, 'BR')
const plan1 = buildPlanWithInterest(price, 1)
const plan12 = buildPlanWithInterest(price, 12)
plan1.installments[0].format() // => 'R$ 1.000,00'
plan12.installments[0].format() // => 'R$ 91,84' (with 10.2% interest)
plan12.total.format() // => 'R$ 1.102,00'Displaying the plan options
ts
function printPlanOptions(price: MoneyContract, counts: number[]): void {
console.log(`Price: ${price.format()}\n`)
for (const count of counts) {
const plan = buildInstallmentPlan(price, count)
const first = plan.installments[0]
if (count === 1) {
console.log(` 1× ${first.format()} (full)`)
} else {
console.log(` ${count}× ${first.format()}`)
}
}
}
printPlanOptions(from(599.90, 'BR'), [1, 2, 3, 6, 12])Output:
Price: R$ 599,90
1× R$ 599,90 (full)
2× R$ 299,95
3× R$ 199,97
6× R$ 99,99
12× R$ 50,00