From e923daf825d7b0c084a32d89037f397aa45a5a40 Mon Sep 17 00:00:00 2001 From: LincPay Date: Fri, 7 Aug 2026 18:58:14 +0530 Subject: [PATCH] =?UTF-8?q?feat(integration):=20DPD=20history=20tab=20?= =?UTF-8?q?=E2=80=94=20coverage,=20generation=20and=20backfill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fourth tab on /officer/integration reading the loan book as a training set. The verdict banner is the point of the screen: DPD history is the model's label, and three thousand generated snapshots look identical on a chart to three thousand real ones, so the split is stated in words at the top rather than left to be inferred from a percentage. Four tiles, and "unknown provenance" is one of them — rows written before stamping existed are shown as their own bucket rather than counted as real. Generation and purge sit beside a backfill-from-core control, with the honest note that a backfill from a simulated adapter produces simulated rows and only a bank's own core counts toward the real total. The demo fixture deliberately reports a book that is NOT trainable. A fixture claiming a healthy training set would defeat the entire purpose of the panel. Co-Authored-By: Claude Opus 5 (1M context) --- src/features/officer/IntegrationPage.tsx | 251 ++++++++++++++++++++++- src/lib/api.ts | 55 +++++ src/lib/demo/connectors.ts | 25 +++ 3 files changed, 328 insertions(+), 3 deletions(-) diff --git a/src/features/officer/IntegrationPage.tsx b/src/features/officer/IntegrationPage.tsx index 6985bd8..b7d9259 100644 --- a/src/features/officer/IntegrationPage.tsx +++ b/src/features/officer/IntegrationPage.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { errMsg, get, post, put } from '../../lib/api' +import { del, errMsg, get, post, put } from '../../lib/api' import type { CanonicalColumns, ConnectorCatalogEntry, @@ -8,6 +8,8 @@ import type { ConnectorSyncResponse, ConnectorSyncRun, ConnectorTestResult, + LoanHistoryCoverage, + LoanHistoryGenerateResult, } from '../../lib/api' import { Card, @@ -20,7 +22,7 @@ import { StatusPill, inputCls, } from '../../components/ui/bits' -import { CANONICAL, demoCatalog, demoProfiles, demoRuns } from '../../lib/demo/connectors' +import { CANONICAL, demoCatalog, demoCoverage, demoProfiles, demoRuns } from '../../lib/demo/connectors' /** * Core banking / LMS integration. @@ -41,7 +43,7 @@ import { CANONICAL, demoCatalog, demoProfiles, demoRuns } from '../../lib/demo/c * waits for a person. */ -type Tab = 'connections' | 'mapping' | 'history' +type Tab = 'connections' | 'mapping' | 'history' | 'loanhistory' /** * Whether a column name carries borrower identity — the same test the backend @@ -131,6 +133,52 @@ export function IntegrationPage() { onError: (e) => setError(errMsg(e)), }) + const coverageQ = useQuery({ + queryKey: ['loan-history-coverage'], + queryFn: () => get('/admin/loan-history/coverage'), + }) + const coverage = coverageQ.data ?? demoCoverage() + + async function refreshHistory() { + await queryClient.invalidateQueries({ queryKey: ['loan-history-coverage'] }) + await queryClient.invalidateQueries({ queryKey: ['connector-runs'] }) + } + + const generateM = useMutation({ + mutationFn: (months: number) => + post(`/admin/loan-history/generate?months=${months}`), + onSuccess: async (data) => { + setError(null) + setNotice(`${data.snapshotsWritten} snapshots written across ${data.loans} loans — ${data.note}`) + await refreshHistory() + }, + onError: (e) => setError(errMsg(e)), + }) + + const purgeM = useMutation({ + mutationFn: () => del('/admin/loan-history/generated'), + onSuccess: async (removed) => { + setError(null) + setNotice(`${removed} generated snapshots removed. Real extracts were left untouched.`) + await refreshHistory() + }, + onError: (e) => setError(errMsg(e)), + }) + + const backfillM = useMutation({ + mutationFn: (vars: { code: string; from: string; to: string }) => + post( + `/admin/connectors/profiles/${vars.code}/backfill?from=${vars.from}&to=${vars.to}`, + ), + onSuccess: async (data) => { + setSyncResult(data) + setError(null) + setNotice(data.run.message) + await refreshHistory() + }, + onError: (e) => setError(errMsg(e)), + }) + const connected = profiles.filter((p) => p.active).length const live = profiles.filter((p) => p.active && !p.simulated).length const failing = profiles.filter((p) => p.lastStatus === 'FAILED').length @@ -175,6 +223,7 @@ export function IntegrationPage() { ['connections', 'Connections'], ['mapping', 'Field mapping'], ['history', 'Sync history'], + ['loanhistory', 'DPD history'], ] as [Tab, string][] ).map(([key, label]) => (