Happy House - Ecommerce Docs
Developer Resources

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)

SurfaceRouteAuth
CustomerGET /api/mobile/emi/products/{productId}/optionspublic, PUBLIC_READ
CustomerGET /api/mobile/emi/products/{productId}/calculate?bankId={uuid}&tenureMonths={n}public, PUBLIC_READ
AdminGET/PUT /api/admin/emi/settingsEmi_READ / Emi_UPDATE
AdminGET/PUT /api/admin/emi/tenuresEmi_READ / Emi_UPDATE
AdminGET/POST/DELETE /api/admin/emi/eligibility/categories · GET/POST/DELETE .../products · GET .../effective-productsEmi_*
AdminGET/POST/PATCH/DELETE/RESTORE /api/admin/emi/banks (+ PATCH /reorder)EmiBanks_*
AdminGET/POST/DELETE /api/admin/emi/banks/{bankId}/ratesEmiBanks_*
AdminGET/PUT /api/admin/emi/banks/{bankId}/tenuresEmiBanks_*

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 rationalr = 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

CapValueWhy
tenureMonths1…120Denial-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
annualRateBps0…10000100.00% per annum, mirrors chk_emi_bank_rate_bps_range
principal≤ 900,659,232,695,700 paisaBeyond it, totalRepayment exceeds MAX_SAFE_INTEGER; checked BEFORE any BigInt is built

6. Money and rates

  • Money is integer minor units (paisa). 1165704 is Rs 11,657.04. Divide by 100 for display; never do arithmetic on the divided value.
  • Interest rates are basis points. 1200 is 12.00%, 1349 is 13.49%. Divide by 100 to show a percentage. A numeric would 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

PageFor
Features and flowsThe customer estimator, admin configuration journeys, and the edge-case matrix
BackendThe exact arithmetic, schema, services, caching, queues and operations
API referenceAll 25 endpoints with request and response shapes and error codes