Happy House - Ecommerce Docs

EMI Calculator Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the EMI Calculator module.

EMI Calculator - Features and Flows

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Technical design.omc/plans/emicalc/master-plan.md, consumer-handoff.mdDesign rationale, the frozen contract
APIcustomer/emi-customer.controller.ts, admin/*/*.controller.ts25 routes, permissions, rate limits
Backendcustomer/emi-customer.service.ts, shared/*.service.tsEstimator, offerability, eligibility
Schemapackages/db/src/schema/emi/*.ts6 tables, enums, constraints
Mathapps/api/src/utils/emi/emi-math.util.tsThe exact-rational arithmetic
QA.omc/plans/emicalc/qa-gate-ledger.mdVerified behavior and how

2. Feature Summary

FieldValue
Moduleemi
Submodulecustomer (2 routes), admin (settings, tenures, eligibility, banks, rates, bank-tenures)
Primary user valueA customer sees "from Rs X/month" options and a precise installment for any offered bank and period — computed exactly, never by the browser
Actorsguest (public customer routes), admin (merchandiser), admin (rate-entry)
Main entry points2 public GET routes; 23 admin routes
Main outputsQuotes (monthly EMI, total repayment, total interest), bank comparisons, configuration state
Related docsBackend, API

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
GuestRead options and calculate for a live, eligible productAnything elseNone (@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/productsEnter interest ratesEmi_*Emi_UPDATE is the WIDER grant — it carries the kill switch
Admin (rate-entry)Manage banks, their rate history, their offered periodsTag categories, flip the switchEmiBanks_*Rate history is append-only

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
List EMI optionsCustomerGuestGET /api/mobile/emi/products/{productId}/optionsproduct, settings, banks§8.1
Calculate one quoteCustomerGuestGET .../calculate?bankId&tenureMonthsproduct, settings, banks§8.2
Read/update settingsAdminMerchandiserGET/PUT /api/admin/emi/settingssettingssettings + cache invalidation§8.3/8.4
List/replace tenure vocabularyAdminMerchandiserGET/PUT /api/admin/emi/tenurestenurestenures + invalidation§8.5/8.6
Eligibility: categoriesAdminMerchandiserGET/POST/DELETE /api/admin/emi/eligibility/categoriescategories, expansionselection tables + invalidation§8.7–8.9
Eligibility: productsAdminMerchandiserGET/POST/DELETE .../eligibility/productsproductsselection tables + invalidation§8.10–8.12
Effective product listAdminMerchandiserGET .../eligibility/effective-productssettings, expansion, products§8.13
Banks CRUD + reorderAdminRate-entryGET/POST/PATCH/DELETE/RESTORE /api/admin/emi/banks (+PATCH /reorder)banksbanks + invalidation§8.14–8.19
Bank rate historyAdminRate-entryGET/POST/DELETE /api/admin/emi/banks/{bankId}/ratesratesrates + invalidation§8.20–8.22
Bank tenuresAdminRate-entryGET/PUT /api/admin/emi/banks/{bankId}/tenurestenures, banklink rows + invalidation§8.23/8.24

5. User-Facing Flows

5.1 The customer estimator

Summary

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.

Preconditions

  • The feature is enabled store-wide and the product is eligible.
  • Both routes are public; neither requires login.

Main Flow

StepActor/SystemActionResultSource
1CustomerOpens the product pagePage calls /options (no variantId yet)emi-customer.service.ts
2BackendResolves product, settings, banks200 with available: true, variantId: null + rollup quotessame
2bCustomerChooses a configurationPage re-calls /options?variantId=…same
2cBackendResolves that variant, scoped to the product200 with variantId echoed + that configuration's quotes, or 404 EMI_VARIANT_NOT_FOUND if it does not belong to this productresolvePricingBasis
3CustomerSelects bank + periodPage calls /calculate with the SAME variantIdsame
4BackendValidates bank offerability, tenure whitelist, loan limits200 with the exact breakdown, variantId/variantName echoedsame

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Feature disabledisEnabled: false/options 200 available: false; /calculate 403EMI_DISABLED
Product ineligibleNot in selection/options 200 available: false; /calculate 403EMI_PRODUCT_NOT_ELIGIBLE
Product price zerosellingPrice <= 0 or base ≤ 0/options 200 available: false; /calculate 409EMI_PRICE_NOT_AVAILABLE
No banksNone offerable/options 200 available: false; /calculate 409EMI_NO_BANK_AVAILABLE
Unknown bankNot offered/calculate 404EMI_BANK_NOT_FOUND — re-fetch /options
Unsupported tenureNot in bank's set/calculate 400EMI_TENURE_NOT_SUPPORTED
Below/above loan limitsPrice outside bank's range/calculate 409EMI_AMOUNT_BELOW_BANK_MINIMUM / ..._ABOVE_BANK_MAXIMUM
Product goneDeleted or non-liveBoth 404EMI_PRODUCT_NOT_FOUND
Variant not this product'sWithdrawn, soft-deleted, or belongs to a different productBoth 404 — identity failure, not an eligibility answerEMI_VARIANT_NOT_FOUND
Unexpected query paramCampaign tag or cache-buster appendedBoth 400 (forbidNonWhitelisted) — a real behavior change from before the variant work, when /options took no query DTOVALIDATION_FAILED
Arithmetic out of rangeGuard has a hole422EMI_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.

6. Admin Flows

6.1 Turning the feature on

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.

6.2 Entering a bank and a rate

StepActionResult
1POST /banks (name, logo, loan limits, display order)Bank live if not soft-deleted and active
2POST /banks/{id}/rates (bps, effectiveFrom)Rate scheduled; becomes the quoted rate at effective_from
3PUT /banks/{id}/tenures (months from the vocabulary)Offered periods replaced wholesale, in a transaction with a FOR UPDATE lock
4ReorderPATCH /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.

7. Lifecycle and State Transitions

EntityFromEvent/ActionToGuard ConditionSide Effects
emi_bankdeletedRestorelivenoneOfferable again once rate + tenure exist
emi_bankliveDeletesoft-deletednot referenced (RESTRICT)Disappears from catalog
emi_bank_ratescheduledTime passesin forceeffective_from <= now()Becomes the quoted rate
emi_bank_ratescheduledDeletecancelledeffective_from > now() onlyCancel a not-yet-effective change
emi_bank_ratein forceDeleterefusedEMI_RATE_IN_FORCE_NOT_DELETABLECorrection = new row
emi_tenureactiveDeactivateinactivenot referenced by bank tenures (RESTRICT)Hidden from new assignments

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeAnalyticsNotifications
Customer readsnonenone
Settings updateemi_setting upsertemi domain invalidation
Tenure replaceemi_tenure rowsemi invalidation
Eligibility changesselection tablesemi invalidation
Bank CRUDemi_bankemi invalidation
Rate create/deleteemi_bank_rateemi invalidation
Bank tenure replacelink 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.

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Redis downCache read failsCustomer reads fall through to DB (warn logged)Automaticemi-config.service.ts, emi-bank-catalog.service.ts
Rate not yet effectiveBefore effective_fromOld rate still quotedAutomatic at the boundary (TTL cap)catalog service
Bank structurally unusableActive but no rate/tenureFiltered from catalogAdmin adds the missing rowcatalog service
Concurrent tenure replaceTwo adminsSerialised by FOR UPDATE on the bank rowbank-tenure service
Concurrent bank reorderTwo adminsTransactional rewrite, last winsbank service
RESTRICT refusalDelete in-use tenure/bankCoded error, not a raw 23503/23001Operator removes references firstemi.constants.ts (isReferencedRowError)

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
OptionsInapplicable banks greyed with reasonGuest/optionsEmpty quotes + unavailableReasoncomputed per bankcustomer service
OptionsbasePrice echoedGuest/options, /calculateReprice detectable client-sidesame
Optionsunlisted products includedGuest/optionsDetail-page estimate worksEMI_LIVE_PRODUCT_STATUSESconstants
CalculateisEstimate: true alwaysGuest/calculateUI states it is an estimatesame
SettingsDefaults match column defaultsAdminmissing rowisEnabled false, selling_price, selectedDEFAULT_SETTINGSconfig service
TenuresReplace is transactionalAdminPUT /tenuresSet replaced wholesaledeactivate vs deletetenure service
Bank createName NFC-normalisedAdminPOST /banksVisually identical names blockednormalizeBankNameconstants
Rate createFull ISO instantAdminPOST /ratesNo TZ guessingmidnight Asia/Kathmandu coercionrate service
Rate deleteOnly future ratesAdminDELETE /ratesScheduled change cancellableEMI_RATE_IN_FORCE_NOT_DELETABLErate service
Effective productssellingPrice > 0 filterAdmin.../effective-productsZero-price products excludedmirrored customer gateeligibility service
ReorderIdempotent rewriteAdminPATCH /banks/reorderMissing ids → 404displayOrder by indexbank service

12.2 Business Process Diagram Pack

The estimator flow (§5.1) and the enablement flow (§6.1) cover the business processes; the admin bank/rate entry is in §6.2.

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
Exact-rational arithmeticFloat EMI is wrong by a paisaCustomer sees the right numberemi-math.util.tsBigInt only31 unit tests, 3 mutations caught
Round-half-up + ceil(P/n) floorCeiling overcharged Rs 909Never a negative interestmath utiltheorem, not clampprobe, 8,640 combos
0% reports zero interestUniform formula lies on 0% plansCorrect 0% quotesmath utilnamed branchunit tests
Tenure ≤ 120DoS control, exponentBounded requestsmath util + DTO400 beyond0.004ms vs 572ms measuredunit tests
Two price sourcesConstraint makes "higher/lower" meaninglessSimpler admin choiceenum + products CHECKschema-contract spec asserts the products constraintemi-schema-contract.int.spec.ts
Tenure vocabularyComparison table must line upNo 13-vs-12 columnsemi_tenure + FKEMI_TENURE_NOT_IN_VOCABULARYdomain, not union
Rate history append-onlyRecord of what was advertisedCorrect via supersessionrate service + partial uniqueEMI_RATE_IN_FORCE_NOT_DELETABLEno PATCHint spec
Offerable = 4 conditionsTwo are unconstrainableNo unusable banks showncatalog filteractive + rate + tenureint spec
Bank list TTL capped at next rateClock events invalidate nothingNo stale advertised rateresolveTtlSecondsmin(VOLATILE, boundary), ≥1int spec
Nothing price-derived cachedStale price = wrong installmentRepriced product quotes right next requestno price keysconfig-only cache
Redis down = uncached, not 500getOrSet rethrows on lock pathFeature survives Redis losscachedOrDirectwarn + direct read

12.4 Tradeoffs and Product Rationale

Product DecisionUser BenefitEngineering BenefitAlternativeTradeoffRisk
Server computes all quotesBrowser can't driftExactness where it mattersClient-side formulaPayload sizenil — 160 BigInt ops ≈ 0.004ms each
available:false instead of 4xxDistinguishes "no EMI" from "no product"4xx
Off by defaultShips inertSafe deploy orderingOn by defaultOne extra admin step
Basis points not floatsFeeds exact arithmeticSchema-enforceablenumericReader surprise (1200 = 12%)documented
30s config cacheFast readsBounded staleness5 min30s wait on changesa longer TTL would keep offering withdrawn products
Append-only ratesAudit of advertised rateseditable columndelete restricted to future
Separate Emi/EmiBanks permsMerchandiser ≠ rate-entryLeast privilegeone modulemore grantsEmi_UPDATE is the wider one — non-obvious

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
Optionsunlisted productDirect URLAvailable if eligibleestimate shownconstants
OptionsZero selling priceBad dataavailable: falseno quotecustomer service
CalculateTenure 12.5Bad input400 VALIDATION_FAILEDDTOcustomer DTO
CalculateTenure 1,000,000DoS attempt400 VALIDATION_FAILEDboundedmath util
CalculateBank offered but period notHand-made request400 EMI_TENURE_NOT_SUPPORTEDre-fetch optionscustomer service
Rate deleteRate in forceAdmin mistake409 EMI_RATE_IN_FORCE_NOT_DELETABLEsupersede insteadrate service
Rate deleteRate scheduled, same date re-enteredCancel + correctsecond insert allowedpartial unique on live rowsrate schema
Bank tenure replaceDeactivated periodNew assignment400 EMI_TENURE_NOT_IN_VOCABULARYname the periodbank-tenure service
EligibilityCategory cycleAdmin selectsprevented by products-schema no-cycle checkscategory schema
ConcurrencyTwo tenure replacesBoth adminsserialised, not unionFOR UPDATE

12.6 Flow-to-Data Trace

FlowReadsWritesCacheJobs/EventsResponse Fields
Optionsproduct, settings, bankssettings/banks readavailable, reason, banks, quotes
Calculateproduct, settings, bankssamebreakdown + isEstimate
Settings updateemi_settinginvalidate emisettings DTO
Bank createemi_bankinvalidate emibank DTO
Rate createbankemi_bank_rateinvalidate emirate DTO
Tenure replacebank, vocabularylink rowsinvalidate emitenure list

12.7 Experience Quality Checklist

  • 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).

13. Completion Checklist

  • 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).

See Also

  • API doc: /docs/developer/emi/api
  • Backend doc: /docs/developer/emi/backend
  • TDD: not yet published