/** * 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 { deriveKycExceptionFields, KycExceptionInput, KYC_EXCEPTION_AADHAAR, KYC_EXCEPTION_PAN } from './kyc-exception.util'; const TODAY = '05 August 2026'; /** Individual client, nothing previously recorded — the client-creation case. */ const newIndividual = (panVerified: boolean, aadhaarVerified: boolean): KycExceptionInput => ({ panVerified, aadhaarVerified, aadhaarApplicable: true, hadException: false, previousReason: null, todayFormatted: TODAY }); /** Individual client that already carries an open exception. */ const existingException = ( panVerified: boolean, aadhaarVerified: boolean, previousReason: string ): KycExceptionInput => ({ panVerified, aadhaarVerified, aadhaarApplicable: true, hadException: true, previousReason, todayFormatted: TODAY }); describe('deriveKycExceptionFields', () => { describe('at client creation', () => { it('marks both PAN and Aadhaar when neither is entered or verified', () => { expect(deriveKycExceptionFields(newIndividual(false, false))).toEqual({ isException: true, exceptionReason: `${KYC_EXCEPTION_PAN}, ${KYC_EXCEPTION_AADHAAR}`, exceptionResolved: false, exceptionResolvedOn: null }); }); it('marks only PAN when Aadhaar verified but PAN did not', () => { expect(deriveKycExceptionFields(newIndividual(false, true))).toEqual({ isException: true, exceptionReason: KYC_EXCEPTION_PAN, exceptionResolved: false, exceptionResolvedOn: null }); }); it('marks only Aadhaar when PAN verified but Aadhaar did not', () => { expect(deriveKycExceptionFields(newIndividual(true, false))).toEqual({ isException: true, exceptionReason: KYC_EXCEPTION_AADHAAR, exceptionResolved: false, exceptionResolvedOn: null }); }); it('raises nothing when both verify, and does not mark the client resolved', () => { // A clean client never had an exception, so it must not appear in the Resolved tab either. expect(deriveKycExceptionFields(newIndividual(true, true))).toEqual({ isException: false, exceptionReason: null, exceptionResolved: false, exceptionResolvedOn: null }); }); it('does not raise an Aadhaar exception for a non-individual client', () => { // Companies submit a dummy masked Aadhaar; checking it would mean a permanent exception. expect( deriveKycExceptionFields({ panVerified: true, aadhaarVerified: false, aadhaarApplicable: false, hadException: false, previousReason: null, todayFormatted: TODAY }).isException ).toBe(false); }); }); describe('resolving', () => { it('closes the exception once both checks pass, keeping the reason for the audit trail', () => { const both = `${KYC_EXCEPTION_PAN}, ${KYC_EXCEPTION_AADHAAR}`; expect(deriveKycExceptionFields(existingException(true, true, both))).toEqual({ isException: false, exceptionReason: both, exceptionResolved: true, exceptionResolvedOn: TODAY }); }); it('narrows to the outstanding item when only one of two verifies', () => { const both = `${KYC_EXCEPTION_PAN}, ${KYC_EXCEPTION_AADHAAR}`; // PAN verified, Aadhaar still outstanding — stays open, listing Aadhaar alone. expect(deriveKycExceptionFields(existingException(true, false, both))).toEqual({ isException: true, exceptionReason: KYC_EXCEPTION_AADHAAR, exceptionResolved: false, exceptionResolvedOn: null }); }); it('narrows the other way round too', () => { const both = `${KYC_EXCEPTION_PAN}, ${KYC_EXCEPTION_AADHAAR}`; expect(deriveKycExceptionFields(existingException(false, true, both)).exceptionReason).toBe(KYC_EXCEPTION_PAN); }); }); describe('re-opening', () => { it('clears a prior resolution so a new exception returns to Pending', () => { // Regression: without exceptionResolved=false the record still matched the Resolved query // and a brand-new exception appeared as already closed. const result = deriveKycExceptionFields({ panVerified: false, aadhaarVerified: true, aadhaarApplicable: true, hadException: false, previousReason: 'PAN Card Verification Failed', todayFormatted: TODAY }); expect(result.exceptionResolved).toBe(false); expect(result.exceptionResolvedOn).toBeNull(); expect(result.isException).toBe(true); }); }); describe('officer-entered reasons', () => { it('keeps a manual reason alongside the automatic ones', () => { const result = deriveKycExceptionFields({ ...newIndividual(true, true), manualReasons: ['Address proof pending'] }); expect(result).toEqual({ isException: true, exceptionReason: 'Address proof pending', exceptionResolved: false, exceptionResolvedOn: null }); }); it('drops a stale automatic reason left in the form once the check passes', () => { // The form still holds 'PAN Card Verification Failed' from a previous load; PAN has since // verified, so it must not survive into the payload. const result = deriveKycExceptionFields({ ...newIndividual(true, true), manualReasons: [KYC_EXCEPTION_PAN] }); expect(result.isException).toBe(false); expect(result.exceptionReason).toBeNull(); }); it('does not duplicate a manual reason that matches an outstanding automatic one', () => { const result = deriveKycExceptionFields({ ...newIndividual(false, true), manualReasons: [KYC_EXCEPTION_PAN] }); expect(result.exceptionReason).toBe(KYC_EXCEPTION_PAN); }); }); });