/** * 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/. */ /** * Single source of truth for every age-derived client classification. * * These rules were previously re-implemented as bare literals in ten separate components (the client * stepper plus each product's account-details step), which is how a client could end up classified one * way on the client screen and another way on the account screen -- and, in the FD case, how a * 24-year-old ended up with a senior-citizen interest rate. Anything that needs to know whether a * client is a minor or a senior citizen must derive it from here rather than comparing ages inline. */ /** Below this age a client is a minor: guardian mapping required, restricted KYC/OVD document set. */ export const MINOR_AGE_THRESHOLD = 10; /** At or above this age a client qualifies for senior-citizen benefits (higher deposit rates). */ export const SENIOR_CITIZEN_AGE_THRESHOLD = 60; /** * Age at which a minor account must be converted to a regular one. Deliberately independent of * {@link MINOR_AGE_THRESHOLD}: legal majority in India is 18 regardless of the age at which this CBS * chooses to stop applying minor-specific onboarding rules. */ export const AGE_OF_MAJORITY = 18; /** Parses the backend's date-of-birth representation -- either a `[yyyy, m, d]` array or a date string. */ export function parseClientDob(dob: any): Date | null { if (!dob) return null; if (Array.isArray(dob) && dob.length >= 3) { return new Date(dob[0], dob[1] - 1, dob[2]); } const parsed = new Date(dob); return isNaN(parsed.getTime()) ? null : parsed; } /** * Completed years between `dob` and `asOf`. Pass the tenant's business date as `asOf` wherever it is * available, so a back-dated or forward-dated business date classifies the client the same way the * rest of the system does. */ export function calculateAge(dob: Date, asOf?: Date | null): number { const today = asOf ?? new Date(); let age = today.getFullYear() - dob.getFullYear(); const monthDiff = today.getMonth() - dob.getMonth(); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) { age--; } return age; } /** True when the age qualifies as a minor. `null` (unknown DOB) is never a minor. */ export function isMinorAge(age: number | null | undefined): boolean { return age !== null && age !== undefined && age < MINOR_AGE_THRESHOLD; } /** True when the age qualifies for senior-citizen benefits. `null` (unknown DOB) never qualifies. */ export function isSeniorCitizenAge(age: number | null | undefined): boolean { return age !== null && age !== undefined && age >= SENIOR_CITIZEN_AGE_THRESHOLD; } /** * Whether a client record represents a natural person. Only a natural person can be a minor or a * senior citizen -- an entity/company has no age, so age-derived benefits must never apply to one * even if some incorporation date happens to be stored against it. */ export function isIndividualClient(client: any): boolean { const legalForm = client?.legalForm; if (!legalForm) return true; // absent legal form: treat as individual, matching the backend default const code = String(legalForm.code ?? '').toLowerCase(); if (code) return code.includes('individual') && !code.includes('nonindividual'); const value = String(legalForm.value ?? legalForm.name ?? '').toLowerCase(); return value === 'individual'; } /** * The senior-citizen decision for a whole client record: individual, known DOB, and old enough. * Returns the derived age alongside the verdict so callers can show "why" without recomputing it. */ export function deriveSeniorCitizenStatus( client: any, asOf?: Date | null ): { isSeniorCitizen: boolean; age: number | null; isIndividual: boolean } { const isIndividual = isIndividualClient(client); const dob = parseClientDob(client?.dateOfBirth); const age = dob ? calculateAge(dob, asOf) : null; return { isSeniorCitizen: isIndividual && isSeniorCitizenAge(age), age, isIndividual }; }