A judge who cannot run the code reads the README, and until now the reasoning behind this system lived only in Java comments and commit messages — including the one thing a NABARD panel will check first, which is why the apex tier cannot reach a borrower. README covers the institutional model and why NABARD's restriction is the point rather than a limitation, how to run both projects, the seven console logins with what each sees, the demo path, and the design decisions worth defending. It also states plainly what is real and what is not: live Agmarknet and Open-Meteo data and a working rule engine, a forecast that is deterministic rather than machine-learned, and a seeded loan book. It says that trust-seeded defaults to true and that the honest setting turns 34 trip-wires into 5. A demo that implies more than it has is worth less than one that is straight about the gap. API-SPEC documents every endpoint across the four surfaces, the response envelope, the tenancy rule, small-cell suppression, effective dating and the rejection reasons — plus the one defect found while writing it: SecurityConfig whitelists /health but nothing is mapped there, so it returns 500. The working probe is /auth/health. Every credential and figure quoted was verified against the running service: seven logins, the enterprise PIN, and the 44 / 39 / 59 / 12 portfolio counts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
207 lines
10 KiB
Markdown
207 lines
10 KiB
Markdown
# UdyamLinc — API Specification
|
|
|
|
Base URL: `http://localhost:8082/api/v1/udyam`
|
|
|
|
Every response uses one envelope. The UI's `api.ts` unwraps `.data`, so the shape is stable across every endpoint.
|
|
|
|
```json
|
|
{ "success": true, "message": "OK", "data": { } }
|
|
```
|
|
|
|
A failure keeps the envelope and nulls the payload:
|
|
|
|
```json
|
|
{ "success": false, "message": "No enterprise MSME-1409", "data": null }
|
|
```
|
|
|
|
---
|
|
|
|
## 1. Authentication and tiers
|
|
|
|
Three surfaces, three institutional tiers. **They are not ranks of one permission** — an apex user cannot reach a bank endpoint, and a bank user cannot reach an apex one. Both get `403`.
|
|
|
|
| Scope | Who | May read |
|
|
|---|---|---|
|
|
| `NABARD` | the apex institution | aggregates across every partner bank; **no borrower row at all** |
|
|
| `BANK` | a lending institution | its own book in full, optionally narrowed to a branch |
|
|
| `ENTERPRISE` | a borrower | itself |
|
|
|
|
### The tenancy rule
|
|
|
|
The institution is a **signed claim in the JWT**. It is never read from a path variable, query parameter or request body. A tenancy key the caller can type is not a tenancy key.
|
|
|
|
`TenantContext` is resolved once per request from the token and injected into handlers as an ordinary argument, so an endpoint that needs scoping cannot forget to ask for it. The filter **fails closed**: a caller with no institution sees nothing, not everything.
|
|
|
|
Narrowing parameters that select a *view* — `level`, `zone`, `state`, `block` — do come from the request, because they grant nothing. An apex user may read every zone, so naming one changes only what is displayed.
|
|
|
|
### Cross-tenant reads return 404, not 403
|
|
|
|
Requesting a borrower on another bank's book returns exactly what a fabricated id returns:
|
|
|
|
```
|
|
GET /officer/enterprises/MSME-1409 as dccb.admin (owner) → 200
|
|
GET /officer/enterprises/MSME-1409 as mpgb.admin (other) → 404
|
|
{"success":false,"message":"No enterprise MSME-1409","data":null}
|
|
```
|
|
|
|
A distinguishable `403` would confirm the id is real and on somebody's book — enough to enumerate a competitor's portfolio without reading a single field.
|
|
|
|
### Endpoints
|
|
|
|
| Method | Path | Notes |
|
|
|---|---|---|
|
|
| `POST` | `/auth/officer-login` | Both staff tiers. Issues an apex or bank token depending on the user's institution. |
|
|
| `POST` | `/auth/app-login` | Enterprise PWA. `{ enterpriseId, pin }`. |
|
|
| `GET` | `/auth/health` | `{ status: "UP", service: "udyamlinc-backend" }` |
|
|
|
|
> **Known defect.** `SecurityConfig` whitelists `/health`, but no handler is mapped there — it returns `500`. The working probe is `/auth/health`. Worth fixing before any container health check points at it.
|
|
|
|
---
|
|
|
|
## 2. `/nabard/**` — the apex surface
|
|
|
|
Requires `SCOPE_NABARD`. **Not one endpoint here returns an enterprise id, a borrower name, or any row joinable back to a person.** That is verified rather than asserted: a check across all six read endpoints returns zero `MSME-` identifiers.
|
|
|
|
**Small-cell suppression.** Any cell with fewer than `udyam.apex.min-cell-size` (default 5) borrowers is returned with `suppressed: true` and no figures. A district with two borrowers and one alert discloses that borrower as surely as naming them. Suppressed rows are **returned, not omitted** — a missing district reads as "no activity", a different and wrong claim.
|
|
|
|
| Method | Path | Returns |
|
|
|---|---|---|
|
|
| `GET` | `/nabard/dashboard` | national totals, stress rate, portfolio value, open alerts, `minCellSize`, `dataAgeDays` |
|
|
| `GET` | `/nabard/banks` | partner-bank scorecard |
|
|
| `GET` | `/nabard/banks/{code}/branches` | one bank by branch — **the deepest this tier goes** |
|
|
| `GET` | `/nabard/geography?level=&zone=&state=` | `level` = `ZONE` \| `STATE` \| `DISTRICT` |
|
|
| `GET` | `/nabard/districts` | stress by district |
|
|
| `GET` | `/nabard/sectors` | stress by sector |
|
|
| `GET` | `/nabard/digest` | districts, sectors and slowest-responding banks |
|
|
| `GET` | `/nabard/institutions` | the institution and branch register |
|
|
|
|
**Response rate** on the scorecard is the share of raised alerts a bank has acted on. Stress rate is largely a fact about a bank's districts and sectors; response rate is a fact about the bank, and it is the number no bank self-reports.
|
|
|
|
---
|
|
|
|
## 3. `/officer/**` — the bank console
|
|
|
|
Requires `SCOPE_BANK`. Every response is filtered to the caller's institution, and to their branch when they are branch-scoped.
|
|
|
|
| Method | Path | Notes |
|
|
|---|---|---|
|
|
| `GET` | `/officer/portfolio?block=&month=` | counts plus rows |
|
|
| `GET` | `/officer/enterprises?block=` | ranked by stress score |
|
|
| `GET` | `/officer/enterprises/{enterpriseId}` | score, reasons, forecast, alerts, visits, recent ledger |
|
|
| `GET` | `/officer/alerts?block=&state=` | scoped through visible borrowers |
|
|
| `PUT` | `/officer/alerts/{alertId}` | `OPEN` \| `ACKNOWLEDGED` \| `ESCALATED` \| `SNOOZED` \| `CLOSED`; re-checks tenancy |
|
|
| `GET` | `/officer/visits?block=` | |
|
|
| `POST` | `/officer/visits` | schedule from the playbook |
|
|
| `PUT` | `/officer/visits/{visitId}/outcome` | |
|
|
| `GET` | `/officer/playbook` | intervention catalogue |
|
|
| `GET` | `/officer/sectors?block=` | sector rollup |
|
|
| `GET` | `/forecast/{enterpriseId}` | stored quantile path, first breach, shortfall |
|
|
| `GET` | `/forecast/runs` | |
|
|
|
|
---
|
|
|
|
## 4. `/admin/**` — configuration and operations
|
|
|
|
`GET` is open to any console tier. Writes require `ROLE_SUPERVISOR`, `ROLE_BANK_ADMIN` or `ROLE_DATA_STEWARD`.
|
|
|
|
### Ingestion
|
|
|
|
| Method | Path | Notes |
|
|
|---|---|---|
|
|
| `POST` | `/admin/ingestion/run` | all sources |
|
|
| `POST` | `/admin/ingestion/run/{source}` | `AGMARKNET` \| `OPEN_METEO` |
|
|
| `GET` | `/admin/ingestion/runs?source=&limit=` | **every outcome, including a skip** |
|
|
| `GET` | `/admin/ingestion/calls` | outbound call log; the API key is redacted |
|
|
| `GET` | `/admin/ingestion/signals/market?weeks=` | carries `source`: `AGMARKNET` or `MANUAL` |
|
|
| `GET` | `/admin/ingestion/signals/climate?weeks=` | |
|
|
| `GET` | `/admin/ingestion/freshness` | per-district staleness — what a trip-wire checks first |
|
|
|
|
### Features
|
|
|
|
| Method | Path | Notes |
|
|
|---|---|---|
|
|
| `POST` | `/admin/features/compute` | whole book |
|
|
| `POST` | `/admin/features/compute/{enterpriseId}` | |
|
|
| `GET` | `/admin/features/definitions` | catalogue with staleness tolerances |
|
|
| `GET` | `/admin/features/snapshot/{enterpriseId}` | wide row |
|
|
| `GET` | `/admin/features/values/{enterpriseId}` | tall rows **including nulls and their reasons** |
|
|
| `GET` | `/admin/features/coverage` | populated share per feature |
|
|
|
|
A feature that cannot be computed is stored as null with a reason — `NO_SIGNAL`, `NO_LEDGER`, `NO_CBS`, `NO_CONSENT`, `NO_FORECAST`, `INSUFFICIENT_HISTORY`. "We did not know" and "we never looked" are different facts.
|
|
|
|
### Scoring and rules
|
|
|
|
| Method | Path | Notes |
|
|
|---|---|---|
|
|
| `POST` | `/admin/scoring/run` | whole book |
|
|
| `POST` | `/admin/scoring/run/{enterpriseId}` | |
|
|
| `GET` | `/admin/scoring/runs` | |
|
|
| `GET` | `/admin/scoring/explain/{enterpriseId}` | every rule's outcome, no writes |
|
|
| `GET` `POST` `PUT` `DELETE` | `/admin/rules`, `/admin/rules/{code}` | trip-wire CRUD; version bumps on edit |
|
|
| `PUT` | `/admin/sectors/{code}/weights` | sub-score weights in basis points |
|
|
| `GET` | `/admin/sectors`, `/admin/districts`, `/admin/commodities` | masters |
|
|
|
|
`explain` returns one row per rule with a status that distinguishes the cases that matter:
|
|
|
|
```
|
|
TW-01 FIRED DSCR(P10) < 1.00
|
|
TW-02 SKIPPED_MISSING upi_inflow_change (NO_CONSENT)
|
|
TW-03 NOT_FIRED no entry ≥ 14 days
|
|
TW-05 SKIPPED_MISSING price_z_score (INSUFFICIENT_HISTORY)
|
|
```
|
|
|
|
`NOT_FIRED` means the enterprise is fine. `SKIPPED_STALE` and `SKIPPED_MISSING` mean the rule could not run. These must never look the same.
|
|
|
|
### Manual and bulk entry
|
|
|
|
For the feeds no API serves — egg, milk, fodder — and the loan book.
|
|
|
|
| Method | Path | Notes |
|
|
|---|---|---|
|
|
| `GET` | `/admin/manual/template/market` | CSV template |
|
|
| `GET` | `/admin/manual/template/loans` | CSV template |
|
|
| `POST` | `/admin/manual/upload?type=` | multipart; `MARKET_SIGNAL` \| `LOAN_RECORD`. **Stages a preview and writes nothing.** |
|
|
| `POST` | `/admin/manual/{batchId}/commit` | only a `PREVIEW` batch may commit |
|
|
| `DELETE` | `/admin/manual/{batchId}` | discard |
|
|
| `POST` | `/admin/manual/entry` | one row, validated and committed |
|
|
| `GET` | `/admin/manual/batches`, `/admin/manual/batches/{id}` | audit trail |
|
|
|
|
An upload returns `accepted`, `rejected`, `overwriting` and `warnings` as **separate lists**. Merging them hides the only two questions the person clicking Commit has: what will not land, and what will land on top of something already there. An overwriting row carries `priorModalPrice`.
|
|
|
|
**Effective dating.** `effective_date` is the date the fact describes and comes from the file. `observedAt` is when we learned it and is stamped at commit — the uploader cannot reach it. A March rate uploaded in August must stay invisible to a March scoring run, or the model trains on data that did not exist yet.
|
|
|
|
**Rejection reasons.** `UNKNOWN_DISTRICT`, `UNKNOWN_COMMODITY`, `UNKNOWN_ENTERPRISE`, `FUTURE_DATE`, `BAD_DATE`, `BAD_NUMBER`, `UNIT_MISMATCH`, `PRICE_ORDER`, `DUPLICATE_IN_FILE`, `MISSING_REQUIRED`, `IDENTIFYING_COLUMN`.
|
|
|
|
A price far from last week's **warns and is accepted** — a genuine price shock is the event this system exists to catch, and rejecting it would be backwards.
|
|
|
|
**The loan book carries no borrower identity.** A file whose headers include a name, phone, Aadhaar, PAN, account number or address is **refused whole**, with the offending columns named. Dropping the column quietly would let a bank believe it had sent something we promised to keep and had not.
|
|
|
|
---
|
|
|
|
## 5. `/app/**` — the enterprise PWA
|
|
|
|
Requires `SCOPE_ENTERPRISE`, pinned to one enterprise id.
|
|
|
|
| Method | Path | Notes |
|
|
|---|---|---|
|
|
| `GET` | `/app/home` | |
|
|
| `GET` | `/app/ledger` | |
|
|
| `POST` | `/app/ledger/sync` | offline queue; `clientId` is generated on the phone so an offline entry keeps identity through sync |
|
|
| `GET` | `/app/alerts` | |
|
|
| `PUT` | `/app/consent` | |
|
|
|
|
---
|
|
|
|
## 6. External feeds
|
|
|
|
Never called in a request path. A nightly job writes signals; the API serves only what is already in Postgres.
|
|
|
|
| Feed | Source | Notes |
|
|
|---|---|---|
|
|
| Mandi prices | data.gov.in resource `35985678-0d79-46b4-9ed6-6f13308a1d24` | filters are `State`/`District`/`Commodity`, capitalised and exact; `sort[Arrival_Date]=desc` is required |
|
|
| Rainfall, temperature | Open-Meteo archive | no key; meters by request cost, so a rate-limited call retries with backoff |
|
|
| Egg, milk, fodder | **no usable API** | manual entry |
|
|
| Loan book | CBS/LMS extract | manual upload until a nightly feed exists |
|
|
|
|
See `ARCHITECTURE.md` §4 for provenance and the degradation rules.
|