EMI Calculator API Reference
Complete API contracts for the EMI Calculator module, including routes, auth, DTOs, responses, errors, examples, and integration notes.
EMI Calculator - API Reference
Audience: Frontend engineers (storefront + admin panel), backend engineers, QA, and API consumers.
Scope: The 25 routes — 2 public customer routes under /api/mobile/emi/..., 23 admin routes under /api/admin/emi/....
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | apps/api/src/modules/emi/customer/emi-customer.controller.ts, admin/{settings,tenure,eligibility,bank,bank-rate,bank-tenure}/*.controller.ts | Routes, methods, guards, permissions, rate limits |
| DTOs | customer/dto/*.ts, admin/*/dto/*.ts | Request/query/response validation, examples |
| Services | customer/emi-customer.service.ts, shared/*.service.ts, admin/*/*.service.ts | Behavior, error codes |
| Schema | packages/db/src/schema/emi/*.ts | Tables, enums, constraints |
| Math | apps/api/src/utils/emi/emi-math.util.ts | Exact arithmetic, caps |
| Existing docs | consumer-handoff.md, Fumadocs formats | Frozen contract and format baseline |
2. Module Summary
| Field | Value |
|---|---|
| Module name | emi |
| Module slug | emi |
| Primary actors | guest (customer), admin (merchandiser), admin (rate-entry) |
| API surfaces | mobile (2 public), admin (23) |
| Base route prefixes | /api/mobile/emi/products/{productId}/..., /api/admin/emi/... |
| Auth model | @Public() (customer); JwtAuthGuard + RoleGuard + Emi_*/EmiBanks_* (admin) |
| Persistence | PostgreSQL (6 tables); Redis cache (emi:v1: keys) |
| Runtime source of truth | Settings row + rate history + selection tables; quotes from computeEmi |
| Sibling docs | Backend, Features and flows |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
annualRateBps | Basis points — hundredths of a percent. 1200 = 12.00% | emi-math.util.ts | Customer + admin responses |
basePrice | The price the quote is based on (selling price or MRP per settings) | customer service | Options + calculate |
isApplicable | Bank's loan limits contain the product price (inclusive) | customer service | Options |
isEstimate | Always true — no loan exists; equal installments cannot exactly retire an amortising loan | customer service | Calculate |
tenureMonths | Repayment period, integer 1…120 — the exponent of the calculation | math util | Both customer routes |
effectiveFrom | When a rate takes force — full ISO instant | rate schema | Rate admin |
| Offerable | Active + not deleted + rate in force + ≥1 tenure | catalog service | Customer routes |
4. API Surface Map
| Surface | Method | Path | Actor | Auth/Guard | Permission | Controller | Purpose |
|---|---|---|---|---|---|---|---|
| Mobile | GET | /api/mobile/emi/products/{productId}/options | Guest | @Public() | — | EmiCustomer | Bank comparison with computed quotes |
| Mobile | GET | /api/mobile/emi/products/{productId}/calculate | Guest | @Public() | — | EmiCustomer | One exact breakdown |
| Admin | GET | /api/admin/emi/settings | Admin | JWT+Role | Emi_READ | Settings | Read the switch/source/mode |
| Admin | PUT | /api/admin/emi/settings | Admin | JWT+Role | Emi_UPDATE | Settings | Flip the switch, set source/mode |
| Admin | GET | /api/admin/emi/tenures | Admin | JWT+Role | Emi_READ | Tenure | List the vocabulary |
| Admin | PUT | /api/admin/emi/tenures | Admin | JWT+Role | Emi_UPDATE | Tenure | Replace the vocabulary |
| Admin | GET | /api/admin/emi/eligibility/categories | Admin | JWT+Role | Emi_READ | Eligibility | List selected categories |
| Admin | POST | /api/admin/emi/eligibility/categories | Admin | JWT+Role | Emi_CREATE | Eligibility | Select a category |
| Admin | DELETE | /api/admin/emi/eligibility/categories/{categoryId} | Admin | JWT+Role | Emi_DELETE | Eligibility | Deselect |
| Admin | GET | /api/admin/emi/eligibility/products | Admin | JWT+Role | Emi_READ | Eligibility | List selected products |
| Admin | POST | /api/admin/emi/eligibility/products | Admin | JWT+Role | Emi_CREATE | Eligibility | Select a product |
| Admin | DELETE | /api/admin/emi/eligibility/products/{productId} | Admin | JWT+Role | Emi_DELETE | Eligibility | Deselect |
| Admin | GET | /api/admin/emi/eligibility/effective-products | Admin | JWT+Role | Emi_READ | Eligibility | What customers would see |
| Admin | GET | /api/admin/emi/banks | Admin | JWT+Role | EmiBanks_READ | Bank | List (sorted, paginated) |
| Admin | POST | /api/admin/emi/banks | Admin | JWT+Role | EmiBanks_CREATE | Bank | Create |
| Admin | PATCH | /api/admin/emi/banks/reorder | Admin | JWT+Role | EmiBanks_UPDATE | Bank | Reorder display |
| Admin | GET | /api/admin/emi/banks/{bankId} | Admin | JWT+Role | EmiBanks_READ | Bank | Detail |
| Admin | PATCH | /api/admin/emi/banks/{bankId} | Admin | JWT+Role | EmiBanks_UPDATE | Bank | Update |
| Admin | DELETE | /api/admin/emi/banks/{bankId} | Admin | JWT+Role | EmiBanks_DELETE | Bank | Soft delete |
| Admin | POST | /api/admin/emi/banks/{bankId}/restore | Admin | JWT+Role | EmiBanks_RESTORE | Bank | Restore |
| Admin | GET | /api/admin/emi/banks/{bankId}/rates | Admin | JWT+Role | EmiBanks_READ | Rate | Rate history |
| Admin | POST | /api/admin/emi/banks/{bankId}/rates | Admin | JWT+Role | EmiBanks_CREATE | Rate | Schedule a rate |
| Admin | DELETE | /api/admin/emi/banks/{bankId}/rates/{rateId} | Admin | JWT+Role | EmiBanks_DELETE | Rate | Cancel a future rate |
| Admin | GET | /api/admin/emi/banks/{bankId}/tenures | Admin | JWT+Role | EmiBanks_READ | BankTenure | Offered periods |
| Admin | PUT | /api/admin/emi/banks/{bankId}/tenures | Admin | JWT+Role | EmiBanks_UPDATE | BankTenure | Replace offered periods |
5. Auth, Identity, and Permissions
| Surface | Guard/Decorator | Identity Shape | Permission | Guest Allowed | Notes |
|---|---|---|---|---|---|
| Customer | @Public() | None | — | Yes | PUBLIC_READ 60/min; no guest identity |
| Admin | JwtAuthGuard, RoleGuard | req.user.id | Emi_* or EmiBanks_* | No | ADMIN_READ 30/min reads, ADMIN_WRITE 10/min mutations |
Two permission modules. Emi_* = switch, price source, eligibility mode, tenure vocabulary,
product/category eligibility. EmiBanks_* = banks, rates, tenures. Emi_UPDATE is the wider
grant — it carries the store-wide kill switch.
6. DTO and Model Reference
6.1 EmiProductParamsDto — customer routes
| Field | Type | Required | Validation |
|---|---|---|---|
productId | uuid | Yes | uuid v7 — a v4 is rejected VALIDATION_FAILED |
6.2 EmiVariantQueryDto — /options, and CalculateEmiQueryDto — /calculate
Both routes bind their query with whitelist: true, forbidNonWhitelisted: true (the global
ValidationPipe), so any unexpected query key — a campaign tag like utm_source, a
cache-buster like ?_=169… — is a 400 VALIDATION_FAILED on what used to be a lenient public
route. This is a genuine behavior change from before the variant work: /options took no query
DTO at all previously.
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
variantId (/options, via EmiVariantQueryDto) | uuid | No | uuid v7 | product_variant.public_id. Omit for the product-page "from" estimate (cheapest live configuration); supply it once the customer has chosen. A variant that is not live, or belongs to a different product, is 404 EMI_VARIANT_NOT_FOUND — an identity failure, not an eligibility answer |
bankId (/calculate) | uuid | Yes | uuid v7 | — |
variantId (/calculate, on CalculateEmiQueryDto) | uuid | No | uuid v7 | Same meaning and same EMI_VARIANT_NOT_FOUND rule; pass the same variantId used to list the options or the plan quoted back is a different configuration's |
tenureMonths (/calculate) | number | Yes | integer 1…120 | 12.5 or 1000000 → VALIDATION_FAILED |
6.3 UpdateEmiSettingsDto — PUT /settings
| Field | Type | Required | Validation |
|---|---|---|---|
isEnabled | boolean | Yes | — |
priceSource | enum | Yes | selling_price / mrp |
eligibilityMode | enum | Yes | entire_store / selected |
6.4 Bank DTOs
CreateEmiBankDto: name (non-blank, ≤255), logoKey/logoAlt (alt requires key), minLoanAmount
/ maxLoanAmount (integers, min ≤ max, nullable), displayOrder (≥0). UpdateEmiBankDto: same
fields, optional. ReorderEmiBanksDto: bankIds array.
6.5 Rate DTO
CreateEmiBankRateDto: annualRateBps (integer 0…10000), effectiveFrom (ISO instant,
bounded < 2100).
6.6 Response DTOs
EmiOptionsDto:available,unavailableReason,productId,productName,variantId(uuid | null — the configuration these quotes are for; null meansbasePriceis the product's "from" figure),variantName(string | null — null both when no variant was requested AND when the requested variant is the product's sole configuration with no label of its own),basePrice(0 when unavailable),banks[](see the handoff body in §8.1).EmiCalculationDto:productId,productName,variantId(uuid | null — the configuration financed; null means the quote is the product's "from" estimate and must not be presented beside a chosen variant),variantName(string | null),bankId,bankName,basePrice,annualRateBps,tenureMonths,monthlyEmi,totalRepayment,totalInterest,isEstimate: true.EmiBankAdminDto/EmiBankRateAdminDto/EmiTenureAdminDto/ settings DTO: admin CRUD shapes, all money in paisa, rates in bps.
7. Enum Reference
| Enum | Value | Meaning | Runtime Effect | Source |
|---|---|---|---|---|
emi_price_source | selling_price / mrp | What the quote is based on | Exactly two — the products CHECK makes "higher/lower of" meaningless | enums.ts |
emi_eligibility_mode | entire_store / selected | How the eligible set is decided | selected = union of both selection tables | enums.ts |
8. Endpoint Reference
8.1 GET /api/mobile/emi/products/{productId}/options
Purpose
The storefront asks this before rendering any "Calculate EMI" affordance. It returns
eligibility, the base price, and every offerable bank with its rate, loan limits, applicability
and already computed quotes per offered period. It never returns 4xx for an ineligible
product — available: false distinguishes "no EMI" from "no such product". Only a genuinely
missing product 404s, and so does a variantId that names no live configuration OF THIS PRODUCT
— that is an identity failure, not an eligibility answer.
Pass variantId (optional, EmiVariantQueryDto) once the customer has chosen a configuration.
Without it the quotes are the product's "from" estimate, computed from the cheapest live
configuration, and variantId comes back null in the response so a client can tell which it
received.
Auth and Permissions
- Auth:
@Public()· Rate limit:PUBLIC_READ60/min
Response
{
"success": true,
"message": "EMI options fetched successfully",
"data": {
"available": true,
"unavailableReason": null,
"productId": "019fcc90-e010-7698-ae7a-faec1e7bed42",
"productName": "Galaxy S24 Ultra",
"variantId": "019fcc90-1234-7698-ae7a-faec1e7bed99",
"variantName": "512GB",
"basePrice": 13120091,
"banks": [
{
"id": "0199b000-0000-7000-8000-00000000ba01",
"name": "Nabil Bank",
"logo": { "url": "/uploads/banks/nabil.png", "alt": "Nabil Bank" },
"annualRateBps": 1200,
"minLoanAmount": 500000,
"maxLoanAmount": 50000000,
"isApplicable": true,
"unavailableReason": null,
"quotes": [
{ "tenureMonths": 6, "monthlyEmi": 2263850, "totalRepayment": 13583100, "totalInterest": 463009 },
{ "tenureMonths": 12, "monthlyEmi": 1165704, "totalRepayment": 13988448, "totalInterest": 868357 }
]
}
]
}
}When unavailable: available: false, unavailableReason populated, basePrice: 0, banks: [].
quotes is already computed — render those numbers, do not recompute them. A bank with
isApplicable: false has empty quotes and a populated unavailableReason (price outside the
bank's loan limits).
Side Effects
None — reads only.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | EMI_PRODUCT_NOT_FOUND | Product missing, deleted, or not published/unlisted |
| 404 | EMI_VARIANT_NOT_FOUND | variantId supplied, and it is not live, or belongs to a different product |
| 400 | VALIDATION_FAILED | Bad variantId uuid, or any query key other than variantId (forbidNonWhitelisted) |
8.2 GET /api/mobile/emi/products/{productId}/calculate
Purpose
One bank, one tenure, full exact breakdown. The client has already seen what is on offer, so a failure here means something changed underneath or the request was hand-made — every failure raises a specific code.
Pass the same variantId that was used to list the options, or the plan quoted back will be a
different configuration's. The response echoes variantId and variantName so a client can
detect both a reprice and a configuration mismatch.
Auth and Permissions
- Auth:
@Public()· Rate limit:PUBLIC_READ60/min
Request
| Part | Required | Details |
|---|---|---|
| Params | Yes | productId (uuid v7) |
| Query | Yes | bankId (uuid v7), tenureMonths (int 1…120); variantId (uuid v7) optional — omitting it prices the product's "from" configuration. Any OTHER query key is a 400 (forbidNonWhitelisted) |
Response
{
"success": true,
"message": "EMI calculated successfully",
"data": {
"productId": "019fcc90-…", "productName": "Galaxy S24 Ultra",
"variantId": "019fcc90-1234-7698-ae7a-faec1e7bed99", "variantName": "512GB",
"bankId": "0199b000-…", "bankName": "Nabil Bank",
"basePrice": 13120091,
"annualRateBps": 1200,
"tenureMonths": 12,
"monthlyEmi": 1165704,
"totalRepayment": 13988448,
"totalInterest": 868357,
"isEstimate": true
}
}basePrice is echoed so the client can detect the product was repriced between listing options
and calculating.
Side Effects
None. The server computes it as an exact rational in BigInt with no floating point anywhere — do not recompute client-side; the server is right.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 403 | EMI_DISABLED | Feature off store-wide |
| 403 | EMI_PRODUCT_NOT_ELIGIBLE | Product not on the scheme |
| 404 | EMI_PRODUCT_NOT_FOUND | No such live product |
| 404 | EMI_VARIANT_NOT_FOUND | variantId supplied, and it is not live, or belongs to a different product |
| 409 | EMI_PRICE_NOT_AVAILABLE | Price resolves to zero |
| 409 | EMI_NO_BANK_AVAILABLE | No participating bank offerable |
| 404 | EMI_BANK_NOT_FOUND | Unknown or no-longer-offered bank |
| 400 | EMI_TENURE_NOT_SUPPORTED | That bank does not offer the period |
| 409 | EMI_AMOUNT_BELOW_BANK_MINIMUM | Price under the bank's floor |
| 409 | EMI_AMOUNT_ABOVE_BANK_MAXIMUM | Price over the bank's ceiling |
| 422 | EMI_CALCULATION_OUT_OF_RANGE | Should never happen — report it |
| 400 | VALIDATION_FAILED | Bad uuid, non-integer or out-of-range tenure, or any query key other than bankId/variantId/tenureMonths (forbidNonWhitelisted) |
8.3 GET /api/admin/emi/settings
Purpose
Read the store-wide switch, price source and eligibility mode.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:Emi_READ· Rate limit:ADMIN_READ
8.4 PUT /api/admin/emi/settings
Purpose
Flip the store-wide switch, set the price source, set the eligibility mode. Takes effect within 30 seconds (config cache).
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:Emi_UPDATE· Rate limit:ADMIN_WRITE
Side Effects
Singleton upsert (CHECK (id = 1)); emi cache domain invalidated after commit.
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 400 | validation | Missing/invalid fields |
8.5 GET /api/admin/emi/tenures
Purpose
List the tenure vocabulary (~9 seeded periods: 3, 6, 9, 12, 18, 24, 36, 48, 60).
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:Emi_READ· Rate limit:ADMIN_READ
8.6 PUT /api/admin/emi/tenures
Purpose
Replace the vocabulary. Periods must stay within 1…120; deactivate rather than delete a period still assigned to a bank.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:Emi_UPDATE· Rate limit:ADMIN_WRITE
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 400 | EMI_TENURE_NOT_IN_VOCABULARY | A period outside 1…120 (or the replace list is otherwise invalid) |
| 409 | EMI_TENURE_IN_USE | Deleting a period still referenced by a bank |
8.7–8.9 GET/POST/DELETE /api/admin/emi/eligibility/categories
Purpose
Select and deselect categories as eligible. A selected category confers EMI on all its descendants (tree expansion at read time).
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permissions:Emi_READ/Emi_CREATE/Emi_DELETE· Rate limit:ADMIN_READ/ADMIN_WRITE
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | EMI_CATEGORY_NOT_FOUND | No such category |
| 409 | EMI_CATEGORY_ALREADY_ELIGIBLE | Duplicate selection |
8.10–8.12 GET/POST/DELETE /api/admin/emi/eligibility/products
Purpose
Select and deselect individual products. Membership is the union of the category and
product selections (when mode is selected).
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permissions:Emi_READ/Emi_CREATE/Emi_DELETE
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | EMI_PRODUCT_NOT_FOUND | No such live product |
| 409 | EMI_PRODUCT_ALREADY_ELIGIBLE | Duplicate selection |
8.13 GET /api/admin/emi/eligibility/effective-products
Purpose
The product list a customer would actually see — mirrors the customer gate: live status,
sellingPrice > 0, and the current eligibility mode (expansion when selected; everything
when entire_store).
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:Emi_READ· Rate limit:ADMIN_READ
8.14 GET /api/admin/emi/banks
Purpose
List banks with rates and tenures attached, sorted (displayOrder/name/createdAt — a
closed set), paginated.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_READ· Rate limit:ADMIN_READ
8.15 POST /api/admin/emi/banks
Purpose
Create a bank. Name is NFC-normalised and unique among live banks.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_CREATE· Rate limit:ADMIN_WRITE
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 409 | EMI_BANK_NAME_ALREADY_EXISTS | Duplicate live name |
| 400 | EMI_BANK_LOAN_RANGE_INVALID | min > max |
| 400 | EMI_BANK_LOGO_ALT_REQUIRES_KEY | alt without a key |
8.16 PATCH /api/admin/emi/banks/reorder
Purpose
Rewrite displayOrder from the supplied id order. All ids must exist and be live (404
otherwise). Transactional.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_UPDATE· Rate limit:ADMIN_WRITE
8.17 GET /api/admin/emi/banks/{bankId}
Purpose
One bank with its rate history and offered tenures.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_READ· Rate limit:ADMIN_READ
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 404 | EMI_BANK_NOT_FOUND | Missing or soft-deleted |
8.18 PATCH /api/admin/emi/banks/{bankId}
Purpose
Update name/logo/loan limits/display order/active flag.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_UPDATE· Rate limit:ADMIN_WRITE
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 409 | EMI_BANK_NAME_ALREADY_EXISTS | Duplicate live name |
| 400 | EMI_BANK_LOAN_RANGE_INVALID | min > max (cross-field CHECK — the DTO mirrors it, the DB enforces) |
| 400 | EMI_BANK_LOGO_ALT_REQUIRES_KEY | alt without a key |
8.19 DELETE /api/admin/emi/banks/{bankId}
Purpose
Soft delete. A bank disappears from the customer catalog.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_DELETE· Rate limit:ADMIN_WRITE
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 409 | EMI_BANK_INACTIVE | Acting on an already-deactivated bank |
| 409 | referenced-row | RESTRICT refusal mapped via isReferencedRowError |
8.20 POST /api/admin/emi/banks/{bankId}/restore
Purpose
Restore a soft-deleted bank.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_RESTORE· Rate limit:ADMIN_WRITE
8.21 GET /api/admin/emi/banks/{bankId}/rates
Purpose
The append-only rate history.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_READ· Rate limit:ADMIN_READ
8.22 POST /api/admin/emi/banks/{bankId}/rates
Purpose
Schedule a rate (annualRateBps 0…10000, effectiveFrom ISO instant). It becomes the quoted
rate at effective_from. There is no PATCH — a rate is corrected by superseding it.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_CREATE· Rate limit:ADMIN_WRITE
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 409 | EMI_RATE_EFFECTIVE_DATE_ALREADY_EXISTS | Same effective date on a live row |
| 400 | EMI_RATE_EFFECTIVE_DATE_OUT_OF_RANGE | Date ≥ 2100 |
| 404 | EMI_BANK_NOT_FOUND | Missing bank |
8.23 DELETE /api/admin/emi/banks/{bankId}/rates/{rateId}
Purpose
Cancel a scheduled rate (only while effective_from > now()). A rate that is or has been
in force is refused — the record of what was quoted.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_DELETE· Rate limit:ADMIN_WRITE
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 409 | EMI_RATE_IN_FORCE_NOT_DELETABLE | Rate in force or past |
| 409 | EMI_BANK_RATE_NOT_CONFIGURED | No rate row at all |
8.24 GET /api/admin/emi/banks/{bankId}/tenures
Purpose
The bank's offered periods.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_READ· Rate limit:ADMIN_READ
8.25 PUT /api/admin/emi/banks/{bankId}/tenures
Purpose
Replace the bank's offered periods wholesale. Every month must be in the vocabulary and
active (deactivation blocks new assignments). Serialised with FOR UPDATE so two
concurrent replaces cannot produce the union of their sets.
Auth and Permissions
- Auth:
JwtAuthGuard+RoleGuard· Permission:EmiBanks_UPDATE· Rate limit:ADMIN_WRITE
Error Cases
| HTTP | Code | Condition |
|---|---|---|
| 400 | EMI_TENURE_NOT_IN_VOCABULARY | A month is missing from the vocabulary or deactivated — the message names the period |
| 404 | EMI_BANK_NOT_FOUND | Missing bank |
9. Flow Diagrams
9.1 Route Ownership
9.2 Request Sequence (options)
9.3 Error Branch (calculate)
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination Type | Default Size | Max Size | Sort Fields | Filters | Result Cap |
|---|---|---|---|---|---|---|
GET /api/admin/emi/banks | offset page/size | 20 | 100 | displayOrder / name / createdAt (closed set) | none | — |
GET /api/admin/emi/eligibility/effective-products | offset page/size | 20 | 100 | standard | none (mode drives the set) | — |
11. Caching, Jobs, and External Integrations
| Integration | Used? | Details | Source |
|---|---|---|---|
| Redis cache | Yes | emi:v1:settings (30s), emi:v1:eligible-categories (30s), emi:v1:banks (min(30s, next rate boundary)); nothing price-derived; cache-down → uncached read | emi.constants.ts |
| BullMQ | No | No queue exists by design | — |
| External API | No | No bank is contacted; references are never validated | — |
13. Mandatory Deep API Documentation Pack
13.1 Route-by-Route Completeness Matrix
| Route | Controller Method | DTOs | Service Method | Guards | Permissions | Cache | Jobs | DB Touches | Errors | Documented? |
|---|---|---|---|---|---|---|---|---|---|---|
GET /options | options | EmiProductParamsDto + EmiVariantQueryDto | getOptions | Public+Throttle | — | settings/banks read | — | product, variant, settings, banks | 404/400 | Yes |
GET /calculate | calculate | EmiProductParamsDto + CalculateEmiQueryDto | calculate | Public+Throttle | — | settings/banks read | — | product, variant, settings, banks | 12 codes | Yes |
GET /settings | get | — | get | JWT+Role+Throttle | Emi_READ | — | — | settings | — | Yes |
PUT /settings | update | UpdateEmiSettingsDto | update | JWT+Role+Throttle | Emi_UPDATE | invalidate emi | — | settings | 400 | Yes |
GET /tenures | list | — | list | JWT+Role+Throttle | Emi_READ | — | — | tenures | — | Yes |
PUT /tenures | replace | ReplaceEmiTenuresDto | replace | JWT+Role+Throttle | Emi_UPDATE | invalidate | — | tenures | 400/409 | Yes |
GET/POST/DELETE /eligibility/categories | 3 methods | category DTOs | eligibility service | JWT+Role+Throttle | Emi_* | invalidate | — | selection tables | 404/409 | Yes |
GET/POST/DELETE /eligibility/products | 3 methods | product DTOs | eligibility service | JWT+Role+Throttle | Emi_* | invalidate | — | selection tables | 404/409 | Yes |
GET /eligibility/effective-products | listEffectiveProducts | query | same | JWT+Role+Throttle | Emi_READ | — | — | settings, expansion, products | — | Yes |
GET /banks | list | query | list | JWT+Role+Throttle | EmiBanks_READ | — | — | banks+rates+tenures | — | Yes |
POST /banks | create | CreateEmiBankDto | create | JWT+Role+Throttle | EmiBanks_CREATE | invalidate | — | banks | 409/400 | Yes |
PATCH /banks/reorder | reorder | ReorderEmiBanksDto | reorder | JWT+Role+Throttle | EmiBanks_UPDATE | invalidate | — | banks (tx) | 404 | Yes |
GET /banks/{bankId} | findOne | — | findOne | JWT+Role+Throttle | EmiBanks_READ | — | — | bank+rates+tenures | 404 | Yes |
PATCH /banks/{bankId} | update | UpdateEmiBankDto | update | JWT+Role+Throttle | EmiBanks_UPDATE | invalidate | — | bank | 409/400 | Yes |
DELETE /banks/{bankId} | remove | — | remove | JWT+Role+Throttle | EmiBanks_DELETE | invalidate | — | bank | 409 | Yes |
POST /banks/{bankId}/restore | restore | — | restore | JWT+Role+Throttle | EmiBanks_RESTORE | invalidate | — | bank | 404 | Yes |
GET /banks/{bankId}/rates | list | — | rate service list | JWT+Role+Throttle | EmiBanks_READ | — | — | rates | 404 | Yes |
POST /banks/{bankId}/rates | create | CreateEmiBankRateDto | rate service create | JWT+Role+Throttle | EmiBanks_CREATE | invalidate | — | rates | 409/400/404 | Yes |
DELETE .../rates/{rateId} | remove | — | rate service remove | JWT+Role+Throttle | EmiBanks_DELETE | invalidate | — | rates | 409 | Yes |
GET /banks/{bankId}/tenures | list | — | bank-tenure list | JWT+Role+Throttle | EmiBanks_READ | — | — | tenures | 404 | Yes |
PUT /banks/{bankId}/tenures | replace | ReplaceEmiBankTenuresDto | bank-tenure replace | JWT+Role+Throttle | EmiBanks_UPDATE | invalidate | — | link rows (tx+lock) | 400/404 | Yes |
13.2 Request/Response Exhaustiveness
Covered in §6 and §8: minimal and full payloads for every body endpoint, the two customer responses with real verified numbers (§8.1/8.2), every nullable field, per-endpoint error tables with the exact codes, and the uuid-v7 / tenure-integer validation notes.
13.3 API Diagram Pack
Route ownership (§9.1), options sequence (§9.2), calculate error tree (§9.3), plus the estimator and rate-boundary flows in the feature/backend docs.
13.4 Consumer Integration Notes
| Consumer | Required Knowledge | Failure Handling | Contract Stability |
|---|---|---|---|
| Storefront (product detail) | Call /options before rendering any EMI affordance; pass variantId once chosen, omit it for the pre-selection "from" estimate; render quotes as given — never recompute; basePrice echo detects repricing; never append an extra query param (campaign tag, cache-buster) — forbidNonWhitelisted turns it into a 400 | available: false → hide affordance (reason available); 404 → 404 the page; 404 EMI_VARIANT_NOT_FOUND → the variant vanished or belongs elsewhere, refetch the product | Stable |
| Storefront (calculate) | bankId/tenureMonths from the options response; same variantId used for /options; uuid v7; tenure int 1…120; same no-extra-query-param rule | 403/409 → hide; 404 bank/variant → re-fetch options; 400 tenure → re-fetch options | Stable |
| Storefront (UI copy) | isEstimate: true always; equal installments cannot exactly retire an amortising loan (final month absorbs residue) | — | Stable |
| Admin (merchandiser) | Emi_* grants; Emi_UPDATE carries the kill switch; 30s cache lag | 409 duplicate selections | Stable |
| Admin (rate-entry) | EmiBanks_* grants; rates are append-only — no PATCH, future-only delete | 409 in-force delete → supersede instead | Stable |
| QA | 24 error codes; exact arithmetic; 0% branch; offerability four conditions | Reproduce via exact codes | Stable |
13.5 API Tradeoffs and Rationale
| Decision | Chosen Behavior | Alternatives Considered | Why This Tradeoff | Risk | Mitigation |
|---|---|---|---|---|---|
| Server computes all quotes | quotes in /options | Client-side formula | Browser float drift — the server is right | Payload size | nil (0.004ms/quote) |
available: false instead of 4xx | Options never errors on ineligibility | 4xx | Distinguish "no EMI" from "no product" | — | — |
| Coded errors on calculate | Specific codes | Generic 400 | Client has seen the offer; failure = change underneath | — | — |
| Tenure as query int | tenureMonths=12 | body/path | Simple; DTO bounds the DoS exponent | — | — |
| uuid v7 required | v4 rejected | accept any uuid | platform convention | client surprise | documented |
| No PATCH on rates | supersede-only | editable rate | record of what was advertised | operator must re-enter | documented |
13.6 API Change Impact
| Change | Affected Consumers | Backend Impact | Data Impact | Migration Needed? | Compatibility Plan |
|---|---|---|---|---|---|
| New bank/rate/tenure admin fields | Admin panel forms | DTO/service | none | No | Additive |
| New eligibility mode | Admin + storefront | enum + service | none | No | Additive |
| Price-source list grows | Admin panel | enum | none | No | Additive (contract spec guards) |
emi:v1: shape change | none (server-rendered quotes) | cache prefix bump | none | No | bump to v2 |
14. Zero-Omission API Checklist
- Every controller route is documented (§4, §8, §13.1).
- Every parent route prefix and runtime URL is documented (§2, §4) — including the
/api/mobile/emi/...and NO-/api/emi/...fact. - Every DTO field, enum, default, transform, and validator is documented (§6, §7).
- Every response field, nullable field, and generated field is documented (§6.6, §8).
- Every auth, guard, permission, and public branch is documented (§5).
- Every success, validation, auth, not-found, conflict, and rate-limit branch is documented (§8).
- Every database read/write, cache hit/miss/invalidation, queue job, and external call is documented (§8 side effects, §11).
- Every route has examples for minimal request, success response, and representative failures (§8).
- Every endpoint family has route, sequence, and error diagrams (§9).
- Every tradeoff and compatibility risk is documented (§13.5, §13.6).
- The API doc links to backend and features/flows (See Also).
15. Integration Checklist
- Every route from controllers is documented (25/25).
- Every DTO field is documented.
- Every enum value is documented.
- Every response envelope is documented.
- Every error code is documented (25/25, including
EMI_VARIANT_NOT_FOUND). - Every auth guard and permission is documented.
- Every cache key, queue job, and external call is documented — no queue exists by design.
- Every diagram matches the current code.
- The API doc links to backend and features/flows.
See Also
- Backend doc:
/docs/developer/emi/backend - Features and flows doc:
/docs/developer/emi/feature - TDD: not yet published
EMI Calculator Module Overview
The exact-rational EMI estimator — bank offerability, tenure vocabulary, rate history, and two customer read routes, all off by default.
EMI Calculator Backend Documentation
Backend architecture, data model, exact arithmetic, services, caching, and operational behavior for the EMI Calculator module.