EMI Calculator Module Overview
The exact-rational EMI estimator — bank offerability, tenure vocabulary, rate history, and two customer read routes, all off by default.
Audience: Product owners, QA, frontend and backend developers Scope: The EMI estimator, bank configuration, eligibility, the tenure vocabulary, rate history, and the exact arithmetic behind every quote
EMI Calculator Module - Overview
1. What the module is
EMI answers one question: if a customer finances a product's price at a bank's annual rate over a
period, what is the monthly payment? Two customer routes compute it — and the platform is not a
lender. It processes no loans, approves no financing, contacts no bank, reserves nothing and
affects no price. Every customer route is a read and isEstimate is always true.
The feature is off by default (is_enabled = false), so the backend ships inert until an
administrator turns it on.
2. The routes — 25 (2 customer, 23 admin)
| Surface | Route | Auth |
|---|---|---|
| Customer | GET /api/mobile/emi/products/{productId}/options | public, PUBLIC_READ |
| Customer | GET /api/mobile/emi/products/{productId}/calculate?bankId={uuid}&tenureMonths={n} | public, PUBLIC_READ |
| Admin | GET/PUT /api/admin/emi/settings | Emi_READ / Emi_UPDATE |
| Admin | GET/PUT /api/admin/emi/tenures | Emi_READ / Emi_UPDATE |
| Admin | GET/POST/DELETE /api/admin/emi/eligibility/categories · GET/POST/DELETE .../products · GET .../effective-products | Emi_* |
| Admin | GET/POST/PATCH/DELETE/RESTORE /api/admin/emi/banks (+ PATCH /reorder) | EmiBanks_* |
| Admin | GET/POST/DELETE /api/admin/emi/banks/{bankId}/rates | EmiBanks_* |
| Admin | GET/PUT /api/admin/emi/banks/{bankId}/tenures | EmiBanks_* |
The customer routes are /api/mobile/emi/..., and there is no /api/emi/.... This API mounts
customer surfaces only under the mobile prefix — the route baseline holds /api/mobile/products
and no /api/products. The un-prefixed path is a 404.
3. The arithmetic is exact — that is the module's whole point
The textbook EMI formula in IEEE-754 doubles is a catastrophic-cancellation case, and at 0%
interest it is literally 0/0. So computeEmi runs in BigInt over an exact rational —
r = bps/120000, 1 + r = (120000 + bps)/120000, and the whole expression reduces to one integer
division with exactly one rounding at the end.
Not theoretical: at Rs 45,000 / 0.01% / 1 month the true value is exactly 4500037.5 paisa; the
naive Math.pow form computes 4500037.4999705, so Math.round returns one paisa less.
4. The rounding rule is round-half-up, floored at ceil(P/n)
Not ceiling. Ceiling was the first design and a probe overturned it: it disagreed with every
published EMI calculator by one paisa (Rs 5,00,000 at 10.5% over 60 months: 10,746.95 true,
10,746.96 ceiling), and overcharged Rs 909 at the worst permitted input. The ceil(P/n) floor is
what makes totalInterest >= 0 a theorem instead of a clamp — it binds only where the true
interest is smaller than the rounding residue.
A 0% scheme reports exactly zero interest, by definition. Applying monthlyEmi × n − P
uniformly would report 119 paisa of "interest" on a Rs 1.21 / 120-month 0% plan — 98% of the
principal.
5. The caps
| Cap | Value | Why |
|---|---|---|
tenureMonths | 1…120 | Denial-of-service control, not a preference — the value is an exponent. 120 costs 0.004 ms; 1,000,000 blocks the event loop for 572 ms on one HTTP call |
annualRateBps | 0…10000 | 100.00% per annum, mirrors chk_emi_bank_rate_bps_range |
principal | ≤ 900,659,232,695,700 paisa | Beyond it, totalRepayment exceeds MAX_SAFE_INTEGER; checked BEFORE any BigInt is built |
6. Money and rates
- Money is integer minor units (paisa).
1165704is Rs 11,657.04. Divide by 100 for display; never do arithmetic on the divided value. - Interest rates are basis points.
1200is 12.00%,1349is 13.49%. Divide by 100 to show a percentage. Anumericwould work; a float would not — this column feeds an exact-rational calculation.
7. Two price sources, not the four a reader expects
The specification lists Selling Price, MRP, Higher of the two, Lower of the two. Only two can
mean anything distinct: product carries chk_product_selling_price_not_above_mrp, so "higher
of" is ALWAYS mrp and "lower of" is ALWAYS selling_price, for every row that can exist.
Shipping four names for two behaviours would let an operator believe the choice adapts per product.
An integration test asserts that products constraint still exists — the guard, because if it ever
relaxes, this enum quietly stops meaning what the comment says.
8. The tenure vocabulary exists on purpose
Without it the only bound on a period is 1–120, and a bank could be offered 13 months while every
other offers 12 — making the comparison table meaningless. The vocabulary (seeded 3…60, nine
periods) is the DOMAIN the per-bank sets are subsets of, checkable in one direction:
emi_bank_tenure.months must appear here. Deactivation rather than deletion: retiring a period
must not silently invalidate banks already offering it.
9. Rate history is append-only
A rate is corrected by superseding it, never by editing it — editing rewrites what was
advertised. Delete is permitted only while effective_from > now() (a scheduled change that has
not taken force); a rate that is or has been in force raises EMI_RATE_IN_FORCE_NOT_DELETABLE,
because without that rule deleting the current row silently promotes its predecessor, or none.
10. Caching — configuration for 30 seconds, nothing price-derived, ever
Settings, eligible-category ids and the offerable bank list are cached (emi:v1: keys,
CACHE_TTL.VOLATILE = 30s). The bank list's TTL is additionally capped at the next rate
boundary — a rate becoming effective is a clock event, not a write, so nothing invalidates the
cache at that instant; a TTL that expires on its own has no failure mode. Product prices are
never cached by this module — a cached installment computed from a stale price is exactly the
wrong number this module exists to get right. Redis being down degrades to uncached reads, never
a 500.
11. Two permission modules — and the opposite of the obvious reading
Emi_*— the global switch, price source, eligibility mode, the tenure vocabulary, and product/category eligibility.EmiBanks_*— banks, their interest-rate history, and their offered periods.
Split deliberately: the merchandiser who tags a category and the person who enters an advertised
interest rate are different people. Emi_UPDATE is the wider grant — it carries the
store-wide kill switch and the "entire store" eligibility mode.
12. Where to go next
| Page | For |
|---|---|
| Features and flows | The customer estimator, admin configuration journeys, and the edge-case matrix |
| Backend | The exact arithmetic, schema, services, caching, queues and operations |
| API reference | All 25 endpoints with request and response shapes and error codes |
Reports & Analytics Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for Reports & Analytics.
EMI Calculator API Reference
Complete API contracts for the EMI Calculator module, including routes, auth, DTOs, responses, errors, examples, and integration notes.