feat(connector): core banking / LMS seam, one real adapter and four dialects
The loan book is the training label and the one input no public feed
supplies. Integrating with a lender's core is not a technical unknown —
it is an onboarding process per bank, with a schema agreed in a room.
What can be built before that room is the seam, and this is it.
CoreBankingConnector fetches and returns rows; it never writes. Mapped
rows go to LoanBookEntryService.stage — the same validator an uploaded
spreadsheet goes through, landing in the same manual_entry_batch with
the same preview. A parallel validator for the automated path would be
the obvious shape and would eventually enforce the no-PII rule
differently.
FileDropConnector is real: it reads the newest matching file from a
directory a bank delivers to, and leaves it there so a failed run is
repeatable. Finacle, FLEXCUBE, BaNCS and an in-house REST LMS are
simulated, each emitting the same portfolio in its own column dialect —
DPD_DAYS, OVERDUE_DAYS, DAYS_OVERDUE, daysPastDue — which is what makes
the case for a mapping layer rather than asserting it.
The mapping is {canonical: vendor}, keyed by canonical name, so an
unmapped vendor column has no route through and is dropped at the
boundary. CUST_NAME and MOBILE_NO stay out structurally rather than by
anyone remembering. Auto-commit is off on every profile: a sync stages
and stops, keeping the rule that nothing is written until someone has
seen what will be. simulated is stamped per run, not read from the
profile, so switching a profile live cannot make old runs look real.
Fixes a real defect this surfaced: FORBIDDEN_COLUMNS matched on the
exact normalised name, so CUST_NAME — what Finacle actually calls it —
was missed, and a directly uploaded bank file would have kept the
borrower's name in manual_entry_line.raw_row. Matching now splits
separators and camelCase, catching borrowerMobile too, while
loan_account_ref still splits clean.
Verified against a running instance: Finacle 44/44 accepted with 4
columns dropped, the file drop 2 accepted and 1 UNKNOWN_ENTERPRISE
rejected from a real CSV, and both CUST_NAME and borrowerMobile refused
at mapping save.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+27
-1
@@ -176,6 +176,32 @@ A price far from last week's **warns and is accepted** — a genuine price shock
|
||||
|
||||
**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.
|
||||
|
||||
A header matches on the whole normalised name **and on each part**, splitting separators and camelCase alike — a real core ships `CUST_NAME` and an in-house LMS ships `borrowerMobile`, neither of which an exact-match list catches. `loan_account_ref` splits to `loan`/`account`/`ref` and stays clean, which is why this is part-matching and not substring-matching.
|
||||
|
||||
### Core banking / LMS integration
|
||||
|
||||
The loan book is the one input no public feed supplies, and no two lenders expose it the same way. What exists is the seam, not an integration: an adapter per transport, a mapping per bank, and one validator — mapped rows are handed to the same `LoanBookEntryService.stage` an uploaded spreadsheet goes through, and land in the same `manual_entry_batch` with the same preview.
|
||||
|
||||
| Method | Path | Notes |
|
||||
|---|---|---|
|
||||
| `GET` | `/admin/connectors/catalog` | every adapter, its dialect, and `simulated` |
|
||||
| `GET` | `/admin/connectors/canonical-columns` | the template a mapping targets, plus what may never be mapped in |
|
||||
| `GET` | `/admin/connectors/profiles`, `/admin/connectors/profiles/{code}` | configured links |
|
||||
| `POST` | `/admin/connectors/profiles` | onboard a lender |
|
||||
| `PUT` | `/admin/connectors/profiles/{code}` | mapping and settings; **an invalid mapping is refused here**, not at the next nightly run |
|
||||
| `POST` | `/admin/connectors/profiles/{code}/test` | reaches the source and shows the columns. **Stages nothing.** |
|
||||
| `POST` | `/admin/connectors/profiles/{code}/sync` | pulls and stages; writes only if the profile has `autoCommit` |
|
||||
| `GET` | `/admin/connectors/runs` | every attempt, successful or not |
|
||||
| `GET` | `/admin/connectors/runs/{runId}/preview` | the staged batch behind a run |
|
||||
|
||||
**One adapter is real.** `FILE_DROP` reads an actual extract from a directory a bank delivers to (`udyam.connector.file-drop.dir`, default `./data/cbs-drop`), newest file matching the profile's glob, left where it was found so a failed run is repeatable. `FINACLE`, `FLEXCUBE`, `BANCS` and `CUSTOM_API` are **simulated**: they generate an extract in each core's own column dialect so the mapping layer is exercisable end to end. `simulated` is stamped per run, not read from the profile — switching a profile to a live adapter must not retroactively make its old runs look real.
|
||||
|
||||
**The mapping is `{canonical_column: vendor_column}`, and the direction matters.** Keyed by the canonical name, so a vendor column nobody mapped has no route through: unmapped columns are **dropped at the boundary** and counted on the run. That is how `CUST_NAME` and `MOBILE_NO` stay out of a system that promised not to hold them — structurally, rather than by anyone remembering.
|
||||
|
||||
**Auto-commit is off by default.** A sync stages and stops, preserving the manual path's rule that nothing is written until someone has seen what will be. A bank that wants the nightly feed unattended turns it on per profile, knowingly.
|
||||
|
||||
A failed run is still a run. Nothing to read is `SUCCESS` with a message, not `FAILED` — a bank that has not delivered tonight's file is a normal state, and conflating it with a broken connector trains everyone to ignore the status.
|
||||
|
||||
---
|
||||
|
||||
## 5. `/app/**` — the enterprise PWA
|
||||
@@ -201,6 +227,6 @@ Never called in a request path. A nightly job writes signals; the API serves onl
|
||||
| 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 |
|
||||
| Loan book | CBS/LMS extract | connector seam built; one real adapter (file drop), four simulated dialects. Manual upload remains the fallback |
|
||||
|
||||
See `ARCHITECTURE.md` §4 for provenance and the degradation rules.
|
||||
|
||||
Reference in New Issue
Block a user