gemcap-be-common 1.2.140 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/classes/bank-transaction-item.d.ts +17 -0
- package/classes/bank-transaction-item.js +64 -0
- package/classes/bank-transaction-item.ts +66 -0
- package/classes/bank-uploaded-transaction.d.ts +17 -0
- package/classes/bank-uploaded-transaction.js +35 -0
- package/classes/bank-uploaded-transaction.ts +35 -0
- package/classes/inventory-item.d.ts +41 -0
- package/classes/inventory-item.js +44 -0
- package/classes/inventory-item.ts +63 -0
- package/classes/payable-account-item.d.ts +22 -0
- package/classes/payable-account-item.js +27 -0
- package/classes/payable-account-item.ts +35 -0
- package/classes/quickbook-item.d.ts +37 -0
- package/classes/quickbook-item.js +51 -0
- package/classes/quickbook-item.ts +59 -0
- package/classes/receivable-item.d.ts +26 -0
- package/classes/receivable-item.js +28 -0
- package/classes/receivable-item.ts +38 -0
- package/constants/date-formats.contsants.d.ts +1 -0
- package/constants/date-formats.contsants.js +4 -0
- package/constants/date-formats.contsants.ts +1 -0
- package/db/brokers.db.d.ts +185 -0
- package/db/brokers.db.js +35 -2
- package/db/brokers.db.ts +34 -1
- package/db/collateral-adjustments.db.d.ts +34 -0
- package/db/collateral-adjustments.db.js +52 -0
- package/db/collateral-adjustments.db.ts +54 -0
- package/db/collaterals.db.d.ts +1 -1
- package/db/equipment.db.d.ts +40 -0
- package/db/equipment.db.js +55 -0
- package/db/equipment.db.ts +56 -0
- package/db/financial-spreading.db.ts +2 -1
- package/db/groups.d.ts +5 -0
- package/db/groups.js +57 -0
- package/db/groups.ts +52 -0
- package/db/inventories.d.ts +91 -0
- package/db/inventories.js +449 -0
- package/db/inventories.ts +481 -0
- package/db/inventory-availability.d.ts +3 -0
- package/db/inventory-availability.js +103 -0
- package/db/inventory-availability.ts +113 -0
- package/db/new-summary.d.ts +31 -0
- package/db/new-summary.js +1295 -0
- package/db/new-summary.ts +1509 -0
- package/db/payable-accounts.d.ts +30 -0
- package/db/payable-accounts.js +55 -0
- package/db/payable-accounts.ts +50 -0
- package/db/reserve.db.d.ts +34 -0
- package/db/reserve.db.js +52 -0
- package/db/reserve.db.ts +48 -0
- package/db/uploads.db.d.ts +2 -0
- package/db/uploads.db.js +29 -0
- package/db/uploads.db.ts +24 -0
- package/helpers/main.helper.d.ts +31 -0
- package/helpers/main.helper.js +63 -0
- package/helpers/main.helper.ts +63 -0
- package/models/AccountPayableItem.model.d.ts +6 -6
- package/models/AllocatedBankTransaction.model.d.ts +54 -0
- package/models/AllocatedBankTransaction.model.js +70 -0
- package/models/AllocatedBankTransaction.model.ts +94 -0
- package/models/AllocatedData.model.d.ts +33 -0
- package/models/AllocatedData.model.js +19 -0
- package/models/AllocatedData.model.ts +24 -0
- package/models/BBCDate.model.d.ts +3 -3
- package/models/BBCSheet.model.d.ts +3 -3
- package/models/Banks.model.d.ts +3 -3
- package/models/Borrower.model.d.ts +3 -3
- package/models/BorrowerData.model.d.ts +3 -3
- package/models/BorrowerDataInsurance.model.d.ts +3 -3
- package/models/BorrowerDataTerm.model.d.ts +3 -3
- package/models/BorrowerSummary.model.js +1 -1
- package/models/BorrowerSummary.model.ts +1 -1
- package/models/CalandarDay.model.d.ts +40 -0
- package/models/CalandarDay.model.js +47 -0
- package/models/CalandarDay.model.ts +61 -0
- package/models/CashAllocationProduct.model.d.ts +119 -0
- package/models/CashAllocationProduct.model.js +102 -0
- package/models/CashAllocationProduct.model.ts +112 -0
- package/models/CashAllocationReference.model.d.ts +37 -0
- package/models/CashAllocationReference.model.js +27 -0
- package/models/CashAllocationReference.model.ts +40 -0
- package/models/CollateralAdjustment.model.d.ts +51 -0
- package/models/CollateralAdjustment.model.js +61 -0
- package/models/CollateralAdjustment.model.ts +98 -0
- package/models/Company.model.d.ts +35 -0
- package/models/Company.model.js +18 -0
- package/models/Company.model.ts +29 -0
- package/models/CustomerAPGroup.model.d.ts +32 -0
- package/models/CustomerAPGroup.model.js +24 -0
- package/models/CustomerAPGroup.model.ts +31 -0
- package/models/Equipment.model.d.ts +53 -0
- package/models/Equipment.model.js +140 -0
- package/models/Equipment.model.ts +172 -0
- package/models/FinancialCompliance.model.d.ts +39 -0
- package/models/FinancialCompliance.model.js +64 -0
- package/models/FinancialCompliance.model.ts +78 -0
- package/models/FinancialComplianceBorrower.model.d.ts +58 -0
- package/models/FinancialComplianceBorrower.model.js +82 -0
- package/models/FinancialComplianceBorrower.model.ts +118 -0
- package/models/FinancialIndexes.model.d.ts +36 -0
- package/models/FinancialIndexes.model.js +27 -0
- package/models/FinancialIndexes.model.ts +37 -0
- package/models/Inventory.model.d.ts +18 -18
- package/models/InventoryAvailability.model.d.ts +21 -21
- package/models/InventoryAvailabilityItem.model.d.ts +6 -6
- package/models/InventoryItem.model.d.ts +24 -24
- package/models/InventoryManualEntry.model.d.ts +9 -9
- package/models/InventorySeasonalRates.model.d.ts +3 -3
- package/models/LoanBroker.model.d.ts +3 -3
- package/models/LoanCharges.model.d.ts +12 -12
- package/models/LoanProducts.model.d.ts +9 -9
- package/models/LoanStatementStatus.model.d.ts +35 -0
- package/models/LoanStatementStatus.model.js +34 -0
- package/models/LoanStatementStatus.model.ts +45 -0
- package/models/LoanStatementTransaction.model.d.ts +9 -9
- package/models/LoanTransactionFile.model.d.ts +41 -0
- package/models/LoanTransactionFile.model.js +44 -0
- package/models/LoanTransactionFile.model.ts +61 -0
- package/models/MappedGroup.model.d.ts +37 -0
- package/models/MappedGroup.model.js +33 -0
- package/models/MappedGroup.model.ts +46 -0
- package/models/MonthEndData.Model.d.ts +41 -0
- package/models/MonthEndData.Model.js +42 -0
- package/models/MonthEndData.Model.ts +53 -0
- package/models/OrganizationEmails.model.d.ts +44 -0
- package/models/OrganizationEmails.model.js +40 -0
- package/models/OrganizationEmails.model.ts +54 -0
- package/models/ProductBroker.model.d.ts +9 -9
- package/models/QuickbooksAccount.model.d.ts +39 -0
- package/models/QuickbooksAccount.model.js +43 -0
- package/models/QuickbooksAccount.model.ts +57 -0
- package/models/Receivable.model.d.ts +12 -12
- package/models/ReceivableAvailability.model.d.ts +54 -54
- package/models/ReceivableAvailabilityItem.model.d.ts +57 -57
- package/models/ReceivableItem.model.d.ts +6 -6
- package/models/Reserve.model.d.ts +51 -0
- package/models/Reserve.model.js +96 -0
- package/models/Reserve.model.ts +125 -0
- package/models/TermLoan.model.d.ts +3 -3
- package/models/TermLoanCalculated.model.d.ts +6 -6
- package/models/TransactionAttachedFile.Model.d.ts +35 -0
- package/models/TransactionAttachedFile.Model.js +37 -0
- package/models/TransactionAttachedFile.Model.ts +48 -0
- package/models/UploadedBankTransaction.model.d.ts +56 -0
- package/models/UploadedBankTransaction.model.js +78 -0
- package/models/UploadedBankTransaction.model.ts +110 -0
- package/models/UploadedData.model.d.ts +36 -0
- package/models/UploadedData.model.js +23 -0
- package/models/UploadedData.model.ts +35 -0
- package/models/UploadedFile.model.d.ts +40 -0
- package/models/UploadedFile.model.js +41 -0
- package/models/UploadedFile.model.ts +57 -0
- package/models/UploadedSheet.model.d.ts +46 -0
- package/models/UploadedSheet.model.js +27 -0
- package/models/UploadedSheet.model.ts +51 -0
- package/package.json +10 -1
- package/repositories/globals.repository.d.ts +8 -0
- package/repositories/globals.repository.js +24 -0
- package/repositories/globals.repository.ts +21 -0
- package/services/attached-files.service.d.ts +57 -0
- package/services/attached-files.service.js +103 -0
- package/services/attached-files.service.ts +123 -0
- package/services/availability.service.d.ts +77 -0
- package/services/availability.service.js +897 -0
- package/services/availability.service.ts +1034 -0
- package/services/bank-uploaded-transactions.service.d.ts +33 -0
- package/services/bank-uploaded-transactions.service.js +430 -0
- package/services/bank-uploaded-transactions.service.ts +475 -0
- package/services/banks.service.d.ts +36 -0
- package/services/banks.service.js +91 -0
- package/services/banks.service.ts +95 -0
- package/services/borrower-summary.service.d.ts +35 -0
- package/services/borrower-summary.service.js +310 -0
- package/services/borrower-summary.service.ts +334 -0
- package/services/borrowers.service.d.ts +103 -0
- package/services/borrowers.service.js +268 -0
- package/services/borrowers.service.ts +302 -0
- package/services/brokers.service.d.ts +212 -0
- package/services/brokers.service.js +160 -0
- package/services/brokers.service.ts +200 -0
- package/services/calendar.service.d.ts +53 -0
- package/services/calendar.service.js +108 -0
- package/services/calendar.service.ts +128 -0
- package/services/cash-allocation.service.d.ts +40 -0
- package/services/cash-allocation.service.js +92 -0
- package/services/cash-allocation.service.ts +105 -0
- package/services/collateral-adjustments.service.d.ts +38 -0
- package/services/collateral-adjustments.service.js +82 -0
- package/services/collateral-adjustments.service.ts +95 -0
- package/services/collaterals.service.d.ts +69 -0
- package/services/collaterals.service.js +279 -0
- package/services/collaterals.service.ts +319 -0
- package/services/companies.service.d.ts +5 -0
- package/services/companies.service.js +21 -0
- package/services/companies.service.ts +23 -0
- package/services/compliance-borrowers.service.d.ts +152 -0
- package/services/compliance-borrowers.service.js +569 -0
- package/services/compliance-borrowers.service.ts +617 -0
- package/services/equipment.service.d.ts +42 -0
- package/services/equipment.service.js +120 -0
- package/services/equipment.service.ts +149 -0
- package/services/file-manager.service.d.ts +44 -0
- package/services/file-manager.service.js +120 -0
- package/services/file-manager.service.ts +146 -0
- package/services/financial-compliance.service.d.ts +58 -0
- package/services/financial-compliance.service.js +281 -0
- package/services/financial-compliance.service.ts +309 -0
- package/services/financial-indexes.service.d.ts +20 -0
- package/services/financial-indexes.service.js +241 -0
- package/services/financial-indexes.service.ts +257 -0
- package/services/financial-spreading.service.d.ts +74 -0
- package/services/financial-spreading.service.js +450 -0
- package/services/financial-spreading.service.ts +517 -0
- package/services/globals.service.d.ts +5 -0
- package/services/globals.service.js +11 -0
- package/services/globals.service.ts +8 -0
- package/services/groups.service.d.ts +39 -0
- package/services/groups.service.js +65 -0
- package/services/groups.service.ts +64 -0
- package/services/inventory-availability.service.d.ts +13 -0
- package/services/inventory-availability.service.js +170 -0
- package/services/inventory-availability.service.ts +187 -0
- package/services/inventory.service.d.ts +118 -0
- package/services/inventory.service.js +239 -0
- package/services/inventory.service.ts +276 -0
- package/services/loan-charges.service.d.ts +83 -0
- package/services/loan-charges.service.js +343 -0
- package/services/loan-charges.service.ts +396 -0
- package/services/loan-payments.service.d.ts +94 -0
- package/services/loan-payments.service.js +485 -0
- package/services/loan-payments.service.ts +541 -0
- package/services/loan-products.service.d.ts +12 -0
- package/services/loan-products.service.js +55 -0
- package/services/loan-products.service.ts +58 -0
- package/services/loan-statement-balance.service.d.ts +16 -0
- package/services/loan-statement-balance.service.js +106 -0
- package/services/loan-statement-balance.service.ts +113 -0
- package/services/loan-statement-effects.service.d.ts +8 -0
- package/services/loan-statement-effects.service.js +42 -0
- package/services/loan-statement-effects.service.ts +41 -0
- package/services/loan-statement-status.service.d.ts +208 -0
- package/services/loan-statement-status.service.js +159 -0
- package/services/loan-statement-status.service.ts +177 -0
- package/services/loan-statement.service.d.ts +186 -0
- package/services/loan-statement.service.js +935 -0
- package/services/loan-statement.service.ts +1040 -0
- package/services/loan-transactions.service.d.ts +169 -0
- package/services/loan-transactions.service.js +941 -0
- package/services/loan-transactions.service.ts +1042 -0
- package/services/lock.service.d.ts +6 -0
- package/services/lock.service.js +45 -0
- package/services/lock.service.ts +45 -0
- package/services/manual-entry.service.d.ts +20 -0
- package/services/manual-entry.service.js +186 -0
- package/services/manual-entry.service.ts +201 -0
- package/services/month-end-data.service.d.ts +34 -0
- package/services/month-end-data.service.js +30 -0
- package/services/month-end-data.service.ts +35 -0
- package/services/nodemailer.service.d.ts +96 -0
- package/services/nodemailer.service.js +689 -0
- package/services/nodemailer.service.ts +774 -0
- package/services/organization-emails.service.d.ts +31 -0
- package/services/organization-emails.service.js +10 -0
- package/services/organization-emails.service.ts +7 -0
- package/services/organizations.service.d.ts +34 -0
- package/services/organizations.service.js +74 -0
- package/services/organizations.service.ts +84 -0
- package/services/pdf.service.d.ts +61 -0
- package/services/pdf.service.js +547 -0
- package/services/pdf.service.ts +642 -0
- package/services/quickbooks.service.d.ts +99 -0
- package/services/quickbooks.service.js +640 -0
- package/services/quickbooks.service.ts +734 -0
- package/services/reports/investor-summary.service.d.ts +28 -0
- package/services/reports/investor-summary.service.js +136 -0
- package/services/reports/investor-summary.service.ts +159 -0
- package/services/reports.service.d.ts +126 -0
- package/services/reports.service.js +584 -0
- package/services/reports.service.ts +702 -0
- package/services/reserve.service.d.ts +37 -0
- package/services/reserve.service.js +76 -0
- package/services/reserve.service.ts +79 -0
- package/services/sentry.service.d.ts +11 -0
- package/services/sentry.service.js +49 -0
- package/services/sentry.service.ts +33 -0
- package/services/signs.service.d.ts +69 -0
- package/services/signs.service.js +230 -0
- package/services/signs.service.ts +260 -0
- package/services/term-loan.service.d.ts +30 -0
- package/services/term-loan.service.js +614 -0
- package/services/term-loan.service.ts +696 -0
- package/services/uploads.service.d.ts +134 -0
- package/services/uploads.service.js +587 -0
- package/services/uploads.service.ts +643 -0
- package/services/user-logs.service.d.ts +23 -0
- package/services/user-logs.service.js +160 -0
- package/services/user-logs.service.ts +177 -0
- package/services/users.service.d.ts +4 -4
- package/services/yield.service.d.ts +46 -0
- package/services/yield.service.js +42 -12
- package/services/yield.service.ts +38 -8
- package/tsconfig.json +5 -5
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import Decimal from 'decimal.js';
|
|
4
|
+
import mongoose from 'mongoose';
|
|
5
|
+
|
|
6
|
+
import { ELoanChargeType } from '../enums/loan-charge-type.enum';
|
|
7
|
+
import { ELoanTypes } from '../enums/loan-types.enum';
|
|
8
|
+
import { roundToXDigits } from '../helpers/numbers.helper';
|
|
9
|
+
import {
|
|
10
|
+
ITermLoanSettingsOnly,
|
|
11
|
+
TERM_LOAN_SETTINGS_FIELDS,
|
|
12
|
+
TermLoanSettingsModel,
|
|
13
|
+
} from '../models/TermLoanSettings.model';
|
|
14
|
+
import {
|
|
15
|
+
ETermLoanStatus,
|
|
16
|
+
ITermLoan,
|
|
17
|
+
ITermLoanView,
|
|
18
|
+
TERM_LOAN_FIELDS,
|
|
19
|
+
TermLoanModel,
|
|
20
|
+
} from '../models/TermLoan.model';
|
|
21
|
+
import {
|
|
22
|
+
ITermCalculatedDaily,
|
|
23
|
+
ITermLoanCalculated,
|
|
24
|
+
ITermLoanCalculatedDoc,
|
|
25
|
+
TermLoanCalculatedModel,
|
|
26
|
+
} from '../models/TermLoanCalculated.model';
|
|
27
|
+
import {
|
|
28
|
+
ILoanTransactionDoc,
|
|
29
|
+
} from '../models/LoanTransaction.model';
|
|
30
|
+
import { LoanProduct } from '../models/LoanProducts.model';
|
|
31
|
+
|
|
32
|
+
import { EFinancialIndex, FinancialIndexesService } from './financial-indexes.service';
|
|
33
|
+
import { LoanChargesService } from './loan-charges.service';
|
|
34
|
+
import { LoanStatementStatusService } from './loan-statement-status.service';
|
|
35
|
+
import { LoanTransactionsService } from './loan-transactions.service';
|
|
36
|
+
import { UploadsService } from './uploads.service';
|
|
37
|
+
|
|
38
|
+
const newEmptySettings: ITermLoanSettingsOnly = {
|
|
39
|
+
termMonths: 12,
|
|
40
|
+
amortizationProfileMonths: 12,
|
|
41
|
+
gracePeriodMonths: 1,
|
|
42
|
+
principalDueDay: 7,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export class TermLoanService {
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly financialIndexesService: FinancialIndexesService,
|
|
49
|
+
private readonly loanChargesService: LoanChargesService,
|
|
50
|
+
private readonly getLoanStatementStatusService: () => LoanStatementStatusService,
|
|
51
|
+
private readonly getLoanTransactionsService: () => LoanTransactionsService,
|
|
52
|
+
private readonly getUploadsService: () => UploadsService,
|
|
53
|
+
) {
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async createTermLoan(productId: string, actual: boolean) {
|
|
57
|
+
const product = await this.loanChargesService.getLoanProductById(productId);
|
|
58
|
+
const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
|
|
59
|
+
if (charges.length === 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const adminCharge = charges.find((charge) => charge.chargeType === ELoanChargeType.ADMIN_FEE);
|
|
63
|
+
const interestCharge = charges.find((charge) => charge.chargeType === ELoanChargeType.INTEREST_FEE);
|
|
64
|
+
if (!adminCharge || !interestCharge) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const primeRate = await this.financialIndexesService.getFinancialIndexValue(EFinancialIndex.PRIME_RATE);
|
|
69
|
+
|
|
70
|
+
const newEmptyTerm: ITermLoan = {
|
|
71
|
+
productId: new mongoose.Types.ObjectId(String(product._id)),
|
|
72
|
+
initialBalance: product.commitment,
|
|
73
|
+
adminRate: adminCharge.percent,
|
|
74
|
+
interestRate: interestCharge.percent,
|
|
75
|
+
loanStartDate: product.startDate,
|
|
76
|
+
primeRate: primeRate,
|
|
77
|
+
actual,
|
|
78
|
+
isMismatched: false,
|
|
79
|
+
calculationStatus: ETermLoanStatus.NEW,
|
|
80
|
+
lastCalculated: null,
|
|
81
|
+
};
|
|
82
|
+
return TermLoanModel.findOneAndUpdate({ productId, actual }, newEmptyTerm, { upsert: true, new: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async createTermLoanSettings(termLoanId: string) {
|
|
86
|
+
return TermLoanSettingsModel.findOneAndUpdate({ termLoanId }, { termLoanId, ...newEmptySettings }, {
|
|
87
|
+
upsert: true,
|
|
88
|
+
new: true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async getCreateTermLoan(productId: string, actual: boolean) {
|
|
93
|
+
if (!productId) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const foundTermLoan = await TermLoanModel.findOne({ productId, actual }).lean();
|
|
97
|
+
const termLoan = foundTermLoan ? foundTermLoan : await this.createTermLoan(productId, actual);
|
|
98
|
+
if (!termLoan) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const foundSettings = termLoan ? await TermLoanSettingsModel.findOne({ termLoanId: termLoan._id }).lean() : null;
|
|
103
|
+
const settings = foundSettings ? foundSettings : await this.createTermLoanSettings(String(termLoan._id));
|
|
104
|
+
|
|
105
|
+
const pureTermLoan = _.pick(termLoan as unknown as ITermLoan & { _id: mongoose.Types.ObjectId }, TERM_LOAN_FIELDS);
|
|
106
|
+
|
|
107
|
+
const termLoanFull = {
|
|
108
|
+
...pureTermLoan,
|
|
109
|
+
settings: _.pick(settings, TERM_LOAN_SETTINGS_FIELDS) as ITermLoanSettingsOnly,
|
|
110
|
+
};
|
|
111
|
+
return termLoanFull as ITermLoanView;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async getTermLoan(productId: string, actual: boolean): Promise<ITermLoanView & {
|
|
115
|
+
calculated: ITermLoanCalculatedDoc[]
|
|
116
|
+
}> {
|
|
117
|
+
const termLoan = await this.getCreateTermLoan(productId, actual);
|
|
118
|
+
if (!termLoan) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const calculatedTermLoan = await TermLoanCalculatedModel
|
|
122
|
+
.find({ termLoanId: termLoan._id })
|
|
123
|
+
.sort({ relevantStatement: 1, openingBalance: -1, monthlyPrincipal: -1 })
|
|
124
|
+
.lean();
|
|
125
|
+
const mappedCalculatedTermLoan = calculatedTermLoan.map((c) => {
|
|
126
|
+
const startDate = termLoan.loanStartDate;
|
|
127
|
+
const minStart = dayjs.max(dayjs(startDate), dayjs(c.relevantStatement).startOf('month'));
|
|
128
|
+
const period = `${minStart.format('MM/DD/YYYY')} - ${dayjs(c.relevantStatement).format('MM/DD/YYYY')}`;
|
|
129
|
+
return {
|
|
130
|
+
...c,
|
|
131
|
+
period,
|
|
132
|
+
} as unknown as (ITermLoanCalculatedDoc & { period: string });
|
|
133
|
+
});
|
|
134
|
+
return { ...termLoan, calculated: mappedCalculatedTermLoan };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async saveTermLoanSettings(productId: string, actual: boolean, settings: ITermLoanSettingsOnly): Promise<any> {
|
|
138
|
+
let termLoan = await TermLoanModel.findOne({ productId, actual }).lean();
|
|
139
|
+
if (!termLoan || termLoan.calculationStatus === ETermLoanStatus.NEW) {
|
|
140
|
+
termLoan = await this.createTermLoan(productId, actual);
|
|
141
|
+
}
|
|
142
|
+
const updatedSettings = await TermLoanSettingsModel.findOneAndUpdate({ termLoanId: termLoan._id }, settings, { new: true });
|
|
143
|
+
return { productId, settings: _.pick(updatedSettings, TERM_LOAN_SETTINGS_FIELDS) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async calculateAllTermLoans() {
|
|
147
|
+
const products = await LoanProduct.find({ type: ELoanTypes.TERM }).lean();
|
|
148
|
+
for (const product of products) {
|
|
149
|
+
await this.calculateTermLoan(product._id.toString(), true);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async setTermLoanExpected(productId: string, recalculate = false) {
|
|
154
|
+
const termLoan = await this.getCreateTermLoan(productId, true);
|
|
155
|
+
if (termLoan) {
|
|
156
|
+
await this.updateTermLoanStatus(termLoan._id.toString(), ETermLoanStatus.EXPECTED);
|
|
157
|
+
if (recalculate) {
|
|
158
|
+
await this.calculateTermLoan(productId, true);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async updateTermLoanStatus(termLoanId: string, calculationStatus: ETermLoanStatus, lastCalculated: Date = null) {
|
|
164
|
+
try {
|
|
165
|
+
await TermLoanModel.findByIdAndUpdate(termLoanId, { calculationStatus });
|
|
166
|
+
if (lastCalculated) {
|
|
167
|
+
await TermLoanModel.findByIdAndUpdate(termLoanId, { lastCalculated });
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.error(e);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async calculateTermLoan(productId: string, actual: boolean): Promise<any> {
|
|
175
|
+
const loanTransactionsService = this.getLoanTransactionsService();
|
|
176
|
+
let termLoan = await this.getCreateTermLoan(productId, actual);
|
|
177
|
+
if (!termLoan || termLoan.calculationStatus === ETermLoanStatus.NEW) {
|
|
178
|
+
await this.createTermLoan(productId, actual);
|
|
179
|
+
termLoan = await this.getCreateTermLoan(productId, actual);
|
|
180
|
+
}
|
|
181
|
+
if (!termLoan) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await this.updateTermLoanStatus(termLoan._id.toString(), ETermLoanStatus.CALCULATING);
|
|
186
|
+
|
|
187
|
+
const daysInYear = 360;
|
|
188
|
+
const initialTransaction = await loanTransactionsService.getLastTransactionForDate(productId, termLoan.loanStartDate);
|
|
189
|
+
|
|
190
|
+
const initialData = actual
|
|
191
|
+
? { date: termLoan.loanStartDate, amount: initialTransaction ? initialTransaction.balance : 0 }
|
|
192
|
+
: { date: termLoan.loanStartDate, amount: termLoan.initialBalance };
|
|
193
|
+
|
|
194
|
+
const initialBalance = actual ? initialData.amount : termLoan.initialBalance;
|
|
195
|
+
|
|
196
|
+
const startDate = dayjs(initialData.date);
|
|
197
|
+
await TermLoanModel.findByIdAndUpdate(termLoan._id, { loanStartDate: startDate, initialBalance: initialBalance });
|
|
198
|
+
const endGraceDate = dayjs(startDate).add(termLoan.settings.gracePeriodMonths, 'month');
|
|
199
|
+
const endTermMonths = dayjs(startDate).add(termLoan.settings.termMonths, 'month');
|
|
200
|
+
const endAmortizationDate = dayjs(startDate).add(termLoan.settings.amortizationProfileMonths, 'month');
|
|
201
|
+
|
|
202
|
+
const product = await this.loanChargesService.getLoanProductById(productId);
|
|
203
|
+
const minPercent = product.minPercent ?? 0;
|
|
204
|
+
const maxPercent = (product.maxPercent ?? 0) === 0 ? 1 : product.maxPercent;
|
|
205
|
+
|
|
206
|
+
const getDynamicInterestRate = async (date: Date): Promise<number> => {
|
|
207
|
+
const primeRate = await this.financialIndexesService.getFinancialIndexValue(EFinancialIndex.PRIME_RATE, date);
|
|
208
|
+
const interestRate = new Decimal(primeRate).add(termLoan.interestRate).toDP(4).toNumber();
|
|
209
|
+
return Math.min(Math.max(interestRate, minPercent), maxPercent);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
let currentDate = startDate;
|
|
213
|
+
|
|
214
|
+
const getMonthPayments = async (date: dayjs.Dayjs) => {
|
|
215
|
+
const transactions = await loanTransactionsService.getPaymentsForPeriod(productId, date.toDate(), date.endOf('month').toDate());
|
|
216
|
+
if (!initialTransaction) {
|
|
217
|
+
return transactions;
|
|
218
|
+
}
|
|
219
|
+
return transactions.filter((transaction) => {
|
|
220
|
+
return (dayjs(transaction.date).format('YYYY-MM-DD') !== dayjs(initialTransaction.date).format('YYYY-MM-DD'));
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const getMonthDisbursements = async (date: dayjs.Dayjs) => {
|
|
225
|
+
const transactions = await loanTransactionsService.getLastDisbursementTransactionForDate(productId, date.toDate(), date.endOf('month').toDate());
|
|
226
|
+
if (!initialTransaction) {
|
|
227
|
+
return transactions;
|
|
228
|
+
}
|
|
229
|
+
return transactions.filter((transaction) => {
|
|
230
|
+
return (dayjs(transaction.date).format('YYYY-MM-DD') !== dayjs(initialTransaction.date).format('YYYY-MM-DD'));
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const checkGracePeriodDate = (checkDate: dayjs.Dayjs) => {
|
|
235
|
+
return checkDate < endGraceDate.endOf('month');
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const interestRates = await this.financialIndexesService.getFinancialAllIndexes(EFinancialIndex.PRIME_RATE);
|
|
239
|
+
const transactions = await loanTransactionsService.getLoanTransactionsForProduct(productId);
|
|
240
|
+
|
|
241
|
+
const findLastIndexValue = (date: Date) => {
|
|
242
|
+
const found = interestRates.find((i) => i.date.getTime() <= date.getTime());
|
|
243
|
+
const primeRate = found ? found.value : 0;
|
|
244
|
+
const interestRate = new Decimal(primeRate).add(termLoan.interestRate).toDP(4).toNumber();
|
|
245
|
+
return Math.min(Math.max(interestRate, minPercent), maxPercent);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const findLastTransaction = (date: Date) => {
|
|
249
|
+
return transactions.find((i) => i.date.getTime() <= date.getTime());
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const getComplexEMI = async (termLoan: ITermLoanView) => {
|
|
253
|
+
const calculateBalance = async (termLoan: ITermLoanView, EMI: number) => {
|
|
254
|
+
console.log({ EMI });
|
|
255
|
+
let balance = initialData.amount;
|
|
256
|
+
let accumulatedMonthlyInterest = 0;
|
|
257
|
+
let lastMonthInterest = 0;
|
|
258
|
+
let EMICurrentDate = currentDate;
|
|
259
|
+
let firstPeriod = true;
|
|
260
|
+
const endAmortization = endAmortizationDate;
|
|
261
|
+
let monthPayments: ILoanTransactionDoc[] = [];
|
|
262
|
+
while (EMICurrentDate.startOf('day').isBefore(endAmortization.startOf('day')) || EMICurrentDate.startOf('day').isSame(endAmortization.startOf('day'))) {
|
|
263
|
+
|
|
264
|
+
const lastTransaction = findLastTransaction(EMICurrentDate.subtract(1, 'day').toDate());
|
|
265
|
+
if (actual && lastTransaction && dayjs(lastTransaction.date).format('YYYY-MM-DD') === EMICurrentDate.subtract(1, 'day').format('YYYY-MM-DD')) {
|
|
266
|
+
balance = lastTransaction.balance;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const interestRate = findLastIndexValue(EMICurrentDate.toDate());
|
|
270
|
+
const interestRateDaily = interestRate / daysInYear;
|
|
271
|
+
const dailyInterest = EMICurrentDate.startOf('day').isAfter(endAmortizationDate.startOf('day')) ? 0 : new Decimal(balance).mul(interestRateDaily).toDP(2).toNumber();
|
|
272
|
+
accumulatedMonthlyInterest = new Decimal(accumulatedMonthlyInterest).add(dailyInterest).toDP(2).toNumber();
|
|
273
|
+
|
|
274
|
+
if (termLoan.actual) {
|
|
275
|
+
if (
|
|
276
|
+
(EMICurrentDate.format('YYYY-MM-DD') === EMICurrentDate.startOf('month').format('YYYY-MM-DD')) ||
|
|
277
|
+
(EMICurrentDate.format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD'))
|
|
278
|
+
) {
|
|
279
|
+
monthPayments = await getMonthPayments(EMICurrentDate);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
(EMICurrentDate.format('YYYY-MM-DD') === EMICurrentDate.endOf('month').format('YYYY-MM-DD')) ||
|
|
285
|
+
(EMICurrentDate.format('YYYY-MM-DD') === endAmortizationDate.format('YYYY-MM-DD'))
|
|
286
|
+
) {
|
|
287
|
+
if (EMICurrentDate.format('YYYY-MM-DD') === endAmortizationDate.format('YYYY-MM-DD')) {
|
|
288
|
+
lastMonthInterest = lastMonthInterest + accumulatedMonthlyInterest;
|
|
289
|
+
} else {
|
|
290
|
+
lastMonthInterest = accumulatedMonthlyInterest;
|
|
291
|
+
}
|
|
292
|
+
accumulatedMonthlyInterest = 0;
|
|
293
|
+
firstPeriod = false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (monthPayments.length > 0) {
|
|
297
|
+
const principal = monthPayments
|
|
298
|
+
.filter((p) => dayjs(p.date).format('YYYY-MM-DD') === EMICurrentDate.format('YYYY-MM-DD'))
|
|
299
|
+
.reduce((acc, p) => acc + Math.abs(p.amount), 0);
|
|
300
|
+
if (principal > 0) {
|
|
301
|
+
balance = new Decimal(balance).sub(principal).toDP(2).toNumber();
|
|
302
|
+
lastMonthInterest = 0;
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
if ((EMICurrentDate.format('YYYY-MM-DD') === EMICurrentDate.date(termLoan.settings.principalDueDay).format('YYYY-MM-DD'))) {
|
|
306
|
+
const isGracePeriod = checkGracePeriodDate(EMICurrentDate);
|
|
307
|
+
const principal = (isGracePeriod || firstPeriod) ? 0 : new Decimal(EMI).sub(lastMonthInterest).toDP(2).toNumber();
|
|
308
|
+
balance = new Decimal(balance).sub(principal).toDP(2).toNumber();
|
|
309
|
+
lastMonthInterest = 0;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
EMICurrentDate = EMICurrentDate.add(1, 'day');
|
|
313
|
+
}
|
|
314
|
+
const principal = new Decimal(EMI).sub(lastMonthInterest).toDP(2).toNumber();
|
|
315
|
+
balance = new Decimal(balance).sub(principal).toDP(2).toNumber();
|
|
316
|
+
return balance;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
let lowerBound = 0;
|
|
320
|
+
let upperBound = initialBalance;
|
|
321
|
+
let optimalEMI = 0;
|
|
322
|
+
const tolerance = 0.01;
|
|
323
|
+
while (new Decimal(upperBound).sub(lowerBound).toDP(2).toNumber() > tolerance) {
|
|
324
|
+
const mid = new Decimal(lowerBound).add(upperBound).div(2).toDP(2).toNumber();
|
|
325
|
+
const currentBalance = await calculateBalance(termLoan, mid);
|
|
326
|
+
if (currentBalance < 0) {
|
|
327
|
+
upperBound = mid;
|
|
328
|
+
} else {
|
|
329
|
+
lowerBound = mid;
|
|
330
|
+
optimalEMI = mid;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return new Decimal(optimalEMI).add(tolerance).toDP(2).toNumber();
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const getNextPaymentDate = (date: dayjs.Dayjs) => {
|
|
337
|
+
let paymentDate = date.date(termLoan.settings.principalDueDay);
|
|
338
|
+
if (paymentDate < date) {
|
|
339
|
+
paymentDate = paymentDate.add(1, 'month').date(termLoan.settings.principalDueDay);
|
|
340
|
+
}
|
|
341
|
+
return paymentDate;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const complexEMI = await getComplexEMI(termLoan);
|
|
345
|
+
console.log({ complexEMI });
|
|
346
|
+
|
|
347
|
+
const getDaysInMonth = (date: dayjs.Dayjs) => {
|
|
348
|
+
const maxStartDate = dayjs.max(startDate, date.startOf('month')).startOf('day');
|
|
349
|
+
const maxEndDate = dayjs.min(endAmortizationDate, date.endOf('month')).startOf('day');
|
|
350
|
+
return maxEndDate.diff(maxStartDate, 'day') + 1;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
let calculatedDailyResults = [];
|
|
354
|
+
const calculatedMonthlyResults: ITermLoanCalculated[] = [];
|
|
355
|
+
|
|
356
|
+
let monthOpeningBalance = initialBalance;
|
|
357
|
+
let openingBalance = initialBalance;
|
|
358
|
+
let closingBalance = openingBalance;
|
|
359
|
+
|
|
360
|
+
let monthInterest = 0;
|
|
361
|
+
|
|
362
|
+
let lastMonthInterest = 0;
|
|
363
|
+
let lastMonthPrincipal = 0;
|
|
364
|
+
|
|
365
|
+
let dayAdminFee = 0;
|
|
366
|
+
|
|
367
|
+
let monthAdminFee = 0;
|
|
368
|
+
|
|
369
|
+
const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
|
|
370
|
+
const adminCharge = charges.find((charge) => charge.chargeType === ELoanChargeType.ADMIN_FEE);
|
|
371
|
+
const minAdminFee = adminCharge ? adminCharge.minimumAmount : 0;
|
|
372
|
+
|
|
373
|
+
let monthPayments: ILoanTransactionDoc[] = [];
|
|
374
|
+
let monthDisbursements: ILoanTransactionDoc[] = [];
|
|
375
|
+
let isLastPeriod = false;
|
|
376
|
+
|
|
377
|
+
while (
|
|
378
|
+
(currentDate.startOf('day').isBefore(endAmortizationDate.startOf('day')) || currentDate.startOf('day').isSame(endAmortizationDate.startOf('day'))) &&
|
|
379
|
+
!isLastPeriod
|
|
380
|
+
) {
|
|
381
|
+
if (
|
|
382
|
+
(currentDate.format('YYYY-MM-DD') === currentDate.startOf('month').format('YYYY-MM-DD')) ||
|
|
383
|
+
(currentDate.format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD'))
|
|
384
|
+
) {
|
|
385
|
+
if (actual) {
|
|
386
|
+
monthPayments = await getMonthPayments(currentDate.startOf('day'));
|
|
387
|
+
monthDisbursements = await getMonthDisbursements(currentDate.startOf('day'));
|
|
388
|
+
}
|
|
389
|
+
monthOpeningBalance = openingBalance;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const lastTransaction = await loanTransactionsService.getLastTransactionForDate(productId, currentDate.subtract(1, 'day').toDate());
|
|
393
|
+
if (actual && lastTransaction && dayjs(lastTransaction.date).format('YYYY-MM-DD') === currentDate.subtract(1, 'day').format('YYYY-MM-DD')) {
|
|
394
|
+
openingBalance = lastTransaction.balance;
|
|
395
|
+
}
|
|
396
|
+
if (currentDate.format('YYYY-MM-DD') === currentDate.date(termLoan.settings.principalDueDay).add(1, 'day').format('YYYY-MM-DD')) {
|
|
397
|
+
openingBalance = closingBalance;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
dayAdminFee = openingBalance * termLoan.adminRate / getDaysInMonth(currentDate);
|
|
401
|
+
monthAdminFee = monthAdminFee + dayAdminFee;
|
|
402
|
+
const interestRate = await getDynamicInterestRate(currentDate.toDate());
|
|
403
|
+
const interestRateDaily = interestRate / daysInYear;
|
|
404
|
+
const dailyInterest = new Decimal(openingBalance).mul(interestRateDaily).toDP(2).toNumber();
|
|
405
|
+
|
|
406
|
+
const principalDueDay = getNextPaymentDate(currentDate);
|
|
407
|
+
|
|
408
|
+
if (monthPayments.length > 0) {
|
|
409
|
+
const foundPayment = monthPayments.find((payment) => dayjs(payment.date).format('YYYY-MM-DD') === currentDate.format('YYYY-MM-DD'));
|
|
410
|
+
if (foundPayment) {
|
|
411
|
+
closingBalance = foundPayment.balance;
|
|
412
|
+
}
|
|
413
|
+
const lastTransaction = await loanTransactionsService.getLastTransactionForDate(productId, currentDate.toDate());
|
|
414
|
+
if (actual && lastTransaction && dayjs(lastTransaction.date).format('YYYY-MM-DD') === currentDate.format('YYYY-MM-DD')) {
|
|
415
|
+
closingBalance = lastTransaction.balance;
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
if (currentDate.format('YYYY-MM-DD') === principalDueDay.format('YYYY-MM-DD')) {
|
|
419
|
+
const monthlyPayment = new Decimal(complexEMI).sub(lastMonthInterest).toDP(2).toNumber();
|
|
420
|
+
const isLastPeriod = monthlyPayment > openingBalance;
|
|
421
|
+
const isGracePeriod = checkGracePeriodDate(principalDueDay);
|
|
422
|
+
const monthlyPrincipal = (isGracePeriod) ? 0 : (isLastPeriod ? openingBalance : monthlyPayment);
|
|
423
|
+
closingBalance = roundToXDigits(openingBalance - monthlyPrincipal);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
monthInterest = roundToXDigits(monthInterest + dailyInterest);
|
|
428
|
+
|
|
429
|
+
const dailyResult: ITermCalculatedDaily = {
|
|
430
|
+
day: currentDate.format('YYYY-MM-DD'),
|
|
431
|
+
interestRate: interestRate,
|
|
432
|
+
interest: dailyInterest,
|
|
433
|
+
paymentDate: currentDate.date() === termLoan.settings.principalDueDay,
|
|
434
|
+
};
|
|
435
|
+
calculatedDailyResults.push(dailyResult);
|
|
436
|
+
if (
|
|
437
|
+
(currentDate.format('YYYY-MM-DD') === currentDate.endOf('month').format('YYYY-MM-DD')) ||
|
|
438
|
+
(currentDate.format('YYYY-MM-DD') === endAmortizationDate.format('YYYY-MM-DD')) ||
|
|
439
|
+
(currentDate.format('YYYY-MM-DD') === endTermMonths.format('YYYY-MM-DD'))
|
|
440
|
+
) {
|
|
441
|
+
if (monthPayments.length > 0 && calculatedMonthlyResults.length > 0) {
|
|
442
|
+
calculatedMonthlyResults[calculatedMonthlyResults.length - 1].payments = monthPayments.map((p) => ({
|
|
443
|
+
paymentId: new mongoose.Types.ObjectId(String(p._id)),
|
|
444
|
+
amount: Math.abs(p.amount),
|
|
445
|
+
}));
|
|
446
|
+
calculatedMonthlyResults[calculatedMonthlyResults.length - 1].monthlyPrincipal = monthPayments.reduce((acc, p) => acc + Math.abs(p.amount), 0);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const actualMonthlyPayment = new Decimal(complexEMI).sub(monthInterest).toDP(2).toNumber();
|
|
450
|
+
isLastPeriod = actualMonthlyPayment > monthOpeningBalance || (currentDate.format('YYYY-MM-DD') === endTermMonths.format('YYYY-MM-DD'));
|
|
451
|
+
const isGracePeriod = checkGracePeriodDate(principalDueDay);
|
|
452
|
+
const monthlyPrincipal = (isGracePeriod) ? 0 : (isLastPeriod ? monthOpeningBalance : actualMonthlyPayment);
|
|
453
|
+
const totalPayment = monthPayments.length > 0 ? monthPayments.reduce((acc, p) => acc + Math.abs(p.amount), 0) : lastMonthPrincipal;
|
|
454
|
+
const totalDisbursements = monthDisbursements.length > 0 ? monthDisbursements.reduce((acc, p) => acc + Math.abs(p.amount), 0) : 0;
|
|
455
|
+
const closingBalance = new Decimal(monthOpeningBalance).sub(totalPayment).add(totalDisbursements).toDP(2).toNumber();
|
|
456
|
+
|
|
457
|
+
const monthlyResult: ITermLoanCalculated = {
|
|
458
|
+
termLoanId: termLoan._id,
|
|
459
|
+
openingBalance: monthOpeningBalance,
|
|
460
|
+
relevantStatement: currentDate.endOf('month').format('YYYY-MM-DD'),
|
|
461
|
+
monthlyInterest: monthInterest,
|
|
462
|
+
monthlyPrincipal: monthlyPrincipal,
|
|
463
|
+
closingBalance: closingBalance,
|
|
464
|
+
adminFee: Math.max(roundToXDigits(monthAdminFee), minAdminFee),
|
|
465
|
+
interestRate: interestRate,
|
|
466
|
+
interestDaily: monthOpeningBalance * interestRate / daysInYear,
|
|
467
|
+
gracePeriod: isGracePeriod,
|
|
468
|
+
amortization: true, // TODO calculate
|
|
469
|
+
paymentDueDate: principalDueDay.date(termLoan.settings.principalDueDay).toDate(),
|
|
470
|
+
payments: null,
|
|
471
|
+
disbursements: totalDisbursements,
|
|
472
|
+
dailyResults: calculatedDailyResults,
|
|
473
|
+
};
|
|
474
|
+
calculatedMonthlyResults.push(monthlyResult);
|
|
475
|
+
lastMonthInterest = monthInterest;
|
|
476
|
+
lastMonthPrincipal = monthlyPrincipal;
|
|
477
|
+
monthInterest = 0;
|
|
478
|
+
monthAdminFee = 0;
|
|
479
|
+
calculatedDailyResults = [];
|
|
480
|
+
}
|
|
481
|
+
currentDate = currentDate.add(1, 'day');
|
|
482
|
+
}
|
|
483
|
+
const paymentDates = [...new Set(calculatedMonthlyResults.map((res) => dayjs(res.paymentDueDate).format('YYYY-MM-DD')))];
|
|
484
|
+
const mergedResults = paymentDates.reduce((acc, paymentDate) => {
|
|
485
|
+
console.log(paymentDate, 'processing results');
|
|
486
|
+
const filteredDateResults = calculatedMonthlyResults.filter((res) => dayjs(res.paymentDueDate).format('YYYY-MM-DD') === paymentDate);
|
|
487
|
+
if (filteredDateResults.length === 1) {
|
|
488
|
+
return [...acc, ...filteredDateResults];
|
|
489
|
+
}
|
|
490
|
+
const totalMonthlyInterest = filteredDateResults.reduce((acc, res) => {
|
|
491
|
+
return roundToXDigits(acc + res.monthlyInterest);
|
|
492
|
+
}, 0);
|
|
493
|
+
const mergedOpeningBalance = filteredDateResults[0].openingBalance;
|
|
494
|
+
const totalMonthlyPrincipal = Math.min(filteredDateResults[0].closingBalance, roundToXDigits(complexEMI - totalMonthlyInterest));
|
|
495
|
+
const mergedClosingBalance = filteredDateResults[0].closingBalance;
|
|
496
|
+
const totalAdminFee = filteredDateResults.reduce((acc, res) => {
|
|
497
|
+
return roundToXDigits(acc + res.adminFee);
|
|
498
|
+
}, 0);
|
|
499
|
+
const lastStatement: ITermLoanCalculated = {
|
|
500
|
+
termLoanId: filteredDateResults[0].termLoanId,
|
|
501
|
+
openingBalance: mergedOpeningBalance,
|
|
502
|
+
relevantStatement: filteredDateResults[0].relevantStatement,
|
|
503
|
+
monthlyInterest: totalMonthlyInterest,
|
|
504
|
+
monthlyPrincipal: totalMonthlyPrincipal,
|
|
505
|
+
closingBalance: mergedClosingBalance,
|
|
506
|
+
adminFee: totalAdminFee,
|
|
507
|
+
interestRate: filteredDateResults[0].interestRate,
|
|
508
|
+
interestDaily: filteredDateResults[0].openingBalance * filteredDateResults[0].interestRate / daysInYear,
|
|
509
|
+
gracePeriod: filteredDateResults[0].gracePeriod,
|
|
510
|
+
amortization: true, // TODO calculate
|
|
511
|
+
paymentDueDate: filteredDateResults[0].paymentDueDate,
|
|
512
|
+
payments: null,
|
|
513
|
+
disbursements: filteredDateResults[0].disbursements,
|
|
514
|
+
dailyResults: filteredDateResults[0].dailyResults, // totalCalculatedDailyResults
|
|
515
|
+
};
|
|
516
|
+
const lastPaymentDate = dayjs(filteredDateResults[1].paymentDueDate).isBefore(dayjs(filteredDateResults[1].relevantStatement))
|
|
517
|
+
? getNextPaymentDate(dayjs(filteredDateResults[1].relevantStatement)).toDate()
|
|
518
|
+
: filteredDateResults[1].paymentDueDate;
|
|
519
|
+
const emptyStatement: ITermLoanCalculated = {
|
|
520
|
+
termLoanId: filteredDateResults[1].termLoanId,
|
|
521
|
+
openingBalance: mergedClosingBalance,
|
|
522
|
+
relevantStatement: filteredDateResults[1].relevantStatement,
|
|
523
|
+
monthlyInterest: 0,
|
|
524
|
+
monthlyPrincipal: endTermMonths.format() !== endAmortizationDate.format() ? mergedClosingBalance : 0,
|
|
525
|
+
closingBalance: 0,
|
|
526
|
+
adminFee: 0,
|
|
527
|
+
interestRate: 0,
|
|
528
|
+
interestDaily: 0,
|
|
529
|
+
gracePeriod: filteredDateResults[1].gracePeriod,
|
|
530
|
+
amortization: true, // TODO calculate
|
|
531
|
+
paymentDueDate: lastPaymentDate,
|
|
532
|
+
payments: null,
|
|
533
|
+
disbursements: filteredDateResults[1].disbursements,
|
|
534
|
+
dailyResults: filteredDateResults[1].dailyResults,
|
|
535
|
+
};
|
|
536
|
+
return [...acc, lastStatement, emptyStatement];
|
|
537
|
+
}, <ITermLoanCalculated[]>[]);
|
|
538
|
+
if (mergedResults[mergedResults.length - 1].closingBalance !== 0) {
|
|
539
|
+
// mergedResults[mergedResults.length - 1].monthlyPrincipal = mergedResults[mergedResults.length - 1].closingBalance;
|
|
540
|
+
const lastUnfinishedResult = mergedResults[mergedResults.length - 1];
|
|
541
|
+
const finishingResult = JSON.parse(JSON.stringify(lastUnfinishedResult));
|
|
542
|
+
finishingResult.openingBalance = lastUnfinishedResult.closingBalance;
|
|
543
|
+
finishingResult.monthlyInterest = 0;
|
|
544
|
+
finishingResult.monthlyPrincipal = 0;
|
|
545
|
+
finishingResult.adminFee = 0;
|
|
546
|
+
finishingResult.interestRate = 0;
|
|
547
|
+
finishingResult.interestDaily = 0;
|
|
548
|
+
finishingResult.closingBalance = 0;
|
|
549
|
+
finishingResult.dailyResults = [];
|
|
550
|
+
mergedResults.push(finishingResult);
|
|
551
|
+
}
|
|
552
|
+
console.log('saving...');
|
|
553
|
+
const filteredResults = mergedResults
|
|
554
|
+
.map((res) => ({ ...res, monthlyPrincipal: res.monthlyPrincipal > 0 ? res.monthlyPrincipal : 0 }));
|
|
555
|
+
const filteredRes = (await Promise.all(filteredResults.map(async (res) => {
|
|
556
|
+
if (!actual) {
|
|
557
|
+
return res;
|
|
558
|
+
}
|
|
559
|
+
const trDate = typeof res.paymentDueDate === 'string' ? new Date(res.paymentDueDate) : res.paymentDueDate;
|
|
560
|
+
const lastTransaction = await loanTransactionsService.getLastTransactionForDate(product._id.toString(), trDate);
|
|
561
|
+
if (lastTransaction && lastTransaction.balance > 0) {
|
|
562
|
+
return res;
|
|
563
|
+
}
|
|
564
|
+
}))).filter((res) => !!res);
|
|
565
|
+
await this.saveCalculatedTermLoan(filteredRes, productId);
|
|
566
|
+
await this.updateTermLoanStatus(termLoan._id.toString(), ETermLoanStatus.CALCULATED, new Date());
|
|
567
|
+
return await this.getTermLoan(productId, actual);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async saveCalculatedTermLoan(calculatedTermLoans: ITermLoanCalculated[], productId: string) {
|
|
571
|
+
if (calculatedTermLoans.length === 0) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const termLoanId = calculatedTermLoans[0].termLoanId;
|
|
575
|
+
await TermLoanCalculatedModel.deleteMany({ termLoanId, payments: null });
|
|
576
|
+
await Promise.all(calculatedTermLoans.map(async (calculatedTermLoan) => {
|
|
577
|
+
if (calculatedTermLoan.payments === null) {
|
|
578
|
+
await TermLoanCalculatedModel.findOneAndDelete({
|
|
579
|
+
termLoanId: calculatedTermLoan.termLoanId,
|
|
580
|
+
relevantStatement: calculatedTermLoan.relevantStatement,
|
|
581
|
+
});
|
|
582
|
+
const newCalculatedTermLoan = new TermLoanCalculatedModel(calculatedTermLoan);
|
|
583
|
+
await newCalculatedTermLoan.save();
|
|
584
|
+
} else {
|
|
585
|
+
const foundTermLoanCalculated = await TermLoanCalculatedModel.findOne({
|
|
586
|
+
termLoanId: calculatedTermLoan.termLoanId,
|
|
587
|
+
relevantStatement: calculatedTermLoan.relevantStatement,
|
|
588
|
+
}).lean();
|
|
589
|
+
if (!foundTermLoanCalculated) {
|
|
590
|
+
const newCalculatedTermLoan = new TermLoanCalculatedModel(calculatedTermLoan);
|
|
591
|
+
await newCalculatedTermLoan.save();
|
|
592
|
+
} else {
|
|
593
|
+
const loanStatementStatusService = this.getLoanStatementStatusService();
|
|
594
|
+
const statementStatus = await loanStatementStatusService.getStatementStatus(productId, new Date(calculatedTermLoan.relevantStatement));
|
|
595
|
+
if (!statementStatus.locked) {
|
|
596
|
+
await TermLoanCalculatedModel.findByIdAndUpdate(foundTermLoanCalculated._id, calculatedTermLoan);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async getTermLoanExcel(productId: string, actual: boolean, short: boolean) {
|
|
604
|
+
const termLoan = await this.getTermLoan(productId, actual);
|
|
605
|
+
const header = short
|
|
606
|
+
? ['PERIOD', 'OPENING BALANCE', 'PRINCIPAL', 'INTEREST', 'CLOSING BALANCE', 'ADMIN FEE']
|
|
607
|
+
: ['DATE', 'OPENING BALANCE', 'INTEREST', 'DAILY INTEREST ACCRUED', 'PRINCIPAL', 'CLOSING BALANCE', 'STATEMENT DATE'];
|
|
608
|
+
|
|
609
|
+
const getMonthlyData = (data: ITermLoanView & { calculated: ITermLoanCalculatedDoc[] }) => {
|
|
610
|
+
return data.calculated.map((row) => {
|
|
611
|
+
return [
|
|
612
|
+
new Date(row.relevantStatement),
|
|
613
|
+
row.openingBalance,
|
|
614
|
+
row.monthlyPrincipal,
|
|
615
|
+
row.monthlyInterest,
|
|
616
|
+
row.closingBalance,
|
|
617
|
+
row.adminFee,
|
|
618
|
+
];
|
|
619
|
+
});
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const getDailyData = (data: ITermLoanView & { calculated: ITermLoanCalculatedDoc[] }) => {
|
|
623
|
+
let lastMonthClosingBalance = null;
|
|
624
|
+
|
|
625
|
+
let lastPaymentClosingBalance = null;
|
|
626
|
+
let lastPaymentMonthlyPrincipal = null;
|
|
627
|
+
let lastPaymentDate = null;
|
|
628
|
+
const calculatedResults = data.calculated.reduce((acc, row) => {
|
|
629
|
+
let isPaymentDay = false;
|
|
630
|
+
let openingBalance = lastMonthClosingBalance ?? row.openingBalance;
|
|
631
|
+
let closingBalance = openingBalance;
|
|
632
|
+
return [
|
|
633
|
+
...acc,
|
|
634
|
+
...row.dailyResults.map((daily, index) => {
|
|
635
|
+
isPaymentDay = isPaymentDay || daily.paymentDate;
|
|
636
|
+
openingBalance = closingBalance;
|
|
637
|
+
if (dayjs(daily.day).format('YYYY-MM-DD') === dayjs(lastPaymentDate).format('YYYY-MM-DD')) {
|
|
638
|
+
closingBalance = lastPaymentClosingBalance - lastPaymentMonthlyPrincipal;
|
|
639
|
+
}
|
|
640
|
+
const newRow = [
|
|
641
|
+
new Date(daily.day),
|
|
642
|
+
openingBalance,
|
|
643
|
+
daily.interestRate,
|
|
644
|
+
daily.interest,
|
|
645
|
+
daily.paymentDate ? lastPaymentMonthlyPrincipal : null,
|
|
646
|
+
closingBalance,
|
|
647
|
+
row.relevantStatement,
|
|
648
|
+
];
|
|
649
|
+
if (dayjs(daily.day).format('YYYY-MM-DD') === dayjs(lastPaymentDate).format('YYYY-MM-DD')) {
|
|
650
|
+
openingBalance = closingBalance;
|
|
651
|
+
}
|
|
652
|
+
isPaymentDay = isPaymentDay || daily.paymentDate;
|
|
653
|
+
if (daily.paymentDate) {
|
|
654
|
+
lastPaymentClosingBalance = row.closingBalance;
|
|
655
|
+
lastPaymentMonthlyPrincipal = row.monthlyPrincipal;
|
|
656
|
+
lastPaymentDate = row.paymentDueDate;
|
|
657
|
+
}
|
|
658
|
+
if (index + 1 === row.dailyResults.length) {
|
|
659
|
+
lastMonthClosingBalance = row.closingBalance;
|
|
660
|
+
}
|
|
661
|
+
return newRow;
|
|
662
|
+
})];
|
|
663
|
+
}, []);
|
|
664
|
+
const lastRow = data.calculated[data.calculated.length - 1];
|
|
665
|
+
const lastPayment = [
|
|
666
|
+
new Date(lastRow.paymentDueDate),
|
|
667
|
+
lastRow.openingBalance,
|
|
668
|
+
null,
|
|
669
|
+
null,
|
|
670
|
+
lastRow.monthlyPrincipal,
|
|
671
|
+
lastRow.closingBalance,
|
|
672
|
+
lastRow.relevantStatement,
|
|
673
|
+
];
|
|
674
|
+
return [...calculatedResults, lastPayment];
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const product = await LoanProduct.findById(productId).lean();
|
|
678
|
+
const preparedData = short ? getMonthlyData(termLoan) : getDailyData(termLoan);
|
|
679
|
+
const dataToConvert = [{ [product.name]: [header, ...preparedData] }];
|
|
680
|
+
const uploadsService = this.getUploadsService();
|
|
681
|
+
return await uploadsService.convertDataToFile(dataToConvert);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async isBorrowerTermLoanActual(borrowerId: string) {
|
|
685
|
+
const products = await this.loanChargesService.getLoanProducts(borrowerId);
|
|
686
|
+
const termLoanProduct = products.find((product) => product.type === ELoanTypes.TERM);
|
|
687
|
+
if (!termLoanProduct) {
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
const termLoan = await TermLoanModel.findOne({ productId: termLoanProduct._id, actual: true });
|
|
691
|
+
if (!termLoan) {
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
return termLoan.calculationStatus === ETermLoanStatus.CALCULATED;
|
|
695
|
+
}
|
|
696
|
+
}
|