feat(integration): core banking console at /officer/integration
Three tabs. Connections lists each bank's configured link with its last status, and lets an operations user test or run a sync. Field mapping is the screen that carries the argument: one row per loan-book field, one column per lender's name for it, which is what "every bank's core is different" actually reduces to. Sync history reads as an audit — a failed run is a row, because a feed that stopped three weeks ago must be visible as something other than silence. Simulated adapters are badged everywhere they appear, including in the run history, because the failure mode that matters here is a demo mistaken for a live feed. Unmapped vendor columns are shown being dropped at the boundary, with identifying ones marked never stored. The client-side identifying-column check mirrors the backend's, matching on the whole name and on each part with separators and camelCase split alike — so CUST_NAME and borrowerMobile are refused next to the field rather than after a round trip. Follows the house pattern: useQuery against the real endpoint, in-file demo fallback, "Demo data" pill when the backend is unreachable. The demo fixtures deliberately include one connection that has never run and one that last failed. Build and oxlint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -467,6 +467,98 @@ export interface ManualEntryPreview {
|
||||
warnings: ManualEntryLine[]
|
||||
}
|
||||
|
||||
// ── Core banking / LMS integration ───────────────────────────────
|
||||
// The loan book is the training label, and no two lenders expose it the same
|
||||
// way. What is built is the seam: an adapter per transport, a mapping per bank,
|
||||
// and one validator — the same one an uploaded spreadsheet goes through.
|
||||
|
||||
/** An adapter the platform can run. `simulated` is rendered, never hidden. */
|
||||
export interface ConnectorCatalogEntry {
|
||||
vendor: string
|
||||
displayName: string
|
||||
transport: 'FILE' | 'REST' | 'JDBC'
|
||||
simulated: boolean
|
||||
nativeColumns: string[]
|
||||
/** What a bank would have to do to make this one live. */
|
||||
activationNote: string
|
||||
suggestedMapping: Record<string, string>
|
||||
}
|
||||
|
||||
/** One bank's configured link — which adapter, pointed where, whose column means what. */
|
||||
export interface ConnectorProfileRow {
|
||||
code: string
|
||||
displayName: string
|
||||
vendor: string
|
||||
vendorName: string
|
||||
transport: string | null
|
||||
simulated: boolean
|
||||
activationNote: string | null
|
||||
institutionCode: string | null
|
||||
endpoint: string | null
|
||||
scheduleCron: string | null
|
||||
autoCommit: boolean
|
||||
active: boolean
|
||||
lastSyncAt: string | null
|
||||
lastStatus: 'SUCCESS' | 'PARTIAL' | 'FAILED' | 'NEVER_RUN' | null
|
||||
/** { canonical_column: vendor_column }. Unmapped means dropped at the boundary. */
|
||||
mapping: Record<string, string>
|
||||
sourceColumns: string[]
|
||||
}
|
||||
|
||||
export interface ConnectorSyncRun {
|
||||
id: number
|
||||
profileCode: string
|
||||
vendor: string | null
|
||||
institutionCode: string | null
|
||||
trigger: 'MANUAL' | 'SCHEDULED'
|
||||
triggeredBy: string
|
||||
status: 'SUCCESS' | 'PARTIAL' | 'FAILED'
|
||||
/** Stamped per run, not read from the profile — old runs must not turn real. */
|
||||
simulated: boolean
|
||||
asOfDate: string | null
|
||||
rowsFetched: number | null
|
||||
rowsAccepted: number | null
|
||||
rowsRejected: number | null
|
||||
rowsOverwriting: number | null
|
||||
columnsDropped: number | null
|
||||
batchId: number | null
|
||||
committed: boolean
|
||||
durationMs: number | null
|
||||
startedAt: string
|
||||
message: string | null
|
||||
}
|
||||
|
||||
/** What a connection test found, without staging anything. */
|
||||
export interface ConnectorTestResult {
|
||||
profileCode: string
|
||||
vendor: string
|
||||
simulated: boolean
|
||||
transport: string
|
||||
reachable: boolean
|
||||
label?: string
|
||||
message?: string
|
||||
rowsAvailable?: number
|
||||
sourceColumns: string[]
|
||||
sampleRow?: Record<string, string>
|
||||
mappedSampleRow?: Record<string, string>
|
||||
droppedColumns?: string[]
|
||||
missingRequired?: string[]
|
||||
}
|
||||
|
||||
export interface ConnectorSyncResponse {
|
||||
run: ConnectorSyncRun
|
||||
droppedColumns: string[]
|
||||
extractLabel: string | null
|
||||
preview?: ManualEntryPreview
|
||||
}
|
||||
|
||||
/** The template a mapping targets, and the columns that may never be mapped in. */
|
||||
export interface CanonicalColumns {
|
||||
columns: string[]
|
||||
required: string[]
|
||||
neverStored: string[]
|
||||
}
|
||||
|
||||
/** Multipart, so it bypasses the JSON helpers. */
|
||||
export async function uploadManual(file: File, type: 'MARKET_SIGNAL' | 'LOAN_RECORD') {
|
||||
const form = new FormData()
|
||||
|
||||
Reference in New Issue
Block a user