/** * 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 }; }