Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the EMI Calculator module.
Source Type Files or Docs What Was Extracted Technical design .omc/plans/emicalc/master-plan.md, consumer-handoff.mdDesign rationale, the frozen contract API customer/emi-customer.controller.ts, admin/*/*.controller.ts25 routes, permissions, rate limits Backend customer/emi-customer.service.ts, shared/*.service.tsEstimator, offerability, eligibility Schema packages/db/src/schema/emi/*.ts6 tables, enums, constraints Math apps/api/src/utils/emi/emi-math.util.tsThe exact-rational arithmetic QA .omc/plans/emicalc/qa-gate-ledger.mdVerified behavior and how
Field Value Module emiSubmodule customer (2 routes), admin (settings, tenures, eligibility, banks, rates, bank-tenures)Primary user value A customer sees "from Rs X/month" options and a precise installment for any offered bank and period — computed exactly, never by the browser Actors guest (public customer routes), admin (merchandiser), admin (rate-entry)Main entry points 2 public GET routes; 23 admin routes Main outputs Quotes (monthly EMI, total repayment, total interest), bank comparisons, configuration state Related docs Backend , API
Actor Can Do Cannot Do Auth Requirement Notes Guest Read options and calculate for a live, eligible product Anything else None (@Public()) Both routes are GETs; nothing is created Admin (merchandiser) Flip the store-wide switch, set price source, set eligibility mode, manage the tenure vocabulary, tag categories/products Enter interest rates Emi_*Emi_UPDATE is the WIDER grant — it carries the kill switchAdmin (rate-entry) Manage banks, their rate history, their offered periods Tag categories, flip the switch EmiBanks_*Rate history is append-only
Capability Surface Actor Route/Trigger State Read State Written Linked API Section List EMI options Customer Guest GET /api/mobile/emi/products/{productId}/optionsproduct, settings, banks — §8.1 Calculate one quote Customer Guest GET .../calculate?bankId&tenureMonthsproduct, settings, banks — §8.2 Read/update settings Admin Merchandiser GET/PUT /api/admin/emi/settingssettings settings + cache invalidation §8.3/8.4 List/replace tenure vocabulary Admin Merchandiser GET/PUT /api/admin/emi/tenurestenures tenures + invalidation §8.5/8.6 Eligibility: categories Admin Merchandiser GET/POST/DELETE /api/admin/emi/eligibility/categoriescategories, expansion selection tables + invalidation §8.7–8.9 Eligibility: products Admin Merchandiser GET/POST/DELETE .../eligibility/productsproducts selection tables + invalidation §8.10–8.12 Effective product list Admin Merchandiser GET .../eligibility/effective-productssettings, expansion, products — §8.13 Banks CRUD + reorder Admin Rate-entry GET/POST/PATCH/DELETE/RESTORE /api/admin/emi/banks (+PATCH /reorder)banks banks + invalidation §8.14–8.19 Bank rate history Admin Rate-entry GET/POST/DELETE /api/admin/emi/banks/{bankId}/ratesrates rates + invalidation §8.20–8.22 Bank tenures Admin Rate-entry GET/PUT /api/admin/emi/banks/{bankId}/tenurestenures, bank link rows + invalidation §8.23/8.24
On a product detail page, the storefront asks /options before rendering any "Calculate EMI"
affordance. The response says whether EMI is available at all, why not if it is not, and — when
available — gives every offerable bank with its rate, loan limits, applicability, and already
computed quotes per offered period. The customer then picks a bank and period; /calculate
returns the single precise breakdown.
Both routes take an optional variantId. Before the customer has chosen a configuration, the
page omits it and gets the product's "from" estimate — the same rollup the card shows. Once a
configuration is chosen, the page passes its variantPublicId and both routes reprice against
THAT configuration's own MRP/selling price instead of the cheapest sibling's — a multi-configuration
product financed off the rollup would have quoted the expensive configuration an installment plan
computed from the cheap one's price.
The feature is enabled store-wide and the product is eligible.
Both routes are public; neither requires login.
Step Actor/System Action Result Source 1 Customer Opens the product page Page calls /options (no variantId yet) emi-customer.service.ts2 Backend Resolves product, settings, banks 200 with available: true, variantId: null + rollup quotes same 2b Customer Chooses a configuration Page re-calls /options?variantId=… same 2c Backend Resolves that variant, scoped to the product 200 with variantId echoed + that configuration's quotes, or 404 EMI_VARIANT_NOT_FOUND if it does not belong to this product resolvePricingBasis3 Customer Selects bank + period Page calls /calculate with the SAME variantId same 4 Backend Validates bank offerability, tenure whitelist, loan limits 200 with the exact breakdown, variantId/variantName echoed same
Branch Condition Behavior Error/Result Feature disabled isEnabled: false/options 200 available: false; /calculate 403EMI_DISABLEDProduct ineligible Not in selection /options 200 available: false; /calculate 403EMI_PRODUCT_NOT_ELIGIBLEProduct price zero sellingPrice <= 0 or base ≤ 0/options 200 available: false; /calculate 409EMI_PRICE_NOT_AVAILABLENo banks None offerable /options 200 available: false; /calculate 409EMI_NO_BANK_AVAILABLEUnknown bank Not offered /calculate 404EMI_BANK_NOT_FOUND — re-fetch /optionsUnsupported tenure Not in bank's set /calculate 400EMI_TENURE_NOT_SUPPORTEDBelow/above loan limits Price outside bank's range /calculate 409EMI_AMOUNT_BELOW_BANK_MINIMUM / ..._ABOVE_BANK_MAXIMUMProduct gone Deleted or non-live Both 404 EMI_PRODUCT_NOT_FOUNDVariant not this product's Withdrawn, soft-deleted, or belongs to a different product Both 404 — identity failure, not an eligibility answer EMI_VARIANT_NOT_FOUNDUnexpected query param Campaign tag or cache-buster appended Both 400 (forbidNonWhitelisted) — a real behavior change from before the variant work, when /options took no query DTO VALIDATION_FAILEDArithmetic out of range Guard has a hole 422 EMI_CALCULATION_OUT_OF_RANGE — report it
/options never returns 4xx for an ineligible product — it answers 200 with
available: false, deliberately, so the storefront can distinguish "this product has no EMI"
from "no such product". Only a genuinely missing product 404s.
Settings, eligible-category ids and the bank list are cached for 30 seconds — an administrator
flipping the switch, adding a bank, changing a rate or changing eligibility takes effect within 30
seconds, not instantly.
Step Action Result 1 POST /banks (name, logo, loan limits, display order)Bank live if not soft-deleted and active 2 POST /banks/{id}/rates (bps, effectiveFrom)Rate scheduled; becomes the quoted rate at effective_from 3 PUT /banks/{id}/tenures (months from the vocabulary)Offered periods replaced wholesale, in a transaction with a FOR UPDATE lock 4 Reorder PATCH /banks/reorder rewrites display order
A bank is offerable only when all four hold: active, not deleted, a rate in force, and at
least one period. The last two are cross-table cardinality minimums no constraint can enforce, so
the catalog filters — a live, structurally unusable bank is a legal database state reachable by
one valid admin call.
Entity From Event/Action To Guard Condition Side Effects emi_bankdeleted Restore live none Offerable again once rate + tenure exist emi_banklive Delete soft-deleted not referenced (RESTRICT) Disappears from catalog emi_bank_ratescheduled Time passes in force effective_from <= now()Becomes the quoted rate emi_bank_ratescheduled Delete cancelled effective_from > now() onlyCancel a not-yet-effective change emi_bank_ratein force Delete refused EMI_RATE_IN_FORCE_NOT_DELETABLECorrection = new row emi_tenureactive Deactivate inactive not referenced by bank tenures (RESTRICT) Hidden from new assignments
Flow DB Writes Cache Effects Jobs Realtime Analytics Notifications Customer reads none none — — — — Settings update emi_setting upsertemi domain invalidation— — — — Tenure replace emi_tenure rowsemi invalidation— — — — Eligibility changes selection tables emi invalidation— — — — Bank CRUD emi_bankemi invalidation— — — — Rate create/delete emi_bank_rateemi invalidation— — — — Bank tenure replace link rows (tx + lock) emi invalidation— — — —
Every admin mutation invalidates the emi cache domain after commit (fire-and-forget by
contract). No queue, no realtime, no analytics writes.
Scenario Trigger User/System Experience Recovery Source Redis down Cache read fails Customer reads fall through to DB (warn logged) Automatic emi-config.service.ts, emi-bank-catalog.service.tsRate not yet effective Before effective_from Old rate still quoted Automatic at the boundary (TTL cap) catalog service Bank structurally unusable Active but no rate/tenure Filtered from catalog Admin adds the missing row catalog service Concurrent tenure replace Two admins Serialised by FOR UPDATE on the bank row — bank-tenure service Concurrent bank reorder Two admins Transactional rewrite, last wins — bank service RESTRICT refusal Delete in-use tenure/bank Coded error, not a raw 23503/23001 Operator removes references first emi.constants.ts (isReferencedRowError)
Feature Minor Behavior Actor Trigger User/System Result Backend Side Effect Source Options Inapplicable banks greyed with reason Guest /optionsEmpty quotes + unavailableReason computed per bank customer service Options basePrice echoedGuest /options, /calculateReprice detectable client-side — same Options unlisted products includedGuest /optionsDetail-page estimate works EMI_LIVE_PRODUCT_STATUSESconstants Calculate isEstimate: true alwaysGuest /calculateUI states it is an estimate — same Settings Defaults match column defaults Admin missing row isEnabled false, selling_price, selectedDEFAULT_SETTINGSconfig service Tenures Replace is transactional Admin PUT /tenuresSet replaced wholesale deactivate vs delete tenure service Bank create Name NFC-normalised Admin POST /banksVisually identical names blocked normalizeBankNameconstants Rate create Full ISO instant Admin POST /ratesNo TZ guessing midnight Asia/Kathmandu coercion rate service Rate delete Only future rates Admin DELETE /ratesScheduled change cancellable EMI_RATE_IN_FORCE_NOT_DELETABLErate service Effective products sellingPrice > 0 filterAdmin .../effective-productsZero-price products excluded mirrored customer gate eligibility service Reorder Idempotent rewrite Admin PATCH /banks/reorderMissing ids → 404 displayOrder by index bank service
The estimator flow (§5.1) and the enablement flow (§6.1) cover the business processes; the
admin bank/rate entry is in §6.2.
Rule Business Reason Actor Impact Enforced In API Impact Backend Impact Tests Exact-rational arithmetic Float EMI is wrong by a paisa Customer sees the right number emi-math.util.ts— BigInt only 31 unit tests, 3 mutations caught Round-half-up + ceil(P/n) floor Ceiling overcharged Rs 909 Never a negative interest math util — theorem, not clamp probe, 8,640 combos 0% reports zero interest Uniform formula lies on 0% plans Correct 0% quotes math util — named branch unit tests Tenure ≤ 120 DoS control, exponent Bounded requests math util + DTO 400 beyond 0.004ms vs 572ms measured unit tests Two price sources Constraint makes "higher/lower" meaningless Simpler admin choice enum + products CHECK — schema-contract spec asserts the products constraint emi-schema-contract.int.spec.tsTenure vocabulary Comparison table must line up No 13-vs-12 columns emi_tenure + FKEMI_TENURE_NOT_IN_VOCABULARYdomain, not union — Rate history append-only Record of what was advertised Correct via supersession rate service + partial unique EMI_RATE_IN_FORCE_NOT_DELETABLEno PATCH int spec Offerable = 4 conditions Two are unconstrainable No unusable banks shown catalog filter — active + rate + tenure int spec Bank list TTL capped at next rate Clock events invalidate nothing No stale advertised rate resolveTtlSeconds— min(VOLATILE, boundary), ≥1 int spec Nothing price-derived cached Stale price = wrong installment Repriced product quotes right next request no price keys — config-only cache — Redis down = uncached, not 500 getOrSet rethrows on lock pathFeature survives Redis loss cachedOrDirect— warn + direct read —
Product Decision User Benefit Engineering Benefit Alternative Tradeoff Risk Server computes all quotes Browser can't drift Exactness where it matters Client-side formula Payload size nil — 160 BigInt ops ≈ 0.004ms each available:false instead of 4xxDistinguishes "no EMI" from "no product" — 4xx — — Off by default Ships inert Safe deploy ordering On by default One extra admin step — Basis points not floats Feeds exact arithmetic Schema-enforceable numeric Reader surprise (1200 = 12%) documented 30s config cache Fast reads Bounded staleness 5 min 30s wait on changes a longer TTL would keep offering withdrawn products Append-only rates Audit of advertised rates — editable column delete restricted to future — Separate Emi/EmiBanks perms Merchandiser ≠ rate-entry Least privilege one module more grants Emi_UPDATE is the wider one — non-obvious
Flow Edge Case Trigger Expected Behavior User/System Feedback Source Options unlisted productDirect URL Available if eligible estimate shown constants Options Zero selling price Bad data available: falseno quote customer service Calculate Tenure 12.5 Bad input 400 VALIDATION_FAILED DTO customer DTO Calculate Tenure 1,000,000 DoS attempt 400 VALIDATION_FAILED bounded math util Calculate Bank offered but period not Hand-made request 400 EMI_TENURE_NOT_SUPPORTED re-fetch options customer service Rate delete Rate in force Admin mistake 409 EMI_RATE_IN_FORCE_NOT_DELETABLE supersede instead rate service Rate delete Rate scheduled, same date re-entered Cancel + correct second insert allowed partial unique on live rows rate schema Bank tenure replace Deactivated period New assignment 400 EMI_TENURE_NOT_IN_VOCABULARY name the period bank-tenure service Eligibility Category cycle Admin selects prevented by products-schema no-cycle checks — category schema Concurrency Two tenure replaces Both admins serialised, not union — FOR UPDATE
Flow Reads Writes Cache Jobs/Events Response Fields Options product, settings, banks — settings/banks read — available, reason, banks, quotes Calculate product, settings, banks — same — breakdown + isEstimate Settings update — emi_settinginvalidate emi — settings DTO Bank create — emi_bankinvalidate emi — bank DTO Rate create bank emi_bank_rateinvalidate emi — rate DTO Tenure replace bank, vocabulary link rows invalidate emi — tenure list
The doc explains what the actor is trying to accomplish (estimate, configure, enter rates).
The doc explains what the backend does the actor does not see (exact arithmetic, offerability filter, TTL cap).
Every minor flow and branch is covered (inapplicable banks, 0% plans, unlisted products, cache-down).
User, admin and system flows are included.
Business logic, tradeoffs and rationale are explained (§12.3, §12.4).
Every flow maps to API routes and backend side effects (§4, §9, §12.6).
Diagrams fit each flow type (sequence, activity, state).
Edge cases and failure recovery are covered (§5.1, §10, §12.5).
Every feature, minor action, and submodule capability is listed (§4, §12.1).
Every actor has allowed and forbidden behavior (§3).
Every major and minor flow includes steps, branches, and diagrams (§5, §6, §12.2).
Every lifecycle has a transition table and state diagram (§7).
Every flow links to the API and backend docs (§4, §12.6).
TDD dependencies are called out where they shape behavior (probe, mutation checks, §12.3).
API doc: /docs/developer/emi/api
Backend doc: /docs/developer/emi/backend
TDD: not yet published