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:
@@ -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 & 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) {
|
||||
|
||||
Reference in New Issue
Block a user