feat(api-management): set up each CIC bureau as an API vendor
CIBIL, Experian, Equifax and CRIF are vendors under API Management (active = whitelisted) and the Credit Bureau dashboard reads them from there; the separate CIC whitelist and API mapping screens are removed. Mapping tables show the value each row finds in the sample payload, and bureau cards link to their set-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -307,88 +307,148 @@ export function buildRequestPreview(vendor: ApiVendor, a: VendorApi): RequestPre
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The credit information companies. Each bureau is a vendor of its own - set up, switched on (whitelisted) and mapped
|
||||||
|
* here like any other API; the Credit Bureau dashboard fetches alerts only from the bureaus that are active.
|
||||||
|
*/
|
||||||
|
function cicVendors(): ApiVendor[] {
|
||||||
|
const bureau = (
|
||||||
|
id: number,
|
||||||
|
code: string,
|
||||||
|
name: string,
|
||||||
|
active: boolean,
|
||||||
|
environment: ApiVendor['environment'],
|
||||||
|
baseUrl: string,
|
||||||
|
path: string,
|
||||||
|
lastCall: number | undefined
|
||||||
|
): ApiVendor => ({
|
||||||
|
id,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
work: 'CIC enquiry alerts: tells us when another lender enquires on one of our customers (the lender is not named)',
|
||||||
|
configNote:
|
||||||
|
'Active = whitelisted for this bank. The Credit Bureau dashboard fetches alerts every 60 minutes from each active bureau, looking back 7 days; the sender / member id is the bank member code at the bureau.',
|
||||||
|
environment,
|
||||||
|
baseUrl,
|
||||||
|
authType: environment === 'SIMULATOR' ? 'NONE' : 'BASIC',
|
||||||
|
username: environment === 'SIMULATOR' ? '' : 'MH00123456_API',
|
||||||
|
apiKeyHeader: 'X-API-KEY',
|
||||||
|
hasSecret: environment !== 'SIMULATOR',
|
||||||
|
senderId: active ? 'MH00123456' : '',
|
||||||
|
timeoutMs: 30000,
|
||||||
|
retries: 1,
|
||||||
|
defaultHeaders: [kv('Accept', 'application/json', 'Every call asks for JSON')],
|
||||||
|
active,
|
||||||
|
lastCallOn: lastCall === undefined ? undefined : ago(lastCall),
|
||||||
|
links: [],
|
||||||
|
apis: [
|
||||||
|
api({
|
||||||
|
id: id * 100 + 1,
|
||||||
|
name: 'Enquiry alerts',
|
||||||
|
direction: 'IN',
|
||||||
|
mode: 'PULL',
|
||||||
|
method: 'POST',
|
||||||
|
path,
|
||||||
|
usedFor: `Every 60 minutes the CBS asks ${code} for new enquiries on our customers by other lenders`,
|
||||||
|
queryParams: [kv('version', '1.0', 'API version of the bureau')],
|
||||||
|
headers: [
|
||||||
|
kv('X-Member-Code', '{{bank.memberCode}}', "The bank's member code at the bureau"),
|
||||||
|
kv('X-Request-Id', '{{request.reference}}', 'Our reference for this fetch')
|
||||||
|
],
|
||||||
|
requestBody: pretty({
|
||||||
|
requestHeader: { memberCode: 'MH00123456', referenceNumber: 'CICF000001' },
|
||||||
|
criteria: { fromDate: '01102026', toDate: '08102026', scope: 'OTHER_MEMBERS' }
|
||||||
|
}),
|
||||||
|
successCodes: '200',
|
||||||
|
responseHeaders: [
|
||||||
|
kv('Content-Type', 'application/json'),
|
||||||
|
kv('X-RateLimit-Remaining', '98', 'Calls left in this window')
|
||||||
|
],
|
||||||
|
responseBody: pretty({
|
||||||
|
responseHeader: { status: 'SUCCESS' },
|
||||||
|
data: {
|
||||||
|
totalAlerts: 1,
|
||||||
|
alerts: [
|
||||||
|
{
|
||||||
|
alertId: `${code}-20261008-1`,
|
||||||
|
consumer: { name: 'RAHUL SHARMA', pan: 'ABXPS4421K', aadhaar: 'XXXXXXXX4421', dob: '12051988' },
|
||||||
|
enquiry: {
|
||||||
|
productType: 'PERSONAL LOAN',
|
||||||
|
amount: 450000,
|
||||||
|
date: '08102026',
|
||||||
|
purpose: 'NEW LOAN',
|
||||||
|
location: 'PUNE'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
errorBody: pretty({
|
||||||
|
responseHeader: { status: 'ERROR', errorCode: 'E102', errorMessage: 'Invalid member code' }
|
||||||
|
}),
|
||||||
|
requestRows: [
|
||||||
|
row('bank.memberCode', 'requestHeader.memberCode', 'NONE', true),
|
||||||
|
row('request.reference', 'requestHeader.referenceNumber', 'NONE', true),
|
||||||
|
row('request.fromDate', 'criteria.fromDate', 'DATE_DDMMYYYY', true),
|
||||||
|
row('request.toDate', 'criteria.toDate', 'DATE_DDMMYYYY', true),
|
||||||
|
row('', 'criteria.scope', 'NONE', false, 'OTHER_MEMBERS')
|
||||||
|
],
|
||||||
|
responseRows: [
|
||||||
|
row('data.alerts', 'alerts', 'NONE', true),
|
||||||
|
row('responseHeader.errorCode', 'errorCode'),
|
||||||
|
row('responseHeader.errorMessage', 'errorMessage'),
|
||||||
|
row('alertId', 'alert.reference', 'NONE', true),
|
||||||
|
row('consumer.pan', 'customer.pan', 'UPPER'),
|
||||||
|
row('consumer.aadhaar', 'customer.aadhaar', 'DIGITS_ONLY'),
|
||||||
|
row('consumer.name', 'customer.name', 'UPPER'),
|
||||||
|
row('consumer.dob', 'customer.dob', 'DATE_DDMMYYYY'),
|
||||||
|
row('enquiry.productType', 'alert.loanType'),
|
||||||
|
row('enquiry.amount', 'alert.amount', 'NUMBER'),
|
||||||
|
row('enquiry.date', 'alert.enquiryDate', 'DATE_DDMMYYYY', true),
|
||||||
|
row('enquiry.purpose', 'alert.purpose'),
|
||||||
|
row('enquiry.location', 'alert.location')
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
return [
|
||||||
|
bureau(11, 'CIBIL', 'TransUnion CIBIL', true, 'SIMULATOR', '', '/monitoring/enquiry-alerts', 55),
|
||||||
|
bureau(
|
||||||
|
12,
|
||||||
|
'EXPERIAN',
|
||||||
|
'Experian Credit Information Company of India',
|
||||||
|
true,
|
||||||
|
'SIMULATOR',
|
||||||
|
'',
|
||||||
|
'/ecv-api/alerts/enquiries',
|
||||||
|
53
|
||||||
|
),
|
||||||
|
bureau(
|
||||||
|
13,
|
||||||
|
'EQUIFAX',
|
||||||
|
'Equifax Credit Information Services',
|
||||||
|
false,
|
||||||
|
'UAT',
|
||||||
|
'https://uat.equifax.co.in',
|
||||||
|
'/alerts/v1/enquiries',
|
||||||
|
undefined
|
||||||
|
),
|
||||||
|
bureau(
|
||||||
|
14,
|
||||||
|
'CRIF',
|
||||||
|
'CRIF High Mark Credit Information Services',
|
||||||
|
false,
|
||||||
|
'UAT',
|
||||||
|
'https://test.crifhighmark.com',
|
||||||
|
'/api/monitor/enquiryAlerts',
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function buildVendors(): ApiVendor[] {
|
function buildVendors(): ApiVendor[] {
|
||||||
return [
|
return [
|
||||||
{
|
...cicVendors(),
|
||||||
id: 1,
|
|
||||||
code: 'CIC',
|
|
||||||
name: 'CIC — Credit Information Companies',
|
|
||||||
work: 'Enquiry alerts: tells us when another lender enquires on one of our customers (CIBIL, Experian, Equifax, CRIF)',
|
|
||||||
configNote:
|
|
||||||
'Each bureau is whitelisted and connected on the CIC Whitelist & Configuration screen; this API set is its alert call.',
|
|
||||||
environment: 'SIMULATOR',
|
|
||||||
baseUrl: '',
|
|
||||||
authType: 'NONE',
|
|
||||||
username: '',
|
|
||||||
apiKeyHeader: 'X-API-KEY',
|
|
||||||
hasSecret: false,
|
|
||||||
senderId: 'MH00123456',
|
|
||||||
timeoutMs: 30000,
|
|
||||||
retries: 1,
|
|
||||||
defaultHeaders: [kv('Accept', 'application/json', 'Every call asks for JSON')],
|
|
||||||
active: true,
|
|
||||||
lastCallOn: ago(55),
|
|
||||||
links: [
|
|
||||||
{ label: 'Whitelist & configuration', route: '/credit-bureau/bureaus' },
|
|
||||||
{ label: 'API mapping', route: '/credit-bureau/api-mapping' },
|
|
||||||
{ label: 'Credit Bureau dashboard', route: '/credit-bureau/dashboard' }
|
|
||||||
],
|
|
||||||
apis: [
|
|
||||||
api({
|
|
||||||
id: 101,
|
|
||||||
name: 'Enquiry alerts',
|
|
||||||
direction: 'IN',
|
|
||||||
mode: 'PULL',
|
|
||||||
method: 'POST',
|
|
||||||
path: '/monitoring/enquiry-alerts',
|
|
||||||
usedFor: 'Every 60 minutes the CBS asks each whitelisted bureau for new enquiries on our customers',
|
|
||||||
queryParams: [kv('version', '1.0', 'API version of the bureau')],
|
|
||||||
headers: [
|
|
||||||
kv('X-Member-Code', '{{bank.memberCode}}', "The bank's member code at the bureau"),
|
|
||||||
kv('X-Request-Id', '{{request.reference}}', 'Our reference for this fetch')
|
|
||||||
],
|
|
||||||
requestBody: pretty({
|
|
||||||
requestHeader: { memberCode: 'MH00123456', referenceNumber: 'CICF000001' },
|
|
||||||
criteria: { fromDate: '01102026', toDate: '08102026', scope: 'OTHER_MEMBERS' }
|
|
||||||
}),
|
|
||||||
successCodes: '200',
|
|
||||||
responseHeaders: [
|
|
||||||
kv('Content-Type', 'application/json'),
|
|
||||||
kv('X-RateLimit-Remaining', '98', 'Calls left in this window')
|
|
||||||
],
|
|
||||||
responseBody: pretty({
|
|
||||||
responseHeader: { status: 'SUCCESS' },
|
|
||||||
data: {
|
|
||||||
totalAlerts: 1,
|
|
||||||
alerts: [
|
|
||||||
{
|
|
||||||
alertId: 'CIBIL-20261008-1',
|
|
||||||
consumer: { pan: 'ABXPS4421K' },
|
|
||||||
enquiry: { productType: 'PERSONAL LOAN', amount: 450000, date: '08102026' }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
errorBody: pretty({
|
|
||||||
responseHeader: { status: 'ERROR', errorCode: 'E102', errorMessage: 'Invalid member code' }
|
|
||||||
}),
|
|
||||||
requestRows: [
|
|
||||||
row('bank.memberCode', 'requestHeader.memberCode'),
|
|
||||||
row('request.reference', 'requestHeader.referenceNumber', 'NONE', true),
|
|
||||||
row('request.fromDate', 'criteria.fromDate', 'DATE_DDMMYYYY', true),
|
|
||||||
row('request.toDate', 'criteria.toDate', 'DATE_DDMMYYYY', true),
|
|
||||||
row('', 'criteria.scope', 'NONE', false, 'OTHER_MEMBERS')
|
|
||||||
],
|
|
||||||
responseRows: [
|
|
||||||
row('data.alerts', 'alerts', 'NONE', true),
|
|
||||||
row('consumer.pan', 'customer.pan', 'UPPER'),
|
|
||||||
row('enquiry.productType', 'alert.loanType'),
|
|
||||||
row('enquiry.amount', 'alert.amount', 'NUMBER'),
|
|
||||||
row('enquiry.date', 'alert.enquiryDate', 'DATE_DDMMYYYY')
|
|
||||||
]
|
|
||||||
})
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
code: 'SMS_NOTICE',
|
code: 'SMS_NOTICE',
|
||||||
@@ -684,7 +744,8 @@ function buildLogs(vendors: ApiVendor[]): ApiLog[] {
|
|||||||
return seed / 4294967296;
|
return seed / 4294967296;
|
||||||
};
|
};
|
||||||
for (let i = 0; i < 64; i++) {
|
for (let i = 0; i < 64; i++) {
|
||||||
const v = vendors[Math.floor(next() * vendors.length)];
|
const live = vendors.filter((x) => x.active && x.apis.some((a) => a.active));
|
||||||
|
const v = live[Math.floor(next() * live.length)];
|
||||||
const apis = v.apis.filter((a) => a.active);
|
const apis = v.apis.filter((a) => a.active);
|
||||||
const a = apis[Math.floor(next() * apis.length)];
|
const a = apis[Math.floor(next() * apis.length)];
|
||||||
const failed = next() < 0.14;
|
const failed = next() < 0.14;
|
||||||
@@ -755,12 +816,17 @@ function jsonError(text: string): string | null {
|
|||||||
export class ApiManagementService {
|
export class ApiManagementService {
|
||||||
private vendorList = buildVendors();
|
private vendorList = buildVendors();
|
||||||
private logList = buildLogs(this.vendorList);
|
private logList = buildLogs(this.vendorList);
|
||||||
private nextVendorId = 10;
|
private nextVendorId = 20;
|
||||||
private nextApiId = 1000;
|
private nextApiId = 1000;
|
||||||
private nextLogId = 1000;
|
private nextLogId = 1000;
|
||||||
|
|
||||||
// ── vendors ──
|
// ── vendors ──
|
||||||
|
|
||||||
|
/** Synchronous read for other static services (the Credit Bureau dashboard reads the bureaus' set-up from here). */
|
||||||
|
vendorByCode(code: string): ApiVendor | undefined {
|
||||||
|
return this.vendorList.find((v) => v.code === code);
|
||||||
|
}
|
||||||
|
|
||||||
vendors(): Observable<ApiVendor[]> {
|
vendors(): Observable<ApiVendor[]> {
|
||||||
return reply(this.vendorList);
|
return reply(this.vendorList);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -515,6 +515,10 @@
|
|||||||
|
|
||||||
<h4 style="margin: 16px 0 6px; font-size: 13px">
|
<h4 style="margin: 16px 0 6px; font-size: 13px">
|
||||||
{{ mappingTitle('request') }}
|
{{ mappingTitle('request') }}
|
||||||
|
<span class="muted" style="font-weight: 400; font-size: 12px">
|
||||||
|
· {{ matchedCount('request') }} of {{ editing.requestRows.length }} rows found in the sample
|
||||||
|
{{ 'request' === 'request' ? 'payload' : 'body' }}
|
||||||
|
</span>
|
||||||
<button class="cic-btn sm" style="margin-left: 8px" (click)="generateRows('request')">
|
<button class="cic-btn sm" style="margin-left: 8px" (click)="generateRows('request')">
|
||||||
Build rows from the payload
|
Build rows from the payload
|
||||||
</button>
|
</button>
|
||||||
@@ -527,11 +531,13 @@
|
|||||||
<th>{{ toLabel('request') }}</th>
|
<th>{{ toLabel('request') }}</th>
|
||||||
<th>Transform</th>
|
<th>Transform</th>
|
||||||
<th>Fixed / default value</th>
|
<th>Fixed / default value</th>
|
||||||
|
<th>In the sample payload</th>
|
||||||
<th>Required</th>
|
<th>Required</th>
|
||||||
<th style="width: 40px"></th>
|
<th style="width: 40px"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@let requestMatch = sampleMatches('request');
|
||||||
@for (r of editing.requestRows; track $index; let i = $index) {
|
@for (r of editing.requestRows; track $index; let i = $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -555,6 +561,15 @@
|
|||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td><input [(ngModel)]="r.defaultValue" /></td>
|
<td><input [(ngModel)]="r.defaultValue" /></td>
|
||||||
|
<td class="cic-mono" style="font-size: 12px; white-space: nowrap">
|
||||||
|
@if (requestMatch[i].found) {
|
||||||
|
<span class="tone-good">✓ {{ requestMatch[i].value }}</span>
|
||||||
|
} @else if (r.defaultValue) {
|
||||||
|
<span class="muted">fixed: {{ r.defaultValue }}</span>
|
||||||
|
} @else {
|
||||||
|
<span class="tone-bad">✗ {{ requestMatch[i].value || 'no path' }}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td style="text-align: center">
|
<td style="text-align: center">
|
||||||
<input type="checkbox" [(ngModel)]="r.mandatory" style="width: auto" />
|
<input type="checkbox" [(ngModel)]="r.mandatory" style="width: auto" />
|
||||||
</td>
|
</td>
|
||||||
@@ -606,6 +621,10 @@
|
|||||||
|
|
||||||
<h4 style="margin: 16px 0 6px; font-size: 13px">
|
<h4 style="margin: 16px 0 6px; font-size: 13px">
|
||||||
{{ mappingTitle('response') }}
|
{{ mappingTitle('response') }}
|
||||||
|
<span class="muted" style="font-weight: 400; font-size: 12px">
|
||||||
|
· {{ matchedCount('response') }} of {{ editing.responseRows.length }} rows found in the sample
|
||||||
|
{{ 'response' === 'request' ? 'payload' : 'body' }}
|
||||||
|
</span>
|
||||||
<button class="cic-btn sm" style="margin-left: 8px" (click)="generateRows('response')">
|
<button class="cic-btn sm" style="margin-left: 8px" (click)="generateRows('response')">
|
||||||
Build rows from the body
|
Build rows from the body
|
||||||
</button>
|
</button>
|
||||||
@@ -618,11 +637,13 @@
|
|||||||
<th>{{ toLabel('response') }}</th>
|
<th>{{ toLabel('response') }}</th>
|
||||||
<th>Transform</th>
|
<th>Transform</th>
|
||||||
<th>Fixed / default value</th>
|
<th>Fixed / default value</th>
|
||||||
|
<th>In the sample payload</th>
|
||||||
<th>Required</th>
|
<th>Required</th>
|
||||||
<th style="width: 40px"></th>
|
<th style="width: 40px"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@let responseMatch = sampleMatches('response');
|
||||||
@for (r of editing.responseRows; track $index; let i = $index) {
|
@for (r of editing.responseRows; track $index; let i = $index) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -646,6 +667,15 @@
|
|||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td><input [(ngModel)]="r.defaultValue" /></td>
|
<td><input [(ngModel)]="r.defaultValue" /></td>
|
||||||
|
<td class="cic-mono" style="font-size: 12px; white-space: nowrap">
|
||||||
|
@if (responseMatch[i].found) {
|
||||||
|
<span class="tone-good">✓ {{ responseMatch[i].value }}</span>
|
||||||
|
} @else if (r.defaultValue) {
|
||||||
|
<span class="muted">fixed: {{ r.defaultValue }}</span>
|
||||||
|
} @else {
|
||||||
|
<span class="tone-bad">✗ {{ responseMatch[i].value || 'no path' }}</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
<td style="text-align: center">
|
<td style="text-align: center">
|
||||||
<input type="checkbox" [(ngModel)]="r.mandatory" style="width: auto" />
|
<input type="checkbox" [(ngModel)]="r.mandatory" style="width: auto" />
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -272,6 +272,60 @@ export class ApiVendorSetupComponent implements OnInit {
|
|||||||
return 'CBS field';
|
return 'CBS field';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parsedCache = new Map<string, unknown>();
|
||||||
|
|
||||||
|
private parsed(text: string): unknown {
|
||||||
|
if (!this.parsedCache.has(text)) {
|
||||||
|
let v: unknown = undefined;
|
||||||
|
try {
|
||||||
|
v = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
v = undefined;
|
||||||
|
}
|
||||||
|
if (this.parsedCache.size > 20) this.parsedCache.clear();
|
||||||
|
this.parsedCache.set(text, v);
|
||||||
|
}
|
||||||
|
return this.parsedCache.get(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads a.b[0].c out of a parsed payload; undefined when it is not there. */
|
||||||
|
private readPath(root: unknown, path: string): unknown {
|
||||||
|
let cur: any = root;
|
||||||
|
for (const part of path.match(/[^.[\]]+/g) ?? []) {
|
||||||
|
if (cur === null || cur === undefined) return undefined;
|
||||||
|
cur = cur[/^\d+$/.test(part) ? Number(part) : part];
|
||||||
|
}
|
||||||
|
return cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How a mapping row matches the sample payload on its side: the value found at the row's JSON path. Rows after a
|
||||||
|
* list (e.g. data.alerts) are read from the first item of that list, as the CBS does for each item.
|
||||||
|
*/
|
||||||
|
sampleMatches(side: 'request' | 'response'): { found: boolean; value: string }[] {
|
||||||
|
if (!this.editing) return [];
|
||||||
|
const body = this.parsed(side === 'request' ? this.editing.requestBody : this.editing.responseBody);
|
||||||
|
const pathIsTo = this.fromIsCbs(side);
|
||||||
|
let item: unknown = undefined;
|
||||||
|
return this.rowsOf(side).map((r) => {
|
||||||
|
const path = (pathIsTo ? r.to : r.from).trim();
|
||||||
|
if (!path || body === undefined) return { found: false, value: body === undefined ? 'payload is not JSON' : '' };
|
||||||
|
let v = this.readPath(body, path);
|
||||||
|
if (v === undefined && item !== undefined) v = this.readPath(item, path);
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
if (item === undefined && v.length) item = v[0];
|
||||||
|
return { found: true, value: `list of ${v.length}` };
|
||||||
|
}
|
||||||
|
if (v === undefined) return { found: false, value: 'not in the sample payload' };
|
||||||
|
const text = typeof v === 'object' ? JSON.stringify(v) : String(v);
|
||||||
|
return { found: true, value: text.length > 40 ? text.slice(0, 40) + '…' : text };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedCount(side: 'request' | 'response'): number {
|
||||||
|
return this.sampleMatches(side).filter((m) => m.found).length;
|
||||||
|
}
|
||||||
|
|
||||||
addRow(side: 'request' | 'response'): void {
|
addRow(side: 'request' | 'response'): void {
|
||||||
this.rowsOf(side).push({ from: '', to: '', transform: 'NONE', defaultValue: '', mandatory: false });
|
this.rowsOf(side).push({ from: '', to: '', transform: 'NONE', defaultValue: '', mandatory: false });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,21 +249,7 @@
|
|||||||
<span class="ai-card-ic"><fa-icon icon="cogs"></fa-icon></span>
|
<span class="ai-card-ic"><fa-icon icon="cogs"></fa-icon></span>
|
||||||
<span class="ai-card-body">
|
<span class="ai-card-body">
|
||||||
<span class="ai-card-t">API Vendors & Setup</span>
|
<span class="ai-card-t">API Vendors & Setup</span>
|
||||||
<span class="ai-card-d">Vendor, work and API set — IN / OUT (CIC, SMS, WhatsApp …)</span>
|
<span class="ai-card-d">Vendor, work and API set — IN / OUT (CIC bureaus, SMS, WhatsApp …)</span>
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button mat-menu-item class="ai-card" routerLink="/credit-bureau/bureaus" tabindex="0">
|
|
||||||
<span class="ai-card-ic"><fa-icon icon="id-card"></fa-icon></span>
|
|
||||||
<span class="ai-card-body">
|
|
||||||
<span class="ai-card-t">CIC Whitelist & Configuration</span>
|
|
||||||
<span class="ai-card-d">Choose and connect CIBIL, Experian, Equifax, CRIF</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button mat-menu-item class="ai-card" routerLink="/credit-bureau/api-mapping" tabindex="0">
|
|
||||||
<span class="ai-card-ic"><fa-icon icon="project-diagram"></fa-icon></span>
|
|
||||||
<span class="ai-card-body">
|
|
||||||
<span class="ai-card-t">CIC API Mapping</span>
|
|
||||||
<span class="ai-card-d">Map CBS fields to the bureau's request and response</span>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item class="ai-card" routerLink="/api-management/logs" tabindex="0">
|
<button mat-menu-item class="ai-card" routerLink="/api-management/logs" tabindex="0">
|
||||||
|
|||||||
@@ -1,489 +0,0 @@
|
|||||||
<!--
|
|
||||||
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 — Enquiry Alerts</h1>
|
|
||||||
<p>
|
|
||||||
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"
|
|
||||||
(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>API</label>
|
|
||||||
<select [(ngModel)]="endpointType" (ngModelChange)="load()">
|
|
||||||
@for (t of catalog?.endpointTypes ?? []; track t) {
|
|
||||||
<option [value]="t">
|
|
||||||
{{ t === 'ENQUIRY_ALERT' ? 'Enquiry alerts (our customer enquired elsewhere)' : t }}
|
|
||||||
</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 the bureau's own API specification and check them with Test
|
|
||||||
Response.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (loading) {
|
|
||||||
<div class="cic-card cic-empty">Loading…</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (endpoint && !loading) {
|
|
||||||
<div class="cic-tabs">
|
|
||||||
<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 === 'test'" (click)="tab = 'test'">Test response</button>
|
|
||||||
<button [class.active]="tab === 'preview'" (click)="tab = 'preview'">Preview request</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RESPONSE -->
|
|
||||||
@if (tab === 'response') {
|
|
||||||
<div class="cic-card">
|
|
||||||
<h2>
|
|
||||||
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: 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>
|
|
||||||
<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="criteria.fromDate"
|
|
||||||
/>
|
|
||||||
</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 value that is empty stops the fetch before the bureau is called.</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="/monitoring/enquiry-alerts"
|
|
||||||
/>
|
|
||||||
</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>{{ '{{' }}fetch.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>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- 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 — nothing is sent</span></h2>
|
|
||||||
<div class="cic-grid cols-4" style="align-items: end">
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Alerts from</label>
|
|
||||||
<input type="date" [(ngModel)]="previewFrom" />
|
|
||||||
</div>
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Alerts to</label>
|
|
||||||
<input type="date" [(ngModel)]="previewTo" />
|
|
||||||
</div>
|
|
||||||
<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">
|
|
||||||
{{ previewing ? 'Building…' : 'Build preview' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (previewResult) {
|
|
||||||
@if (previewResult.missingMandatory?.length) {
|
|
||||||
<div class="cic-alert error">
|
|
||||||
The fetch would be refused — mandatory values are empty:
|
|
||||||
<ul>
|
|
||||||
@for (m of previewResult.missingMandatory; track m) {
|
|
||||||
<li>{{ m }}</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="cic-split">
|
|
||||||
<div class="cic-card">
|
|
||||||
<h2>Request body</h2>
|
|
||||||
<pre class="cic-code">{{ prettyJson(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>Window</dt>
|
|
||||||
<dd>{{ previewResult.fromDate }} → {{ previewResult.toDate }}</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>
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { HasPermissionDirective } from 'app/directives/has-permission/has-permission.directive';
|
|
||||||
import { CIC_MATCH_LABELS, CicBureau, CicCatalog, CicEndpoint, CicMapping, CicService, cicError } from '../cic.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The per-bureau API mapping of the enquiry-alert call, maintained from the UI: endpoint (method, path, headers, base
|
|
||||||
* body), REQUEST rows that put the fetch window and the bank's identity into the bureau's request, and RESPONSE rows
|
|
||||||
* that read the bureau's alert list and each alert's fields (customer, lender, loan type, amount ...). A dry-run
|
|
||||||
* preview shows the request; Test Response reads a sample answer through the mapping without saving anything.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'mifosx-cic-api-mapping',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
RouterModule,
|
|
||||||
HasPermissionDirective
|
|
||||||
],
|
|
||||||
templateUrl: './cic-api-mapping.component.html',
|
|
||||||
styleUrls: ['../cic-shared.scss']
|
|
||||||
})
|
|
||||||
export class CicApiMappingComponent implements OnInit {
|
|
||||||
private cic = inject(CicService);
|
|
||||||
private route = inject(ActivatedRoute);
|
|
||||||
|
|
||||||
catalog: CicCatalog | null = null;
|
|
||||||
bureaus: CicBureau[] = [];
|
|
||||||
bureauId: number | null = null;
|
|
||||||
endpointType = 'ENQUIRY_ALERT';
|
|
||||||
|
|
||||||
endpoint: CicEndpoint | null = null;
|
|
||||||
requestRows: CicMapping[] = [];
|
|
||||||
responseRows: CicMapping[] = [];
|
|
||||||
tab: 'request' | 'response' | 'endpoint' | 'preview' | 'test' = 'response';
|
|
||||||
|
|
||||||
loading = false;
|
|
||||||
saving = false;
|
|
||||||
dirty = false;
|
|
||||||
error: string | null = null;
|
|
||||||
info: string | null = null;
|
|
||||||
jsonErrors: { headers?: string; body?: string } = {};
|
|
||||||
|
|
||||||
// preview
|
|
||||||
previewFrom = '';
|
|
||||||
previewTo = '';
|
|
||||||
previewResult: any = null;
|
|
||||||
previewing = false;
|
|
||||||
|
|
||||||
// test response
|
|
||||||
sampleText = '';
|
|
||||||
testResult: any = null;
|
|
||||||
testing = false;
|
|
||||||
|
|
||||||
cbsFieldList: { key: string; label: string }[] = [];
|
|
||||||
resultFieldList: { key: string; label: string }[] = [];
|
|
||||||
transformList: { key: string; label: string }[] = [];
|
|
||||||
|
|
||||||
readonly matchLabels = CIC_MATCH_LABELS;
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.cic.catalog().subscribe({
|
|
||||||
next: (c) => {
|
|
||||||
this.catalog = c;
|
|
||||||
this.cbsFieldList = Object.entries(c.cbsFields).map(
|
|
||||||
([
|
|
||||||
key,
|
|
||||||
label
|
|
||||||
]) => ({ key, label })
|
|
||||||
);
|
|
||||||
this.resultFieldList = Object.entries(c.resultFields).map(
|
|
||||||
([
|
|
||||||
key,
|
|
||||||
label
|
|
||||||
]) => ({ key, label })
|
|
||||||
);
|
|
||||||
this.transformList = Object.entries(c.transforms).map(
|
|
||||||
([
|
|
||||||
key,
|
|
||||||
label
|
|
||||||
]) => ({ key, label })
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (e) => (this.error = cicError(e))
|
|
||||||
});
|
|
||||||
this.cic.bureaus().subscribe({
|
|
||||||
next: (b) => {
|
|
||||||
this.bureaus = b;
|
|
||||||
const wanted = Number(this.route.snapshot.queryParamMap.get('bureauId'));
|
|
||||||
this.bureauId = b.find((x) => x.id === wanted)?.id ?? b[0]?.id ?? null;
|
|
||||||
this.load();
|
|
||||||
},
|
|
||||||
error: (e) => (this.error = cicError(e, 'Could not load the bureaus.'))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get bureau(): CicBureau | undefined {
|
|
||||||
return this.bureaus.find((b) => b.id === this.bureauId);
|
|
||||||
}
|
|
||||||
|
|
||||||
isRoot(target: string): boolean {
|
|
||||||
return (this.catalog?.rootFields ?? []).includes(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
load(): void {
|
|
||||||
if (!this.bureauId) return;
|
|
||||||
if (this.dirty && !confirm('Discard unsaved mapping changes?')) return;
|
|
||||||
this.loading = true;
|
|
||||||
this.error = null;
|
|
||||||
this.info = null;
|
|
||||||
this.previewResult = null;
|
|
||||||
this.testResult = null;
|
|
||||||
this.cic.endpoint(this.bureauId, this.endpointType).subscribe({
|
|
||||||
next: (e) => {
|
|
||||||
this.apply(e);
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
this.loading = false;
|
|
||||||
this.error = cicError(err, 'Could not load the mapping.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private apply(e: CicEndpoint): void {
|
|
||||||
this.endpoint = { ...e, headersJson: this.prettyJson(e.headersJson), baseBody: this.prettyJson(e.baseBody) };
|
|
||||||
this.requestRows = e.requestMappings.map((m) => ({ ...m }));
|
|
||||||
this.responseRows = e.responseMappings.map((m) => ({ ...m }));
|
|
||||||
this.dirty = false;
|
|
||||||
this.jsonErrors = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
markDirty(): void {
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
addRow(direction: 'request' | 'response'): void {
|
|
||||||
const row: CicMapping = {
|
|
||||||
sourceField: null,
|
|
||||||
targetField: '',
|
|
||||||
transform: 'NONE',
|
|
||||||
defaultValue: null,
|
|
||||||
mandatory: false
|
|
||||||
};
|
|
||||||
(direction === 'request' ? this.requestRows : this.responseRows).push(row);
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeRow(direction: 'request' | 'response', index: number): void {
|
|
||||||
(direction === 'request' ? this.requestRows : this.responseRows).splice(index, 1);
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
move(direction: 'request' | 'response', index: number, delta: number): void {
|
|
||||||
const rows = direction === 'request' ? this.requestRows : this.responseRows;
|
|
||||||
const to = index + delta;
|
|
||||||
if (to < 0 || to >= rows.length) return;
|
|
||||||
[
|
|
||||||
rows[index],
|
|
||||||
rows[to]
|
|
||||||
] = [
|
|
||||||
rows[to],
|
|
||||||
rows[index]
|
|
||||||
];
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Local checks before the server's own validation. */
|
|
||||||
private validJson(): boolean {
|
|
||||||
this.jsonErrors = {};
|
|
||||||
for (const [
|
|
||||||
key,
|
|
||||||
text
|
|
||||||
] of [
|
|
||||||
[
|
|
||||||
'headers',
|
|
||||||
this.endpoint?.headersJson
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'body',
|
|
||||||
this.endpoint?.baseBody
|
|
||||||
]
|
|
||||||
] as const) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(text || '{}');
|
|
||||||
if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null)
|
|
||||||
throw new Error('must be a JSON object');
|
|
||||||
} catch (e: any) {
|
|
||||||
this.jsonErrors[key] = `Not a valid JSON object: ${e?.message ?? e}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return !this.jsonErrors.headers && !this.jsonErrors.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
save(): void {
|
|
||||||
if (!this.bureauId || !this.endpoint) return;
|
|
||||||
if (!this.validJson()) {
|
|
||||||
this.tab = 'endpoint';
|
|
||||||
this.error = 'Fix the JSON on the Endpoint tab before saving.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.saving = true;
|
|
||||||
this.error = null;
|
|
||||||
this.info = null;
|
|
||||||
const clean = (rows: CicMapping[]) =>
|
|
||||||
rows.map((r, i) => ({
|
|
||||||
sourceField: r.sourceField || null,
|
|
||||||
targetField: (r.targetField || '').trim(),
|
|
||||||
transform: r.transform || 'NONE',
|
|
||||||
defaultValue: r.defaultValue === '' ? null : r.defaultValue,
|
|
||||||
mandatory: !!r.mandatory,
|
|
||||||
sortOrder: (i + 1) * 10
|
|
||||||
}));
|
|
||||||
this.cic
|
|
||||||
.saveEndpoint(this.bureauId, this.endpointType, {
|
|
||||||
httpMethod: this.endpoint.httpMethod,
|
|
||||||
path: this.endpoint.path,
|
|
||||||
contentType: this.endpoint.contentType,
|
|
||||||
headersJson: JSON.stringify(JSON.parse(this.endpoint.headersJson || '{}')),
|
|
||||||
baseBody: JSON.stringify(JSON.parse(this.endpoint.baseBody || '{}')),
|
|
||||||
isActive: this.endpoint.isActive,
|
|
||||||
requestMappings: clean(this.requestRows),
|
|
||||||
responseMappings: clean(this.responseRows)
|
|
||||||
})
|
|
||||||
.subscribe({
|
|
||||||
next: (e) => {
|
|
||||||
this.saving = false;
|
|
||||||
this.apply(e);
|
|
||||||
this.info = `${this.bureau?.code} enquiry-alert mapping saved (${this.requestRows.length} request, ${this.responseRows.length} response fields).`;
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
this.saving = false;
|
|
||||||
this.error = cicError(err, 'Could not save the mapping.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private requireSaved(): boolean {
|
|
||||||
if (this.dirty) {
|
|
||||||
this.error = 'Save the mapping first — preview and test use the saved mapping.';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
runPreview(): void {
|
|
||||||
if (!this.bureauId || !this.requireSaved()) return;
|
|
||||||
this.previewing = true;
|
|
||||||
this.error = null;
|
|
||||||
this.cic
|
|
||||||
.preview(this.bureauId, { fromDate: this.previewFrom || undefined, toDate: this.previewTo || undefined })
|
|
||||||
.subscribe({
|
|
||||||
next: (r) => {
|
|
||||||
this.previewing = false;
|
|
||||||
this.previewResult = r;
|
|
||||||
},
|
|
||||||
error: (e) => {
|
|
||||||
this.previewing = false;
|
|
||||||
this.error = cicError(e, 'Preview failed.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
runTest(): void {
|
|
||||||
if (!this.bureauId || !this.requireSaved()) return;
|
|
||||||
this.testing = true;
|
|
||||||
this.error = null;
|
|
||||||
this.cic.parseTest(this.bureauId, this.sampleText.trim() || null).subscribe({
|
|
||||||
next: (r) => {
|
|
||||||
this.testing = false;
|
|
||||||
this.testResult = r;
|
|
||||||
if (r.sampleResponse && !this.sampleText.trim()) this.sampleText = this.prettyJson(r.sampleResponse);
|
|
||||||
},
|
|
||||||
error: (e) => {
|
|
||||||
this.testing = false;
|
|
||||||
this.error = cicError(e, 'Test failed.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
prettyJson(text?: string | null): string {
|
|
||||||
if (!text) return '{}';
|
|
||||||
try {
|
|
||||||
return JSON.stringify(JSON.parse(text), null, 2);
|
|
||||||
} catch {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headerEntries(h: Record<string, string> | null | undefined): { k: string; v: string }[] {
|
|
||||||
return Object.entries(h ?? {}).map(
|
|
||||||
([
|
|
||||||
k,
|
|
||||||
v
|
|
||||||
]) => ({ k, v })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
<!--
|
|
||||||
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 Whitelist & Configuration</h1>
|
|
||||||
<p>
|
|
||||||
Choose which Credit Information Companies this bank receives enquiry alerts from, and how each is reached. Only
|
|
||||||
whitelisted bureaus are fetched.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="cic-actions">
|
|
||||||
<a class="cic-btn" routerLink="/credit-bureau/dashboard">← Dashboard</a>
|
|
||||||
<a class="cic-btn" routerLink="/credit-bureau/api-mapping">API Mapping</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (error) {
|
|
||||||
<div class="cic-alert error">{{ error }}</div>
|
|
||||||
}
|
|
||||||
@if (info) {
|
|
||||||
<div class="cic-alert success">{{ info }}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (loading && !bureaus.length) {
|
|
||||||
<div class="cic-card cic-empty">Loading…</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="cic-grid cols-2">
|
|
||||||
@for (b of bureaus; track b.id) {
|
|
||||||
<div class="cic-card" style="margin-bottom: 0">
|
|
||||||
<div style="display: flex; justify-content: space-between; gap: 12px; align-items: flex-start">
|
|
||||||
<div style="min-width: 0">
|
|
||||||
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap">
|
|
||||||
<b style="font-size: 16px">{{ b.code }}</b>
|
|
||||||
<span class="cic-badge" [class]="'cic-badge ' + envTone(b.environment)">{{ b.environment }}</span>
|
|
||||||
@if (b.isDefault) {
|
|
||||||
<span class="cic-badge brand">Default</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="muted" style="font-size: 12px; margin-top: 3px">{{ b.name }}</div>
|
|
||||||
</div>
|
|
||||||
<label style="display: flex; gap: 8px; align-items: center; font-size: 12px; font-weight: 600">
|
|
||||||
{{ b.whitelisted ? 'Whitelisted' : 'Not whitelisted' }}
|
|
||||||
<span class="cic-switch">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
[checked]="b.whitelisted"
|
|
||||||
[disabled]="toggling === b.id"
|
|
||||||
(change)="toggle(b, $event)"
|
|
||||||
*mifosxHasPermission="'UPDATE_CICBUREAU'"
|
|
||||||
/>
|
|
||||||
<span></span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl class="cic-kv" style="margin-top: 14px; grid-template-columns: 140px minmax(0, 1fr)">
|
|
||||||
<dt>Base URL</dt>
|
|
||||||
<dd class="cic-mono">{{ b.environment === 'SIMULATOR' ? '(simulator)' : b.baseUrl || '— not set —' }}</dd>
|
|
||||||
<dt>Member code</dt>
|
|
||||||
<dd>{{ b.memberCode || '—' }}</dd>
|
|
||||||
<dt>Authentication</dt>
|
|
||||||
<dd>
|
|
||||||
{{ authLabels[b.authType] || b.authType }}
|
|
||||||
@if (b.authType !== 'NONE') {
|
|
||||||
<span class="cic-badge" [class.good]="b.hasSecret" [class.bad]="!b.hasSecret">{{
|
|
||||||
b.hasSecret ? 'credential stored' : 'no credential'
|
|
||||||
}}</span>
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
<dt>Endpoints</dt>
|
|
||||||
<dd>{{ b.activeEndpoints }} active</dd>
|
|
||||||
<dt>Alert fetch</dt>
|
|
||||||
<dd>
|
|
||||||
{{ b.alertAutoFetch ? 'Automatic, every ' + b.alertFetchIntervalMin + ' min' : 'Manual only' }}
|
|
||||||
<span class="muted">· looks back {{ b.alertLookbackDays }} days</span>
|
|
||||||
</dd>
|
|
||||||
<dt>Last fetch</dt>
|
|
||||||
<dd>
|
|
||||||
@if (b.lastFetchOn) {
|
|
||||||
<span
|
|
||||||
class="cic-badge"
|
|
||||||
[class.good]="b.lastFetchStatus === 'SUCCESS'"
|
|
||||||
[class.bad]="b.lastFetchStatus === 'FAILED'"
|
|
||||||
>{{ b.lastFetchStatus }}</span
|
|
||||||
>
|
|
||||||
<span class="muted" style="font-size: 12px"> {{ b.lastFetchOn | slice: 0 : 16 }}</span>
|
|
||||||
<div class="muted" style="font-size: 12px">{{ b.lastFetchMessage }}</div>
|
|
||||||
} @else {
|
|
||||||
<span class="muted">never</span>
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
<dt>Last test</dt>
|
|
||||||
<dd>
|
|
||||||
@if (b.lastTestStatus) {
|
|
||||||
<span
|
|
||||||
class="cic-badge"
|
|
||||||
[class.good]="b.lastTestStatus === 'OK'"
|
|
||||||
[class.bad]="b.lastTestStatus !== 'OK'"
|
|
||||||
>{{ b.lastTestStatus }}</span
|
|
||||||
>
|
|
||||||
<span class="muted" style="font-size: 12px"> {{ b.lastTestedOn | slice: 0 : 16 }}</span>
|
|
||||||
<div class="muted" style="font-size: 12px">{{ b.lastTestMessage }}</div>
|
|
||||||
} @else {
|
|
||||||
<span class="muted">never</span>
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
<dt>Alerts (30d)</dt>
|
|
||||||
<dd>
|
|
||||||
{{ b.alertsLast30Days }}
|
|
||||||
@if (b.newAlerts) {
|
|
||||||
<span class="cic-badge bad">{{ b.newAlerts }} new</span>
|
|
||||||
}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<div class="cic-actions" style="margin-top: 14px">
|
|
||||||
<button class="cic-btn" (click)="edit(b)" *mifosxHasPermission="'UPDATE_CICBUREAU'">Configure</button>
|
|
||||||
<button
|
|
||||||
class="cic-btn"
|
|
||||||
(click)="test(b)"
|
|
||||||
[disabled]="testing === b.id"
|
|
||||||
*mifosxHasPermission="'UPDATE_CICBUREAU'"
|
|
||||||
>
|
|
||||||
{{ testing === b.id ? 'Testing…' : 'Test Connection' }}
|
|
||||||
</button>
|
|
||||||
@if (b.whitelisted) {
|
|
||||||
<button
|
|
||||||
class="cic-btn"
|
|
||||||
(click)="fetchNow(b)"
|
|
||||||
[disabled]="fetching === b.id"
|
|
||||||
*mifosxHasPermission="'FETCH_CICALERT'"
|
|
||||||
>
|
|
||||||
{{ fetching === b.id ? 'Fetching…' : 'Fetch alerts now' }}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
<a class="cic-btn" routerLink="/credit-bureau/api-mapping" [queryParams]="{ bureauId: b.id }">API Mapping</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cic-alert info" style="margin-top: 16px">
|
|
||||||
<b>Going live with a bureau:</b> 1) subscribe to the bureau's enquiry-alert (portfolio monitoring) service and get
|
|
||||||
the bank's member code, API URL and credentials; 2) Configure them here (environment UAT first); 3) adapt the
|
|
||||||
request / response fields under API Mapping to the bureau's specification and check them with Preview and Test
|
|
||||||
Response; 4) Test Connection and Fetch alerts now; 5) switch to PRODUCTION and whitelist it. Alerts then arrive
|
|
||||||
automatically.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (editing) {
|
|
||||||
<div class="cic-modal-backdrop" (click)="editing = null">
|
|
||||||
<div class="cic-modal" style="width: min(820px, 100%)" (click)="$event.stopPropagation()">
|
|
||||||
<h3>Configure {{ editing.code }}</h3>
|
|
||||||
<div class="cic-grid cols-2">
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Display name</label>
|
|
||||||
<input [(ngModel)]="form.name" />
|
|
||||||
</div>
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Environment</label>
|
|
||||||
<select [(ngModel)]="form.environment">
|
|
||||||
<option value="SIMULATOR">SIMULATOR — built-in test bureau, no network call</option>
|
|
||||||
<option value="UAT">UAT — bureau's test environment</option>
|
|
||||||
<option value="PRODUCTION">PRODUCTION — live bureau</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="cic-field" style="grid-column: 1 / -1">
|
|
||||||
<label>Base URL</label>
|
|
||||||
<input class="cic-mono" [(ngModel)]="form.baseUrl" placeholder="https://api.bureau.example/v1" />
|
|
||||||
<span class="hint"
|
|
||||||
>Endpoint paths from API Mapping are appended to this. PRODUCTION requires https://.</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Bank's member code / id</label>
|
|
||||||
<input [(ngModel)]="form.memberCode" />
|
|
||||||
</div>
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Authentication</label>
|
|
||||||
<select [(ngModel)]="form.authType">
|
|
||||||
@for (a of catalog?.authTypes ?? []; track a) {
|
|
||||||
<option [value]="a">{{ authLabels[a] || a }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
@if (form.authType === 'BASIC') {
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>User id</label>
|
|
||||||
<input [(ngModel)]="form.username" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (form.authType === 'API_KEY') {
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>API key header name</label>
|
|
||||||
<input class="cic-mono" [(ngModel)]="form.apiKeyHeader" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (form.authType === 'OAUTH2_CLIENT_CREDENTIALS') {
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Token URL</label>
|
|
||||||
<input
|
|
||||||
class="cic-mono"
|
|
||||||
[(ngModel)]="form.tokenUrl"
|
|
||||||
placeholder="https://auth.bureau.example/oauth/token"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Client id</label>
|
|
||||||
<input [(ngModel)]="form.clientId" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (needsSecret(form.authType)) {
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>{{ secretLabel(form.authType) }}</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
[(ngModel)]="form.secret"
|
|
||||||
autocomplete="new-password"
|
|
||||||
[placeholder]="editing.hasSecret ? '•••••• stored — leave blank to keep' : 'Enter to store (encrypted)'"
|
|
||||||
/>
|
|
||||||
@if (editing.hasSecret) {
|
|
||||||
<label style="display: flex; gap: 6px; align-items: center; font-weight: 500; font-size: 12px">
|
|
||||||
<input type="checkbox" [(ngModel)]="form.clearSecret" style="width: auto" /> Remove the stored
|
|
||||||
credential
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Timeout (ms)</label>
|
|
||||||
<input type="number" min="1000" max="300000" [(ngModel)]="form.timeoutMs" />
|
|
||||||
</div>
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Fetch alerts automatically every (minutes)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="15"
|
|
||||||
max="1440"
|
|
||||||
[(ngModel)]="form.alertFetchIntervalMin"
|
|
||||||
[disabled]="!form.alertAutoFetch"
|
|
||||||
/>
|
|
||||||
<label style="display: flex; gap: 6px; align-items: center; font-weight: 500; font-size: 12px">
|
|
||||||
<input type="checkbox" [(ngModel)]="form.alertAutoFetch" style="width: auto" /> Fetch automatically
|
|
||||||
(15–1440 min)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="cic-field">
|
|
||||||
<label>Look back (days)</label>
|
|
||||||
<input type="number" min="1" max="90" [(ngModel)]="form.alertLookbackDays" />
|
|
||||||
<span class="hint"
|
|
||||||
>Each fetch asks for alerts of this many past days; ones already received are skipped.</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="cic-field" style="grid-column: 1 / -1">
|
|
||||||
<label>Description</label>
|
|
||||||
<input [(ngModel)]="form.description" />
|
|
||||||
</div>
|
|
||||||
<label style="display: flex; gap: 8px; align-items: center; font-size: 13px">
|
|
||||||
<input type="checkbox" [(ngModel)]="form.isDefault" /> Default bureau (listed first)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="foot">
|
|
||||||
<button class="cic-btn" (click)="editing = null">Cancel</button>
|
|
||||||
<button class="cic-btn primary" (click)="save()" [disabled]="saving">
|
|
||||||
{{ saving ? 'Saving…' : 'Save' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { HasPermissionDirective } from 'app/directives/has-permission/has-permission.directive';
|
|
||||||
import { CicBureau, CicCatalog, CicService, cicError } from '../cic.service';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Which CICs the bank receives enquiry alerts from (whitelist), and how each is reached: environment, URL, member
|
|
||||||
* code, authentication and credentials (stored encrypted, never shown back), timeout, and the alert fetch schedule.
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'mifosx-cic-bureau-config',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
FormsModule,
|
|
||||||
RouterModule,
|
|
||||||
HasPermissionDirective
|
|
||||||
],
|
|
||||||
templateUrl: './cic-bureau-config.component.html',
|
|
||||||
styleUrls: ['../cic-shared.scss']
|
|
||||||
})
|
|
||||||
export class CicBureauConfigComponent implements OnInit {
|
|
||||||
private cic = inject(CicService);
|
|
||||||
|
|
||||||
catalog: CicCatalog | null = null;
|
|
||||||
bureaus: CicBureau[] = [];
|
|
||||||
loading = false;
|
|
||||||
error: string | null = null;
|
|
||||||
info: string | null = null;
|
|
||||||
|
|
||||||
editing: CicBureau | null = null;
|
|
||||||
form: any = {};
|
|
||||||
saving = false;
|
|
||||||
testing: number | null = null;
|
|
||||||
toggling: number | null = null;
|
|
||||||
fetching: number | null = null;
|
|
||||||
|
|
||||||
readonly authLabels: Record<string, string> = {
|
|
||||||
NONE: 'None (IP whitelisting / mutual TLS)',
|
|
||||||
BASIC: 'Basic (user id + password)',
|
|
||||||
API_KEY: 'API key in a header',
|
|
||||||
BEARER_TOKEN: 'Static bearer token',
|
|
||||||
OAUTH2_CLIENT_CREDENTIALS: 'OAuth2 client credentials'
|
|
||||||
};
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.cic.catalog().subscribe({ next: (c) => (this.catalog = c), error: (e) => (this.error = cicError(e)) });
|
|
||||||
this.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
load(): void {
|
|
||||||
this.loading = true;
|
|
||||||
this.cic.bureaus().subscribe({
|
|
||||||
next: (b) => {
|
|
||||||
this.bureaus = b;
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
error: (e) => {
|
|
||||||
this.error = cicError(e, 'Could not load the bureaus.');
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle(b: CicBureau, event: Event): void {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
const want = target.checked;
|
|
||||||
target.checked = b.whitelisted; // reflect the server's answer, not the click
|
|
||||||
this.error = null;
|
|
||||||
this.info = null;
|
|
||||||
this.toggling = b.id;
|
|
||||||
this.cic.setWhitelisted(b.id, want).subscribe({
|
|
||||||
next: (u) => {
|
|
||||||
this.toggling = null;
|
|
||||||
this.replace(u);
|
|
||||||
this.info = `${u.code} is now ${u.whitelisted ? 'whitelisted — its enquiry alerts will be fetched' : 'not whitelisted'}.`;
|
|
||||||
},
|
|
||||||
error: (e) => {
|
|
||||||
this.toggling = null;
|
|
||||||
this.error = cicError(e, 'Could not change the whitelist.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
edit(b: CicBureau): void {
|
|
||||||
this.editing = b;
|
|
||||||
this.info = null;
|
|
||||||
this.error = null;
|
|
||||||
this.form = {
|
|
||||||
name: b.name,
|
|
||||||
description: b.description ?? '',
|
|
||||||
environment: b.environment,
|
|
||||||
baseUrl: b.baseUrl ?? '',
|
|
||||||
memberCode: b.memberCode ?? '',
|
|
||||||
authType: b.authType,
|
|
||||||
username: b.username ?? '',
|
|
||||||
apiKeyHeader: b.apiKeyHeader ?? 'X-API-KEY',
|
|
||||||
secret: '',
|
|
||||||
clearSecret: false,
|
|
||||||
tokenUrl: b.tokenUrl ?? '',
|
|
||||||
clientId: b.clientId ?? '',
|
|
||||||
timeoutMs: b.timeoutMs,
|
|
||||||
alertAutoFetch: b.alertAutoFetch,
|
|
||||||
alertFetchIntervalMin: b.alertFetchIntervalMin,
|
|
||||||
alertLookbackDays: b.alertLookbackDays,
|
|
||||||
isDefault: b.isDefault
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
save(): void {
|
|
||||||
if (!this.editing) return;
|
|
||||||
this.saving = true;
|
|
||||||
this.error = null;
|
|
||||||
const body = { ...this.form };
|
|
||||||
if (!body.secret) delete body.secret;
|
|
||||||
this.cic.updateBureau(this.editing.id, body).subscribe({
|
|
||||||
next: (u) => {
|
|
||||||
this.saving = false;
|
|
||||||
this.editing = null;
|
|
||||||
this.info = `${u.code} configuration saved.`;
|
|
||||||
this.load();
|
|
||||||
},
|
|
||||||
error: (e) => {
|
|
||||||
this.saving = false;
|
|
||||||
this.error = cicError(e, 'Could not save the configuration.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
test(b: CicBureau): void {
|
|
||||||
this.testing = b.id;
|
|
||||||
this.error = null;
|
|
||||||
this.info = null;
|
|
||||||
this.cic.testConnection(b.id).subscribe({
|
|
||||||
next: (r) => {
|
|
||||||
this.testing = null;
|
|
||||||
if (r.status === 'OK') this.info = `${b.code}: ${r.message}`;
|
|
||||||
else this.error = `${b.code}: ${r.message}`;
|
|
||||||
this.load();
|
|
||||||
},
|
|
||||||
error: (e) => {
|
|
||||||
this.testing = null;
|
|
||||||
this.error = cicError(e, 'Connection test failed.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchNow(b: CicBureau): void {
|
|
||||||
this.fetching = b.id;
|
|
||||||
this.error = null;
|
|
||||||
this.info = null;
|
|
||||||
this.cic.fetch(b.id).subscribe({
|
|
||||||
next: (r) => {
|
|
||||||
this.fetching = null;
|
|
||||||
if (r.status === 'SUCCESS') this.info = `${b.code}: ${r.message}`;
|
|
||||||
else this.error = `${b.code}: ${r.message}`;
|
|
||||||
this.load();
|
|
||||||
},
|
|
||||||
error: (e) => {
|
|
||||||
this.fetching = null;
|
|
||||||
this.error = cicError(e, 'Fetch failed.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
needsSecret(auth: string): boolean {
|
|
||||||
return auth !== 'NONE';
|
|
||||||
}
|
|
||||||
|
|
||||||
secretLabel(auth: string): string {
|
|
||||||
return auth === 'BASIC'
|
|
||||||
? 'Password'
|
|
||||||
: auth === 'API_KEY'
|
|
||||||
? 'API key'
|
|
||||||
: auth === 'BEARER_TOKEN'
|
|
||||||
? 'Bearer token'
|
|
||||||
: 'Client secret';
|
|
||||||
}
|
|
||||||
|
|
||||||
envTone(env: string): string {
|
|
||||||
return env === 'PRODUCTION' ? 'good' : env === 'UAT' ? 'ok' : 'warn';
|
|
||||||
}
|
|
||||||
|
|
||||||
private replace(u: CicBureau): void {
|
|
||||||
this.bureaus = this.bureaus.map((b) => (b.id === u.id ? u : b));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
}
|
}
|
||||||
@if (data && !whitelisted.length) {
|
@if (data && !whitelisted.length) {
|
||||||
<div class="cic-alert warn">
|
<div class="cic-alert warn">
|
||||||
No CIC is whitelisted yet, so no alerts are being received. Whitelist a bureau from the AI / Intelligence Modules
|
No CIC is whitelisted yet, so no alerts are being received. Switch a bureau on (CIBIL, Experian, Equifax or CRIF)
|
||||||
menu → API Management → CIC Whitelist & Configuration.
|
under AI / Intelligence Modules → API Management → API Vendors & Setup.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (loading && !data) {
|
@if (loading && !data) {
|
||||||
@@ -227,6 +227,13 @@
|
|||||||
@if (b.lastFetchMessage) {
|
@if (b.lastFetchMessage) {
|
||||||
<div class="s" style="font-size: 11px">{{ b.lastFetchMessage }}</div>
|
<div class="s" style="font-size: 11px">{{ b.lastFetchMessage }}</div>
|
||||||
}
|
}
|
||||||
|
<div class="s" style="font-size: 11px; margin-top: 4px">
|
||||||
|
@if (b.environment === 'SIMULATOR') {
|
||||||
|
Simulator: no call goes to {{ b.code }}; the alerts are made up by the CBS for testing.
|
||||||
|
} @else {
|
||||||
|
Calls {{ b.code }} {{ b.environment }} with the API set up under API Management.
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@if (b.whitelisted) {
|
@if (b.whitelisted) {
|
||||||
<button
|
<button
|
||||||
class="cic-btn sm"
|
class="cic-btn sm"
|
||||||
@@ -238,6 +245,7 @@
|
|||||||
{{ fetching === b.id ? 'Fetching…' : 'Fetch now' }}
|
{{ fetching === b.id ? 'Fetching…' : 'Fetch now' }}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
<a class="cic-btn sm" style="margin-top: 8px; margin-left: 4px" [routerLink]="setupRoute(b.code)">Set up</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Component, OnInit, inject } from '@angular/core';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { ApiManagementService } from 'app/api-management/api-management.service';
|
||||||
import { HasPermissionDirective } from 'app/directives/has-permission/has-permission.directive';
|
import { HasPermissionDirective } from 'app/directives/has-permission/has-permission.directive';
|
||||||
import { CicAlertDetailComponent } from '../cic-alert-detail/cic-alert-detail.component';
|
import { CicAlertDetailComponent } from '../cic-alert-detail/cic-alert-detail.component';
|
||||||
import { CicService, cicError, cicStatusTone } from '../cic.service';
|
import { CicService, cicError, cicStatusTone } from '../cic.service';
|
||||||
@@ -18,8 +19,8 @@ import { CicService, cicError, cicStatusTone } from '../cic.service';
|
|||||||
/**
|
/**
|
||||||
* Credit Bureau dashboard: our customers who applied for a loan at another bank / NBFC, as reported by the CICs -
|
* Credit Bureau dashboard: our customers who applied for a loan at another bank / NBFC, as reported by the CICs -
|
||||||
* who, what loan and how much (the CIC does not name the lender) - with the customers applying elsewhere repeatedly,
|
* who, what loan and how much (the CIC does not name the lender) - with the customers applying elsewhere repeatedly,
|
||||||
* the loan types, the trend, and the fetch status of each bureau. The other pages (fetch log, CIC configuration, API
|
* the loan types, the trend, and the fetch status of each bureau. Each bureau is set up under API Management → API
|
||||||
* mapping) are reached from the buttons at the top.
|
* Vendors & Setup.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-cic-dashboard',
|
selector: 'mifosx-cic-dashboard',
|
||||||
@@ -36,6 +37,16 @@ import { CicService, cicError, cicStatusTone } from '../cic.service';
|
|||||||
})
|
})
|
||||||
export class CicDashboardComponent implements OnInit {
|
export class CicDashboardComponent implements OnInit {
|
||||||
private cic = inject(CicService);
|
private cic = inject(CicService);
|
||||||
|
private apis = inject(ApiManagementService);
|
||||||
|
|
||||||
|
/** Each bureau is set up as a vendor under API Management (connection, whitelist, request / response mapping). */
|
||||||
|
setupRoute(code: string): any[] {
|
||||||
|
const v = this.apis.vendorByCode(code);
|
||||||
|
return v ? [
|
||||||
|
'/api-management/vendors',
|
||||||
|
v.id
|
||||||
|
] : ['/api-management/vendors'];
|
||||||
|
}
|
||||||
|
|
||||||
days = 30;
|
days = 30;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { Observable, delay, of, throwError } from 'rxjs';
|
import { Observable, delay, of, throwError } from 'rxjs';
|
||||||
|
|
||||||
|
import { ApiManagementService } from 'app/api-management/api-management.service';
|
||||||
import {
|
import {
|
||||||
CicAlert,
|
CicAlert,
|
||||||
CicBureau,
|
CicBureau,
|
||||||
@@ -592,6 +593,7 @@ function reply<T>(value: T): Observable<T> {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CicStaticService extends CicService {
|
export class CicStaticService extends CicService {
|
||||||
|
private apis = inject(ApiManagementService);
|
||||||
private bureauList = buildBureaus();
|
private bureauList = buildBureaus();
|
||||||
private alertList = buildAlerts();
|
private alertList = buildAlerts();
|
||||||
private endpoints = new Map<number, CicEndpoint>(
|
private endpoints = new Map<number, CicEndpoint>(
|
||||||
@@ -704,7 +706,23 @@ export class CicStaticService extends CicService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Each bureau is set up as a vendor under API Management: active = whitelisted, plus its environment and member code. */
|
||||||
|
private syncFromApiManagement(): void {
|
||||||
|
for (const b of this.bureauList) {
|
||||||
|
const v = this.apis.vendorByCode(b.code);
|
||||||
|
if (!v) continue;
|
||||||
|
b.name = v.name;
|
||||||
|
b.whitelisted = v.active;
|
||||||
|
b.environment = v.environment;
|
||||||
|
b.baseUrl = v.baseUrl || undefined;
|
||||||
|
b.memberCode = v.senderId || undefined;
|
||||||
|
b.authType = v.authType;
|
||||||
|
b.timeoutMs = v.timeoutMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private refreshCounts(): void {
|
private refreshCounts(): void {
|
||||||
|
this.syncFromApiManagement();
|
||||||
const since = Date.now() - 30 * DAY;
|
const since = Date.now() - 30 * DAY;
|
||||||
for (const b of this.bureauList) {
|
for (const b of this.bureauList) {
|
||||||
b.alertsLast30Days = this.alertList.filter(
|
b.alertsLast30Days = this.alertList.filter(
|
||||||
@@ -1036,10 +1054,11 @@ export class CicStaticService extends CicService {
|
|||||||
// ── fetch / import ──
|
// ── fetch / import ──
|
||||||
|
|
||||||
override fetch(bureauId: number, _body: { fromDate?: string; toDate?: string } = {}): Observable<CicFetchRun> {
|
override fetch(bureauId: number, _body: { fromDate?: string; toDate?: string } = {}): Observable<CicFetchRun> {
|
||||||
|
this.syncFromApiManagement();
|
||||||
const b = this.bureauList.find((x) => x.id === bureauId);
|
const b = this.bureauList.find((x) => x.id === bureauId);
|
||||||
if (!b) return fail('Credit bureau was not found.');
|
if (!b) return fail('Credit bureau was not found.');
|
||||||
if (!b.whitelisted)
|
if (!b.whitelisted)
|
||||||
return fail(`${b.code} is not whitelisted for this bank. Whitelist it under CIC Configuration first.`);
|
return fail(`${b.code} is not whitelisted for this bank. Switch it on under API Management → Vendors first.`);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const r = rng(now.getTime() % 100000);
|
const r = rng(now.getTime() % 100000);
|
||||||
const created: CicAlert[] = [];
|
const created: CicAlert[] = [];
|
||||||
@@ -1175,6 +1194,7 @@ export class CicStaticService extends CicService {
|
|||||||
// ── dashboard ──
|
// ── dashboard ──
|
||||||
|
|
||||||
override dashboard(days = 30): Observable<any> {
|
override dashboard(days = 30): Observable<any> {
|
||||||
|
this.syncFromApiManagement();
|
||||||
const today = daysAgo(0);
|
const today = daysAgo(0);
|
||||||
const since = daysAgo(days - 1);
|
const since = daysAgo(days - 1);
|
||||||
const matched = this.alertList.filter((a) => a.clientId);
|
const matched = this.alertList.filter((a) => a.clientId);
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import { Routes, RouterModule } from '@angular/router';
|
|||||||
import { Route } from '../core/route/route.service';
|
import { Route } from '../core/route/route.service';
|
||||||
import { CicDashboardComponent } from './cic-dashboard/cic-dashboard.component';
|
import { CicDashboardComponent } from './cic-dashboard/cic-dashboard.component';
|
||||||
import { CicFetchLogComponent } from './cic-fetch-log/cic-fetch-log.component';
|
import { CicFetchLogComponent } from './cic-fetch-log/cic-fetch-log.component';
|
||||||
import { CicBureauConfigComponent } from './cic-bureau-config/cic-bureau-config.component';
|
|
||||||
import { CicApiMappingComponent } from './cic-api-mapping/cic-api-mapping.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
Route.withShell([
|
Route.withShell([
|
||||||
@@ -36,16 +34,9 @@ const routes: Routes = [
|
|||||||
component: CicFetchLogComponent,
|
component: CicFetchLogComponent,
|
||||||
data: { title: 'Fetch Log', breadcrumb: 'Fetch Log' }
|
data: { title: 'Fetch Log', breadcrumb: 'Fetch Log' }
|
||||||
},
|
},
|
||||||
{
|
// The bureaus are set up, whitelisted and mapped under API Management like every other vendor.
|
||||||
path: 'bureaus',
|
{ path: 'bureaus', redirectTo: '/api-management/vendors', pathMatch: 'full' },
|
||||||
component: CicBureauConfigComponent,
|
{ path: 'api-mapping', redirectTo: '/api-management/vendors', pathMatch: 'full' }
|
||||||
data: { title: 'CIC Whitelist & Configuration', breadcrumb: 'CIC Configuration' }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'api-mapping',
|
|
||||||
component: CicApiMappingComponent,
|
|
||||||
data: { title: 'CIC API Mapping', breadcrumb: 'API Mapping' }
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -13,14 +13,12 @@ import { CreditBureauRoutingModule } from './credit-bureau-routing.module';
|
|||||||
import { CicDashboardComponent } from './cic-dashboard/cic-dashboard.component';
|
import { CicDashboardComponent } from './cic-dashboard/cic-dashboard.component';
|
||||||
import { CicFetchLogComponent } from './cic-fetch-log/cic-fetch-log.component';
|
import { CicFetchLogComponent } from './cic-fetch-log/cic-fetch-log.component';
|
||||||
import { CicAlertDetailComponent } from './cic-alert-detail/cic-alert-detail.component';
|
import { CicAlertDetailComponent } from './cic-alert-detail/cic-alert-detail.component';
|
||||||
import { CicBureauConfigComponent } from './cic-bureau-config/cic-bureau-config.component';
|
|
||||||
import { CicApiMappingComponent } from './cic-api-mapping/cic-api-mapping.component';
|
|
||||||
import { CicService } from './cic.service';
|
import { CicService } from './cic.service';
|
||||||
import { CicStaticService } from './cic-static.service';
|
import { CicStaticService } from './cic-static.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CIC / credit bureau (CIBIL, Experian, Equifax, CRIF) - its own top-level menu: enquiry alerts on our customers from
|
* CIC / credit bureau (CIBIL, Experian, Equifax, CRIF) - its own top-level menu: enquiry alerts on our customers from
|
||||||
* other lenders, the CIC whitelist and configuration, and the API mapping.
|
* other lenders and the fetch log. Each bureau's connection, whitelist and API mapping live under API Management.
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -28,9 +26,7 @@ import { CicStaticService } from './cic-static.service';
|
|||||||
CreditBureauRoutingModule,
|
CreditBureauRoutingModule,
|
||||||
CicDashboardComponent,
|
CicDashboardComponent,
|
||||||
CicFetchLogComponent,
|
CicFetchLogComponent,
|
||||||
CicAlertDetailComponent,
|
CicAlertDetailComponent
|
||||||
CicBureauConfigComponent,
|
|
||||||
CicApiMappingComponent
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// Static demo data, no backend call. Remove this line to use the real /v1/cic API again.
|
// Static demo data, no backend call. Remove this line to use the real /v1/cic API again.
|
||||||
|
|||||||
Reference in New Issue
Block a user