diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html index 3483e695c..98fc6d4de 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.html +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.html @@ -136,7 +136,7 @@ | translate }} - + @if (createClientForm.controls.fullname.hasError('required')) { Client name is required } @else if (createClientForm.controls.fullname.hasError('pattern')) { @@ -214,7 +214,7 @@ {{ 'labels.inputs.First Name' | translate }} badge - + @if (createClientForm.controls.firstname.hasError('required')) { First name is required } @else if (createClientForm.controls.firstname.hasError('pattern')) { @@ -228,7 +228,7 @@ {{ 'labels.inputs.Middle Name' | translate }} person_outline - + @if ( createClientForm.controls.middlename?.hasError('pattern') && createClientForm.controls.middlename?.touched @@ -241,7 +241,7 @@ {{ 'labels.inputs.Last Name' | translate }} person - + @if (createClientForm.controls.lastname.hasError('required')) { Last name is required } @else if (createClientForm.controls.lastname.hasError('pattern')) { diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index 21c7bfa4d..dba14f5e8 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -108,6 +108,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges * m_code_value (code "PwdCategory") via the client template, same as occupationCategoryOptions. * Admins can add/reorder/deactivate entries from System -> Manage Codes without a code change. */ pwdCategoryOptions: any[] = []; + riskRules: any[] = []; /** Set true after a save attempt so the UI can highlight a missing required document */ seniorPwdDocsTouched = false; @@ -546,6 +547,10 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges ngOnInit() { this.maxDate = this.settingsService.businessDate; this.setOptions(); + + this.clientService.getClientRiskRules().subscribe((rules) => { + this.riskRules = rules || []; + }); // Fetch marital status options from code values this.clientService.getCodeValuesByName('MaritalStatus').subscribe((options) => { this.maritalStatusOptions = options || []; @@ -2120,6 +2125,74 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges return generalDetails; } + + get calculatedRisk(): { category: number; reason: string } { + const legalFormId = this.createClientForm.get('legalFormId')?.value || 1; // 1 = Person, 2 = Entity + const occCatId = this.createClientForm.get('occupationCategoryCvId')?.value; + const occId = this.createClientForm.get('occupationCvId')?.value; + + const monthlyIncome = parseFloat(this.createClientForm.get('monthlyIncome')?.value) || 0; + const annualIncome = parseFloat(this.createClientForm.get('annualIncome')?.value) || 0; + const monthlyTurnover = parseFloat(this.createClientForm.get('monthlyTurnover')?.value) || 0; + const annualTurnover = parseFloat(this.createClientForm.get('annualTurnover')?.value) || 0; + const agricultureIncome = parseFloat(this.createClientForm.get('agricultureIncome')?.value) || 0; + const nonAgricultureIncome = parseFloat(this.createClientForm.get('nonAgricultureIncome')?.value) || 0; + + const totalAnnualIncome = annualIncome + agricultureIncome + nonAgricultureIncome; + + // Evaluate rules from DB (ordered by orderPosition ASC) + if (this.riskRules && this.riskRules.length > 0) { + for (const rule of this.riskRules) { + // Match legal form + if (rule.legalFormId != null && rule.legalFormId !== legalFormId) { + continue; + } + + // Match occupation category + if (rule.occupationCategoryCvId != null && rule.occupationCategoryCvId !== occCatId) { + continue; + } + + // Match occupation + if (rule.occupationCvId != null && rule.occupationCvId !== occId) { + continue; + } + + // Match monthly income ranges + if (rule.minMonthlyIncome != null && monthlyIncome < rule.minMonthlyIncome) { + continue; + } + if (rule.maxMonthlyIncome != null && monthlyIncome > rule.maxMonthlyIncome) { + continue; + } + + // Match annual income ranges (evaluated against totalAnnualIncome) + if (rule.minAnnualIncome != null && totalAnnualIncome < rule.minAnnualIncome) { + continue; + } + if (rule.maxAnnualIncome != null && totalAnnualIncome > rule.maxAnnualIncome) { + continue; + } + + // Match turnover ranges + if (rule.minAnnualTurnover != null && annualTurnover < rule.minAnnualTurnover) { + continue; + } + if (rule.maxAnnualTurnover != null && annualTurnover > rule.maxAnnualTurnover) { + continue; + } + + // All matched! + return { category: rule.riskCategory, reason: rule.reason }; + } + } + + // Default Fallbacks + if (legalFormId !== LegalFormId.PERSON) { + return { category: 2, reason: 'Non-Individual / Entity profile (Default Medium Risk)' }; + } + return { category: 1, reason: 'Default Low Risk profile (Salary / Service / Agriculture / Others)' }; + } } /** diff --git a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts index fdf2f9250..e92614174 100644 --- a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts +++ b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts @@ -49,6 +49,10 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { /** Minor clients can't hold utility bills, bank statements or an adult-only OVD in their own * name — restricts the address-proof picker to Aadhaar/Passport when true. */ @Input() isMinor = false; + /** Non-individual (entity) clients — hides personal OVDs (Aadhaar/Passport/DL/Voter ID/NREGA/ + * NPR/Birth Certificate/School ID) from the proof-of-address picker; a company's registered + * office address is evidenced by utility bills, GST/rent documents etc., not a person's ID. */ + @Input() isNonIndividual = false; activeTab = 0; sameAsPermanent = false; @@ -282,8 +286,8 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { this.clientsService.getKycProofTypes().subscribe({ next: (res: any) => { const activeFilter = (p: any) => p?.active !== false; - this.addressProofs = this.filterAddressProofsForMinor( - (res?.addressProofs || []).filter(activeFilter).map(normalizeProofType) + this.addressProofs = this.filterAddressProofsForNonIndividual( + this.filterAddressProofsForMinor((res?.addressProofs || []).filter(activeFilter).map(normalizeProofType)) ); this.addressGroups = groupProofsByCategory(this.addressProofs); this.loadingProofs = false; @@ -309,6 +313,26 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { ); } + /** Personal OVDs — not valid proof of a registered office/entity address. */ + private static readonly NON_INDIVIDUAL_EXCLUDED_ADDRESS_PROOF_CODES = new Set([ + 'AADHAAR', + 'PASSPORT', + 'VOTER_ID', + 'DRIVING_LICENSE', + 'NREGA_JOB_CARD', + 'NPR_LETTER', + 'BIRTH_CERTIFICATE', + 'SCHOOL_ID_CARD' + ]); + + private filterAddressProofsForNonIndividual(proofs: ProofType[]): ProofType[] { + if (!this.isNonIndividual) return proofs; + return proofs.filter( + (p) => + !ClientIndiaAddressStepComponent.NON_INDIVIDUAL_EXCLUDED_ADDRESS_PROOF_CODES.has((p.code || '').toUpperCase()) + ); + } + selectAddrProof(proof: ProofType): void { this.selectedAddrProof = proof; this.permanentForm.get('proofId')?.setValue(proof.id); @@ -425,6 +449,14 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges { } } } + // Legal form toggled on an already-initialized instance — re-fetch rather than filter + // `addressProofs` in place, since the non-individual filter only ever removes entries and + // toggling back to individual could never recover what a prior pass had stripped out. + if (changes['isNonIndividual'] && !changes['isNonIndividual'].firstChange) { + this.selectedAddrProof = null; + this.permanentForm.get('proofId')?.setValue(''); + this.loadProofTypes(); + } } /** diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html index efce2ddb7..11b4e42c7 100644 --- a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html +++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html @@ -1049,6 +1049,12 @@
+
+ + Override Auto-calculated Risk Category + +
+ Risk Category @@ -1086,6 +1092,64 @@ Auto: Low 10yr, Medium 8yr, High 2yr + @if (riskCalculationReason && !kycForm.get('allowRiskOverride')?.value && !readOnly) { +
+ info + Auto-determined: {{ riskCalculationReason }} +
+ } + +
+ +
+ + @if (showRiskRulesTable) { +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Risk CategoryPrimary Criteria / BasisExamples / Thresholds
High RiskPEPs, High-Risk Industries, High Income / Turnovers + Politicians; Real Estate, Import/Export; Commission Agents; Income > ₹20L/yr; Turnover > ₹1 Crore +
Medium RiskSelf-Employed, Professionals, Cross-Border, Moderate Income + Business Owners, Traders; Doctors, Lawyers, CAs; NRI, PIO, OCI; Income > ₹6L/yr; Turnover > ₹20L +
Low RiskNon-Earning, Government Employment, Agriculture / ManualStudents, Minors, Homemakers; Govt/PSU Employees; Farmers, Daily Wage; Income <= ₹6L/yr
+
+ } + {{ isKycVerified ? 'Reason for Re-edit *' : 'Remarks' }}