All files / types loan.ts

65.51% Statements 19/29
25.8% Branches 8/31
40% Functions 2/5
69.23% Lines 18/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158                                                                                                                                                                                        2x                                           2x           2x 5x     2x 2x     2x           2x                 2x 1x   1x 1x 1x     2x 2x 2x 2x 2x    
import {formatDateOnly} from '../utils/dates';
 
export type LoanStatus =
	'requested'
	| 'under_review'
	| 'pending_acceptance'
	| 'approved'
	| 'denied'
	| 'active'
	| 'disbursed'
	| 'paid_off'
	| 'defaulted'
	| 'closed';
 
export interface Loan {
	loanId: number;
	accountId: number;
	investorId: number;
	investorName?: string;
	investorEmail?: string;
	accountNumber?: string;
	accountBalance?: string;
	loanType: string;
	requestedAmount?: string;
	requestedTermMonths?: number;
	principleAmount?: string;
	outstandingBalance?: string;
	interestRate?: string;
	servicingFeeRate?: string;
	termMonths?: number;
	monthlyPayment?: string;
	totalInterest?: string;
	totalServicing?: string;
	totalRepayment?: string;
	startDate?: string;
	maturityDate?: string;
	nextPaymentDue?: string;
	status: LoanStatus;
	requestedAt?: string;
	reviewedAt?: string;
	activatedAt?: string;
	acceptedAt?: string;
	declinedAt?: string;
	declineReason?: string;
	denialReason?: string;
	approvalNotes?: string;
}
 
export interface LoanEligibility {
	eligible: boolean;
	allowMultipleLoans: boolean;
	reason: string;
	maxLoanAmount: string;
	currentBalance: string;
	ltvPercentage: string;
	minRequiredBalance: string;
}
 
export interface PaymentScheduleItem {
	scheduleId: number;
	loanId: number;
	paymentNumber: number;
	dueDate: string;
	expectedAmount: string;
	principalPortion: string;
	interestPortion: string;
	servicingPortion?: string;
	isPaid: boolean;
	paidDate?: string;
}
 
export interface CheckEligibilityResponse {
	success: boolean;
	data: { eligibility: LoanEligibility; availableTerms: number[]; defaultInterestRate: string; servicingFeeRate: string };
}
 
export interface LoanResponse {
	success: boolean;
	message?: string;
	data: { loan: Loan };
}
 
export interface LoansResponse {
	success: boolean;
	data: { loans: Loan[] };
}
 
export interface PendingLoansResponse {
	success: boolean;
	data: { loans: Loan[]; config: { defaultInterestRate: string; servicingFeeRate: string; ltvPercentage: string }; availableTerms: number[] };
}
 
export const getLoanStatusDisplay = (status: LoanStatus): {
	label: string;
	color: 'warning' | 'info' | 'success' | 'error' | 'primary' | 'default'
} => {
	const map: Record<LoanStatus, {
		label: string;
		color: 'warning' | 'info' | 'success' | 'error' | 'primary' | 'default'
	}> = {
		requested: {label: 'Pending Review', color: 'warning'},
		under_review: {label: 'Under Review', color: 'info'},
		pending_acceptance: {label: 'Awaiting Your Acceptance', color: 'warning'},
		approved: {label: 'Approved', color: 'success'},
		denied: {label: 'Denied', color: 'error'},
		active: {label: 'Active', color: 'primary'},
		disbursed: {label: 'Disbursed', color: 'primary'},
		paid_off: {label: 'Paid Off', color: 'success'},
		defaulted: {label: 'Defaulted', color: 'error'},
		closed: {label: 'Closed', color: 'default'}
	};
	return map[status] ?? {label: status, color: 'default'};
};
 
export const formatCurrency = (amount: string | number | undefined | null): string => {
	if (amount === undefined || amount === null || amount === '') return '$0.00';
	const num = typeof amount === 'string' ? parseFloat(amount) : amount;
	return isNaN(num) ? '$0.00' : new Intl.NumberFormat('en-US', {style: 'currency', currency: 'USD'}).format(num);
};
 
export const formatDate = (dateString: string | undefined | null): string => {
	if (!dateString) return '-';
	// Delegate to the timezone-safe helper (FSC-82): a SQL DATE like
	// "2026-06-02" must not render as the previous day west of UTC.
	const formatted = formatDateOnly(dateString, {year: 'numeric', month: 'short', day: 'numeric'});
	return formatted === 'Invalid Date' ? '-' : formatted;
};
 
export const formatPercent = (rate: string | number | undefined | null): string => {
	if (rate === undefined || rate === null || rate === '') return '-';
	const num = typeof rate === 'string' ? parseFloat(rate) : rate;
	return isNaN(num) ? '-' : `${num.toFixed(2)}%`;
};
 
export const estimateLoanSummary = (
	principal: number,
	annualRate: number,
	termMonths: number,
	servicingRate = 0
) => {
	// Standard amortization: fixed payment, interest on the declining balance.
	// Servicing (FSC-51) is folded in by amortizing at the combined rate; its
	// share of the finance charge is servicingRate / combinedRate.
	if (termMonths <= 0) {
		return {monthlyPayment: 0, totalInterest: 0, totalServicing: 0, totalRepayment: principal};
	}
	const combined = annualRate + servicingRate;
	const r = combined / 100 / 12;
	const monthlyPayment = r === 0
		? Math.round((principal / termMonths) * 100) / 100
		: Math.round((principal * (r * Math.pow(1 + r, termMonths)) / (Math.pow(1 + r, termMonths) - 1)) * 100) / 100;
	const totalRepayment = Math.round(monthlyPayment * termMonths * 100) / 100;
	const financeCharge = totalRepayment - principal;
	const totalServicing = combined > 0 ? Math.round(financeCharge * (servicingRate / combined) * 100) / 100 : 0;
	const totalInterest = Math.round((financeCharge - totalServicing) * 100) / 100;
	return {monthlyPayment, totalInterest, totalServicing, totalRepayment};
};