EMI Calculator Backend Documentation
Backend architecture, data model, exact arithmetic, services, caching, and operational behavior for the EMI Calculator module.
EMI Calculator - Backend Documentation
1. Documentation Evidence
| Area | Files Inspected | Verified Details |
|---|---|---|
| Module wiring | apps/api/src/modules/emi/*.module.ts | Aggregate + customer + admin aggregate + 6 admin leaves + shared |
| Controllers | customer/emi-customer.controller.ts, admin/*/*.controller.ts | 25 routes, permissions, rate limits |
| Services | customer/emi-customer.service.ts, shared/*.service.ts, admin/*/*.service.ts | Estimator, offerability, eligibility, config |
| DTOs | customer/dto/*.ts, admin/*/dto/*.ts | Validation, examples |
| Schema | packages/db/src/schema/emi/*.ts | 6 tables, 2 enums, constraints |
| Math | apps/api/src/utils/emi/emi-math.util.ts | The exact-rational arithmetic |
| Jobs | n/a | No queue exists |
| Cache | emi.constants.ts, cache-invalidation.tags.ts | emi:v1: keys, emi domain |
2. Backend Scope and Boundaries
Owns
- The exact-rational EMI arithmetic (
computeEmi). - The customer estimator (2 public GET routes).
- Bank offerability (active + rate in force + tenure present).
- Configuration: the store-wide switch, price source, eligibility mode (cached 30s).
- The tenure vocabulary.
- Bank CRUD, append-only rate history, bank tenure replacement.
- Eligibility selection tables and the category-descendant expansion.
- The
emicache domain.
Does Not Own
- Product pricing — reads
product.selling_price/mrp, writes nothing. - Category hierarchy — reads it for descendant expansion, owns nothing.
- Any loan, payment, reservation or price mutation. The platform is not a lender.
- A queue — nothing is async.
Source of Truth
| Concern | Source of Truth | Notes |
|---|---|---|
| Configuration | emi_setting row (singleton, CHECK (id = 1)) | Cached 30s; missing row = defaults |
| Rates | emi_bank_rate append-only history | Current = newest effective_from <= now() |
| Offered periods | emi_bank_tenure link rows | Must be in the vocabulary |
| Eligibility | settings mode + selection tables | Category set cached; product set is a PK probe |
| Quotes | computeEmi | Exact rational, no float |
3. Module Composition
| Module | Type | Path | Controllers | Providers | Exports | Responsibility |
|---|---|---|---|---|---|---|
EmiModule | Aggregate | emi.module.ts | None | — | Admin + Customer | Composes |
EmiCustomerModule | Leaf | customer/ | EmiCustomerController | EmiCustomerService | — | 2 public routes |
EmiAdminAggregateModule | Aggregate | admin/emi-admin-aggregate.module.ts | None | — | 6 leaves | Composes |
EmiSettingsAdminModule | Leaf | admin/settings/ | controller | service | — | Global switch, price source, mode |
EmiTenureAdminModule | Leaf | admin/tenure/ | controller | service | — | Vocabulary |
EmiEligibilityAdminModule | Leaf | admin/eligibility/ | controller | service | — | Selections + effective list |
EmiBankAdminModule | Leaf | admin/bank/ | controller | service | — | Banks CRUD + reorder |
EmiBankRateAdminModule | Leaf | admin/bank-rate/ | controller | service | — | Rate history |
EmiBankTenureAdminModule | Leaf | admin/bank-tenure/ | controller | service | — | Per-bank periods |
EmiSharedModule | Shared | shared/ | None | 4 services | 4 services | Config, catalog, expansion, constants |
4. File and Directory Map
apps/api/src/modules/emi/
emi.module.ts
customer/
emi-customer.controller.ts
emi-customer.module.ts
emi-customer.service.ts
dto/emi-customer.dto.ts
admin/
emi-admin-aggregate.module.ts
settings/ (controller, module, service, dto/)
tenure/ (controller, module, service, dto/)
eligibility/ (controller, module, service, dto/)
bank/ (controller, module, service, dto/)
bank-rate/ (controller, module, service, dto/)
bank-tenure/ (controller, module, service, dto/)
shared/
emi.constants.ts
emi-config.service.ts
emi-bank-catalog.service.ts
emi-category-expansion.service.ts
emi-shared.module.ts
apps/api/src/utils/emi/
emi-math.util.ts| File | Purpose | Key Exports | Notes |
|---|---|---|---|
utils/emi/emi-math.util.ts | Exact EMI arithmetic | computeEmi, isCalculableEmi, EMI_MAX_TENURE_MONTHS, EMI_MAX_ANNUAL_RATE_BPS, EMI_MAX_PRINCIPAL | Pure; no NestJS import |
shared/emi.constants.ts | All policy + PG error helpers | cache keys, live statuses, admin sorts, isReferencedRowError, isUniqueViolation, isCheckViolation, normalizeBankName | 23001 AND 23503 both mapped |
shared/emi-config.service.ts | Cached config for reads | getSettings, getEligibleCategoryIds | cachedOrDirect degrades to DB on Redis failure |
shared/emi-bank-catalog.service.ts | Offerable bank list | getOfferableBanks | TTL capped at next rate boundary, clamped ≥1 |
shared/emi-category-expansion.service.ts | Descendant category ids | expandEligibleCategoryIds | Cached as an array, returned as a Set |
customer/emi-customer.service.ts | Estimator | getOptions, calculate | Reads only; 200-with-false vs coded errors |
admin/settings/emi-settings-admin.service.ts | Settings upsert | get, update | Singleton upsert + emi invalidation |
admin/tenure/emi-tenure-admin.service.ts | Vocabulary replace | list, replace | Deactivate vs delete; EMI_TENURE_IN_USE |
admin/eligibility/emi-eligibility-admin.service.ts | Selection tables | list/add/remove categories + products, listEffectiveProducts | Expansion + live filters |
admin/bank/emi-bank-admin.service.ts | Bank CRUD | list, findOne, create, update, remove, restore, reorder | Name NFC-normalised; loan-range checks |
admin/bank-rate/emi-bank-rate-admin.service.ts | Rate history | list, create, remove | Append-only; future-only delete |
admin/bank-tenure/emi-bank-tenure-admin.service.ts | Per-bank periods | list, replace | Vocabulary-validated; FOR UPDATE serialisation |
5. Data Model
5.1 Schema Source
packages/db/src/schema/emi/
enums.ts
emi-setting.ts
emi-tenure.ts
emi-eligibility.ts
emi-bank.ts
emi-bank-rate.ts
emi-bank-tenure.ts5.2 Tables and Collections
emi_setting
| Column | Type | Default | Constraint | Notes |
|---|---|---|---|---|
id | serial | 1 | chk_emi_setting_singleton (id = 1) | A CEILING not a floor — zero rows is legal, repairable state |
is_enabled | boolean | false | — | The store-wide switch |
price_source | enum | selling_price | — | selling_price / mrp |
eligibility_mode | enum | selected | — | entire_store / selected |
updated_by | uuid | — | FK SET NULL | Admin who last changed it |
created_at / updated_at | timestamp | now | — | — |
emi_tenure
The vocabulary. months smallint PK (1…120, chk_emi_tenure_months_range), display_order,
is_active. Seeded: 3, 6, 9, 12, 18, 24, 36, 48, 60. Deactivation, never deletion while
referenced (ON DELETE RESTRICT from emi_bank_tenure).
emi_eligibility
Two link tables: emi_eligible_categories (category_id, FK RESTRICT) and
emi_eligible_products (product_id, FK RESTRICT). Rows removed explicitly on deselect. The
category table is expanded to descendants at read time (with the products schema's own
cycle-prevention checks).
emi_bank
| Column | Type | Notes |
|---|---|---|
public_id | uuid v7 | unique |
name | varchar | uq_emi_bank_name_live on lower(btrim(name)); NFC-normalised on write |
logo_key / logo_alt | text | EMI_BANK_LOGO_ALT_REQUIRES_KEY pairs them |
min_loan_amount / max_loan_amount | bigint | nullable; chk_emi_bank_loan_range_ordered (min ≤ max; min = max permitted) |
display_order | int | chk_emi_bank_display_order_non_negative |
is_active | boolean | offerability condition |
deleted_at / deleted_by | — | soft delete |
emi_bank_rate
| Column | Type | Notes |
|---|---|---|
bank_id | int | FK RESTRICT |
annual_rate_bps | int | 0…10000 (chk_emi_bank_rate_bps_range) |
effective_from | timestamptz | < 2100 (chk_emi_bank_rate_effective_from_bounded); full ISO instant |
created_by / deleted_by | uuid | FK SET NULL |
deleted_at | timestamp | soft delete |
uq_emi_bank_rate_effective is partial on deleted_at IS NULL — cancelling a scheduled
rate then re-entering the same date must not collide with the soft-deleted row. The index also
serves the current-rate read (btree backwards scan).
emi_bank_tenure
Link table (bank_id, months) with PK on both. FK to bank RESTRICT, FK to emi_tenure
RESTRICT. Replaced wholesale in a transaction under FOR UPDATE on the bank row (two
concurrent replaces would otherwise produce the union of their sets).
5.3 Relationship Diagram
6. Services and Responsibilities
6.1 EmiMathUtil (pure functions)
| Function | Reads | Writes | Notes |
|---|---|---|---|
computeEmi(principal, bps, months) | — | — | Exact rational BigInt; one rounding at the end; validates its own inputs |
isCalculableEmi(...) | — | — | For deciding whether to OFFER, never to trust |
The derivation: r = bps/120000 exactly, 1 + r = (120000 + bps)/120000, so
P · bps · (120000 + bps)^n
EMI = ───────────────────────────────
120000 · ((120000 + bps)^n − 120000^n)Numerator and denominator are exact integers; there is exactly ONE rounding. Rounding is
max(roundHalfUp(exact), ceil(P/n)) — the floor makes totalInterest ≥ 0 a theorem. 0% is a
named branch returning zero interest by definition.
6.2 EmiConfigService
| Method | Reads | Writes | Side Effects | Errors |
|---|---|---|---|---|
getSettings | settings (cached) | — | — | none — cache-down falls through |
getEligibleCategoryIds | expansion (cached) | — | — | same |
cachedOrDirect wraps getOrSet because that method rethrows on its lock path regardless of
fallbackOnError — Redis down would otherwise 500 every EMI request.
6.3 EmiBankCatalogService
getOfferableBanks — active, not deleted, a rate in force, at least one tenure (the last two
are cross-table minimums no constraint can enforce; filtering here is the enforcement).
resolveTtlSeconds = max(1, min(VOLATILE, secondsUntilNextRateBoundary)) — a rate becoming
effective is a clock event, so without the cap the cache would advertise a superseded rate for
a full TTL.
6.4 EmiCustomerService
getOptions — 200 with available: false for every ineligibility (feature off, product not
selected, zero price, no banks); only a missing product 404s. Quotes computed server-side per
bank per tenure. calculate — raises specific codes (403/404/409/400/422) because a client
reaching it has already been told what is on offer; a failure means something changed
underneath or the request was hand-made.
resolvePricingBasis(product, variantPublicId?) decides what price this request quotes
against, and both public routes call it before anything else.
(apps/api/src/modules/emi/customer/emi-customer.service.ts:136-184)
- No
variantId— returns{ variantId: null, variantName: null, mrp: product.mrp, sellingPrice: product.sellingPrice }, the product ROLLUP. Deliberate: a product page renders the EMI affordance before a configuration is chosen, and "from Rs X" is the same honest figure the storefront card already shows. variantIdsupplied — one query, scoped toproductVariants.publicId = variantId AND productVariants.productId = product.id AND isActive AND deletedAt IS NULL. TheproductIdscoping is what makes this an IDENTITY check, not merely a lookup: it is what refuses a variant that belongs to a different product rather than silently pricing one product off another's configuration. No match — soft-deleted, deactivated, or another product's — is404 EMI_VARIANT_NOT_FOUND, never a fallback to the rollup. A fallback would answer a question the customer did not ask with a number they cannot be charged, which is the exact defect this method exists to remove.
Why this matters for the money, not only the 404: product.selling_price and product.mrp
are rollups — the MINIMUM across live variants — and financing a chosen configuration off the
rollup would have quoted the Rs 90,000 phone an installment plan computed from the Rs 50,000
sibling's price, with nothing in the response revealing which configuration the figure belonged
to. The special-deal campaign is resolved against the variant's own base price but the
product's identity (a deal targets a product/brand/series, never a variant) — the same call
shape the product page, the cart and the checkout freeze all use, which is what keeps the four in
agreement. (apps/api/src/modules/emi/customer/emi-customer.service.ts:85-124)
6.5 Admin services
- Settings: singleton upsert
ON CONFLICT (id); invalidatesemidomain after commit. - Tenures:
replacedeactivates rather than deletes referenced periods (EMI_TENURE_IN_USE); vocabulary validation names the unusable periods. - Eligibility: add/remove with
ALREADY_ELIGIBLEcodes;listEffectiveProductsmirrors the customer gate (sellingPrice > 0, live statuses). - Banks: create/update validate loan range and logo pairing; delete/restore soft;
reorder rewrites
displayOrdertransactionally, 404 on missing ids. - Rates: append-only — no PATCH; delete only while
effective_from > now(); duplicate effective dates on live rows raiseEMI_RATE_EFFECTIVE_DATE_ALREADY_EXISTS. - Bank tenures: replace validates against the vocabulary (only
is_activeperiods assignable — deactivation blocks NEW assignments only), serialises withFOR UPDATE.
7. Runtime Flows
7.1 The estimator
The sequence in feature.mdx §5.1. Every read goes through the 30s cache; a cache failure
degrades to a direct DB read with a warn log, never a 500.
7.2 The rate boundary
8. Caching
| Cache Key Pattern | Builder | Value | TTL | Invalidation | Caller |
|---|---|---|---|---|---|
emi:v1:settings | EMI_SETTINGS_CACHE_KEY | settings snapshot | VOLATILE (30s) | emi domain on settings write | EmiConfigService |
emi:v1:eligible-categories | EMI_ELIGIBLE_CATEGORIES_CACHE_KEY | id array | 30s | emi domain on eligibility write | EmiConfigService |
emi:v1:banks | EMI_BANKS_CACHE_KEY | offerable banks | min(30s, next rate boundary) | emi domain on bank/rate/tenure write | EmiBankCatalogService |
v1versions the SERIALISED SHAPE, not the data — bump on a response-shape change; stays inside theemi:*invalidation glob.- Nothing price-derived is ever cached.
- Every admin mutation invalidates
emiafter commit, fire-and-forget. - Redis down → warn + uncached read, never a 500.
9. BullMQ, Schedulers, and Async Work
None. No queue, no cron, no scheduler. The rate boundary needs no job — the TTL cap expires on its own, which is the design: a job that must run for an advertised interest rate to be correct fails silently when it does not run; a TTL that expires on its own has no such failure mode.
10. Realtime and Events
None.
11. Security, Auth, and Abuse Controls
- Customer routes:
@Public();PUBLIC_READ60/min. No guest identity, no persistence. - Admin routes:
JwtAuthGuard+RoleGuard;Emi_*orEmiBanks_*per route;ADMIN_READ30/min reads,ADMIN_WRITE10/min mutations. - Tenure DoS control:
tenureMonthsis an exponent; DTO bounds at 1…120 (and the math util validates its own inputs before any exponentiation). - Loan-range boundaries: inclusive at both ends (
min = maxpermitted and reachable). - Injection surface: admin bank sort keys are a closed set (
EMI_BANK_ADMIN_SORTS). - Referenced-row deletes: RESTRICT refusals mapped on BOTH
23001and23503— aDrizzleQueryErrorcarries the drivercodeon.cause, not the outer object; reading only the outer code makes every translation dead code. - Name uniqueness: NFC normalisation on write because an index cannot normalise Unicode composition.
- Audit:
created_by/deleted_by/updated_bycolumns, SET NULL on admin removal; rate history is the record of what customers were quoted.
13. Error Handling
| Error Code | HTTP Status | Thrown By | Condition | Client Action |
|---|---|---|---|---|
EMI_DISABLED | 403 | customer service | feature off store-wide | Hide the affordance |
EMI_PRODUCT_NOT_ELIGIBLE | 403 | customer service | product not selected | Hide the affordance |
EMI_PRICE_NOT_AVAILABLE | 409 | customer service | price resolves to zero | Hide the affordance |
EMI_NO_BANK_AVAILABLE | 409 | customer service | no offerable bank | Hide the affordance |
EMI_BANK_NOT_FOUND | 404 | customer + admin services | unknown/no-longer-offered bank | Re-fetch /options |
EMI_TENURE_NOT_SUPPORTED | 400 | customer service | bank does not offer the period | Re-fetch /options |
EMI_AMOUNT_BELOW_BANK_MINIMUM | 409 | customer service | price under floor | Re-fetch; the flag is there |
EMI_AMOUNT_ABOVE_BANK_MAXIMUM | 409 | customer service | price over ceiling | same |
EMI_CALCULATION_OUT_OF_RANGE | 422 | customer service | arithmetic guard has a hole | Report it |
EMI_PRODUCT_NOT_FOUND | 404 | customer + eligibility | no such live product | 404 the page |
EMI_BANK_NAME_ALREADY_EXISTS | 409 | bank service | duplicate live name | Use another name |
EMI_BANK_LOAN_RANGE_INVALID | 400 | bank service | min > max | Fix the pair |
EMI_BANK_LOGO_ALT_REQUIRES_KEY | 400 | bank service | alt without key | Fix the pair |
EMI_BANK_INACTIVE | 409 | bank service | acting on a deactivated bank | Re-activate first |
EMI_BANK_HAS_NO_TENURES | 409 | bank service | offered periods absent | Assign some |
EMI_BANK_RATE_NOT_CONFIGURED | 409 | rate service | no rate row at all | Create one |
EMI_RATE_IN_FORCE_NOT_DELETABLE | 409 | rate service | deleting an effective rate | Supersede instead |
EMI_RATE_EFFECTIVE_DATE_ALREADY_EXISTS | 409 | rate service | duplicate live effective date | Pick another |
EMI_RATE_EFFECTIVE_DATE_OUT_OF_RANGE | 400 | rate service | date beyond 2100 | Fix the date |
EMI_TENURE_IN_USE | 409 | tenure service | deleting an assigned period | Deactivate instead |
EMI_TENURE_NOT_IN_VOCABULARY | 400 | tenure/bank-tenure services | period outside the vocabulary (or deactivated) | Pick a listed period |
EMI_CATEGORY_NOT_FOUND | 404 | eligibility | no such category | Re-check |
EMI_CATEGORY_ALREADY_ELIGIBLE | 409 | eligibility | duplicate selection | — |
EMI_PRODUCT_ALREADY_ELIGIBLE | 409 | eligibility | duplicate selection | — |
24 codes, all in use (the EMI_* block of error-codes.ts).
14. Observability
| Signal | Location | Purpose |
|---|---|---|
| Log | Logger in config/catalog (cache-down warns), admin services | Cache degradation and admin operations |
| Cache state | Redis keys emi:v1:* | Shape versioning, TTL caps |
| Audit columns | created_by/deleted_by/updated_by | Who changed what |
| Rate history | emi_bank_rate | The record of what was advertised |
No activity/audit-log events — that was designed and withdrawn (master plan §6.2).
15. Testing and Validation
| Test Type | Files | Coverage |
|---|---|---|
| Unit | emi-math.util.spec.ts | 31 tests; 3 mutations each caught by the right test |
| Integration | emi-bank-tenure.int.spec.ts, emi-schema-contract.int.spec.ts | Tenure replace serialisation; products price constraint asserted |
| Probe | .omc/plans/emicalc/probe-constraints.mjs | 8,640-combination arithmetic sweep; 38 schema assertions accept+reject |
| QA ledger | .omc/plans/emicalc/qa-gate-ledger.md | Full gate history, all PASS |
Validation: tsc -b --force (0 new errors), pnpm rules (390 codes), drizzle-kit check +
drift (clean), unit + integration suites.
16. Mandatory Backend Deep-Dive Pack
16.1 Submodule Coverage Matrix
| Unit | Type | Owns | Depends On | Called By | Calls | State Touched | Failure Modes |
|---|---|---|---|---|---|---|---|
computeEmi | pure fn | exact arithmetic | money util types | customer service, catalog | none | none | EmiRangeError |
EmiConfigService | service | cached config | DB, cache, expansion | customer, eligibility | 3 | settings, category cache | cache-down → DB |
EmiBankCatalogService | service | offerable banks | DB, cache | customer | 3 | banks cache | cache-down → DB |
EmiCategoryExpansionService | service | descendant ids | DB | config, eligibility | 1 | — | cycles prevented upstream |
EmiCustomerService | service | estimator | DB, config, catalog, math, storage | customer controller | 6 | none | coded errors |
EmiSettingsAdminService | service | settings write | DB, cache invalidation | settings controller | 2 | settings + cache | — |
EmiTenureAdminService | service | vocabulary | DB | tenure controller | 2 | tenures + cache | IN_USE |
EmiEligibilityAdminService | service | selections | DB, expansion | eligibility controller | 4 | selection tables + cache | duplicate codes |
EmiBankAdminService | service | banks | DB, cache | bank controller | 3 | banks + cache | loan-range, logo pairs |
EmiBankRateAdminService | service | rate history | DB, cache | rate controller | 3 | rates + cache | in-force delete refused |
EmiBankTenureAdminService | service | per-bank periods | DB | bank-tenure controller | 3 | link rows + cache | vocabulary errors |
| Controllers (7) | controllers | routing | services | HTTP | services | — | pipes |
16.2 UML and Architecture Diagram Pack
Component diagram: §3. Sequence diagrams: feature §5.1 (estimator), §7.2 (rate boundary). ER diagram: §5.3. State diagram: feature §7 (rate lifecycle).
16.3 Code Flow Narrative
The estimator is fully narrated in feature §5.1. The rate boundary (§7.2) is the module's
second critical path. Every admin mutation follows one shape: validate → write (in a
transaction where concurrency matters) → invalidate emi domain after commit (fire-and-forget,
never inside the transaction).
16.4 Data Layer Deep Dive
Field-level detail in §5.2. Index/constraint rationale:
| Index/Constraint | Columns | Type | Query/Invariant Supported | Tradeoff |
|---|---|---|---|---|
uq_emi_bank_name_live | lower(btrim(name)) | unique | One live name | NFC must be normalised on write |
chk_emi_bank_loan_range_ordered | min, max | check | min ≤ max, min = max allowed | boundary reachable |
uq_emi_bank_rate_effective | bank, effective_from | partial unique (live) | One live rate per date; serves the current-rate read | two soft-deleted rows with one date possible |
chk_emi_bank_rate_bps_range | bps | check | 0…10000 | mirrored in math util |
chk_emi_setting_singleton | id | check | One settings row | zero rows legal (defaults) |
| FK RESTRICT everywhere | refs | FK | No silent orphaning | refusals must be coded |
16.5 Business Logic and Invariant Catalog
| Invariant | Enforced By | Why It Exists | Failure Error | Tests |
|---|---|---|---|---|
| EMI computed exactly, one rounding | computeEmi | Float form is wrong by a paisa | EmiRangeError / 422 | unit + 8,640 probe |
totalInterest >= 0 | ceil(P/n) floor | No negative reported interest | — | unit |
| 0% reports zero | named branch | Uniform formula lies | — | unit |
| Tenure bounded | util + DTO | DoS (exponent) | 400 | unit |
| Rate in force not deletable | rate service | Record of what was quoted | EMI_RATE_IN_FORCE_NOT_DELETABLE | int |
| Offerable = 4 conditions | catalog filter | Two are unconstrainable | — | int |
| Tenures ∈ vocabulary | FK + service check | Comparison table lines up | EMI_TENURE_NOT_IN_VOCABULARY | int |
| Price sources = 2 | enum + products CHECK | 4 names for 2 behaviours | — | schema-contract spec |
| Nothing price-derived cached | no price keys | Stale price = wrong installment | — | — |
| Redis down ≠ 500 | cachedOrDirect | Feature survives Redis loss | — | — |
16.6 Tradeoffs, Alternatives, and ADR Notes
| Decision | Context | Chosen Option | Alternatives | Why Chosen | Tradeoffs | Revisit Trigger |
|---|---|---|---|---|---|---|
| BigInt exact rational | Float EMI is wrong | computeEmi | Math.pow | One paisa matters | BigInt cost (0.004ms) | — |
| Round-half-up + floor | Ceiling overcharged | max(round, ceil(P/n)) | ceiling | probe-disproven | — | — |
| 2 price sources | Constraint collapses 4→2 | enum + contract spec | 4 names | honest admin choice | breaks silently if products relaxes | products constraint change |
| Tenure vocabulary | Comparison integrity | emi_tenure domain | free-range 1–120 | no 13-vs-12 | ~9-row table | — |
| Append-only rates | Advertised-rate record | history table | editable column | auditability | future-only delete | — |
| TTL-capped bank cache | Clock events invalidate nothing | min(30s, boundary) | scheduled job | no silent job failure | boundary query per fill | — |
| 30s config TTL | Bounded staleness | VOLATILE | 5 min | withdrawn-category speed | 30s wait | — |
| Redis-down degradation | getOrSet rethrows | cachedOrDirect | fix redis service | local, 30+ callers safe | per-module wrap | redis contract change |
| Separate perms | Different people | Emi / EmiBanks | one module | least privilege | Emi_UPDATE is wider — documented | — |
16.7 Operational Runbook
| Operation | How to Inspect | Healthy State | Failure Signal | Recovery |
|---|---|---|---|---|
| Cache | Redis emi:v1:* | 30s TTLs; bank key capped at boundary | warn logs on cache-down | automatic DB fallback |
| Rates | emi_bank_rate history | one in-force per bank | bank missing from /options | add rate or tenure |
| Feature state | GET /settings | isEnabled reflects intent | 30s lag after change | wait or re-check |
| Tenure vocabulary | GET /tenures | 9 seeded periods | bank offers deactivated period | service blocks new assignment |
| RESTRICT refusals | API errors | coded errors | raw 23001/23503 in logs | that is a bug — sqlStateOf should catch |
16.8 Backend Risk Register
| Risk | Area | Impact | Current Mitigation | Remaining Gap |
|---|---|---|---|---|
| Float EMI drift | arithmetic | wrong paisa | BigInt rational | — |
| DoS via tenure | arithmetic | event-loop block | 120 cap + self-validation | — |
| Cache advertises superseded rate | catalog | wrong advertised rate | TTL cap at boundary | boundary query itself cached? no — per fill |
| Products constraint relaxes | price source | 2-source enum quietly wrong | schema-contract spec | spec fails loudly, then fix |
| Redis outage | reads | feature down | cachedOrDirect | lock-path rethrow covered |
| Concurrent tenure replace | bank tenures | union of sets | FOR UPDATE | — |
17. Zero-Omission Backend Checklist
- Every file in the module directory is represented (§4, §16.1).
- Every controller, service, provider, DTO, enum, and schema is documented.
- Every method with business behavior has a code-flow narrative (§6, §7, §16.3).
- Every table has field-level detail (§5.2).
- Every index, constraint, relation, and delete behavior has rationale (§5.2, §16.4).
- Every lifecycle has a state diagram and transition table (feature §7).
- Every read/write flow has sequence and activity diagrams (§7, feature §5.1).
- Every business invariant is cataloged (§16.5).
- Cache keys, invalidation paths, and queue jobs are documented (§8, §9 — no queue exists by design).
- Every architectural tradeoff is documented with alternatives and revisit triggers (§16.6).
- Every operational failure mode has a runbook entry (§16.7).
18. Backend Completion Checklist
- Module boundaries are documented (§2).
- Every controller, service, DTO, schema file, cache key, and event is covered.
- Every table has a field table and relationship diagram (§5).
- Every runtime flow has a diagram and branch notes (§7).
- API and features/flows docs are linked (See Also).
- No claim is made without a source file reference.
See Also
- API doc:
/docs/developer/emi/api - Features and flows doc:
/docs/developer/emi/feature - TDD: not yet published
EMI Calculator API Reference
Complete API contracts for the EMI Calculator module, including routes, auth, DTOs, responses, errors, examples, and integration notes.
EMI Calculator Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the EMI Calculator module.