Merge remote-tracking branch 'origin/demo' into demo

This commit is contained in:
2026-08-06 13:14:26 +05:30
87 changed files with 14919 additions and 1310 deletions
@@ -0,0 +1,98 @@
<!--
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="fm-dialog">
<div class="fm-dialog__header" style="display: flex; align-items: center; gap: 8px;">
<mat-icon class="fm-dialog__icon">people</mat-icon>
<span style="font-weight: 500; font-size: 18px;">CHILDREN</span>
</div>
<mat-dialog-content>
<form [formGroup]="childrenForm">
<div formArrayName="children" style="margin-top: 12px; background: #fff; padding: 12px;">
@for (child of children.controls; track $index) {
<div [formGroupName]="$index"
style="position: relative; padding: 24px 16px 16px; margin-top: 16px; margin-bottom: 24px; border: 1px solid #e0e0e0; border-radius: 8px; background: #fff;">
<div class="fm-grid">
<mat-form-field appearance="outline">
<mat-label>Child Full Name*</mat-label>
<input matInput formControlName="fullName" required placeholder="Child Full Name*" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Gender*</mat-label>
<mat-select formControlName="genderId" required>
@for (g of genderIdOptions; track g.id) {
<mat-option [value]="g.id">{{ g.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" (click)="childDobPicker.open()">
<mat-label>Date of Birth</mat-label>
<input matInput [max]="maxDate" [matDatepicker]="childDobPicker" formControlName="dateOfBirth" />
<mat-datepicker-toggle matSuffix [for]="childDobPicker"></mat-datepicker-toggle>
<mat-datepicker #childDobPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Nationality</mat-label>
<mat-select formControlName="nationality">
<mat-option value="INDIAN">Indian</mat-option>
<mat-option value="NRI">NRI</mat-option>
<mat-option value="FOREIGN_NATIONAL">Foreign National</mat-option>
<mat-option value="OTHER">Other</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Aadhaar Number (Optional)</mat-label>
<input
matInput
formControlName="aadhaarNumber"
maxlength="12"
placeholder="12-digit Aadhaar"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>PAN Number (Optional)</mat-label>
<input
matInput
formControlName="panNumber"
maxlength="10"
placeholder="ABCDE1234F"
style="text-transform: uppercase"
(input)="
$any(children.at($index)).get('panNumber')
?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
"
/>
@if (
$any(children.at($index)).get('panNumber')?.hasError('pattern') && $any(children.at($index)).get('panNumber')?.touched
) {
<mat-error>Invalid PAN format</mat-error>
}
</mat-form-field>
</div>
</div>
@if ($index < children.controls.length - 1) { <div style="border-bottom: 1px dashed #ccc; margin-bottom: 16px;">
</div>
}
}
<div style="border-bottom: 1px dashed #ccc; margin-bottom: 16px;"></div>
<button mat-stroked-button type="button" color="primary" (click)="addChild()">
<mat-icon>add</mat-icon> Add Another Child
</button>
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-stroked-button mat-dialog-close type="button">Cancel</button>
<button mat-flat-button color="primary" type="button" [disabled]="childrenForm.invalid" (click)="submit()">
Add Member
</button>
</mat-dialog-actions>
</div>
@@ -0,0 +1,106 @@
/**
* 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 { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms';
import { MatIcon } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'cbs-client-family-member-child-dialog',
templateUrl: './client-family-member-child-dialog.component.html',
styleUrls: ['../client-family-member-dialog/client-family-member-dialog.component.scss'],
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatDialogModule,
MatIcon,
MatButtonModule,
MatInputModule,
MatSelectModule,
MatDatepickerModule,
MatTooltipModule
]
})
export class ClientFamilyMemberChildDialogComponent implements OnInit {
childrenForm: UntypedFormGroup;
genderIdOptions: any[];
maxDate: Date = new Date();
constructor(
public dialogRef: MatDialogRef<ClientFamilyMemberChildDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private formBuilder: UntypedFormBuilder
) {
this.genderIdOptions = data.genderIdOptions;
this.childrenForm = this.formBuilder.group({
children: this.formBuilder.array([])
});
}
ngOnInit() {
if (this.data.children && this.data.children.length > 0) {
this.data.children.forEach((child: any) => {
this.children.push(this.formBuilder.group({
id: [child.id],
fullName: [child.fullName, Validators.required],
genderId: [child.genderId, Validators.required],
dateOfBirth: [child.dateOfBirth ? new Date(child.dateOfBirth) : ''],
nationality: [child.nationality || 'INDIAN'],
aadhaarNumber: [child.aadhaarNumber || ''],
panNumber: [child.panNumber || '', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
}));
});
} else {
this.addChild();
}
}
get children(): UntypedFormArray {
return this.childrenForm.get('children') as UntypedFormArray;
}
createChildFormGroup(): UntypedFormGroup {
return this.formBuilder.group({
id: [''],
fullName: ['', Validators.required],
genderId: ['', Validators.required],
dateOfBirth: [''],
nationality: ['INDIAN'],
aadhaarNumber: [''],
panNumber: ['', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
});
}
addChild(): void {
this.children.push(this.createChildFormGroup());
}
removeChild(index: number): void {
this.children.removeAt(index);
}
submit(): void {
if (this.childrenForm.valid) {
this.dialogRef.close(this.children.value);
}
}
}
@@ -139,6 +139,21 @@
<span class="fm-readonly-value">{{ familyMemberForm.get('remarks')?.value }}</span>
</div>
}
@if (isSpouse && familyMemberForm.get('children')?.value?.length > 0) {
<div class="fm-readonly-remarks">
<span class="fm-readonly-label">Children</span>
<div class="fm-children-list">
@for (child of familyMemberForm.get('children')?.value; track $index) {
<div class="fm-child-item" style="font-size: 13px; color: #555; margin-top: 4px;">
<span>{{ child.fullName }}</span>
@if (child.dateOfBirth) {
<span> · DOB: {{ child.dateOfBirth | date: 'dd MMM yyyy' }}</span>
}
</div>
}
</div>
</div>
}
}
<!-- ═══ ADD / EDIT MODE ═══ -->
@@ -447,6 +462,19 @@
<mat-label>Remarks</mat-label>
<textarea matInput formControlName="remarks" rows="2"></textarea>
</mat-form-field>
<!-- Children Section -->
@if (isSpouse) {
<div class="fm-section-label" style="margin-top: 16px;">Children</div>
<div class="fm-checkbox-row" style="display: flex; align-items: center; justify-content: space-between;">
<mat-checkbox formControlName="hasChildren">Does this family member have a child/children?</mat-checkbox>
@if (familyMemberForm.get('hasChildren')?.value) {
<button mat-stroked-button type="button" color="primary" (click)="openChildrenDialog()" style="margin-left: 16px;">
<mat-icon>edit</mat-icon> Edit Children Details ({{ children.length }})
</button>
}
</div>
}
}
</form>
</mat-dialog-content>
@@ -17,7 +17,7 @@ import {
MatDialogClose,
MatDialogContent
} from '@angular/material/dialog';
import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { UntypedFormGroup, UntypedFormBuilder, UntypedFormArray, Validators } from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { ClientsService } from 'app/clients/clients.service';
@@ -58,6 +58,8 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
private dialog = inject(MatDialog);
maxDate = new Date();
hasChildrenChecked = false;
familyMemberForm: UntypedFormGroup;
/** Client search */
@@ -197,6 +199,23 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
// Re-fetch linked client details (profile, signature, KYC status)
this.fetchLinkedClientData(m.linkedClientId.toString());
}
if (m.children && m.children.length > 0) {
this.hasChildrenChecked = true;
this.familyMemberForm.patchValue({ hasChildren: true });
m.children.forEach((child: any) => {
this.children.push(
this.formBuilder.group({
id: [child.id],
fullName: [child.fullName, Validators.required],
genderId: [child.genderId, Validators.required],
dateOfBirth: [child.dateOfBirth ? new Date(child.dateOfBirth) : ''],
nationality: [child.nationality || 'INDIAN'],
aadhaarNumber: [child.aadhaarNumber || ''],
panNumber: [child.panNumber || '', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
})
);
});
}
if (this.isViewMode) {
this.familyMemberForm.disable();
}
@@ -225,8 +244,83 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
isAlive: [true],
marriageDate: [''],
marriageAuthority: [''],
linkedClientId: ['']
linkedClientId: [''],
hasChildren: [false],
children: this.formBuilder.array([])
});
this.familyMemberForm.get('hasChildren')?.valueChanges.subscribe(value => {
if (value && !this.hasChildrenChecked) {
this.openChildrenDialog();
} else if (!value) {
this.hasChildrenChecked = false;
this.children.clear();
}
});
}
get children(): UntypedFormArray {
return this.familyMemberForm.get('children') as UntypedFormArray;
}
async openChildrenDialog() {
// dynamically import the dialog to avoid circular dependency issues if any
const { ClientFamilyMemberChildDialogComponent } = await import('../client-family-member-child-dialog/client-family-member-child-dialog.component');
const dialogRef = this.dialog.open(ClientFamilyMemberChildDialogComponent, {
data: {
children: this.children.value,
genderIdOptions: this.data.options.genderIdOptions
},
width: '500px'
});
dialogRef.afterClosed().subscribe((result: any[]) => {
if (result) {
this.children.clear();
result.forEach(child => {
this.children.push(this.formBuilder.group({
id: [child.id],
fullName: [child.fullName, Validators.required],
genderId: [child.genderId, Validators.required],
dateOfBirth: [child.dateOfBirth],
nationality: [child.nationality || 'INDIAN'],
aadhaarNumber: [child.aadhaarNumber || ''],
panNumber: [child.panNumber || '', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
}));
});
this.hasChildrenChecked = true;
this.familyMemberForm.patchValue({ hasChildren: true }, { emitEvent: false });
} else {
if (this.children.length === 0) {
this.hasChildrenChecked = false;
this.familyMemberForm.patchValue({ hasChildren: false }, { emitEvent: false });
}
}
});
}
createChildFormGroup(): UntypedFormGroup {
return this.formBuilder.group({
id: [''],
fullName: ['', Validators.required],
genderId: ['', Validators.required],
dateOfBirth: [''],
nationality: ['INDIAN'],
aadhaarNumber: [''],
panNumber: ['', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
});
}
addChild(): void {
this.children.push(this.createChildFormGroup());
}
removeChild(index: number): void {
this.children.removeAt(index);
if (this.children.length === 0) {
this.familyMemberForm.patchValue({ hasChildren: false });
}
}
/** Handle marriage certificate file selection */
@@ -481,6 +575,25 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
member._linkedClient = this.selectedClient;
}
// Format children dates
if (this.isSpouse && member.children && member.children.length > 0) {
member.children = member.children.map((child: any) => {
const c = { ...child };
if (c.dateOfBirth instanceof Date) {
c.dateOfBirth = this.dateUtils.formatDate(c.dateOfBirth, dateFormat);
} else if (!c.dateOfBirth) {
delete c.dateOfBirth;
}
if (c.panNumber) {
c.panNumber = c.panNumber.toUpperCase();
}
return c;
});
} else {
delete member.children;
}
delete member.hasChildren;
// Attach marriage certificate file if present
if (this.marriageCertFile) {
member._marriageCertFile = this.marriageCertFile;
@@ -533,8 +533,6 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
screeningLogId: number | null = null;
/** Snapshot of the name that was last verified — used to invalidate verification when name changes */
private verifiedName: string = '';
/** True while prefillForEdit() is patching historical monthly/annual values — see linkMonthlyAnnual(). */
private suppressMonthlyAnnualLink = false;
/**
* @param {FormBuilder} formBuilder Form Builder
@@ -711,12 +709,6 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
const data = this.clientData;
this.loadExistingSeniorPwdDocuments();
// Loading an existing client's independently-saved monthly/annual values must not run
// through the auto-calc in linkMonthlyAnnual() — patching monthlyIncome then annualIncome
// (or vice versa) would otherwise have each overwrite the other via its valueChanges,
// instead of keeping the exact historical values as saved.
this.suppressMonthlyAnnualLink = true;
// Patch all base controls that exist on the form
this.createClientForm.patchValue({
officeId: data.officeId,
@@ -751,7 +743,6 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
? new Date(data.timeline.submittedOnDate)
: this.settingsService.businessDate
});
this.suppressMonthlyAnnualLink = false;
// Patch name fields (already created by buildDependencies via legalFormId subscription)
const legalFormId = data.legalForm?.id || LegalFormId.PERSON;
@@ -966,6 +957,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
this.linkMonthlyAnnual('monthlyIncome', 'annualIncome');
this.linkMonthlyAnnual('monthlyTurnover', 'annualTurnover');
this.linkIncomeToTurnover();
}
/**
@@ -1002,6 +994,37 @@ 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).
*/
private linkIncomeToTurnover(): void {
const monthlyIncome = this.createClientForm.get('monthlyIncome');
const monthlyTurnover = this.createClientForm.get('monthlyTurnover');
const annualIncome = this.createClientForm.get('annualIncome');
const annualTurnover = this.createClientForm.get('annualTurnover');
if (monthlyIncome && monthlyTurnover) {
monthlyIncome.valueChanges.subscribe((value) => {
if (this.suppressMonthlyAnnualLink) {
return;
}
const num = parseFloat(value);
monthlyTurnover.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 2) : '', { emitEvent: true });
});
}
if (annualIncome && annualTurnover) {
annualIncome.valueChanges.subscribe((value) => {
if (this.suppressMonthlyAnnualLink) {
return;
}
const num = parseFloat(value);
annualTurnover.setValue(!isNaN(num) && num >= 0 ? this.round2(num * 2) : '', { emitEvent: true });
});
}
}
/**
* Sets select dropdown options.
*/
@@ -2236,26 +2259,26 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
}
// Match monthly income ranges
if (rule.minMonthlyIncome != null && rule.minMonthlyIncome > 0 && monthlyIncome < rule.minMonthlyIncome) {
if (rule.minMonthlyIncome != null && monthlyIncome < rule.minMonthlyIncome) {
continue;
}
if (rule.maxMonthlyIncome != null && rule.maxMonthlyIncome > 0 && monthlyIncome > rule.maxMonthlyIncome) {
if (rule.maxMonthlyIncome != null && monthlyIncome > rule.maxMonthlyIncome) {
continue;
}
// Match annual income ranges (evaluated against totalAnnualIncome)
if (rule.minAnnualIncome != null && rule.minAnnualIncome > 0 && totalAnnualIncome < rule.minAnnualIncome) {
if (rule.minAnnualIncome != null && totalAnnualIncome < rule.minAnnualIncome) {
continue;
}
if (rule.maxAnnualIncome != null && rule.maxAnnualIncome > 0 && totalAnnualIncome > rule.maxAnnualIncome) {
if (rule.maxAnnualIncome != null && totalAnnualIncome > rule.maxAnnualIncome) {
continue;
}
// Match turnover ranges
if (rule.minAnnualTurnover != null && rule.minAnnualTurnover > 0 && annualTurnover < rule.minAnnualTurnover) {
if (rule.minAnnualTurnover != null && annualTurnover < rule.minAnnualTurnover) {
continue;
}
if (rule.maxAnnualTurnover != null && rule.maxAnnualTurnover > 0 && annualTurnover > rule.maxAnnualTurnover) {
if (rule.maxAnnualTurnover != null && annualTurnover > rule.maxAnnualTurnover) {
continue;
}
@@ -723,6 +723,33 @@
<span>PAN verification requires client to be created first. Complete General tab and click Next to save.</span>
</div>
}
<!-- KYC Exception Flag -->
<div class="exception-flag-box" style="margin-top: 20px; padding: 16px; border: 1px solid #f59e0b; background: #fffbeb; border-radius: 8px; clear: both;">
<h5 style="margin: 0 0 12px 0; font-size: 14px; font-weight: 600; color: #b45309; display: flex; align-items: center; gap: 8px;">
<fa-icon icon="exclamation-triangle"></fa-icon>
KYC Exception Flag
</h5>
<div style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap;">
<mat-checkbox formControlName="isException">Mark as KYC Exception</mat-checkbox>
@if (kycForm.get('isException')?.value) {
<mat-form-field appearance="outline" style="flex: 1; min-width: 280px; margin-bottom: -1.25em;">
<mat-label>Exception Reasons</mat-label>
<mat-select formControlName="exceptionReason" multiple>
@if (!isNonIndividual) {
<mat-option value="PAN Card Verification Failed">PAN Card Verification Failed</mat-option>
<mat-option value="Aadhaar Verification Failed">Aadhaar Verification Failed</mat-option>
} @else {
<mat-option value="Enterprise PAN Verification Failed">Enterprise PAN Verification Failed</mat-option>
<mat-option value="GSTIN Verification Failed">GSTIN Verification Failed</mat-option>
<mat-option value="CIN Verification Failed">CIN Verification Failed</mat-option>
}
</mat-select>
</mat-form-field>
}
</div>
</div>
</div>
<!-- ═══ SECTION: KYC Document Management ═══ -->
@@ -1410,6 +1437,7 @@
</div>
</div>
<!-- ═══ SECTION: Photograph & Signature ═══ -->
<div class="section-block">
<h4 class="section-title">
@@ -29,6 +29,7 @@ import { ClientsService } from '../../clients.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
import { ProofType, normalizeProofType, groupProofsByCategory } from '../../shared/kyc-proof-types';
import { deriveKycExceptionFields } from '../../kyc-exception.util';
import { KycUploadHelperService } from '../../shared/kyc-upload-helper.service';
import { CaptureImageDialogComponent } from '../../clients-view/custom-dialogs/capture-image-dialog/capture-image-dialog.component';
import { DrawSignatureDialogComponent } from '../../clients-view/custom-dialogs/draw-signature-dialog/draw-signature-dialog.component';
@@ -152,7 +153,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
kycDocumentName: 'KYC Document Type',
kycDocumentNumber: 'KYC Document Number',
amlCheckStatus: 'AML Check Status',
bannedEntityMatch: 'Banned Entity List Match'
bannedEntityMatch: 'Banned Entity List Match',
remarks: 'Reason for Re-edit'
};
/** Names every currently-invalid `kycForm` control (required-but-empty, or an entered value
@@ -274,6 +276,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
riskCalculationReason = '';
showRiskRulesTable = false;
/**
* Exception state as last read back from the server. Needed to tell "this client never had an
* exception" from "this client had one and has now cleared it" — only the latter should be
* stamped resolved and surface in the Resolved tab.
*/
persistedIsException = false;
persistedExceptionReason: string | null = null;
/** PAN verification */
panVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
panVerifying = false;
@@ -423,8 +433,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
aadhaarNumber: [
'',
[
Validators.pattern(/^\d{12}$/),
Validators.maxLength(12)
Validators.pattern(/^(\d{12}|[X•*\s\d]{12,14})$/),
Validators.maxLength(14)
]
],
panAadhaarSeedingStatus: [''],
@@ -468,6 +478,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
allowRiskOverride: [false],
allowAmlOverride: [false],
remarks: [''],
isException: [false],
exceptionReason: [[]],
amlCheckStatus: [
'CLEAR',
Validators.required
@@ -610,8 +622,12 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
dormantFlag: 'ACTIVE',
accountStatus: 'ACTIVE',
form60Type: 'FORM_60',
form15Type: '15G'
form15Type: '15G',
isException: false,
exceptionReason: []
});
this.persistedIsException = false;
this.persistedExceptionReason = null;
this.kycId = '';
this.kycLoaded = false;
this.kycStatus = 0;
@@ -942,8 +958,18 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
form60Date: parseDate(data.form60Date),
form15Applicable: data.form15Applicable || false,
form15Type: data.form15Type || '15G',
form15FY: data.form15FY || ''
form15FY: data.form15FY || '',
isException: data.isException || false,
exceptionReason: data.exceptionReason ? data.exceptionReason.split(', ') : []
});
// Set remarks as required if KYC is verified
if (this.isKycVerified) {
this.kycForm.get('remarks')?.setValidators(Validators.required);
this.kycForm.get('remarks')?.updateValueAndValidity();
this.kycForm.get('remarks')?.markAsTouched(); // Force red underline so user knows it's blocking
}
this.persistedIsException = data.isException === true;
this.persistedExceptionReason = data.exceptionReason || null;
// Sync chip-picker selection with the prefilled document name
if (data.kycDocumentName) {
this.selectedKycDoc = this.kycDocProofs.find((p) => p.name === data.kycDocumentName) || null;
@@ -1843,7 +1869,21 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
form15Applicable: v.form15Applicable || false,
form15Type: v.form15Type || null,
form15FY: v.form15FY || null,
remarks: v.remarks || null
remarks: v.remarks || null,
// Exception state is DERIVED from verification status, not from a checkbox — an unverified
// or blank PAN/Aadhaar is an exception by definition, so the officer cannot forget to flag
// it. See deriveKycExceptionFields() for the raise / narrow / close rules.
...deriveKycExceptionFields({
panVerified: this.isNonIndividual ? this.entPanVerifyStatus === 'VERIFIED' : this.panVerifyStatus === 'VERIFIED',
aadhaarVerified: this.aadhaarVerifyStatus === 'VERIFIED',
// Non-individual clients submit a dummy masked Aadhaar, so checking it would saddle every
// company with a permanent exception.
aadhaarApplicable: !this.isNonIndividual,
hadException: this.persistedIsException,
previousReason: this.persistedExceptionReason,
manualReasons: Array.isArray(v.exceptionReason) ? v.exceptionReason : [],
todayFormatted: this.dateUtils.formatDate(new Date(), dateFormat)
})
};
}
@@ -1856,7 +1896,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
v.enterprisePan ||
v.cinNumber ||
v.kycDocumentName ||
v.form15Applicable
v.form15Applicable ||
v.isException
);
}