176 lines
6.0 KiB
TypeScript
176 lines
6.0 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/.
|
|
*/
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|