136 lines
5.7 KiB
TypeScript
136 lines
5.7 KiB
TypeScript
/**
|
|
* 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/.
|
|
*/
|
|
|
|
/**
|
|
* Derives a client's KYC exception state from their verification status.
|
|
*
|
|
* A KYC exception is not something an officer should have to remember to tick — it is simply the
|
|
* fact that a mandatory identity check has not passed. Deriving it means the exception list can
|
|
* never drift out of step with reality: raising, narrowing and closing all fall out of whether PAN
|
|
* and Aadhaar are verified at the moment of the save.
|
|
*
|
|
* Shared by the client-creation stepper and the client-view KYC tab so both write identical state.
|
|
*/
|
|
|
|
export const KYC_EXCEPTION_PAN = 'PAN Card Verification Failed';
|
|
export const KYC_EXCEPTION_AADHAAR = 'Aadhaar Verification Failed';
|
|
|
|
/** The two reasons this module owns. Anything else on a record was added by an officer by hand. */
|
|
export const AUTO_MANAGED_KYC_EXCEPTION_REASONS: string[] = [
|
|
KYC_EXCEPTION_PAN,
|
|
KYC_EXCEPTION_AADHAAR
|
|
];
|
|
|
|
export interface KycExceptionInput {
|
|
/** PAN verified against the Income Tax database. Unverified OR simply not entered → exception. */
|
|
panVerified: boolean;
|
|
/**
|
|
* A signed customer declaration has been accepted in place of a passing PAN check.
|
|
*
|
|
* The Income Tax check matches on name *and* date of birth, so a client whose PAN card carries a
|
|
* different spelling — or a different DOB — can never clear it, however many times the officer
|
|
* retries. Once the branch holds a declaration recording exactly what differs, the outstanding
|
|
* item is closed by paperwork rather than by the API, and it must not keep the client sitting in
|
|
* the exception list. PAN still reads as unverified everywhere else; only the exception is waived.
|
|
*/
|
|
panDeclarationTaken?: boolean;
|
|
/** Aadhaar verified against UIDAI. Unverified OR simply not entered → exception. */
|
|
aadhaarVerified: boolean;
|
|
/**
|
|
* The customer's Aadhaar enrolment slip has been sighted and recorded.
|
|
*
|
|
* A customer who has applied for Aadhaar but has not been issued one yet holds only a UIDAI
|
|
* enrolment receipt. There is no Aadhaar number to put through the OTP flow, so the check cannot
|
|
* pass however long the branch waits — the same dead end the PAN declaration exists to resolve.
|
|
* Once the receipt is on file the outstanding item is closed by paperwork, and the client must
|
|
* not keep sitting in the exception list. Aadhaar still reads as unverified everywhere else.
|
|
*/
|
|
aadhaarReceiptTaken?: boolean;
|
|
/**
|
|
* False for non-individual clients, where no Aadhaar is collected (the payload sends a dummy
|
|
* masked value). Without this guard every company would carry a permanent Aadhaar exception.
|
|
*/
|
|
aadhaarApplicable: boolean;
|
|
/** Whether an exception is already recorded — decides raise vs. resolve on a clean save. */
|
|
hadException: boolean;
|
|
/** Reason currently persisted, kept verbatim when closing so the audit trail survives. */
|
|
previousReason: string | null;
|
|
/** Officer-entered reasons beyond the two automatic ones; carried through untouched. */
|
|
manualReasons?: string[];
|
|
/** Today in the tenant's date format — stamped as exceptionResolvedOn when closing. */
|
|
todayFormatted: string;
|
|
}
|
|
|
|
export interface KycExceptionFields {
|
|
isException: boolean;
|
|
exceptionReason: string | null;
|
|
exceptionResolved: boolean;
|
|
exceptionResolvedOn: string | null;
|
|
}
|
|
|
|
/**
|
|
* Maps verification status to the four exception columns the API expects.
|
|
*
|
|
* Three outcomes:
|
|
* - **Something outstanding** → raise/keep the exception, listing only what is still failing. A
|
|
* record that had both PAN and Aadhaar outstanding and has since verified PAN comes back with
|
|
* Aadhaar alone, which is what narrows the entry rather than closing it.
|
|
* - **Nothing outstanding, exception existed** → close it. `exceptionReason` is retained on
|
|
* purpose: the Resolved tab shows what was resolved, and clearing it would erase the record of
|
|
* why the exception was ever raised.
|
|
* - **Nothing outstanding, no exception** → leave every flag off, so a clean client never appears
|
|
* in either tab.
|
|
*/
|
|
export function deriveKycExceptionFields(input: KycExceptionInput): KycExceptionFields {
|
|
const outstanding: string[] = [];
|
|
|
|
// A declaration settles the PAN item without the API ever passing — see `panDeclarationTaken`.
|
|
if (!input.panVerified && !input.panDeclarationTaken) {
|
|
outstanding.push(KYC_EXCEPTION_PAN);
|
|
}
|
|
// An enrolment receipt settles the Aadhaar item when the OTP check has nothing to run against —
|
|
// see `aadhaarReceiptTaken`.
|
|
if (input.aadhaarApplicable && !input.aadhaarVerified && !input.aadhaarReceiptTaken) {
|
|
outstanding.push(KYC_EXCEPTION_AADHAAR);
|
|
}
|
|
|
|
// Officer-added reasons ride along, but the two automatic ones are filtered out first — a stale
|
|
// 'PAN Card Verification Failed' left in the form must not survive PAN actually being verified.
|
|
for (const reason of input.manualReasons || []) {
|
|
const trimmed = (reason || '').trim();
|
|
if (trimmed && !AUTO_MANAGED_KYC_EXCEPTION_REASONS.includes(trimmed) && !outstanding.includes(trimmed)) {
|
|
outstanding.push(trimmed);
|
|
}
|
|
}
|
|
|
|
if (outstanding.length > 0) {
|
|
return {
|
|
isException: true,
|
|
exceptionReason: outstanding.join(', '),
|
|
exceptionResolved: false,
|
|
exceptionResolvedOn: null
|
|
};
|
|
}
|
|
|
|
if (input.hadException) {
|
|
return {
|
|
isException: false,
|
|
exceptionReason: input.previousReason || null,
|
|
exceptionResolved: true,
|
|
exceptionResolvedOn: input.todayFormatted
|
|
};
|
|
}
|
|
|
|
return {
|
|
isException: false,
|
|
exceptionReason: null,
|
|
exceptionResolved: false,
|
|
exceptionResolvedOn: null
|
|
};
|
|
}
|