feat(credit-bureau): CIC enquiry-alert dashboard and generic API Management hub (static UI)

Replace CIBIL fetch screens with a dashboard of our customers' loan enquiries
at other lenders (no lender name), CIC whitelist/API mapping, and a Vendor ->
Work -> API set management UI (IN/OUT, SMS, WhatsApp, CIC) with full HTTP detail
and JSON/cURL/Postman/HAR import.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 15:13:56 +05:30
co-authored by Claude Sonnet 5.5
parent 1b70fb4534
commit 7eac2dcf3b
43 changed files with 6557 additions and 2265 deletions
@@ -9,13 +9,14 @@
<div class="cic-page">
<div class="cic-head">
<div>
<h1>CIC API Mapping</h1>
<h1>CIC API Mapping — Enquiry Alerts</h1>
<p>
Map CBS data to each bureau's request, and the bureau's response back to the score and report — no code change
needed.
How the CBS asks each bureau for enquiry alerts, and how the bureau's answer is read into our alerts (customer,
loan type, amount, enquiry date …) — no code change needed.
</p>
</div>
<div class="cic-actions">
<a class="cic-btn" routerLink="/credit-bureau/dashboard">← Dashboard</a>
<a class="cic-btn" routerLink="/credit-bureau/bureaus">CIC Configuration</a>
<button
class="cic-btn primary"
@@ -39,10 +40,12 @@
</select>
</div>
<div class="cic-field">
<label>Enquiry type</label>
<label>API</label>
<select [(ngModel)]="endpointType" (ngModelChange)="load()">
@for (t of catalog?.endpointTypes ?? []; track t) {
<option [value]="t">{{ t === 'CONSUMER_ENQUIRY' ? 'Consumer enquiry' : 'Commercial enquiry' }}</option>
<option [value]="t">
{{ t === 'ENQUIRY_ALERT' ? 'Enquiry alerts (our customer enquired elsewhere)' : t }}
</option>
}
</select>
</div>
@@ -77,7 +80,8 @@
@if (bureau?.environment === 'SIMULATOR') {
<div class="cic-alert info">
In SIMULATOR mode the built-in test bureau answers in the shape of the seeded generic mapping. When moving to the
bureau's UAT / PRODUCTION, change the paths below to match the bureau's own API specification.
bureau's UAT / PRODUCTION, change the paths below to the bureau's own API specification and check them with Test
Response.
</div>
}
@@ -87,29 +91,124 @@
@if (endpoint && !loading) {
<div class="cic-tabs">
<button [class.active]="tab === 'request'" (click)="tab = 'request'">
Request mapping ({{ requestRows.length }})
</button>
<button [class.active]="tab === 'response'" (click)="tab = 'response'">
Response mapping ({{ responseRows.length }})
</button>
<button [class.active]="tab === 'request'" (click)="tab = 'request'">
Request mapping ({{ requestRows.length }})
</button>
<button [class.active]="tab === 'endpoint'" (click)="tab = 'endpoint'">Endpoint, headers &amp; body</button>
<button [class.active]="tab === 'preview'" (click)="tab = 'preview'">Preview (dry run)</button>
<button [class.active]="tab === 'test'" (click)="tab = 'test'">Test response</button>
<button [class.active]="tab === 'preview'" (click)="tab = 'preview'">Preview request</button>
</div>
<!-- REQUEST -->
@if (tab === 'request') {
<!-- RESPONSE -->
@if (tab === 'response') {
<div class="cic-card">
<h2>
CBS field → bureau request
<span class="cic-card-sub">Paths like <code>consumer.ids[0].number</code> build nested JSON and arrays</span>
Bureau response → CBS alert
<span class="cic-card-sub"
>"alerts", "errorCode" and "errorMessage" are read from the whole response; every other field from each
alert</span
>
</h2>
<div class="cic-table-wrap">
<table class="cic-table">
<thead>
<tr>
<th style="width: 34px">#</th>
<th style="min-width: 200px">CBS field</th>
<th style="min-width: 240px">Bureau response JSON path</th>
<th style="min-width: 220px">CBS alert field</th>
<th style="min-width: 150px">Transform</th>
<th style="min-width: 110px">Default</th>
<th style="width: 110px"></th>
</tr>
</thead>
<tbody>
@for (r of responseRows; track $index; let i = $index) {
<tr>
<td class="muted">{{ i + 1 }}</td>
<td>
<input
class="cic-mono"
[(ngModel)]="r.sourceField"
(ngModelChange)="markDirty()"
[placeholder]="isRoot(r.targetField) ? 'data.alerts' : 'enquiry.amount'"
/>
</td>
<td>
<input
class="cic-mono"
[(ngModel)]="r.targetField"
(ngModelChange)="markDirty()"
list="cic-result-fields"
placeholder="amount"
/>
<div class="muted" style="font-size: 10px">
{{ isRoot(r.targetField) ? 'whole response' : 'each alert' }}
</div>
</td>
<td>
<select [(ngModel)]="r.transform" (ngModelChange)="markDirty()">
@for (t of transformList; track t.key) {
<option [value]="t.key">{{ t.label }}</option>
}
</select>
</td>
<td><input [(ngModel)]="r.defaultValue" (ngModelChange)="markDirty()" /></td>
<td style="white-space: nowrap">
<button class="cic-btn sm" (click)="move('response', i, -1)" [disabled]="i === 0">↑</button>
<button
class="cic-btn sm"
(click)="move('response', i, 1)"
[disabled]="i === responseRows.length - 1"
>
↓
</button>
<button class="cic-btn sm danger" (click)="removeRow('response', i)">✕</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<datalist id="cic-result-fields">
@for (f of resultFieldList; track f.key) {
<option [value]="f.key">{{ f.label }}</option>
}
</datalist>
<div class="cic-actions" style="margin-top: 12px">
<button class="cic-btn" (click)="addRow('response')">+ Add response field</button>
<span class="muted" style="font-size: 12px">
For dates the transform names the format the bureau sends (e.g. Date ddMMyyyy). The customer is matched by
PAN, then Aadhaar (with date of birth or name), then our customer reference.
</span>
</div>
<div class="cic-grid cols-3" style="margin-top: 14px">
@for (f of resultFieldList; track f.key) {
<div style="font-size: 12px">
<code>{{ f.key }}</code> <span class="muted">— {{ f.label }}</span>
</div>
}
</div>
</div>
}
<!-- REQUEST -->
@if (tab === 'request') {
<div class="cic-card">
<h2>
CBS value → bureau request
<span class="cic-card-sub"
>Paths like <code>criteria.fromDate</code> build nested JSON; <code>[0]</code> builds arrays</span
>
</h2>
<div class="cic-table-wrap">
<table class="cic-table">
<thead>
<tr>
<th style="width: 34px">#</th>
<th style="min-width: 220px">CBS value</th>
<th style="min-width: 220px">Bureau JSON path</th>
<th style="min-width: 160px">Transform</th>
<th style="min-width: 120px">Fixed / default value</th>
@@ -134,7 +233,7 @@
class="cic-mono"
[(ngModel)]="r.targetField"
(ngModelChange)="markDirty()"
placeholder="consumer.name.firstName"
placeholder="criteria.fromDate"
/>
</td>
<td>
@@ -181,88 +280,7 @@
<div class="cic-actions" style="margin-top: 12px">
<button class="cic-btn" (click)="addRow('request')">+ Add request field</button>
<span class="muted" style="font-size: 12px"
>A mandatory field missing on the customer stops the enquiry before the bureau is called.</span
>
</div>
</div>
}
<!-- RESPONSE -->
@if (tab === 'response') {
<div class="cic-card">
<h2>
Bureau response → CBS result
<span class="cic-card-sub"
>"score" drives the score shown everywhere; "hit" = false marks a no-hit; "errorCode" marks a failure</span
>
</h2>
<div class="cic-table-wrap">
<table class="cic-table">
<thead>
<tr>
<th style="width: 34px">#</th>
<th style="min-width: 240px">Bureau response JSON path</th>
<th style="min-width: 220px">CBS result field</th>
<th style="min-width: 150px">Transform</th>
<th style="min-width: 110px">Default</th>
<th style="width: 110px"></th>
</tr>
</thead>
<tbody>
@for (r of responseRows; track $index; let i = $index) {
<tr>
<td class="muted">{{ i + 1 }}</td>
<td>
<input
class="cic-mono"
[(ngModel)]="r.sourceField"
(ngModelChange)="markDirty()"
placeholder="report.score.value"
/>
</td>
<td>
<input
class="cic-mono"
[(ngModel)]="r.targetField"
(ngModelChange)="markDirty()"
list="cic-result-fields"
placeholder="score"
/>
</td>
<td>
<select [(ngModel)]="r.transform" (ngModelChange)="markDirty()">
@for (t of transformList; track t.key) {
<option [value]="t.key">{{ t.label }}</option>
}
</select>
</td>
<td><input [(ngModel)]="r.defaultValue" (ngModelChange)="markDirty()" /></td>
<td style="white-space: nowrap">
<button class="cic-btn sm" (click)="move('response', i, -1)" [disabled]="i === 0">↑</button>
<button
class="cic-btn sm"
(click)="move('response', i, 1)"
[disabled]="i === responseRows.length - 1"
>
↓
</button>
<button class="cic-btn sm danger" (click)="removeRow('response', i)">✕</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<datalist id="cic-result-fields">
@for (f of resultFieldList; track f.key) {
<option [value]="f.key">{{ f.label }}</option>
}
</datalist>
<div class="cic-actions" style="margin-top: 12px">
<button class="cic-btn" (click)="addRow('response')">+ Add response field</button>
<span class="muted" style="font-size: 12px"
>Standard result fields: {{ resultFieldList.length }}; any other simple name is kept as an extra
field.</span
>A mandatory value that is empty stops the fetch before the bureau is called.</span
>
</div>
</div>
@@ -287,7 +305,7 @@
class="cic-mono"
[(ngModel)]="endpoint.path"
(ngModelChange)="markDirty()"
placeholder="/consumer/enquiry"
placeholder="/monitoring/enquiry-alerts"
/>
</div>
<div class="cic-field">
@@ -309,7 +327,7 @@
></textarea>
<span class="hint"
>Placeholders like <code>{{ '{{' }}bureau.memberCode{{ '}}' }}</code> or
<code>{{ '{{' }}enquiry.reference{{ '}}' }}</code> are filled in. Authentication headers are added
<code>{{ '{{' }}fetch.reference{{ '}}' }}</code> are filled in. Authentication headers are added
automatically.</span
>
@if (jsonErrors.headers) {
@@ -333,46 +351,100 @@
</div>
}
<!-- TEST RESPONSE -->
@if (tab === 'test') {
<div class="cic-card">
<h2>
Test response
<span class="cic-card-sub"
>reads a bureau answer through the saved response mapping and matches customers — nothing is saved</span
>
</h2>
<div class="cic-field">
<label
>Bureau response (JSON) — paste a sample from the bureau's specification, or leave empty for a simulator
sample</label
>
<textarea class="cic-mono" rows="12" [(ngModel)]="sampleText"></textarea>
</div>
<div class="cic-actions" style="margin-top: 12px">
<button class="cic-btn primary" (click)="runTest()" [disabled]="testing">
{{ testing ? 'Reading…' : 'Read through mapping' }}
</button>
<button class="cic-btn" (click)="sampleText = ''; testResult = null">Clear</button>
</div>
</div>
@if (testResult) {
@if (testResult.errorCode) {
<div class="cic-alert error">Bureau error {{ testResult.errorCode }}: {{ testResult.errorMessage }}</div>
}
@if (!testResult.alertListFound) {
<div class="cic-alert warn">
No alert list was found at the path mapped to "alerts". Check that row of the response mapping.
</div>
}
<div class="cic-card">
<h2>{{ testResult.count }} alert(s) read</h2>
@if (testResult.items?.length) {
<div class="cic-table-wrap">
<table class="cic-table">
<thead>
<tr>
<th>Alert id</th>
<th>Customer (bureau)</th>
<th>PAN</th>
<th>Date</th>
<th>Loan type</th>
<th class="num">Amount</th>
<th>Match</th>
</tr>
</thead>
<tbody>
@for (it of testResult.items; track $index) {
<tr>
<td class="cic-mono">{{ it.alertRef }}</td>
<td>{{ it.customerName }}</td>
<td class="cic-mono">{{ it.pan || it.aadhaar || it.customerRef }}</td>
<td>{{ it.enquiryDate }}</td>
<td>{{ it.loanType }}</td>
<td class="num">{{ it.amount | number: '1.0-0' }}</td>
<td>
@if (it.ownBank) {
<span class="cic-badge">Own bank — skipped</span>
} @else if (it.clientId) {
<span class="cic-badge good">{{ matchLabels[it.matchType] }}</span> {{ it.clientName }}
} @else {
<span class="cic-badge bad">Not matched</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
}
}
<!-- PREVIEW -->
@if (tab === 'preview') {
<div class="cic-card">
<h2>
Preview
<span class="cic-card-sub"
>builds the request from the saved mapping for a real customer — nothing is sent</span
>
</h2>
<h2>Preview <span class="cic-card-sub">builds the request from the saved mapping — nothing is sent</span></h2>
<div class="cic-grid cols-4" style="align-items: end">
<div class="cic-field" style="grid-column: span 2">
<label>Customer</label>
<div style="display: flex; gap: 8px">
<input
[value]="previewClient ? previewClient.name + ' (#' + previewClient.id + ')' : ''"
placeholder="Search a customer"
readonly
/>
<button class="cic-btn sm" (click)="pickPreviewClient()">Search</button>
</div>
<div class="cic-field">
<label>Alerts from</label>
<input type="date" [(ngModel)]="previewFrom" />
</div>
<div class="cic-field">
<label>Amount (₹)</label>
<input type="number" [(ngModel)]="previewAmount" />
<label>Alerts to</label>
<input type="date" [(ngModel)]="previewTo" />
</div>
<div class="cic-field">
<label>Purpose</label>
<select [(ngModel)]="previewPurpose">
@for (p of purposes; track p.code) {
<option [value]="p.code">{{ p.label }}</option>
}
</select>
<div class="muted" style="font-size: 12px; grid-column: span 2">
Empty = the bureau's look-back window up to today.
</div>
</div>
<button
class="cic-btn primary"
style="margin-top: 12px"
(click)="runPreview()"
[disabled]="previewing || !previewClient"
>
<button class="cic-btn primary" style="margin-top: 12px" (click)="runPreview()" [disabled]="previewing">
{{ previewing ? 'Building…' : 'Build preview' }}
</button>
</div>
@@ -380,20 +452,18 @@
@if (previewResult) {
@if (previewResult.missingMandatory?.length) {
<div class="cic-alert error">
This customer would be refused — missing mandatory fields:
The fetch would be refused — mandatory values are empty:
<ul>
@for (m of previewResult.missingMandatory; track m) {
<li>{{ m }}</li>
}
</ul>
</div>
} @else {
<div class="cic-alert success">All mandatory fields are present for this customer.</div>
}
<div class="cic-split">
<div class="cic-card">
<h2>Request body <span class="cic-card-sub">sensitive values masked</span></h2>
<pre class="cic-code">{{ prettyBody(previewResult.body) }}</pre>
<h2>Request body</h2>
<pre class="cic-code">{{ prettyJson(previewResult.body) }}</pre>
</div>
<div class="cic-card">
<h2>Call</h2>
@@ -402,6 +472,8 @@
<dd>{{ previewResult.method }}</dd>
<dt>URL</dt>
<dd class="cic-mono">{{ previewResult.url }}</dd>
<dt>Window</dt>
<dd>{{ previewResult.fromDate }} → {{ previewResult.toDate }}</dd>
<dt>Auth</dt>
<dd>{{ previewResult.authentication }}</dd>
@for (h of headerEntries(previewResult.headers); track h.k) {