feat(clients, alm): add CIN/MCA verification & authorized signatories tab; refactor ALM report print and export
This commit is contained in:
+110
-5
@@ -136,7 +136,14 @@
|
||||
| translate
|
||||
}}
|
||||
</mat-label>
|
||||
<input matInput required formControlName="fullname" autocomplete="off" />
|
||||
<input
|
||||
matInput
|
||||
required
|
||||
formControlName="fullname"
|
||||
autocomplete="off"
|
||||
(blur)="capitalizeControl('fullname')"
|
||||
style="text-transform: capitalize"
|
||||
/>
|
||||
@if (createClientForm.controls.fullname.hasError('required')) {
|
||||
<mat-error>Client name is <strong>required</strong></mat-error>
|
||||
} @else if (createClientForm.controls.fullname.hasError('pattern')) {
|
||||
@@ -151,7 +158,9 @@
|
||||
<div class="full-name-row full-width">
|
||||
<div class="full-name-preview">
|
||||
<span class="full-name-label">Entity Name:</span>
|
||||
<span class="full-name-value">{{ createClientForm.get('fullname')?.value }}</span>
|
||||
<span class="full-name-value" style="text-transform: capitalize">{{
|
||||
createClientForm.get('fullname')?.value
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="verify-inline">
|
||||
@if (!sanctionsVerified && !sanctionsBlocked) {
|
||||
@@ -214,7 +223,14 @@
|
||||
<mat-form-field appearance="outline" class="name-field first-name">
|
||||
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
|
||||
<span matPrefix class="field-prefix-icon"><mat-icon>badge</mat-icon></span>
|
||||
<input matInput required formControlName="firstname" autocomplete="off" />
|
||||
<input
|
||||
matInput
|
||||
required
|
||||
formControlName="firstname"
|
||||
autocomplete="off"
|
||||
(blur)="capitalizeControl('firstname')"
|
||||
style="text-transform: capitalize"
|
||||
/>
|
||||
@if (createClientForm.controls.firstname.hasError('required')) {
|
||||
<mat-error>First name is <strong>required</strong></mat-error>
|
||||
} @else if (createClientForm.controls.firstname.hasError('pattern')) {
|
||||
@@ -228,7 +244,13 @@
|
||||
<mat-form-field appearance="outline" class="name-field middle-name">
|
||||
<mat-label>{{ 'labels.inputs.Middle Name' | translate }}</mat-label>
|
||||
<span matPrefix class="field-prefix-icon"><mat-icon>person_outline</mat-icon></span>
|
||||
<input matInput formControlName="middlename" autocomplete="off" />
|
||||
<input
|
||||
matInput
|
||||
formControlName="middlename"
|
||||
autocomplete="off"
|
||||
(blur)="capitalizeControl('middlename')"
|
||||
style="text-transform: capitalize"
|
||||
/>
|
||||
@if (
|
||||
createClientForm.controls.middlename?.hasError('pattern') &&
|
||||
createClientForm.controls.middlename?.touched
|
||||
@@ -241,7 +263,14 @@
|
||||
<mat-form-field appearance="outline" class="name-field last-name">
|
||||
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
|
||||
<span matPrefix class="field-prefix-icon"><mat-icon>person</mat-icon></span>
|
||||
<input matInput required formControlName="lastname" autocomplete="off" />
|
||||
<input
|
||||
matInput
|
||||
required
|
||||
formControlName="lastname"
|
||||
autocomplete="off"
|
||||
(blur)="capitalizeControl('lastname')"
|
||||
style="text-transform: capitalize"
|
||||
/>
|
||||
@if (createClientForm.controls.lastname.hasError('required')) {
|
||||
<mat-error>Last name is <strong>required</strong></mat-error>
|
||||
} @else if (createClientForm.controls.lastname.hasError('pattern')) {
|
||||
@@ -694,6 +723,9 @@
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Date of Incorporation is captured on the KYC step, next to the entity PAN it is
|
||||
verified against — see client-kyc-step's `incorporationDate` control. -->
|
||||
|
||||
<!-- Authorized Signatories (multiple, registered clients) -->
|
||||
<div class="form-field full-width signatory-section">
|
||||
<h4 class="section-title signatory-title">
|
||||
@@ -1148,6 +1180,79 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!--
|
||||
Section: Turnover & Risk (Non-Individual only)
|
||||
|
||||
An entity has no salary to declare, so the individual Financial & Classification section below
|
||||
never applied to it — which left a company with no turnover on file at all, and its risk stuck
|
||||
on the flat entity default. Turnover is the size signal the branch actually has at onboarding,
|
||||
so it is captured here and the risk band it produces is shown right beside it: the officer sees
|
||||
the grade move as they type, instead of first meeting it on the KYC tab.
|
||||
-->
|
||||
@if (createClientForm.get('legalFormId')?.value === LegalFormId.ENTITY) {
|
||||
<div class="section-block">
|
||||
<h4 class="section-title">
|
||||
<fa-icon icon="chart-line" class="section-icon"></fa-icon>
|
||||
Turnover & Risk Profile
|
||||
</h4>
|
||||
<div class="form-grid">
|
||||
<mat-form-field appearance="outline" class="form-field">
|
||||
<mat-label>Monthly Turnover</mat-label>
|
||||
<span matPrefix class="rupee-prefix">₹</span>
|
||||
<input matInput type="number" formControlName="monthlyTurnover" min="0" />
|
||||
<mat-hint>Yearly turnover fills in automatically (× 12)</mat-hint>
|
||||
@if (
|
||||
createClientForm.get('monthlyTurnover')?.hasError('pattern') &&
|
||||
createClientForm.get('monthlyTurnover')?.touched
|
||||
) {
|
||||
<mat-error>Enter a valid amount</mat-error>
|
||||
} @else if (
|
||||
createClientForm.get('monthlyTurnover')?.hasError('min') && createClientForm.get('monthlyTurnover')?.touched
|
||||
) {
|
||||
<mat-error>Amount cannot be negative</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="form-field">
|
||||
<mat-label>Annual Turnover</mat-label>
|
||||
<span matPrefix class="rupee-prefix">₹</span>
|
||||
<input matInput type="number" formControlName="annualTurnover" min="0" />
|
||||
<mat-hint>Drives the risk grade below</mat-hint>
|
||||
@if (
|
||||
createClientForm.get('annualTurnover')?.hasError('pattern') &&
|
||||
createClientForm.get('annualTurnover')?.touched
|
||||
) {
|
||||
<mat-error>Enter a valid amount</mat-error>
|
||||
} @else if (
|
||||
createClientForm.get('annualTurnover')?.hasError('min') && createClientForm.get('annualTurnover')?.touched
|
||||
) {
|
||||
<mat-error>Amount cannot be negative</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div
|
||||
class="full-width entity-risk-card"
|
||||
[class.entity-risk-card--low]="entityTurnoverRisk.category === 1"
|
||||
[class.entity-risk-card--medium]="entityTurnoverRisk.category === 2"
|
||||
[class.entity-risk-card--high]="entityTurnoverRisk.category === 3"
|
||||
>
|
||||
<div class="entity-risk-card__head">
|
||||
<fa-icon icon="shield-alt" class="entity-risk-card__icon"></fa-icon>
|
||||
<span class="entity-risk-card__title">Auto-calculated Risk Category</span>
|
||||
<span class="entity-risk-card__badge">{{ riskCategoryLabel(entityTurnoverRisk.category) }}</span>
|
||||
</div>
|
||||
<p class="entity-risk-card__reason">{{ entityTurnoverRisk.reason }}</p>
|
||||
<p class="entity-risk-card__scale">
|
||||
Up to ₹1 crore — Low · ₹1 crore to ₹10 crore — Medium · Above ₹10 crore — High
|
||||
</p>
|
||||
<p class="entity-risk-card__note">
|
||||
Carried into the KYC tab, where it can still be overridden with a recorded reason.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Section: Financial & Classification (Individual only) -->
|
||||
@if (createClientForm.get('legalFormId')?.value === LegalFormId.PERSON) {
|
||||
<div class="section-block">
|
||||
|
||||
@@ -56,6 +56,103 @@ form {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* Live risk grade beside the entity's turnover fields. Neutral by default; the accent tracks the
|
||||
band so a jump from Medium to High is visible without reading the sentence. */
|
||||
.entity-risk-card {
|
||||
margin: 4px 0 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-left: 4px solid #94a3b8;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
margin-left: auto;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
text-transform: uppercase;
|
||||
background: #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
&__reason {
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
&__scale,
|
||||
&__note {
|
||||
margin: 4px 0 0;
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&--low {
|
||||
background: #f0fdf4;
|
||||
border-color: #bbf7d0;
|
||||
border-left-color: #16a34a;
|
||||
|
||||
.entity-risk-card__icon {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.entity-risk-card__badge {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
}
|
||||
|
||||
&--medium {
|
||||
background: #fffbeb;
|
||||
border-color: #fde68a;
|
||||
border-left-color: #d97706;
|
||||
|
||||
.entity-risk-card__icon {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.entity-risk-card__badge {
|
||||
background: #fef3c7;
|
||||
color: #b45309;
|
||||
}
|
||||
}
|
||||
|
||||
&--high {
|
||||
background: #fef2f2;
|
||||
border-color: #fecaca;
|
||||
border-left-color: #dc2626;
|
||||
|
||||
.entity-risk-card__icon {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.entity-risk-card__badge {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.senior-citizen-auto-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+179
-24
@@ -32,6 +32,7 @@ import { filter, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { LegalFormId } from 'app/clients/models/legal-form.enum';
|
||||
import { toProperNameCase } from 'app/clients/utils/name-case.util';
|
||||
import { ExternalNationalIdService } from 'app/clients/services/external-national-id.service';
|
||||
|
||||
/** Custom Services */
|
||||
@@ -654,6 +655,11 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
if (this.createClientForm.get('guardianDetails')) {
|
||||
this.createClientForm.removeControl('guardianDetails');
|
||||
}
|
||||
// Same reasoning as the guardian above: the reused component instance would otherwise carry the
|
||||
// previous entity's authorized signatories into a brand new client.
|
||||
this.signatories = [];
|
||||
this.signatorySearchText = '';
|
||||
this.signatorySearchResults = [];
|
||||
// Verification state must not carry over — a previous client's cleared sanctions
|
||||
// screening or verified OTP must not silently apply to a different new client.
|
||||
this.mobileVerified = false;
|
||||
@@ -777,8 +783,13 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
incorpNumber: data.clientNonPersonDetails.incorpNumber || '',
|
||||
remarks: data.clientNonPersonDetails.remarks || '',
|
||||
authorizedSignatory: data.clientNonPersonDetails.authorizedSignatory || '',
|
||||
authorizedSignatoryClientIds: data.clientNonPersonDetails.authorizedSignatoryClientIds || '',
|
||||
additionalInfo: data.clientNonPersonDetails.additionalInfo || ''
|
||||
});
|
||||
this.restoreSignatories(
|
||||
data.clientNonPersonDetails.authorizedSignatoryClientIds,
|
||||
data.clientNonPersonDetails.authorizedSignatory
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -971,7 +982,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
|
||||
/**
|
||||
* Auto-calculates turnover based on income (doubling monthly income for monthly turnover,
|
||||
* and annual income for annual/yearly turnover).
|
||||
* and annual income for annual/yearly turnover), but only if the turnover fields
|
||||
* have not been manually modified or cleared.
|
||||
*/
|
||||
private linkIncomeToTurnover(): void {
|
||||
const monthlyIncome = this.createClientForm.get('monthlyIncome');
|
||||
@@ -979,23 +991,33 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
const annualIncome = this.createClientForm.get('annualIncome');
|
||||
const annualTurnover = this.createClientForm.get('annualTurnover');
|
||||
|
||||
if (monthlyIncome && monthlyTurnover) {
|
||||
const isEmpty = (val: any) => val === '' || val === null || val === undefined;
|
||||
|
||||
if (monthlyIncome && monthlyTurnover && annualTurnover) {
|
||||
monthlyIncome.valueChanges.subscribe((value) => {
|
||||
if (this.suppressMonthlyAnnualLink) {
|
||||
return;
|
||||
}
|
||||
const num = parseFloat(value);
|
||||
monthlyTurnover.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 2) : '', { emitEvent: true });
|
||||
const isPristine = monthlyTurnover.pristine && annualTurnover.pristine;
|
||||
const isTurnoverEmpty = isEmpty(monthlyTurnover.value) && isEmpty(annualTurnover.value);
|
||||
if (isPristine || isTurnoverEmpty) {
|
||||
const num = parseFloat(value);
|
||||
monthlyTurnover.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 2) : '', { emitEvent: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (annualIncome && annualTurnover) {
|
||||
if (annualIncome && annualTurnover && monthlyTurnover) {
|
||||
annualIncome.valueChanges.subscribe((value) => {
|
||||
if (this.suppressMonthlyAnnualLink) {
|
||||
return;
|
||||
}
|
||||
const num = parseFloat(value);
|
||||
annualTurnover.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 2) : '', { emitEvent: true });
|
||||
const isPristine = monthlyTurnover.pristine && annualTurnover.pristine;
|
||||
const isTurnoverEmpty = isEmpty(monthlyTurnover.value) && isEmpty(annualTurnover.value);
|
||||
if (isPristine || isTurnoverEmpty) {
|
||||
const num = parseFloat(value);
|
||||
annualTurnover.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 2) : '', { emitEvent: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1122,6 +1144,10 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
mainBusinessLineId: [''],
|
||||
incorpNumber: [''],
|
||||
authorizedSignatory: [''],
|
||||
// Client ids behind the names above, in the same order — what lets an edit rebuild the
|
||||
// signatory rows with their client links and PAN/DIN state. No input is bound to it;
|
||||
// syncSignatoryNames() keeps it in step with the chips.
|
||||
authorizedSignatoryClientIds: [''],
|
||||
additionalInfo: [''],
|
||||
remarks: ['']
|
||||
})
|
||||
@@ -1343,6 +1369,17 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.removeGuardian();
|
||||
}
|
||||
|
||||
capitalizeControl(controlName: string) {
|
||||
const control = this.createClientForm.get(controlName);
|
||||
if (control && control.value) {
|
||||
const originalValue = control.value;
|
||||
const capitalized = toProperNameCase(originalValue);
|
||||
if (originalValue !== capitalized) {
|
||||
control.setValue(capitalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify client name against sanctions lists via dialog.
|
||||
* Passes the client name to the dialog for real API screening.
|
||||
@@ -1467,6 +1504,11 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
target.mobileNo = data.mobileNo || null;
|
||||
target.emailAddress = data.emailAddress || null;
|
||||
target.officeName = data.officeName || null;
|
||||
// Filled in only when the caller has not supplied them. A signatory restored from stored
|
||||
// ids starts as a bare { id } and needs both to render its row; one picked from the search
|
||||
// dropdown already carries them, and a guardian must keep whatever it was given.
|
||||
target.displayName = target.displayName || data.displayName;
|
||||
target.status = target.status || data.status;
|
||||
target._loading = false;
|
||||
},
|
||||
error: () => {
|
||||
@@ -1480,6 +1522,10 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
target.panVerified = kyc?.panVerified || false;
|
||||
target.aadhaarMasked = kyc?.aadhaarNumberMasked || '-';
|
||||
target.aadhaarVerified = kyc?.aadhaarVerified || false;
|
||||
// Same reasoning as above: a DIN verified in an earlier session lives on the signatory's own
|
||||
// KYC record, so re-reading it here is what stops the edit screen asking for it again.
|
||||
target.dinNumber = target.dinNumber || kyc?.dinNumber || '';
|
||||
target.dinVerified = target.dinVerified || kyc?.dinVerified || false;
|
||||
},
|
||||
error: () => {
|
||||
target.panNumber = '-';
|
||||
@@ -1678,9 +1724,49 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the authorized-signatory rows when an existing entity client is opened for edit.
|
||||
*
|
||||
* Driven by the stored client ids rather than the stored names: the ids are what make each row a
|
||||
* real client again, so its CIF, PAN and DIN verification can be re-read instead of the officer
|
||||
* having to re-add and re-verify signatories that were already recorded. The saved names are used
|
||||
* only as the label until the lookup returns, and as the fallback for a client that can no longer
|
||||
* be fetched, so a row is never silently dropped.
|
||||
*
|
||||
* Clients created before the ids were stored have names but no ids; those still list, as
|
||||
* name-only rows, rather than showing nothing at all.
|
||||
*/
|
||||
private restoreSignatories(storedIds: string | null | undefined, storedNames: string | null | undefined): void {
|
||||
const ids = (storedIds || '')
|
||||
.split(',')
|
||||
.map((id: string) => id.trim())
|
||||
.filter(Boolean);
|
||||
const names = (storedNames || '')
|
||||
.split(',')
|
||||
.map((name: string) => name.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (!ids.length) {
|
||||
this.signatories = names.map((displayName: string) => ({ displayName }));
|
||||
return;
|
||||
}
|
||||
|
||||
this.signatories = ids.map((id: string, index: number) => ({
|
||||
id: Number(id),
|
||||
displayName: names[index] || '',
|
||||
_loading: true
|
||||
}));
|
||||
this.signatories.forEach((sig: any) => this.fetchClientFullDetails(sig.id, sig));
|
||||
}
|
||||
|
||||
private syncSignatoryNames(): void {
|
||||
const names = this.signatories.map((s) => s.displayName).join(', ');
|
||||
this.createClientForm.get('clientNonPersonDetails')?.patchValue({ authorizedSignatory: names });
|
||||
// Ids are written in the same order as the names, so the two columns line up on reload.
|
||||
const clientIds = this.signatories.map((s) => s.id).join(',');
|
||||
this.createClientForm.get('clientNonPersonDetails')?.patchValue({
|
||||
authorizedSignatory: names,
|
||||
authorizedSignatoryClientIds: clientIds
|
||||
});
|
||||
}
|
||||
|
||||
/** Watch MSME turnover & investment for auto-classification */
|
||||
@@ -2164,6 +2250,12 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
delete generalDetails[key];
|
||||
}
|
||||
}
|
||||
// Entity name is normalised here as well as on blur: a pasted/auto-filled value, or one the
|
||||
// user never blurred out of before submitting, would otherwise reach Fineract exactly as typed.
|
||||
if (generalDetails.fullname) {
|
||||
generalDetails.fullname = toProperNameCase(generalDetails.fullname);
|
||||
}
|
||||
|
||||
if (generalDetails.submittedOnDate instanceof Date) {
|
||||
generalDetails.submittedOnDate = this.dateUtils.formatDate(generalDetails.submittedOnDate, dateFormat);
|
||||
}
|
||||
@@ -2233,15 +2325,86 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
];
|
||||
unsupportedFields.forEach((f) => delete generalDetails[f]);
|
||||
|
||||
// Format dateOfRegistration if present
|
||||
if (generalDetails.clientNonPersonDetails?.dateOfRegistration instanceof Date) {
|
||||
generalDetails.clientNonPersonDetails.dateOfRegistration = this.dateUtils.formatDate(
|
||||
generalDetails.clientNonPersonDetails.dateOfRegistration,
|
||||
dateFormat
|
||||
);
|
||||
return generalDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turnover ladder that decides an entity's risk when no configured risk rule matches it.
|
||||
*
|
||||
* A company's size is the one thing about it the branch always knows at onboarding, and the
|
||||
* bigger the book the more exposure it carries — so the band moves up with annual turnover
|
||||
* instead of every entity landing on a flat Medium regardless of whether it turns over ten lakh
|
||||
* or a hundred crore. Thresholds follow the MSME-style split the business already thinks in
|
||||
* (crores), and are only a fallback: a rule configured in m_client_risk_rule with
|
||||
* minAnnualTurnover / maxAnnualTurnover still wins, which is where a bank sets its own numbers.
|
||||
*
|
||||
* Amounts are in rupees. `upTo: null` is the open-ended top band.
|
||||
*/
|
||||
private static readonly ENTITY_TURNOVER_RISK_BANDS: { upTo: number | null; category: number; label: string }[] = [
|
||||
{ upTo: 10000000, category: 1, label: 'up to ₹1 crore' },
|
||||
{ upTo: 100000000, category: 2, label: '₹1 crore to ₹10 crore' },
|
||||
{ upTo: null, category: 3, label: 'above ₹10 crore' }
|
||||
];
|
||||
|
||||
/** "Low Risk" / "Medium Risk" / "High Risk" for a category id. */
|
||||
riskCategoryLabel(category: number): string {
|
||||
if (category === 3) {
|
||||
return 'High Risk';
|
||||
}
|
||||
return category === 2 ? 'Medium Risk' : 'Low Risk';
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity risk from annual turnover, for the fallback path only.
|
||||
*
|
||||
* AML is deliberately left CLEAR even in the top band. A configured High-risk rule defaults to
|
||||
* FLAGGED because someone decided that case warrants it; turnover on its own is a size signal,
|
||||
* not a suspicion, and auto-flagging every large company would put them all in the AML queue.
|
||||
*/
|
||||
private entityRiskFromTurnover(annualTurnover: number): {
|
||||
category: number;
|
||||
reason: string;
|
||||
amlCheckStatus: string;
|
||||
bannedEntityMatch: string;
|
||||
willfulDefaulter: string;
|
||||
ecgcSalFlag: string;
|
||||
dormantFlag: string;
|
||||
} {
|
||||
const base = {
|
||||
amlCheckStatus: 'CLEAR',
|
||||
bannedEntityMatch: 'NO',
|
||||
willfulDefaulter: 'NO',
|
||||
ecgcSalFlag: 'NO',
|
||||
dormantFlag: 'ACTIVE'
|
||||
};
|
||||
|
||||
// Nothing captured yet — the entity default stands until a turnover is entered.
|
||||
if (!annualTurnover || annualTurnover <= 0) {
|
||||
return {
|
||||
category: 2,
|
||||
reason: 'Non-Individual / Entity profile (Default Medium Risk)',
|
||||
...base
|
||||
};
|
||||
}
|
||||
|
||||
return generalDetails;
|
||||
const band =
|
||||
ClientGeneralStepComponent.ENTITY_TURNOVER_RISK_BANDS.find((b) => b.upTo == null || annualTurnover <= b.upTo) ??
|
||||
ClientGeneralStepComponent.ENTITY_TURNOVER_RISK_BANDS[
|
||||
ClientGeneralStepComponent.ENTITY_TURNOVER_RISK_BANDS.length - 1
|
||||
];
|
||||
|
||||
return {
|
||||
category: band.category,
|
||||
reason: `Annual turnover ₹${annualTurnover.toLocaleString('en-IN')} — ${band.label} (${this.riskCategoryLabel(
|
||||
band.category
|
||||
)})`,
|
||||
...base
|
||||
};
|
||||
}
|
||||
|
||||
/** Live risk band for the entity turnover card in the General step. */
|
||||
get entityTurnoverRisk(): { category: number; reason: string } {
|
||||
return this.calculatedRisk;
|
||||
}
|
||||
|
||||
get calculatedRisk(): {
|
||||
@@ -2338,15 +2501,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
||||
|
||||
// Default Fallbacks
|
||||
if (legalFormId !== LegalFormId.PERSON) {
|
||||
return {
|
||||
category: 2,
|
||||
reason: 'Non-Individual / Entity profile (Default Medium Risk)',
|
||||
amlCheckStatus: 'CLEAR',
|
||||
bannedEntityMatch: 'NO',
|
||||
willfulDefaulter: 'NO',
|
||||
ecgcSalFlag: 'NO',
|
||||
dormantFlag: 'ACTIVE'
|
||||
};
|
||||
return this.entityRiskFromTurnover(annualTurnover);
|
||||
}
|
||||
return {
|
||||
category: 1,
|
||||
|
||||
Reference in New Issue
Block a user