113 lines
4.2 KiB
TypeScript
113 lines
4.2 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;
|
|
/** Aadhaar verified against UIDAI. Unverified OR simply not entered → exception. */
|
|
aadhaarVerified: 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[] = [];
|
|
|
|
if (!input.panVerified) {
|
|
outstanding.push(KYC_EXCEPTION_PAN);
|
|
}
|
|
if (input.aadhaarApplicable && !input.aadhaarVerified) {
|
|
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
|
|
};
|
|
}
|