Files
cbsnexora/src/app/clients/kyc-document-renewal.util.ts
T
keshav 7b895a0e2b
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
feat: add KYC document renewal alerts, Aadhaar/PAN exceptions, and AML recurring alerts UI; remove old journal AML marking
2026-08-11 11:11:18 +05:30

153 lines
5.9 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/.
*/
/**
* Decides when the officially valid document (OVD) captured at KYC needs updating.
*
* The KYC step records a document type, its number, an issue date and — for documents that carry
* one — an expiry date. Nothing has ever read those dates back, so a client onboarded on a
* ten-year-old driving licence looked exactly like one onboarded yesterday. This module turns the
* two dates into a single answer to "does this document need updating?", so the KYC tab can say so
* rather than leaving staff to work it out from a date on a form.
*
* Two separate clocks run, and they warn differently because they mean different things:
*
* - **Age.** Five years after issue the document is stale and must be re-collected. It is still a
* valid document, so there is nothing to pre-empt — the alert appears once the date passes.
* - **Expiry.** A printed expiry date is a hard stop: the day after, the document is not a valid
* OVD at all and the account is out of compliance. That one is worth heading off, so it warns
* seven days ahead while the customer can still be asked to bring the renewed document in.
*/
/** A document is treated as due for refresh five years after it was issued. */
export const KYC_DOCUMENT_RENEWAL_YEARS = 5;
/** How many days before a printed expiry date the "expires soon" warning starts. */
export const KYC_DOCUMENT_EXPIRY_WARNING_DAYS = 7;
export type KycDocumentRenewalState =
/** No dates captured — nothing can be said about this document. */
| 'UNKNOWN'
/** Within its life, and not close to either threshold. */
| 'OK'
/** Printed expiry date falls within the next seven days. */
| 'EXPIRING_SOON'
/** Past its printed expiry date: no longer a valid OVD. */
| 'EXPIRED'
/** Past the five-year mark: still a valid document, but must be re-collected. */
| 'DUE';
/** Which of the two clocks decided the outcome. */
export type KycDocumentRenewalReason = 'EXPIRY' | 'AGE' | null;
export interface KycDocumentRenewalInput {
/** Issue date printed on the document. */
issueDate: Date | null;
/** Expiry date printed on the document — only some carry one (passport, DL, visa). */
expiryDate: Date | null;
/** Today, injected so the calculation is testable and matches the tenant's date. */
today: Date;
}
export interface KycDocumentRenewalStatus {
state: KycDocumentRenewalState;
/** The date the document falls due — the earlier of its expiry and its fifth anniversary. */
dueOn: Date | null;
/** Days until `dueOn`; negative once it has passed. Null when nothing can be judged. */
daysRemaining: number | null;
reason: KycDocumentRenewalReason;
}
/** Midnight-aligned, so a document due today is not counted as a fraction of a day overdue. */
function startOfDay(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
/**
* Adds whole years, clamping to the end of the month.
*
* A 29 February issue date has no fifth anniversary. Letting the Date constructor roll it into
* 1 March would push the document into the wrong month, so it is pinned to 28 February instead.
*/
function addYears(date: Date, years: number): Date {
const target = new Date(date.getFullYear() + years, date.getMonth(), date.getDate());
if (target.getMonth() !== date.getMonth()) {
return new Date(target.getFullYear(), target.getMonth(), 0);
}
return target;
}
const MS_PER_DAY = 24 * 60 * 60 * 1000;
function daysBetween(from: Date, to: Date): number {
return Math.round((to.getTime() - from.getTime()) / MS_PER_DAY);
}
/**
* Works out whether the captured document needs updating.
*
* The two dates are not alternatives — whichever falls first governs. A passport issued last year
* but expiring in six months is due in six months, not in four years: treating the five-year rule
* as the only clock would leave an expired passport on file looking current. A document with no
* printed expiry is judged purely on age.
*/
export function deriveKycDocumentRenewal(input: KycDocumentRenewalInput): KycDocumentRenewalStatus {
const today = startOfDay(input.today);
const issue = input.issueDate ? startOfDay(input.issueDate) : null;
const expiry = input.expiryDate ? startOfDay(input.expiryDate) : null;
if (!issue && !expiry) {
return { state: 'UNKNOWN', dueOn: null, daysRemaining: null, reason: null };
}
const ageDue = issue ? addYears(issue, KYC_DOCUMENT_RENEWAL_YEARS) : null;
// Whichever comes first governs — see the doc comment above.
let dueOn: Date;
let reason: KycDocumentRenewalReason;
if (expiry && ageDue) {
if (expiry.getTime() <= ageDue.getTime()) {
dueOn = expiry;
reason = 'EXPIRY';
} else {
dueOn = ageDue;
reason = 'AGE';
}
} else if (expiry) {
dueOn = expiry;
reason = 'EXPIRY';
} else {
dueOn = ageDue as Date;
reason = 'AGE';
}
const daysRemaining = daysBetween(today, dueOn);
let state: KycDocumentRenewalState;
if (reason === 'EXPIRY') {
if (daysRemaining < 0) {
state = 'EXPIRED';
} else if (daysRemaining <= KYC_DOCUMENT_EXPIRY_WARNING_DAYS) {
state = 'EXPIRING_SOON';
} else {
state = 'OK';
}
} else {
// Age alone: a document one day short of five years is entirely fine, so there is nothing to
// warn about in advance. It becomes due the moment the anniversary passes.
state = daysRemaining < 0 ? 'DUE' : 'OK';
}
return { state, dueOn, daysRemaining, reason };
}
/** Whether this state warrants surfacing an alert to staff at all. */
export function kycDocumentNeedsAttention(state: KycDocumentRenewalState): boolean {
return state === 'EXPIRING_SOON' || state === 'EXPIRED' || state === 'DUE';
}