148 lines
3.8 KiB
TypeScript
148 lines
3.8 KiB
TypeScript
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<DepositAccount, 'status' | 'maturityDate'>,
|
|
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<DepositAccount, 'status' | 'maturityDate'>,
|
|
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),
|
|
};
|
|
};
|