feat(cic): Credit Bureau menu - CIBIL / CIC enquiry, consent, whitelist and API mapping

Separate top-nav menu with dashboard, new enquiry, enquiry history and report,
consent register, CIC whitelist & configuration, and API mapping with preview.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 12:18:32 +05:30
co-authored by Claude Opus 5.5
parent 55f5c1cf32
commit 1b70fb4534
23 changed files with 4173 additions and 0 deletions
@@ -0,0 +1,417 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="cic-page">
<div class="cic-head">
<div>
<h1>CIC API Mapping</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.
</p>
</div>
<div class="cic-actions">
<a class="cic-btn" routerLink="/credit-bureau/bureaus">CIC Configuration</a>
<button
class="cic-btn primary"
(click)="save()"
[disabled]="saving || !endpoint"
*mifosxHasPermission="'UPDATE_CICBUREAU'"
>
{{ saving ? 'Saving…' : dirty ? 'Save changes' : 'Save' }}
</button>
</div>
</div>
<div class="cic-card">
<div class="cic-grid cols-3" style="align-items: end">
<div class="cic-field">
<label>Bureau</label>
<select [(ngModel)]="bureauId" (ngModelChange)="load()">
@for (b of bureaus; track b.id) {
<option [ngValue]="b.id">{{ b.code }} — {{ b.name }}</option>
}
</select>
</div>
<div class="cic-field">
<label>Enquiry type</label>
<select [(ngModel)]="endpointType" (ngModelChange)="load()">
@for (t of catalog?.endpointTypes ?? []; track t) {
<option [value]="t">{{ t === 'CONSUMER_ENQUIRY' ? 'Consumer enquiry' : 'Commercial enquiry' }}</option>
}
</select>
</div>
<div>
@if (bureau) {
<span
class="cic-badge"
[class.warn]="bureau.environment === 'SIMULATOR'"
[class.good]="bureau.environment === 'PRODUCTION'"
>{{ bureau.environment }}</span
>
<span class="cic-badge" [class.good]="bureau.whitelisted">{{
bureau.whitelisted ? 'Whitelisted' : 'Not whitelisted'
}}</span>
@if (endpoint && !endpoint.exists) {
<span class="cic-badge warn">Not configured yet</span>
}
@if (dirty) {
<span class="cic-badge warn">Unsaved changes</span>
}
}
</div>
</div>
</div>
@if (error) {
<div class="cic-alert error">{{ error }}</div>
}
@if (info) {
<div class="cic-alert success">{{ info }}</div>
}
@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.
</div>
}
@if (loading) {
<div class="cic-card cic-empty">Loading…</div>
}
@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 === 'endpoint'" (click)="tab = 'endpoint'">Endpoint, headers &amp; body</button>
<button [class.active]="tab === 'preview'" (click)="tab = 'preview'">Preview (dry run)</button>
</div>
<!-- REQUEST -->
@if (tab === 'request') {
<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>
</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: 220px">Bureau JSON path</th>
<th style="min-width: 160px">Transform</th>
<th style="min-width: 120px">Fixed / default value</th>
<th>Mandatory</th>
<th style="width: 110px"></th>
</tr>
</thead>
<tbody>
@for (r of requestRows; track $index; let i = $index) {
<tr>
<td class="muted">{{ i + 1 }}</td>
<td>
<select [(ngModel)]="r.sourceField" (ngModelChange)="markDirty()">
<option [ngValue]="null">— fixed value —</option>
@for (f of cbsFieldList; track f.key) {
<option [ngValue]="f.key">{{ f.label }}</option>
}
</select>
</td>
<td>
<input
class="cic-mono"
[(ngModel)]="r.targetField"
(ngModelChange)="markDirty()"
placeholder="consumer.name.firstName"
/>
</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()"
[placeholder]="r.sourceField ? 'if empty' : 'value'"
/>
</td>
<td style="text-align: center">
<input
type="checkbox"
[(ngModel)]="r.mandatory"
(ngModelChange)="markDirty()"
style="width: auto"
/>
</td>
<td style="white-space: nowrap">
<button class="cic-btn sm" (click)="move('request', i, -1)" [disabled]="i === 0" title="Move up">
↑
</button>
<button
class="cic-btn sm"
(click)="move('request', i, 1)"
[disabled]="i === requestRows.length - 1"
title="Move down"
>
↓
</button>
<button class="cic-btn sm danger" (click)="removeRow('request', i)" title="Remove">✕</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<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
>
</div>
</div>
}
<!-- ENDPOINT -->
@if (tab === 'endpoint') {
<div class="cic-card">
<h2>Endpoint</h2>
<div class="cic-grid cols-4">
<div class="cic-field">
<label>HTTP method</label>
<select [(ngModel)]="endpoint.httpMethod" (ngModelChange)="markDirty()">
@for (m of catalog?.httpMethods ?? []; track m) {
<option [value]="m">{{ m }}</option>
}
</select>
</div>
<div class="cic-field" style="grid-column: span 2">
<label>Path (appended to the bureau's base URL)</label>
<input
class="cic-mono"
[(ngModel)]="endpoint.path"
(ngModelChange)="markDirty()"
placeholder="/consumer/enquiry"
/>
</div>
<div class="cic-field">
<label>Content type</label>
<input class="cic-mono" [(ngModel)]="endpoint.contentType" (ngModelChange)="markDirty()" />
</div>
</div>
<label style="display: flex; gap: 8px; align-items: center; font-size: 13px; margin-top: 12px">
<input type="checkbox" [(ngModel)]="endpoint.isActive" (ngModelChange)="markDirty()" /> Endpoint active
</label>
<div class="cic-grid cols-2" style="margin-top: 14px">
<div class="cic-field">
<label>Static headers (JSON object)</label>
<textarea
class="cic-mono"
rows="10"
[(ngModel)]="endpoint.headersJson"
(ngModelChange)="markDirty()"
></textarea>
<span class="hint"
>Placeholders like <code>{{ '{{' }}bureau.memberCode{{ '}}' }}</code> or
<code>{{ '{{' }}enquiry.reference{{ '}}' }}</code> are filled in. Authentication headers are added
automatically.</span
>
@if (jsonErrors.headers) {
<span class="tone-bad" style="font-size: 12px">{{ jsonErrors.headers }}</span>
}
</div>
<div class="cic-field">
<label>Base request body (JSON object)</label>
<textarea
class="cic-mono"
rows="10"
[(ngModel)]="endpoint.baseBody"
(ngModelChange)="markDirty()"
></textarea>
<span class="hint">Constant parts of the request; request mappings are written into it.</span>
@if (jsonErrors.body) {
<span class="tone-bad" style="font-size: 12px">{{ jsonErrors.body }}</span>
}
</div>
</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>
<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>
<div class="cic-field">
<label>Amount (₹)</label>
<input type="number" [(ngModel)]="previewAmount" />
</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>
</div>
<button
class="cic-btn primary"
style="margin-top: 12px"
(click)="runPreview()"
[disabled]="previewing || !previewClient"
>
{{ previewing ? 'Building…' : 'Build preview' }}
</button>
</div>
@if (previewResult) {
@if (previewResult.missingMandatory?.length) {
<div class="cic-alert error">
This customer would be refused — missing mandatory fields:
<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>
</div>
<div class="cic-card">
<h2>Call</h2>
<dl class="cic-kv" style="grid-template-columns: 120px minmax(0, 1fr)">
<dt>Method</dt>
<dd>{{ previewResult.method }}</dd>
<dt>URL</dt>
<dd class="cic-mono">{{ previewResult.url }}</dd>
<dt>Auth</dt>
<dd>{{ previewResult.authentication }}</dd>
@for (h of headerEntries(previewResult.headers); track h.k) {
<dt class="cic-mono">{{ h.k }}</dt>
<dd class="cic-mono">{{ h.v }}</dd>
}
</dl>
</div>
</div>
}
}
}
</div>