import type { DepositAccount, DepositInterestPayoutType, DepositStatus, } from '../../../types/app'; const MS_PER_DAY = 1000 * 60 * 60 * 24; const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); export const calculateFixedDepositMaturity = ( principal: number, annualRate: number, tenureMonths: number, payoutType: DepositInterestPayoutType = 'cumulative', ) => { if (principal <= 0 || annualRate <= 0 || tenureMonths <= 0) { return 0; } const years = tenureMonths / 12; if (payoutType === 'non-cumulative') { return principal + principal * (annualRate / 100) * years; } const quarterlyRate = annualRate / 100 / 4; const periods = years * 4; return principal * Math.pow(1 + quarterlyRate, periods); }; export const calculateRecurringDepositMaturity = ( monthlyInstallment: number, annualRate: number, tenureMonths: number, ) => { if (monthlyInstallment <= 0 || annualRate <= 0 || tenureMonths <= 0) { return 0; } const monthlyRate = annualRate / 100 / 12; let maturityAmount = 0; for (let month = 0; month < tenureMonths; month += 1) { maturityAmount += monthlyInstallment * Math.pow(1 + monthlyRate, tenureMonths - month); } return maturityAmount; }; export const getDepositCanonicalStatus = ( deposit: Pick, now = new Date(), ): DepositStatus => { if (deposit.status === 'closed') { return 'closed'; } return new Date(deposit.maturityDate).getTime() <= now.getTime() ? 'matured' : 'active'; }; export const getDaysUntilMaturity = (maturityDate: string, now = new Date()) => { const diff = new Date(maturityDate).getTime() - now.getTime(); return Math.ceil(diff / MS_PER_DAY); }; export const isDepositNearMaturity = ( deposit: Pick, now = new Date(), ) => { const daysUntilMaturity = getDaysUntilMaturity(deposit.maturityDate, now); return getDepositCanonicalStatus(deposit, now) === 'active' && daysUntilMaturity <= 7; }; export const getDepositProgress = ( startDate: string, maturityDate: string, now = new Date(), ) => { const start = new Date(startDate).getTime(); const maturity = new Date(maturityDate).getTime(); const current = now.getTime(); if (maturity <= start) { return 1; } return clamp((current - start) / (maturity - start), 0, 1); }; export const calculatePrematureClosureAmount = ( deposit: Pick< DepositAccount, 'kind' | 'principalAmount' | 'monthlyInstallment' | 'interestRate' | 'startDate' | 'maturityDate' | 'maturityAmount' | 'prematurePenaltyRate' | 'status' >, now = new Date(), ) => { const canonicalStatus = getDepositCanonicalStatus(deposit, now); if (canonicalStatus === 'closed') { return { penaltyAmount: 0, netAmount: deposit.maturityAmount, }; } if (canonicalStatus === 'matured') { return { penaltyAmount: 0, netAmount: deposit.maturityAmount, }; } const elapsedMonths = Math.max( 1, Math.round((now.getTime() - new Date(deposit.startDate).getTime()) / (MS_PER_DAY * 30)), ); const currentValue = deposit.kind === 'fd' ? calculateFixedDepositMaturity( deposit.principalAmount, deposit.interestRate, elapsedMonths, 'non-cumulative', ) : calculateRecurringDepositMaturity( deposit.monthlyInstallment ?? 0, deposit.interestRate, elapsedMonths, ); const investedBase = deposit.kind === 'fd' ? deposit.principalAmount : (deposit.monthlyInstallment ?? 0) * elapsedMonths; const penaltyAmount = currentValue * ((deposit.prematurePenaltyRate ?? 0) / 100); return { penaltyAmount, netAmount: Math.max(currentValue - penaltyAmount, investedBase * 0.9), }; };