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.
Files changed (303) hide show
  1. package/classes/bank-transaction-item.d.ts +17 -0
  2. package/classes/bank-transaction-item.js +64 -0
  3. package/classes/bank-transaction-item.ts +66 -0
  4. package/classes/bank-uploaded-transaction.d.ts +17 -0
  5. package/classes/bank-uploaded-transaction.js +35 -0
  6. package/classes/bank-uploaded-transaction.ts +35 -0
  7. package/classes/inventory-item.d.ts +41 -0
  8. package/classes/inventory-item.js +44 -0
  9. package/classes/inventory-item.ts +63 -0
  10. package/classes/payable-account-item.d.ts +22 -0
  11. package/classes/payable-account-item.js +27 -0
  12. package/classes/payable-account-item.ts +35 -0
  13. package/classes/quickbook-item.d.ts +37 -0
  14. package/classes/quickbook-item.js +51 -0
  15. package/classes/quickbook-item.ts +59 -0
  16. package/classes/receivable-item.d.ts +26 -0
  17. package/classes/receivable-item.js +28 -0
  18. package/classes/receivable-item.ts +38 -0
  19. package/constants/date-formats.contsants.d.ts +1 -0
  20. package/constants/date-formats.contsants.js +4 -0
  21. package/constants/date-formats.contsants.ts +1 -0
  22. package/db/brokers.db.d.ts +185 -0
  23. package/db/brokers.db.js +35 -2
  24. package/db/brokers.db.ts +34 -1
  25. package/db/collateral-adjustments.db.d.ts +34 -0
  26. package/db/collateral-adjustments.db.js +52 -0
  27. package/db/collateral-adjustments.db.ts +54 -0
  28. package/db/collaterals.db.d.ts +1 -1
  29. package/db/equipment.db.d.ts +40 -0
  30. package/db/equipment.db.js +55 -0
  31. package/db/equipment.db.ts +56 -0
  32. package/db/financial-spreading.db.ts +2 -1
  33. package/db/groups.d.ts +5 -0
  34. package/db/groups.js +57 -0
  35. package/db/groups.ts +52 -0
  36. package/db/inventories.d.ts +91 -0
  37. package/db/inventories.js +449 -0
  38. package/db/inventories.ts +481 -0
  39. package/db/inventory-availability.d.ts +3 -0
  40. package/db/inventory-availability.js +103 -0
  41. package/db/inventory-availability.ts +113 -0
  42. package/db/new-summary.d.ts +31 -0
  43. package/db/new-summary.js +1295 -0
  44. package/db/new-summary.ts +1509 -0
  45. package/db/payable-accounts.d.ts +30 -0
  46. package/db/payable-accounts.js +55 -0
  47. package/db/payable-accounts.ts +50 -0
  48. package/db/reserve.db.d.ts +34 -0
  49. package/db/reserve.db.js +52 -0
  50. package/db/reserve.db.ts +48 -0
  51. package/db/uploads.db.d.ts +2 -0
  52. package/db/uploads.db.js +29 -0
  53. package/db/uploads.db.ts +24 -0
  54. package/helpers/main.helper.d.ts +31 -0
  55. package/helpers/main.helper.js +63 -0
  56. package/helpers/main.helper.ts +63 -0
  57. package/models/AccountPayableItem.model.d.ts +6 -6
  58. package/models/AllocatedBankTransaction.model.d.ts +54 -0
  59. package/models/AllocatedBankTransaction.model.js +70 -0
  60. package/models/AllocatedBankTransaction.model.ts +94 -0
  61. package/models/AllocatedData.model.d.ts +33 -0
  62. package/models/AllocatedData.model.js +19 -0
  63. package/models/AllocatedData.model.ts +24 -0
  64. package/models/BBCDate.model.d.ts +3 -3
  65. package/models/BBCSheet.model.d.ts +3 -3
  66. package/models/Banks.model.d.ts +3 -3
  67. package/models/Borrower.model.d.ts +3 -3
  68. package/models/BorrowerData.model.d.ts +3 -3
  69. package/models/BorrowerDataInsurance.model.d.ts +3 -3
  70. package/models/BorrowerDataTerm.model.d.ts +3 -3
  71. package/models/BorrowerSummary.model.js +1 -1
  72. package/models/BorrowerSummary.model.ts +1 -1
  73. package/models/CalandarDay.model.d.ts +40 -0
  74. package/models/CalandarDay.model.js +47 -0
  75. package/models/CalandarDay.model.ts +61 -0
  76. package/models/CashAllocationProduct.model.d.ts +119 -0
  77. package/models/CashAllocationProduct.model.js +102 -0
  78. package/models/CashAllocationProduct.model.ts +112 -0
  79. package/models/CashAllocationReference.model.d.ts +37 -0
  80. package/models/CashAllocationReference.model.js +27 -0
  81. package/models/CashAllocationReference.model.ts +40 -0
  82. package/models/CollateralAdjustment.model.d.ts +51 -0
  83. package/models/CollateralAdjustment.model.js +61 -0
  84. package/models/CollateralAdjustment.model.ts +98 -0
  85. package/models/Company.model.d.ts +35 -0
  86. package/models/Company.model.js +18 -0
  87. package/models/Company.model.ts +29 -0
  88. package/models/CustomerAPGroup.model.d.ts +32 -0
  89. package/models/CustomerAPGroup.model.js +24 -0
  90. package/models/CustomerAPGroup.model.ts +31 -0
  91. package/models/Equipment.model.d.ts +53 -0
  92. package/models/Equipment.model.js +140 -0
  93. package/models/Equipment.model.ts +172 -0
  94. package/models/FinancialCompliance.model.d.ts +39 -0
  95. package/models/FinancialCompliance.model.js +64 -0
  96. package/models/FinancialCompliance.model.ts +78 -0
  97. package/models/FinancialComplianceBorrower.model.d.ts +58 -0
  98. package/models/FinancialComplianceBorrower.model.js +82 -0
  99. package/models/FinancialComplianceBorrower.model.ts +118 -0
  100. package/models/FinancialIndexes.model.d.ts +36 -0
  101. package/models/FinancialIndexes.model.js +27 -0
  102. package/models/FinancialIndexes.model.ts +37 -0
  103. package/models/Inventory.model.d.ts +18 -18
  104. package/models/InventoryAvailability.model.d.ts +21 -21
  105. package/models/InventoryAvailabilityItem.model.d.ts +6 -6
  106. package/models/InventoryItem.model.d.ts +24 -24
  107. package/models/InventoryManualEntry.model.d.ts +9 -9
  108. package/models/InventorySeasonalRates.model.d.ts +3 -3
  109. package/models/LoanBroker.model.d.ts +3 -3
  110. package/models/LoanCharges.model.d.ts +12 -12
  111. package/models/LoanProducts.model.d.ts +9 -9
  112. package/models/LoanStatementStatus.model.d.ts +35 -0
  113. package/models/LoanStatementStatus.model.js +34 -0
  114. package/models/LoanStatementStatus.model.ts +45 -0
  115. package/models/LoanStatementTransaction.model.d.ts +9 -9
  116. package/models/LoanTransactionFile.model.d.ts +41 -0
  117. package/models/LoanTransactionFile.model.js +44 -0
  118. package/models/LoanTransactionFile.model.ts +61 -0
  119. package/models/MappedGroup.model.d.ts +37 -0
  120. package/models/MappedGroup.model.js +33 -0
  121. package/models/MappedGroup.model.ts +46 -0
  122. package/models/MonthEndData.Model.d.ts +41 -0
  123. package/models/MonthEndData.Model.js +42 -0
  124. package/models/MonthEndData.Model.ts +53 -0
  125. package/models/OrganizationEmails.model.d.ts +44 -0
  126. package/models/OrganizationEmails.model.js +40 -0
  127. package/models/OrganizationEmails.model.ts +54 -0
  128. package/models/ProductBroker.model.d.ts +9 -9
  129. package/models/QuickbooksAccount.model.d.ts +39 -0
  130. package/models/QuickbooksAccount.model.js +43 -0
  131. package/models/QuickbooksAccount.model.ts +57 -0
  132. package/models/Receivable.model.d.ts +12 -12
  133. package/models/ReceivableAvailability.model.d.ts +54 -54
  134. package/models/ReceivableAvailabilityItem.model.d.ts +57 -57
  135. package/models/ReceivableItem.model.d.ts +6 -6
  136. package/models/Reserve.model.d.ts +51 -0
  137. package/models/Reserve.model.js +96 -0
  138. package/models/Reserve.model.ts +125 -0
  139. package/models/TermLoan.model.d.ts +3 -3
  140. package/models/TermLoanCalculated.model.d.ts +6 -6
  141. package/models/TransactionAttachedFile.Model.d.ts +35 -0
  142. package/models/TransactionAttachedFile.Model.js +37 -0
  143. package/models/TransactionAttachedFile.Model.ts +48 -0
  144. package/models/UploadedBankTransaction.model.d.ts +56 -0
  145. package/models/UploadedBankTransaction.model.js +78 -0
  146. package/models/UploadedBankTransaction.model.ts +110 -0
  147. package/models/UploadedData.model.d.ts +36 -0
  148. package/models/UploadedData.model.js +23 -0
  149. package/models/UploadedData.model.ts +35 -0
  150. package/models/UploadedFile.model.d.ts +40 -0
  151. package/models/UploadedFile.model.js +41 -0
  152. package/models/UploadedFile.model.ts +57 -0
  153. package/models/UploadedSheet.model.d.ts +46 -0
  154. package/models/UploadedSheet.model.js +27 -0
  155. package/models/UploadedSheet.model.ts +51 -0
  156. package/package.json +10 -1
  157. package/repositories/globals.repository.d.ts +8 -0
  158. package/repositories/globals.repository.js +24 -0
  159. package/repositories/globals.repository.ts +21 -0
  160. package/services/attached-files.service.d.ts +57 -0
  161. package/services/attached-files.service.js +103 -0
  162. package/services/attached-files.service.ts +123 -0
  163. package/services/availability.service.d.ts +77 -0
  164. package/services/availability.service.js +897 -0
  165. package/services/availability.service.ts +1034 -0
  166. package/services/bank-uploaded-transactions.service.d.ts +33 -0
  167. package/services/bank-uploaded-transactions.service.js +430 -0
  168. package/services/bank-uploaded-transactions.service.ts +475 -0
  169. package/services/banks.service.d.ts +36 -0
  170. package/services/banks.service.js +91 -0
  171. package/services/banks.service.ts +95 -0
  172. package/services/borrower-summary.service.d.ts +35 -0
  173. package/services/borrower-summary.service.js +310 -0
  174. package/services/borrower-summary.service.ts +334 -0
  175. package/services/borrowers.service.d.ts +103 -0
  176. package/services/borrowers.service.js +268 -0
  177. package/services/borrowers.service.ts +302 -0
  178. package/services/brokers.service.d.ts +212 -0
  179. package/services/brokers.service.js +160 -0
  180. package/services/brokers.service.ts +200 -0
  181. package/services/calendar.service.d.ts +53 -0
  182. package/services/calendar.service.js +108 -0
  183. package/services/calendar.service.ts +128 -0
  184. package/services/cash-allocation.service.d.ts +40 -0
  185. package/services/cash-allocation.service.js +92 -0
  186. package/services/cash-allocation.service.ts +105 -0
  187. package/services/collateral-adjustments.service.d.ts +38 -0
  188. package/services/collateral-adjustments.service.js +82 -0
  189. package/services/collateral-adjustments.service.ts +95 -0
  190. package/services/collaterals.service.d.ts +69 -0
  191. package/services/collaterals.service.js +279 -0
  192. package/services/collaterals.service.ts +319 -0
  193. package/services/companies.service.d.ts +5 -0
  194. package/services/companies.service.js +21 -0
  195. package/services/companies.service.ts +23 -0
  196. package/services/compliance-borrowers.service.d.ts +152 -0
  197. package/services/compliance-borrowers.service.js +569 -0
  198. package/services/compliance-borrowers.service.ts +617 -0
  199. package/services/equipment.service.d.ts +42 -0
  200. package/services/equipment.service.js +120 -0
  201. package/services/equipment.service.ts +149 -0
  202. package/services/file-manager.service.d.ts +44 -0
  203. package/services/file-manager.service.js +120 -0
  204. package/services/file-manager.service.ts +146 -0
  205. package/services/financial-compliance.service.d.ts +58 -0
  206. package/services/financial-compliance.service.js +281 -0
  207. package/services/financial-compliance.service.ts +309 -0
  208. package/services/financial-indexes.service.d.ts +20 -0
  209. package/services/financial-indexes.service.js +241 -0
  210. package/services/financial-indexes.service.ts +257 -0
  211. package/services/financial-spreading.service.d.ts +74 -0
  212. package/services/financial-spreading.service.js +450 -0
  213. package/services/financial-spreading.service.ts +517 -0
  214. package/services/globals.service.d.ts +5 -0
  215. package/services/globals.service.js +11 -0
  216. package/services/globals.service.ts +8 -0
  217. package/services/groups.service.d.ts +39 -0
  218. package/services/groups.service.js +65 -0
  219. package/services/groups.service.ts +64 -0
  220. package/services/inventory-availability.service.d.ts +13 -0
  221. package/services/inventory-availability.service.js +170 -0
  222. package/services/inventory-availability.service.ts +187 -0
  223. package/services/inventory.service.d.ts +118 -0
  224. package/services/inventory.service.js +239 -0
  225. package/services/inventory.service.ts +276 -0
  226. package/services/loan-charges.service.d.ts +83 -0
  227. package/services/loan-charges.service.js +343 -0
  228. package/services/loan-charges.service.ts +396 -0
  229. package/services/loan-payments.service.d.ts +94 -0
  230. package/services/loan-payments.service.js +485 -0
  231. package/services/loan-payments.service.ts +541 -0
  232. package/services/loan-products.service.d.ts +12 -0
  233. package/services/loan-products.service.js +55 -0
  234. package/services/loan-products.service.ts +58 -0
  235. package/services/loan-statement-balance.service.d.ts +16 -0
  236. package/services/loan-statement-balance.service.js +106 -0
  237. package/services/loan-statement-balance.service.ts +113 -0
  238. package/services/loan-statement-effects.service.d.ts +8 -0
  239. package/services/loan-statement-effects.service.js +42 -0
  240. package/services/loan-statement-effects.service.ts +41 -0
  241. package/services/loan-statement-status.service.d.ts +208 -0
  242. package/services/loan-statement-status.service.js +159 -0
  243. package/services/loan-statement-status.service.ts +177 -0
  244. package/services/loan-statement.service.d.ts +186 -0
  245. package/services/loan-statement.service.js +935 -0
  246. package/services/loan-statement.service.ts +1040 -0
  247. package/services/loan-transactions.service.d.ts +169 -0
  248. package/services/loan-transactions.service.js +941 -0
  249. package/services/loan-transactions.service.ts +1042 -0
  250. package/services/lock.service.d.ts +6 -0
  251. package/services/lock.service.js +45 -0
  252. package/services/lock.service.ts +45 -0
  253. package/services/manual-entry.service.d.ts +20 -0
  254. package/services/manual-entry.service.js +186 -0
  255. package/services/manual-entry.service.ts +201 -0
  256. package/services/month-end-data.service.d.ts +34 -0
  257. package/services/month-end-data.service.js +30 -0
  258. package/services/month-end-data.service.ts +35 -0
  259. package/services/nodemailer.service.d.ts +96 -0
  260. package/services/nodemailer.service.js +689 -0
  261. package/services/nodemailer.service.ts +774 -0
  262. package/services/organization-emails.service.d.ts +31 -0
  263. package/services/organization-emails.service.js +10 -0
  264. package/services/organization-emails.service.ts +7 -0
  265. package/services/organizations.service.d.ts +34 -0
  266. package/services/organizations.service.js +74 -0
  267. package/services/organizations.service.ts +84 -0
  268. package/services/pdf.service.d.ts +61 -0
  269. package/services/pdf.service.js +547 -0
  270. package/services/pdf.service.ts +642 -0
  271. package/services/quickbooks.service.d.ts +99 -0
  272. package/services/quickbooks.service.js +640 -0
  273. package/services/quickbooks.service.ts +734 -0
  274. package/services/reports/investor-summary.service.d.ts +28 -0
  275. package/services/reports/investor-summary.service.js +136 -0
  276. package/services/reports/investor-summary.service.ts +159 -0
  277. package/services/reports.service.d.ts +126 -0
  278. package/services/reports.service.js +584 -0
  279. package/services/reports.service.ts +702 -0
  280. package/services/reserve.service.d.ts +37 -0
  281. package/services/reserve.service.js +76 -0
  282. package/services/reserve.service.ts +79 -0
  283. package/services/sentry.service.d.ts +11 -0
  284. package/services/sentry.service.js +49 -0
  285. package/services/sentry.service.ts +33 -0
  286. package/services/signs.service.d.ts +69 -0
  287. package/services/signs.service.js +230 -0
  288. package/services/signs.service.ts +260 -0
  289. package/services/term-loan.service.d.ts +30 -0
  290. package/services/term-loan.service.js +614 -0
  291. package/services/term-loan.service.ts +696 -0
  292. package/services/uploads.service.d.ts +134 -0
  293. package/services/uploads.service.js +587 -0
  294. package/services/uploads.service.ts +643 -0
  295. package/services/user-logs.service.d.ts +23 -0
  296. package/services/user-logs.service.js +160 -0
  297. package/services/user-logs.service.ts +177 -0
  298. package/services/users.service.d.ts +4 -4
  299. package/services/yield.service.d.ts +46 -0
  300. package/services/yield.service.js +42 -12
  301. package/services/yield.service.ts +38 -8
  302. package/tsconfig.json +5 -5
  303. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,1509 @@
1
+ import dayjs from 'dayjs';
2
+ import Decimal from 'decimal.js';
3
+ import ExcelJS from 'exceljs';
4
+ import _ from 'lodash';
5
+
6
+ import { reorderObject } from '../helpers/common.helper';
7
+ import { getLastTransactionForDate } from './loan-transactions.db';
8
+ import { BorrowerModel, IBorrowerDocument } from '../models/Borrower.model';
9
+ import { ILoanProductDoc, LoanProduct } from '../models/LoanProducts.model';
10
+ import { getAverageActualBalance } from './loan-products.db';
11
+ import { LoanStatementTransactionModel } from '../models/LoanStatementTransaction.model';
12
+ import { getLatestSignedBBCDateDoc } from './bbcDates.db';
13
+ import { EReserveTypes } from '../enums/reserve-types.enum';
14
+ import { EComplianceItemStatus } from '../models/ComplianceItem.model';
15
+ import { IExcelJsCell } from '../helpers/excel.helper';
16
+ import { ILoanChargeDoc, LoanCharge } from '../models/LoanCharges.model';
17
+ import { BorrowerCompliance, IComplianceBorrowerDocument } from '../models/BorrowerCompliance.model';
18
+ import { ELoanTransactionTypes } from '../models/LoanTransaction.model';
19
+ import { ELoanTypes } from '../enums/loan-types.enum';
20
+ import { EEquipmentTypes } from '../enums/equipment-types.enum';
21
+
22
+ import { BorrowerSummary } from '../models/BorrowerSummary.model';
23
+ import {
24
+ EFinanceSpreadingBSTotal,
25
+ EFinanceSpreadingPLTotal,
26
+ EFinancialSpreadingType, financialSpreadingTotalDictionary,
27
+ } from '../models/FinancialSpreadingSheet.model';
28
+ import { CRMProspectIndustry } from '../models/ProspectIndustry.model';
29
+ import { FinancialSpreadingDTO } from '../models/FinancialSpreading.model';
30
+
31
+ import { IExcelDataCellWithStyles } from '../services/uploads.service';
32
+ import {
33
+ FinancialSpreadingService,
34
+ getShiftedMonth,
35
+ } from '../services/financial-spreading.service';
36
+ import { ETotalType, YIELD_TOTALS_MAP, YieldService } from '../services/yield.service';
37
+ import { AvailabilityService } from '../services/availability.service';
38
+ import { BorrowerService } from '../services/borrowers.service';
39
+ import { BorrowerSummaryService } from '../services/borrower-summary.service';
40
+ import { ComplianceBorrowersService } from '../services/compliance-borrowers.service';
41
+ import { LoanChargesService } from '../services/loan-charges.service';
42
+ import { LoanTransactionsService } from '../services/loan-transactions.service';
43
+ import { SignsService } from '../services/signs.service';
44
+ import { MonthEndDataService } from '../services/month-end-data.service';
45
+ import { EMonthEndDataType } from '../models/MonthEndData.Model';
46
+
47
+ type DataSheet = {
48
+ [p: string]: IExcelJsCell[][]
49
+ }
50
+
51
+ type ReportRow = {
52
+ borrowerName: string;
53
+ productName: string;
54
+ maturityDate: string;
55
+ payOffDate: string;
56
+ commitmentAmount: string | number;
57
+ endingBalance: string | number;
58
+ endingParticipationBalance: string | number;
59
+ netExposure: string | number;
60
+ lastSignedBBCDate: string | number;
61
+ compliance: string;
62
+ averageBalanceSinceInception: string | number;
63
+ totalIncomeReceived: string | number;
64
+ loanLifeIRR: string | number;
65
+ MOIC: string | number;
66
+ participantBalance: string | number;
67
+ }
68
+
69
+ type BorrowerSummaryRow = {
70
+ title: string;
71
+ grossValue: string | number;
72
+ ineligible: string | number;
73
+ netValue: string | number;
74
+ advanceRate: string | number;
75
+ availability: string | number;
76
+ }
77
+
78
+ type ComplianceItemRow = {
79
+ name: string;
80
+ dueDate: string;
81
+ progress: string;
82
+ submittedDate: string;
83
+ status: string;
84
+ }
85
+
86
+ type FinancialIndexOptions = {
87
+ format: string;
88
+ title: string;
89
+ addEmptyRow: boolean;
90
+ style?: object,
91
+ }
92
+
93
+ const styles: { [styleName: string]: Partial<ExcelJS.Style> } = {
94
+ blackOnWhiteWithTopBorder: {
95
+ fill: {
96
+ type: 'pattern',
97
+ pattern: 'solid',
98
+ fgColor: { argb: 'FFFFFFFF' },
99
+ },
100
+ font: {
101
+ bold: true,
102
+ color: { argb: 'FF000000' },
103
+ },
104
+ border: {
105
+ top: { style: 'thin' },
106
+ },
107
+ },
108
+ blackOnWhiteWithBottomBorder: {
109
+ fill: {
110
+ type: 'pattern',
111
+ pattern: 'solid',
112
+ fgColor: { argb: 'FFFFFFFF' },
113
+ },
114
+ font: {
115
+ bold: true,
116
+ color: { argb: 'FF000000' },
117
+ },
118
+ border: {
119
+ bottom: { style: 'thin' },
120
+ },
121
+ },
122
+ whiteOnBlack: {
123
+ fill: {
124
+ type: 'pattern',
125
+ pattern: 'solid',
126
+ fgColor: { argb: 'FF000000' },
127
+ },
128
+ font: {
129
+ bold: true,
130
+ color: { argb: 'FFFFFFFF' },
131
+ },
132
+ },
133
+ whiteOnGray: {
134
+ fill: {
135
+ type: 'pattern',
136
+ pattern: 'solid',
137
+ fgColor: { argb: 'FF7F7F7F' },
138
+ },
139
+ font: {
140
+ bold: true,
141
+ color: { argb: 'FFFFFFFF' },
142
+ },
143
+ },
144
+ };
145
+
146
+ const numberFormat = {
147
+ fullNumber: '#,##0.00',
148
+ shortNumber: '#,##0',
149
+ xNumber: '0.00"x"',
150
+ xNumber1DP: '0.0"x"',
151
+ thousands: '0.0,',
152
+ percent: '0.00%',
153
+ percent1DP: '0.0%',
154
+ };
155
+
156
+ const borrowerTitleLength = 12;
157
+ const dateFormat = 'MM-DD-YYYY';
158
+
159
+ const productsDataMap = new Map<string, ReportRow>();
160
+ const borrowersMap = new Map<string, IBorrowerDocument>();
161
+ const productsMap = new Map<string, ILoanProductDoc>();
162
+ const chargesMap = new Map<string, ILoanChargeDoc>();
163
+ const complianceBorrowersMap = new Map<string, IComplianceBorrowerDocument>();
164
+
165
+ const allBorrowerSummaryRowFields = {
166
+ commitmentAmount: {
167
+ t: 'n',
168
+ z: numberFormat.thousands,
169
+ },
170
+ endingBalance: {
171
+ t: 'n',
172
+ z: numberFormat.thousands,
173
+ },
174
+ endingParticipationBalance: {
175
+ t: 'n',
176
+ z: numberFormat.thousands,
177
+ },
178
+ netExposure: {
179
+ t: 'n',
180
+ z: numberFormat.thousands,
181
+ },
182
+ averageBalanceSinceInception: {
183
+ t: 'n',
184
+ z: numberFormat.thousands,
185
+ },
186
+ totalIncomeReceived: {
187
+ t: 'n',
188
+ z: numberFormat.thousands,
189
+ },
190
+ loanLifeIRR: {
191
+ t: 'n',
192
+ z: numberFormat.percent,
193
+ },
194
+ MOIC: {
195
+ t: 'n',
196
+ z: numberFormat.xNumber,
197
+ },
198
+ participantBalance: {
199
+ t: 'n',
200
+ z: numberFormat.thousands,
201
+ },
202
+ };
203
+
204
+ const borrowerSummaryRowFields = {
205
+ title: {
206
+ t: 's',
207
+ },
208
+ 'grossValue': {
209
+ t: 'n',
210
+ z: numberFormat.thousands,
211
+ },
212
+ 'ineligible': {
213
+ t: 'n',
214
+ z: numberFormat.thousands,
215
+ },
216
+ 'netValue': {
217
+ t: 'n',
218
+ z: numberFormat.thousands,
219
+ },
220
+ 'advanceRate': {
221
+ t: 'n',
222
+ z: numberFormat.percent1DP,
223
+ },
224
+ 'availability': {
225
+ t: 'n',
226
+ z: numberFormat.thousands,
227
+ },
228
+ };
229
+
230
+ const emptyRow: IExcelJsCell[] = [{ v: '', t: 's' }];
231
+ const negativeCell: IExcelJsCell = { v: 'NA', t: 's', s: { alignment: { horizontal: 'right' } } };
232
+ const rightAlign: { alignment: Partial<ExcelJS.Alignment> } = {
233
+ alignment: { horizontal: 'right' },
234
+ };
235
+
236
+ const styleRowData = (
237
+ rows: BorrowerSummaryRow[],
238
+ style?: Partial<ExcelJS.Style>,
239
+ ): IExcelJsCell[][] => {
240
+ return rows.map((row) => {
241
+ return Object.entries(row).map(([key, v]) => {
242
+ return {
243
+ v,
244
+ t: borrowerSummaryRowFields[key].t,
245
+ z: borrowerSummaryRowFields[key].z,
246
+ ...(style ? { s: style } : {}),
247
+ } as IExcelJsCell;
248
+ });
249
+ });
250
+ };
251
+
252
+ export class NewSummaryExcel {
253
+
254
+ constructor(
255
+ private readonly availabilityService: AvailabilityService,
256
+ private readonly borrowerService: BorrowerService,
257
+ private readonly borrowerSummaryService: BorrowerSummaryService,
258
+ private readonly complianceBorrowersService: ComplianceBorrowersService,
259
+ private readonly financialSpreadingService: FinancialSpreadingService,
260
+ private readonly loanChargesService: LoanChargesService,
261
+ private readonly loanTransactionsService: LoanTransactionsService,
262
+ private readonly signsService: SignsService,
263
+ private readonly yieldService: YieldService,
264
+ private readonly monthEndDataService: MonthEndDataService,
265
+ ) {
266
+ }
267
+
268
+ private async getMainData(borrowerIds: string[], start: Date, end: Date): Promise<DataSheet> {
269
+ const periodDays = 360;
270
+
271
+ const header: ReportRow = {
272
+ borrowerName: 'Client name',
273
+ productName: 'Product name',
274
+ maturityDate: 'Maturity date',
275
+ payOffDate: 'PayOff Date',
276
+ commitmentAmount: 'Commitment amount',
277
+ endingBalance: 'Ending Balance',
278
+ endingParticipationBalance: 'Ending Participation balance',
279
+ netExposure: 'Net Exposure',
280
+ lastSignedBBCDate: 'Last Signed BBC',
281
+ compliance: 'Compliance',
282
+ averageBalanceSinceInception: 'Average Balance Since Inception',
283
+ totalIncomeReceived: 'Total Income Received',
284
+ loanLifeIRR: 'Loan Life IRR',
285
+ MOIC: 'MOIC',
286
+ participantBalance: 'Participant Balance',
287
+ };
288
+
289
+ const headerKeys = Object.keys(header);
290
+
291
+ const allBorrowersSummary: ReportRow[] = [];
292
+
293
+ for (const borrowerId of borrowerIds) {
294
+ let periodEnd = end;
295
+ const borrower = borrowersMap.get(borrowerId);
296
+ if (!borrower) {
297
+ continue;
298
+ }
299
+ const products = [...productsMap.values()].filter((product) => product.borrowerId.toString() === borrowerId);
300
+ const complianceBorrower = [...complianceBorrowersMap.values()].find((complianceBorrower) => complianceBorrower.borrower.toString() === borrowerId);
301
+ for (const product of products) {
302
+ if (product.deactivationDate) {
303
+ continue;
304
+ }
305
+ if (dayjs(product.payoffDate).isBefore(dayjs(periodEnd))) {
306
+ periodEnd = product.payoffDate;
307
+ }
308
+ const daysHeld = dayjs(periodEnd).diff(dayjs(product.startDate), 'day');
309
+ const lastTransaction = await getLastTransactionForDate(product._id.toString(), periodEnd);
310
+ const endingBalance = lastTransaction ? lastTransaction.balance : 0;
311
+ const endingParticipationBalance = product.isParticipant ? endingBalance : 0;
312
+ const netExposure = new Decimal(endingBalance).sub(endingParticipationBalance).toNumber();
313
+ const lastSignedBBC = await this.signsService.getLatestSignedBBCDate(borrowerId, periodEnd);
314
+ const lastSignedBBCDate = lastSignedBBC ? dayjs(lastSignedBBC.bbcDate).format(dateFormat) : '';
315
+ const averageBalanceSinceInception = new Decimal(await getAverageActualBalance(product._id.toString(), {
316
+ start: dayjs(product.startDate),
317
+ end: dayjs(periodEnd),
318
+ })).toDP(2).toNumber();
319
+
320
+ const totalIncomeReceived = await this.getPaidAmount(product._id.toString());
321
+ const MOIC = averageBalanceSinceInception ? new Decimal(totalIncomeReceived).add(Math.abs(averageBalanceSinceInception)).div(Math.abs(averageBalanceSinceInception)).toDP(2).toNumber() : 0;
322
+ const loanLifeIRRMul = daysHeld ? new Decimal(periodDays).div(daysHeld).toNumber() : 0;
323
+ const loanLifeIRR = MOIC ? new Decimal(MOIC).pow(loanLifeIRRMul).sub(1).toDP(4).toNumber() : 0;
324
+ const participantBalance = await this.loanChargesService.getParticipationBalance(product.code, end);
325
+
326
+ const newDataRow: ReportRow = {
327
+ borrowerName: borrower.name,
328
+ productName: product.name,
329
+ maturityDate: product.maturityDate ? dayjs(product.maturityDate).format(dateFormat) : '',
330
+ payOffDate: product.payoffDate ? dayjs(product.payoffDate).format(dateFormat) : '',
331
+ commitmentAmount: product.commitment,
332
+ endingBalance,
333
+ endingParticipationBalance,
334
+ netExposure,
335
+ lastSignedBBCDate,
336
+ compliance: complianceBorrower ? complianceBorrower.fundingStatus : '',
337
+ averageBalanceSinceInception,
338
+ totalIncomeReceived,
339
+ loanLifeIRR,
340
+ MOIC,
341
+ participantBalance
342
+ };
343
+ productsDataMap.set(product._id.toString(), newDataRow);
344
+ allBorrowersSummary.push(newDataRow);
345
+ }
346
+ }
347
+ const sortedData = _.orderBy(allBorrowersSummary, ['borrowerName', 'productName'], ['asc', 'asc']);
348
+ const headerAsArray = reorderObject(header, headerKeys);
349
+ const dataAsArrays = sortedData.map((row) => reorderObject(row, headerKeys));
350
+ const headerWithStyles = Object.values(headerAsArray).reduce((acc, header) => [...acc, {
351
+ v: header,
352
+ t: 's',
353
+ s: styles.whiteOnGray,
354
+ }], []);
355
+ const dataWithStyles = Object.values(dataAsArrays).map((dataRow) => {
356
+ return Object.entries(dataRow).map(([key, v]) => ({
357
+ v,
358
+ t: allBorrowerSummaryRowFields[key] ? allBorrowerSummaryRowFields[key].t : 's',
359
+ z: allBorrowerSummaryRowFields[key] ? allBorrowerSummaryRowFields[key].z : {},
360
+ }));
361
+ });
362
+ const reportPeriod = [
363
+ [{ v: 'Report period', t: 's' }],
364
+ [{ v: 'From', t: 's' }, { v: dayjs(start).utcOffset(0).format(dateFormat), t: 's' }],
365
+ [{ v: 'To', t: 's' }, { v: dayjs(end).utcOffset(0).format(dateFormat), t: 's' }],
366
+ ];
367
+ return { report: [...reportPeriod, emptyRow, headerWithStyles, ...dataWithStyles] };
368
+ };
369
+
370
+ private async getAllBorrowersSummary(borrowerIds: string[], start: Date, end: Date): Promise<DataSheet> {
371
+ const borrowerSummary: { [borrowerName: string]: IExcelJsCell[][] } = {};
372
+ for (const borrowerId of borrowerIds) {
373
+ const borrower = borrowersMap.get(borrowerId);
374
+ const formattedBorrowerName = borrower.name.replace(/[*?:\\/[\]]/g, '-');
375
+ const borrowerNameOrId = borrower ? formattedBorrowerName : borrowerId;
376
+ const borrowerData = await this.getBorrowerSummary(borrowerId, start, end);
377
+ if (borrowerData) {
378
+ borrowerSummary[borrowerNameOrId] = borrowerData;
379
+ }
380
+ }
381
+ return borrowerSummary;
382
+ };
383
+
384
+ private async getBorrowerSummary(borrowerId: string, start: Date, end: Date): Promise<IExcelJsCell[][]> {
385
+ const header: BorrowerSummaryRow = {
386
+ title: '',
387
+ grossValue: 'Gross Value',
388
+ ineligible: 'Ineligible',
389
+ netValue: 'Net Value',
390
+ advanceRate: 'Advance Rate',
391
+ availability: 'Availability',
392
+ };
393
+
394
+ const borrower = borrowersMap.get(borrowerId);
395
+ const borrowerTitle: IExcelDataCellWithStyles[] = [
396
+ { v: borrower?.name ?? borrowerId, s: styles.whiteOnBlack },
397
+ ...Array(borrowerTitleLength).fill({ v: '', s: styles.whiteOnBlack }),
398
+ ];
399
+
400
+ const getBorrowerSubTitle = async (): Promise<IExcelDataCellWithStyles[]> => {
401
+ const borrowerOption = await this.borrowerService.getBorrowerOption(borrowerId, 'NAISC Code');
402
+ if (!borrowerOption) {
403
+ return [{ v: '' }];
404
+ }
405
+ const industry = await CRMProspectIndustry.findOne({ code: borrowerOption.dataValue.toString() });
406
+ if (!industry) {
407
+ return [{ v: `NASIC CODE: ${borrowerOption.dataValue}` }];
408
+ }
409
+ return [{ v: `NASIC CODE: ${borrowerOption.dataValue} INDUSTRY: ${industry.name.toLocaleUpperCase()}` }];
410
+ };
411
+ const borrowerSubTitle = await getBorrowerSubTitle();
412
+
413
+ const borrowerSummary = await BorrowerSummary.findOne({ borrowerId });
414
+
415
+ const getSegment = (title: string) => {
416
+ const segment: IExcelJsCell[] = [
417
+ { v: title, t: 's', s: styles.whiteOnGray },
418
+ ...Array(borrowerTitleLength).fill({ v: '', t: 's', s: styles.whiteOnGray }),
419
+ ];
420
+ return segment;
421
+ };
422
+
423
+ const headerKeys = Object.keys(header);
424
+ const headerAsArray = reorderObject(header, headerKeys);
425
+ const headerWithStyles = Object.values(headerAsArray).reduce((acc, v) => [...acc, {
426
+ v,
427
+ t: 's',
428
+ s: rightAlign,
429
+ }], []);
430
+
431
+ const lastBBC = await getLatestSignedBBCDateDoc(borrowerId, dayjs(end).endOf('day').toDate());
432
+ if (!lastBBC) {
433
+ return null;
434
+ }
435
+ const availability = await this.availabilityService.getAllSummaries(lastBBC._id, null);
436
+
437
+ // INVENTORY
438
+ const inventoryRows = availability.inventory.reduce((acc, inventory) => {
439
+ if (inventory.inventoryName === 'TOTAL') {
440
+ return acc;
441
+ }
442
+ const newRow: BorrowerSummaryRow = {
443
+ title: inventory.inventoryName,
444
+ grossValue: inventory.totalValue,
445
+ ineligible: inventory.advanceRate === 0 ? inventory.totalValue : 0,
446
+ netValue: inventory.advanceRate === 0 ? 0 : inventory.totalValue,
447
+ advanceRate: inventory.advanceRate,
448
+ availability: inventory.availability,
449
+ };
450
+ return [...acc, newRow];
451
+ }, <BorrowerSummaryRow[]>[]);
452
+ inventoryRows.push({
453
+ title: 'Inventory Reserves',
454
+ grossValue: null,
455
+ ineligible: null,
456
+ netValue: null,
457
+ advanceRate: null,
458
+ availability: availability.reserve[EReserveTypes.INVENTORY].availability ?? 0,
459
+ });
460
+ const inventorySumKeys = ['grossValue', 'ineligible', 'netValue', 'availability'];
461
+ const emptyTotalRow: BorrowerSummaryRow = {
462
+ title: 'Total Collateral',
463
+ grossValue: 0,
464
+ ineligible: 0,
465
+ netValue: 0,
466
+ advanceRate: 0,
467
+ availability: 0,
468
+ };
469
+
470
+ const inventoryTotalRow = inventoryRows.reduce((acc, row) => {
471
+ inventorySumKeys.forEach((key) => {
472
+ acc[key] = new Decimal(acc[key]).add(row[key] ?? 0).toNumber();
473
+ });
474
+ acc.advanceRate = acc.netValue ? new Decimal(acc.availability).div(acc.netValue).toDP(4).toNumber() : 0;
475
+ return acc;
476
+ }, { ...emptyTotalRow, title: 'Total Inventory Collateral' });
477
+
478
+ const inventoryDataAsArrays = inventoryRows.map((row) => reorderObject(row, headerKeys));
479
+
480
+ const inventoryRowDataWithStyles = styleRowData(inventoryDataAsArrays);
481
+ const inventoryTotalDataWithStyles = styleRowData([inventoryTotalRow], styles.blackOnWhiteWithTopBorder);
482
+ const inventoryDataWithStyles = [...inventoryRowDataWithStyles, ...inventoryTotalDataWithStyles];
483
+
484
+ // RECEIVABLE
485
+ const receivableRows: BorrowerSummaryRow[] = [];
486
+ const totalComponents = new Decimal(availability.receivable.uninsuredComponent).add(availability.receivable.insuredComponent);
487
+
488
+ const grossUninsured = totalComponents
489
+ ? new Decimal(availability.receivable.uninsuredComponent).div(totalComponents).mul(availability.receivable.invoiceAmount).toDP(4).toNumber()
490
+ : 0;
491
+ const ARUninsuredRow: BorrowerSummaryRow = {
492
+ title: 'AR Uninsured',
493
+ grossValue: grossUninsured, // B
494
+ ineligible: new Decimal(availability.receivable.uninsuredComponent).sub(grossUninsured).toNumber(), // C
495
+ netValue: availability.receivable.uninsuredComponent, // D
496
+ advanceRate: availability.receivable.uninsuredAvailability ? new Decimal(availability.receivable.uninsuredAvailability).div(availability.receivable.uninsuredComponent).toDP(4).toNumber() : 0, // E
497
+ availability: availability.receivable.uninsuredAvailability, // F
498
+ };
499
+ const grossInsured = totalComponents
500
+ ? new Decimal(availability.receivable.insuredComponent).div(totalComponents).mul(availability.receivable.invoiceAmount).toDP(4).toNumber()
501
+ : 0;
502
+ const ARInsuredRow: BorrowerSummaryRow = {
503
+ title: 'AR Insured',
504
+ grossValue: grossInsured, // B
505
+ ineligible: new Decimal(availability.receivable.insuredComponent).sub(grossInsured).toNumber(), // C
506
+ netValue: availability.receivable.insuredComponent, // D
507
+ advanceRate: availability.receivable.insuredAvailability ? new Decimal(availability.receivable.insuredAvailability).div(availability.receivable.insuredComponent).toDP(4).toNumber() : 0, // E
508
+ availability: availability.receivable.insuredAvailability, // F
509
+ };
510
+ const ARReservesRow: BorrowerSummaryRow = {
511
+ title: 'AR Reserves',
512
+ grossValue: null,
513
+ ineligible: null,
514
+ netValue: null,
515
+ advanceRate: null,
516
+ availability: availability.reserve[EReserveTypes.RECEIVABLES].amount ?? 0,
517
+ };
518
+ receivableRows.push(ARUninsuredRow, ARInsuredRow, ARReservesRow);
519
+ const receivableTotalRow = { ...emptyTotalRow, title: 'Total Receivable Collateral' };
520
+ const fields = ['grossValue', 'ineligible', 'netValue', 'availability'];
521
+ fields.forEach((field) => {
522
+ receivableTotalRow[field] = new Decimal(ARUninsuredRow[field]).add(ARInsuredRow[field]).sub(ARReservesRow[field] ?? 0).toNumber();
523
+ });
524
+ receivableTotalRow.advanceRate = receivableTotalRow.netValue ? new Decimal(receivableTotalRow.availability).div(receivableTotalRow.netValue).toDP(4).toNumber() : 0;
525
+
526
+ const receivableDataAsArrays = receivableRows.map((row) => reorderObject(row, headerKeys));
527
+
528
+ const receivableRowDataWithStyles = styleRowData(receivableDataAsArrays);
529
+ const receivableTotalDataWithStyles = styleRowData([receivableTotalRow], styles.blackOnWhiteWithTopBorder);
530
+ const receivableDataWithStyles = [...receivableRowDataWithStyles, ...receivableTotalDataWithStyles];
531
+
532
+ // RESERVES
533
+ const reserveRows: BorrowerSummaryRow[] = [];
534
+ const otherReservesAvailability = Object
535
+ .entries(availability.reserve)
536
+ .reduce((acc, [key, el]) => {
537
+ if (key === EReserveTypes.EQUIPMENT || key === EReserveTypes.INVENTORY || key === EReserveTypes.RECEIVABLES) {
538
+ return acc;
539
+ }
540
+ return new Decimal(acc).sub(el.amount).toNumber();
541
+ }, 0);
542
+ const otherReservesRow: BorrowerSummaryRow = {
543
+ title: 'Other non-equipment reserves',
544
+ grossValue: null,
545
+ ineligible: null,
546
+ netValue: null,
547
+ advanceRate: null,
548
+ availability: otherReservesAvailability,
549
+ };
550
+ const collateralAdjustmentsAvailability = Object.values(availability.collateralAdjustment.summary).reduce((acc, summaryRow) => new Decimal(acc).add(summaryRow.amount).abs().toNumber(), 0);
551
+ const collateralAdjustmentsRow: BorrowerSummaryRow = {
552
+ title: 'Collateral Adjustments',
553
+ grossValue: null,
554
+ ineligible: null,
555
+ netValue: null,
556
+ advanceRate: null,
557
+ availability: collateralAdjustmentsAvailability,
558
+ };
559
+ reserveRows.push(otherReservesRow, collateralAdjustmentsRow);
560
+ const reserveTotalRow = reserveRows.reduce((acc, row) => {
561
+ inventorySumKeys.forEach((key) => {
562
+ acc[key] = new Decimal(acc[key]).add(row[key] ?? 0).toNumber();
563
+ });
564
+ acc.advanceRate = row.netValue ? new Decimal(acc.availability).div(row.netValue).toNumber() : 0;
565
+ return acc;
566
+ }, { ...emptyTotalRow, title: 'Other Reserves & Adjustments' });
567
+ const reserveDataAsArrays = reserveRows.map((row) => reorderObject(row, headerKeys));
568
+
569
+ const reserveRowDataWithStyles = styleRowData(reserveDataAsArrays);
570
+ const reserveTotalDataWithStyles = styleRowData([reserveTotalRow], styles.blackOnWhiteWithTopBorder);
571
+ const reserveDataWithStyles = [...reserveRowDataWithStyles, ...reserveTotalDataWithStyles];
572
+
573
+ const loanBalanceTotalRows: BorrowerSummaryRow[] = [];
574
+ const totalRevolvingCollateralRow: BorrowerSummaryRow = {
575
+ title: 'Total Revolving Collateral',
576
+ grossValue: '',
577
+ ineligible: '',
578
+ netValue: '',
579
+ advanceRate: '',
580
+ availability: new Decimal(inventoryTotalRow.availability).add(receivableTotalRow.availability).sub(reserveTotalRow.availability).toNumber(),
581
+ };
582
+ const loanBalanceRow: BorrowerSummaryRow = {
583
+ title: 'Loan Balance',
584
+ grossValue: '',
585
+ ineligible: '',
586
+ netValue: '',
587
+ advanceRate: '',
588
+ availability: availability.loanBalances.REVOLVER,
589
+ };
590
+ const netRevolverAvailabilityRow: BorrowerSummaryRow = {
591
+ title: 'Net Revolver Availability',
592
+ grossValue: '',
593
+ ineligible: '',
594
+ netValue: '',
595
+ advanceRate: '',
596
+ availability: new Decimal(totalRevolvingCollateralRow.availability).sub(loanBalanceRow.availability).toNumber(),
597
+ };
598
+ const accruedStatementRow: BorrowerSummaryRow = {
599
+ title: 'Accrued Statement',
600
+ grossValue: '',
601
+ ineligible: '',
602
+ netValue: '',
603
+ advanceRate: '',
604
+ availability: availability.accruedStatement,
605
+ };
606
+ const availableToBorrowRow: BorrowerSummaryRow = {
607
+ title: 'Available To Borrow',
608
+ grossValue: '',
609
+ ineligible: '',
610
+ netValue: '',
611
+ advanceRate: '',
612
+ availability: new Decimal(netRevolverAvailabilityRow.availability).sub(accruedStatementRow.availability).toNumber(),
613
+ };
614
+ loanBalanceTotalRows.push(
615
+ totalRevolvingCollateralRow,
616
+ loanBalanceRow,
617
+ netRevolverAvailabilityRow,
618
+ accruedStatementRow,
619
+ availableToBorrowRow,
620
+ { title: '', netValue: null, grossValue: null, ineligible: null, advanceRate: null, availability: null },
621
+ );
622
+
623
+ const borrowerProducts = [...productsMap.values()]
624
+ .filter((product) => product.borrowerId.toString() === borrowerId)
625
+ .sort((a, b) => (a.type > b.type ? 1 : -1));
626
+ for (const product of borrowerProducts) {
627
+ const month = dayjs(end).month() + 1;
628
+ const year = dayjs(end).year();
629
+ const monthEndData = await this.monthEndDataService.getMonthEndData(product._id.toString(), year, month);
630
+ const productType = product.type[0].toUpperCase() + product.type.slice(1).toLowerCase();
631
+ const dataToShow = {
632
+ [EMonthEndDataType.BALANCE]: `Month End ${productType} Loan Balance`,
633
+ [EMonthEndDataType.STATEMENT_BALANCE]: `Month End ${productType} Accrued Statement`,
634
+ };
635
+ Object.entries(dataToShow).forEach(([dataType, title]) => {
636
+ const monthEndDataDoc = monthEndData.find((msd) => msd.dataType === dataType);
637
+ if (monthEndDataDoc) {
638
+ const monthEndLoanBalanceRow: BorrowerSummaryRow = {
639
+ title,
640
+ grossValue: '',
641
+ ineligible: '',
642
+ netValue: '',
643
+ advanceRate: '',
644
+ availability: monthEndDataDoc.dataValue,
645
+ };
646
+ loanBalanceTotalRows.push(monthEndLoanBalanceRow);
647
+ }
648
+ });
649
+ if (monthEndData.length) {
650
+ loanBalanceTotalRows.push({ ...emptyTotalRow, title: '', netValue: null, availability: null, grossValue: null, ineligible: null, advanceRate: null });
651
+ }
652
+ }
653
+
654
+ const loanBalanceTotalDataAsArrays = loanBalanceTotalRows.map((row) => reorderObject(row, headerKeys));
655
+ const totalRows = ['Net Revolver Availability', 'Available To Borrow'];
656
+ const loanBalanceTotalDataAsArraysWithStyles = Object.values([...loanBalanceTotalDataAsArrays]).map((dataRow) => {
657
+ return Object.values(dataRow).map((v) => ({
658
+ v,
659
+ t: typeof v === 'number' ? 'n' : 's',
660
+ z: numberFormat.thousands,
661
+ ...(totalRows.includes(dataRow.title) ? { s: styles.blackOnWhiteWithTopBorder } : {}),
662
+ }));
663
+ });
664
+
665
+ // TERM LOAN
666
+ const getTermLoanSection = async (): Promise<IExcelJsCell[][]> => {
667
+ const termProduct = [...productsMap.values()].find((product) => product.borrowerId.toString() === borrowerId && product.type === ELoanTypes.TERM && product.active);
668
+ if (!termProduct || !availability) {
669
+ return [emptyRow];
670
+ }
671
+
672
+ const equipmentOrder = {
673
+ [EEquipmentTypes.EQUIPMENT]: { title: 'Equipment', s: {} },
674
+ [EEquipmentTypes.REAL_ESTATE]: { title: 'Real estate', s: {} },
675
+ [EEquipmentTypes.BOOT_COLLATERAL]: { title: 'Boot collateral', s: {} },
676
+ 'grossTotal': { title: 'GROSS TOTAL', s: { font: { bold: true } } },
677
+ [EEquipmentTypes.INTELLECTUAL_PROPERTY]: { title: 'Intellectual property', s: {} },
678
+ 'netTotal': { title: 'NET TOTAL', s: { font: { bold: true } } },
679
+ };
680
+
681
+ const rows: IExcelJsCell[][] = [[{ v: 'Inventory type' }, { v: 'Amount' }, { v: 'Advance rate' }, { v: 'Availability' }]];
682
+ Object.entries(equipmentOrder).forEach(([key, desc]) => {
683
+ const newRow: IExcelJsCell[] = [
684
+ { v: desc.title, s: desc.s },
685
+ { v: availability.equipment[key].amount ?? 0, t: 'n', z: numberFormat.thousands, s: desc.s },
686
+ { v: availability.equipment[key].advanceRate ?? 0, t: 'n', z: numberFormat.percent, s: desc.s },
687
+ { v: availability.equipment[key].availability ?? 0, t: 'n', z: numberFormat.thousands, s: desc.s },
688
+ ];
689
+ rows.push(newRow);
690
+ });
691
+
692
+ rows.push(emptyRow);
693
+
694
+ rows.push([{ v: '' }, { v: 'Gross Amount' }, { v: 'Availability' }]);
695
+ rows.push([
696
+ { v: 'Term Reserve' },
697
+ { v: availability.equipment[EEquipmentTypes.EQUIPMENT].amount, t: 'n', z: numberFormat.thousands },
698
+ { v: availability.equipment[EEquipmentTypes.EQUIPMENT].availability, t: 'n', z: numberFormat.thousands },
699
+ ]);
700
+ rows.push([
701
+ { v: 'Loan Balance' },
702
+ { v: 0, t: 'n' },
703
+ { v: availability.loanBalances.TERM, t: 'n', z: numberFormat.thousands },
704
+ ]);
705
+ rows.push([
706
+ { v: 'Net Availability' },
707
+ { v: availability.equipment[EEquipmentTypes.EQUIPMENT].amount, t: 'n', z: numberFormat.thousands },
708
+ {
709
+ v: new Decimal(availability.equipment['netTotal'].availability).sub(availability.loanBalances.TERM).toNumber(),
710
+ t: 'n',
711
+ z: numberFormat.thousands,
712
+ },
713
+ ]);
714
+
715
+ return [
716
+ getSegment('TERM LOAN DETAILS'),
717
+ emptyRow,
718
+ ...rows,
719
+ emptyRow,
720
+ ];
721
+ };
722
+ const termLoanData = await getTermLoanSection();
723
+
724
+ // LOAN ECONOMICS
725
+ const getLoanEconomics = async (): Promise<IExcelJsCell[][]> => {
726
+ const loanEconomicsData: IExcelJsCell[][] = [];
727
+ const products = [...productsMap.values()];
728
+ const yieldProducts = products.filter((product) => product.borrowerId.toString() === borrowerId);
729
+ for (const product of yieldProducts) {
730
+
731
+ const chargesData: IExcelJsCell[][] = [];
732
+
733
+ const productData = productsDataMap.get(product._id.toString());
734
+ loanEconomicsData.push(emptyRow);
735
+ loanEconomicsData.push([
736
+ { v: product.name, s: styles.blackOnWhiteWithBottomBorder },
737
+ ...Array(6).fill({ v: '', s: styles.blackOnWhiteWithBottomBorder }),
738
+ ]);
739
+ if (productData) {
740
+ loanEconomicsData.push([{ v: 'Average Balance Since Inception' }, {
741
+ v: productData.averageBalanceSinceInception,
742
+ t: 'n',
743
+ z: numberFormat.thousands,
744
+ }]);
745
+ loanEconomicsData.push([{ v: 'Total Income Received' }, {
746
+ v: productData.totalIncomeReceived,
747
+ t: 'n',
748
+ z: numberFormat.thousands,
749
+ }]);
750
+ loanEconomicsData.push([{ v: 'Loan Life IRR' }, {
751
+ v: productData.loanLifeIRR,
752
+ t: 'n',
753
+ z: numberFormat.percent,
754
+ }]);
755
+ loanEconomicsData.push([{ v: 'MOIC' }, {
756
+ v: productData.MOIC,
757
+ t: 'n',
758
+ z: numberFormat.xNumber,
759
+ }]);
760
+ }
761
+
762
+ const dataDeep = 5;
763
+ const periodEnd = { month: dayjs(start).month() + 1, year: dayjs(start).year() };
764
+ const periodStart = getShiftedMonth(periodEnd, -dataDeep);
765
+ const monthHeader: IExcelJsCell[] = [];
766
+ const months = [];
767
+ for (let i = 0; i <= dataDeep; i++) {
768
+ const currentMonth = getShiftedMonth(periodEnd, -i);
769
+ monthHeader.push({
770
+ v: `${currentMonth.month}/${currentMonth.year}`,
771
+ t: 's',
772
+ s: { alignment: { horizontal: 'right' } },
773
+ });
774
+ months.push(dayjs(`${currentMonth.year}-${currentMonth.month}-01`).format('YYYY-MM'));
775
+ }
776
+ loanEconomicsData.push(emptyRow);
777
+ loanEconomicsData.push([{ v: '' }, ...monthHeader]);
778
+
779
+ const transactions = await this.loanTransactionsService.getLoanTransactions(
780
+ {
781
+ borrowerId,
782
+ productId: product._id.toString(),
783
+ periodStart: dayjs(`${periodStart.year}-${periodStart.month}-01`).utcOffset(0).toDate(),
784
+ periodEnd: end,
785
+ },
786
+ null,
787
+ false,
788
+ );
789
+
790
+ const grouped = _.groupBy(transactions, (tx) => dayjs(tx.date).format('YYYY-MM'));
791
+ const yieldData = await this.yieldService.getCalculatedYieldTotalsForPeriod(product._id.toString(), periodStart, periodEnd);
792
+ const yieldDataSorted = yieldData.slice().sort((a, b) => {
793
+ if (a.year !== b.year) {
794
+ return b.year - a.year;
795
+ }
796
+ return b.month - a.month;
797
+ });
798
+ const totalDisbursementRow: IExcelJsCell[] = [{ v: 'Total Disbursements in Month' }];
799
+ const countDisbursementRow: IExcelJsCell[] = [{ v: 'Number of Disbursements' }];
800
+ const loanTurnRow: IExcelJsCell[] = [{ v: 'Loan Turn (days)' }];
801
+ const totalCollectionRow: IExcelJsCell[] = [{ v: 'Total Collections in Month' }];
802
+ const countCollectionRow: IExcelJsCell[] = [{ v: 'Number of Collections' }];
803
+
804
+ const charges = [...chargesMap.values()]
805
+ .filter((charge) => charge.productId.toString() === product._id.toString() && charge.includeInYield)
806
+ .sort((a, b) => a.order - b.order);
807
+
808
+ for (const month of months) {
809
+ const txs = grouped[month] ?? [];
810
+ const disbursements = txs.filter((t) => t.transactionType === ELoanTransactionTypes.DISBURSEMENT);
811
+ const collections = txs.filter((t) => t.transactionType === ELoanTransactionTypes.COLLECTION);
812
+ const totalDisbursements = _.sumBy(disbursements, 'amount');
813
+ const totalCollections = Math.abs(_.sumBy(collections, 'amount'));
814
+ totalDisbursementRow.push({ v: totalDisbursements, t: 'n', z: numberFormat.thousands });
815
+ countDisbursementRow.push({ v: disbursements.length, t: 'n', z: numberFormat.shortNumber });
816
+ totalCollectionRow.push({ v: totalCollections, t: 'n', z: numberFormat.thousands });
817
+ countCollectionRow.push({ v: collections.length, t: 'n', z: numberFormat.shortNumber });
818
+ }
819
+
820
+ charges.forEach((charge) => {
821
+ const chargeYieldData = yieldDataSorted.filter((yieldDataDoc) => {
822
+ return yieldDataDoc.chargeId?.toString() === charge._id.toString();
823
+ });
824
+
825
+ const chargeData = chargeYieldData.reduce((acc, chargeYieldDataDoc) => {
826
+ const newRow: IExcelJsCell = chargeYieldDataDoc.valuePercent < 0
827
+ ? negativeCell
828
+ : { v: chargeYieldDataDoc.valuePercent, t: 'n', z: numberFormat.percent };
829
+ return [...acc, newRow];
830
+ }, [{ v: charge.name, t: 's' }]);
831
+ chargesData.push(chargeData);
832
+ });
833
+
834
+ chargesData.push(emptyRow);
835
+
836
+ Object.entries(YIELD_TOTALS_MAP).forEach(([totalType, total]) => {
837
+ const totalYieldData = yieldDataSorted
838
+ .filter((yieldDataDoc) => {
839
+ return yieldDataDoc.totalType === totalType;
840
+ })
841
+ .sort((a, b) => {
842
+ if (a.year !== b.year) {
843
+ return b.year - a.year;
844
+ }
845
+ return b.month - a.month;
846
+ });
847
+ if (totalType === ETotalType.AVERAGE_BALANCE) {
848
+ const loanTurn = totalYieldData.reduce((acc, yieldDataDoc) => {
849
+
850
+ const monthStart = dayjs(`${yieldDataDoc.year}-${yieldDataDoc.month}-01`).utcOffset(0);
851
+ const daysInMonth = monthStart.daysInMonth();
852
+ const txs = grouped[monthStart.format('YYYY-MM')] ?? [];
853
+ const collections = txs.filter((t) => t.transactionType === ELoanTransactionTypes.COLLECTION);
854
+ const totalCollections = _.sumBy(collections, 'amount');
855
+
856
+ const newRow: IExcelJsCell = {
857
+ v: totalCollections === 0 ? 0 : new Decimal(yieldDataDoc.value).div(Math.abs(totalCollections)).mul(daysInMonth).round().toNumber(),
858
+ t: 'n',
859
+ z: numberFormat.thousands,
860
+ };
861
+ return [...acc, newRow];
862
+ }, []);
863
+ loanTurnRow.push(...loanTurn);
864
+ }
865
+
866
+ const replacedValues = [ETotalType.APR_GROSS, ETotalType.APR_LIFE_GROSS];
867
+
868
+ const chargeData = totalYieldData.reduce((acc, yieldDataDoc) => {
869
+ const isPercent = !!yieldDataDoc.valuePercent;
870
+ if (replacedValues.includes(ETotalType[yieldDataDoc.totalType]) && yieldDataDoc.valuePercent < 0) {
871
+ return [...acc, negativeCell];
872
+ }
873
+ const newRow: IExcelJsCell = isPercent
874
+ ? { v: yieldDataDoc.valuePercent, t: 'n', z: numberFormat.percent }
875
+ : { v: yieldDataDoc.value, t: 'n', z: numberFormat.thousands };
876
+ return [...acc, newRow];
877
+ }, [{ v: total.title, t: 's' }]);
878
+ chargesData.push(chargeData);
879
+ });
880
+
881
+ loanEconomicsData.push(totalDisbursementRow);
882
+ loanEconomicsData.push(totalCollectionRow);
883
+ loanEconomicsData.push(loanTurnRow);
884
+ loanEconomicsData.push(countDisbursementRow);
885
+ loanEconomicsData.push(countCollectionRow);
886
+
887
+ loanEconomicsData.push(emptyRow);
888
+
889
+ loanEconomicsData.push(...chargesData);
890
+ }
891
+ return loanEconomicsData;
892
+ };
893
+ const loanEconomicsData = await getLoanEconomics();
894
+
895
+ // FINANCIAL
896
+ const getFinancialDate = async (endDate: Date): Promise<IExcelJsCell[][]> => {
897
+ const month = dayjs(endDate).month();
898
+ const year = dayjs(endDate).year();
899
+ const monthDeep = 11;
900
+ const { data: dataPL, sheets: sheetsPL } = await this.financialSpreadingService.getFinancialSpreadingData({
901
+ borrowerId,
902
+ financialSpreadingType: EFinancialSpreadingType.PROFIT_LOSS,
903
+ selectedMonth: { month, year },
904
+ }, monthDeep);
905
+
906
+ const { data: dataBS, sheets: sheetsBS } = await this.financialSpreadingService.getFinancialSpreadingData({
907
+ borrowerId,
908
+ financialSpreadingType: EFinancialSpreadingType.BALANCE_SHEET,
909
+ selectedMonth: { month, year },
910
+ }, monthDeep);
911
+
912
+ const data = [...dataPL, ...dataBS];
913
+ const sheets = [...sheetsPL, ...sheetsBS];
914
+
915
+ const generateMonthCells = (): IExcelJsCell[] => {
916
+ const result: IExcelJsCell[] = [];
917
+
918
+ for (let i = 0; i <= monthDeep; i++) {
919
+ const date = dayjs(`${year}-${String(month).padStart(2, '0')}-01`).subtract(i, 'month');
920
+ result.push({
921
+ v: date.endOf('month').format(dateFormat),
922
+ t: 's',
923
+ s: rightAlign,
924
+ });
925
+ }
926
+
927
+ return [{ v: 'Last 12 months', t: 's' }, ...result];
928
+ };
929
+
930
+ const percentageIndexes = [EFinanceSpreadingPLTotal.GROSS_MARGIN, EFinanceSpreadingPLTotal.OPERATING_MARGIN] as const;
931
+
932
+ const convertAmount = (index: string, amount: number): number => {
933
+ if ((percentageIndexes as readonly string[]).includes(index)) {
934
+ return new Decimal(amount).div(100).toDP(4).toNumber();
935
+ }
936
+ return amount;
937
+ };
938
+
939
+ const createFinancialIndexes = <T extends Record<string, string>>(
940
+ enumObj: T,
941
+ keys: Array<keyof T>,
942
+ overrides: Partial<Record<keyof T, Partial<FinancialIndexOptions>>> = {},
943
+ ): Record<string, FinancialIndexOptions> =>
944
+ keys.reduce((acc, key) => {
945
+ const enumValue = enumObj[key] as keyof typeof financialSpreadingTotalDictionary;
946
+ acc[enumValue] = {
947
+ format: overrides[key]?.format ?? numberFormat.thousands,
948
+ title: overrides[key]?.title ?? financialSpreadingTotalDictionary[enumValue],
949
+ addEmptyRow: overrides[key]?.addEmptyRow ?? false,
950
+ style: overrides[key]?.style ?? {},
951
+ };
952
+ return acc;
953
+ }, {} as Record<string, FinancialIndexOptions>);
954
+
955
+ const financialPLIndexes = createFinancialIndexes(EFinanceSpreadingPLTotal, [
956
+ EFinanceSpreadingPLTotal.SALES,
957
+ EFinanceSpreadingPLTotal.COST_OF_SALES,
958
+ EFinanceSpreadingPLTotal.GROSS_PROFIT,
959
+ EFinanceSpreadingPLTotal.GROSS_MARGIN,
960
+ EFinanceSpreadingPLTotal.OPERATING_EXPENSES,
961
+ EFinanceSpreadingPLTotal.OPERATING_MARGIN,
962
+ EFinanceSpreadingPLTotal.INCOME_FROM_OPERATIONS,
963
+ EFinanceSpreadingPLTotal.NON_OPERATING_EXPENSES,
964
+ EFinanceSpreadingPLTotal.NON_OPERATING_INCOME,
965
+ EFinanceSpreadingPLTotal.FINANCING_COSTS,
966
+ EFinanceSpreadingPLTotal.DEPRECIATION_AMORTIZATION,
967
+ EFinanceSpreadingPLTotal.TAX,
968
+ EFinanceSpreadingPLTotal.NET_INCOME,
969
+ ], {
970
+ [EFinanceSpreadingPLTotal.GROSS_MARGIN]: {
971
+ format: numberFormat.percent,
972
+ addEmptyRow: true,
973
+ style: { font: { italic: true, name: 'Calibri' } },
974
+ },
975
+ [EFinanceSpreadingPLTotal.OPERATING_MARGIN]: {
976
+ format: numberFormat.percent,
977
+ addEmptyRow: true,
978
+ style: { font: { italic: true, name: 'Calibri' } },
979
+ },
980
+ [EFinanceSpreadingPLTotal.INCOME_FROM_OPERATIONS]: {
981
+ title: 'EBITDA',
982
+ format: numberFormat.thousands,
983
+ style: { font: { bold: true, name: 'Calibri' } },
984
+ },
985
+ [EFinanceSpreadingPLTotal.NON_OPERATING_EXPENSES]: {
986
+ style: { font: { bold: true, name: 'Calibri' } },
987
+ },
988
+ [EFinanceSpreadingPLTotal.NON_OPERATING_INCOME]: {
989
+ style: { font: { bold: true, name: 'Calibri' } },
990
+ },
991
+ [EFinanceSpreadingPLTotal.NET_INCOME]: {
992
+ style: { font: { bold: true, name: 'Calibri' } },
993
+ },
994
+ });
995
+
996
+ const financialBSIndexes = createFinancialIndexes(EFinanceSpreadingBSTotal, [
997
+ EFinanceSpreadingBSTotal.CASH_EQUIVALENTS,
998
+ EFinanceSpreadingBSTotal.TRADE_RECEIVABLES,
999
+ EFinanceSpreadingBSTotal.INVENTORY,
1000
+ EFinanceSpreadingBSTotal.OTHER_CURRENT_ASSETS,
1001
+ EFinanceSpreadingBSTotal.TOTAL_CURRENT_ASSET,
1002
+ EFinanceSpreadingBSTotal.TOTAL_ASSETS,
1003
+ EFinanceSpreadingBSTotal.ACCOUNTS_PAYABLE,
1004
+ EFinanceSpreadingBSTotal.OTHER_CURRENT_LIABILITIES,
1005
+ EFinanceSpreadingBSTotal.TOTAL_CURRENT_LIABILITIES,
1006
+ EFinanceSpreadingBSTotal.COMPANY_DEBT,
1007
+ EFinanceSpreadingBSTotal.OTHER_DEBT,
1008
+ EFinanceSpreadingBSTotal.OTHER_LONG_TERM_LIABILITIES,
1009
+ EFinanceSpreadingBSTotal.TOTAL_LIABILITIES,
1010
+ ], {
1011
+ [EFinanceSpreadingBSTotal.TOTAL_CURRENT_ASSET]: { style: { font: { bold: true, name: 'Calibri' } } },
1012
+ [EFinanceSpreadingBSTotal.TOTAL_ASSETS]: {
1013
+ addEmptyRow: true,
1014
+ style: { font: { bold: true, name: 'Calibri' } },
1015
+ },
1016
+ [EFinanceSpreadingBSTotal.TOTAL_CURRENT_LIABILITIES]: {
1017
+ addEmptyRow: true,
1018
+ style: { font: { bold: true, name: 'Calibri' } },
1019
+ },
1020
+ [EFinanceSpreadingBSTotal.COMPANY_DEBT]: { title: 'Senior Debt' },
1021
+ [EFinanceSpreadingBSTotal.TOTAL_LIABILITIES]: { style: { font: { bold: true, name: 'Calibri' } } },
1022
+ });
1023
+
1024
+ const generateFinancialRows = <T extends Record<string, string>>(financialIndexes: Record<string, FinancialIndexOptions>, enumObj: T): IExcelJsCell[][] => {
1025
+ return Object.entries(financialIndexes).reduce((acc, [financialIndex, options]) => {
1026
+ const sheet = sheets.find((s) => financialIndex === enumObj[s.rowType] && s.isTotal);
1027
+ if (!sheet) {
1028
+ return acc;
1029
+ }
1030
+
1031
+ const dataEntry = data.find((d) => d.sheetId === sheet._id);
1032
+ if (!dataEntry) {
1033
+ return acc;
1034
+ }
1035
+
1036
+ const baseValue = convertAmount(enumObj[financialIndex as keyof T], dataEntry.amount);
1037
+ const monthlyValues = Array.from({ length: monthDeep }, (_, i) => ({
1038
+ v: convertAmount(enumObj[financialIndex as keyof T], dataEntry[`minus_${i + 1}`]),
1039
+ t: 'n',
1040
+ z: options.format,
1041
+ s: options.style,
1042
+ }));
1043
+
1044
+ const row: IExcelJsCell[] = [
1045
+ { v: options.title, t: 's', s: options.style },
1046
+ { v: baseValue, t: 'n', z: options.format },
1047
+ ...monthlyValues,
1048
+ ];
1049
+
1050
+ return options.addEmptyRow ? [...acc, row, emptyRow] : [...acc, row];
1051
+ }, [] as IExcelJsCell[][]);
1052
+ };
1053
+
1054
+ type FinancialRatioFormula = {
1055
+ label: string;
1056
+ dependencies: string[];
1057
+ format?: string;
1058
+ formula: (data: Record<string, FinancialSpreadingDTO>, monthIndex: number) => number;
1059
+ };
1060
+
1061
+ const get = (base: Record<string, FinancialSpreadingDTO>, i: number, key: string) => new Decimal(i === 0 ? base[key]?.amount : base[key]?.[`minus_${i}`] ?? 0);
1062
+
1063
+ const financialRatioFormulas: FinancialRatioFormula[] = [
1064
+ {
1065
+ label: 'Fixed Charge Coverage Ratio',
1066
+ dependencies: [EFinanceSpreadingPLTotal.FINANCING_COSTS, EFinanceSpreadingPLTotal.DEPRECIATION_AMORTIZATION, EFinanceSpreadingPLTotal.INCOME_FROM_OPERATIONS],
1067
+ format: numberFormat.xNumber1DP,
1068
+ formula: (base, i) => {
1069
+ const EBITDA = get(base, i, EFinanceSpreadingPLTotal.INCOME_FROM_OPERATIONS);
1070
+ const DepreciationAmortization = get(base, i, EFinanceSpreadingPLTotal.DEPRECIATION_AMORTIZATION);
1071
+ const FinancingCost = get(base, i, EFinanceSpreadingPLTotal.FINANCING_COSTS);
1072
+ if (FinancingCost.lessThanOrEqualTo(0)) {
1073
+ return null;
1074
+ }
1075
+ const result = EBITDA.sub(DepreciationAmortization).add(FinancingCost).div(FinancingCost).toDP(4);
1076
+ if (result.lessThanOrEqualTo(0)) {
1077
+ return null;
1078
+ }
1079
+ return result.toNumber();
1080
+ },
1081
+ },
1082
+ {
1083
+ label: 'Interest Coverage Ratio',
1084
+ dependencies: [EFinanceSpreadingPLTotal.FINANCING_COSTS, EFinanceSpreadingPLTotal.DEPRECIATION_AMORTIZATION, EFinanceSpreadingPLTotal.INCOME_FROM_OPERATIONS],
1085
+ format: numberFormat.xNumber1DP,
1086
+ formula: (base, i) => {
1087
+ const EBITDA = get(base, i, EFinanceSpreadingPLTotal.INCOME_FROM_OPERATIONS);
1088
+ const DepreciationAmortization = get(base, i, EFinanceSpreadingPLTotal.DEPRECIATION_AMORTIZATION);
1089
+ const FinancingCost = get(base, i, EFinanceSpreadingPLTotal.FINANCING_COSTS);
1090
+ if (FinancingCost.lessThanOrEqualTo(0)) {
1091
+ return null;
1092
+ }
1093
+ const result = EBITDA.add(DepreciationAmortization).div(FinancingCost).toDP(4);
1094
+ if (result.lessThanOrEqualTo(0)) {
1095
+ return null;
1096
+ }
1097
+ return result.toNumber();
1098
+ },
1099
+ },
1100
+ {
1101
+ label: 'Total Debt / EBITDA',
1102
+ dependencies: [EFinanceSpreadingPLTotal.INCOME_FROM_OPERATIONS, EFinanceSpreadingBSTotal.COMPANY_DEBT, EFinanceSpreadingBSTotal.OTHER_DEBT],
1103
+ format: numberFormat.xNumber1DP,
1104
+ formula: (base, i) => {
1105
+ const EBITDA = get(base, i, EFinanceSpreadingPLTotal.INCOME_FROM_OPERATIONS);
1106
+ const CompanyDebt = get(base, i, EFinanceSpreadingBSTotal.COMPANY_DEBT);
1107
+ const OtherDebt = get(base, i, EFinanceSpreadingBSTotal.OTHER_DEBT);
1108
+ if (EBITDA.lessThanOrEqualTo(0)) {
1109
+ return null;
1110
+ }
1111
+ const result = CompanyDebt.add(OtherDebt).abs().div(EBITDA).toDP(4);
1112
+ if (result.lessThanOrEqualTo(0)) {
1113
+ return null;
1114
+ }
1115
+ return result.toNumber();
1116
+ },
1117
+ },
1118
+ {
1119
+ label: 'Senior Debt / EBITDA',
1120
+ dependencies: [EFinanceSpreadingPLTotal.INCOME_FROM_OPERATIONS, EFinanceSpreadingBSTotal.COMPANY_DEBT],
1121
+ format: numberFormat.xNumber1DP,
1122
+ formula: (base, i) => {
1123
+ const EBITDA = get(base, i, EFinanceSpreadingPLTotal.INCOME_FROM_OPERATIONS);
1124
+ const CompanyDebt = get(base, i, EFinanceSpreadingBSTotal.COMPANY_DEBT);
1125
+ if (EBITDA.lessThanOrEqualTo(0)) {
1126
+ return null;
1127
+ }
1128
+ const result = CompanyDebt.div(EBITDA).toDP(4);
1129
+ if (result.lessThanOrEqualTo(0)) {
1130
+ return null;
1131
+ }
1132
+ return result.toNumber();
1133
+ },
1134
+ },
1135
+ {
1136
+ label: 'AR Turnover Days',
1137
+ dependencies: [EFinanceSpreadingPLTotal.SALES, EFinanceSpreadingBSTotal.TRADE_RECEIVABLES],
1138
+ format: numberFormat.fullNumber,
1139
+ formula: (base, i) => {
1140
+ const Sales = get(base, i, EFinanceSpreadingPLTotal.SALES);
1141
+ const TradeReceivables = get(base, i, EFinanceSpreadingBSTotal.TRADE_RECEIVABLES);
1142
+ if (Sales.lessThanOrEqualTo(0)) {
1143
+ return null;
1144
+ }
1145
+ const result = TradeReceivables.abs().div(Sales).mul(30).toDP(0);
1146
+ if (result.lessThanOrEqualTo(0)) {
1147
+ return null;
1148
+ }
1149
+ return result.toNumber();
1150
+ },
1151
+ },
1152
+ {
1153
+ label: 'AP Turnover Days',
1154
+ dependencies: [EFinanceSpreadingPLTotal.COST_OF_SALES, EFinanceSpreadingBSTotal.ACCOUNTS_PAYABLE],
1155
+ format: numberFormat.fullNumber,
1156
+ formula: (base, i) => {
1157
+ const CostOfSales = get(base, i, EFinanceSpreadingPLTotal.COST_OF_SALES);
1158
+ const AccountsPayable = get(base, i, EFinanceSpreadingBSTotal.ACCOUNTS_PAYABLE);
1159
+ if (CostOfSales.lessThanOrEqualTo(0)) {
1160
+ return null;
1161
+ }
1162
+ const result = AccountsPayable.abs().div(CostOfSales).mul(30).toDP(0);
1163
+ if (result.lessThanOrEqualTo(0)) {
1164
+ return null;
1165
+ }
1166
+ return result.toNumber();
1167
+ },
1168
+ },
1169
+ {
1170
+ label: 'Inventory Turnover Days',
1171
+ dependencies: [EFinanceSpreadingPLTotal.COST_OF_SALES, EFinanceSpreadingBSTotal.INVENTORY],
1172
+ format: numberFormat.fullNumber,
1173
+ formula: (base, i) => {
1174
+ const CostOfSales = get(base, i, EFinanceSpreadingPLTotal.COST_OF_SALES);
1175
+ const Inventory = get(base, i, EFinanceSpreadingBSTotal.INVENTORY);
1176
+ if (CostOfSales.lessThanOrEqualTo(0)) {
1177
+ return null;
1178
+ }
1179
+ const result = Inventory.abs().div(CostOfSales).mul(30).toDP(0);
1180
+ if (result.lessThanOrEqualTo(0)) {
1181
+ return null;
1182
+ }
1183
+ return result.toNumber();
1184
+ },
1185
+ },
1186
+ ];
1187
+
1188
+ const generateFinancialRatios = (): IExcelJsCell[][] => {
1189
+ const ratiosData: IExcelJsCell[][] = [[{ v: 'FINANCIAL RATIOS:' }]];
1190
+
1191
+ const financialRatiosBaseKeys = _.uniq(financialRatioFormulas.flatMap((f) => f.dependencies));
1192
+
1193
+ const financialRatiosBase = financialRatiosBaseKeys.reduce((acc, key) => {
1194
+ const sheet = sheets.find((s) => s.rowType === key && s.isTotal);
1195
+ const dataEntry = data.find((d) => d.sheetId.toString() === sheet._id.toString());
1196
+ return { ...acc, [key]: dataEntry };
1197
+ }, {} as Record<string, FinancialSpreadingDTO>);
1198
+
1199
+ const ratioRows = financialRatioFormulas.map(({ label, formula, format }) => {
1200
+ const row: IExcelJsCell[] = [{ v: label, t: 's' }];
1201
+ for (let i = 0; i <= monthDeep; i++) {
1202
+ const result = formula(financialRatiosBase, i);
1203
+ if (result) {
1204
+ row.push({ v: result, t: 'n', z: format ?? numberFormat.thousands });
1205
+ } else {
1206
+ row.push(negativeCell);
1207
+ }
1208
+ }
1209
+ return row;
1210
+ });
1211
+
1212
+ return [...ratiosData, ...ratioRows];
1213
+ };
1214
+
1215
+ const financialPLDataFull = generateFinancialRows(financialPLIndexes, EFinanceSpreadingPLTotal);
1216
+ const financialBSDataFull = generateFinancialRows(financialBSIndexes, EFinanceSpreadingBSTotal);
1217
+ const financialRatios = generateFinancialRatios();
1218
+
1219
+ return [
1220
+ generateMonthCells(),
1221
+ emptyRow,
1222
+ [{ v: 'PROFIT & LOSS HIGHLIGHTS', t: 's' }],
1223
+ ...financialPLDataFull,
1224
+ emptyRow,
1225
+ [{ v: 'BALANCE SHEET HIGHLIGHTS', t: 's' }],
1226
+ ...financialBSDataFull,
1227
+ emptyRow,
1228
+ ...financialRatios,
1229
+ ];
1230
+ };
1231
+ const financialData = await getFinancialDate(end);
1232
+
1233
+ // HEADROOM
1234
+ const getHeadRoomRows = async (): Promise<IExcelJsCell[][]> => {
1235
+ if (!borrowerSummary) {
1236
+ return [];
1237
+ }
1238
+ const headSourceData = borrowerSummary.chartData.LAST_6_MONTH.data.reverse();
1239
+ const headRoomHeader: IExcelJsCell[] = headSourceData.reduce((acc, row) => {
1240
+ return [...acc, { v: row.bbc, t: 's', s: rightAlign }];
1241
+ }, <IExcelJsCell[]>[{ v: 'Last 6 months' }]);
1242
+ const totalRevolver: number[] = headSourceData.map((dataRow) => dataRow.revolverCollateralTotal);
1243
+ const totalRevolverBalance: number[] = headSourceData.map((dataRow) => dataRow.revolverLoanBalance);
1244
+ const headRoom: number[] = headSourceData.map((dataRow) => {
1245
+ if (dataRow.revolverCollateralTotal === 0) {
1246
+ return 0;
1247
+ }
1248
+ return new Decimal(dataRow.revolverCollateralTotal).sub(dataRow.revolverLoanBalance).div(dataRow.revolverCollateralTotal).toDP(2).toNumber();
1249
+ });
1250
+ const totalRevolverWithStyle: IExcelJsCell[] = totalRevolver.map((v) => ({
1251
+ v,
1252
+ t: 'n',
1253
+ z: numberFormat.thousands,
1254
+ }));
1255
+ const totalRevolverBalanceWithStyle: IExcelJsCell[] = totalRevolverBalance.map((v) => ({
1256
+ v,
1257
+ t: 'n',
1258
+ z: numberFormat.thousands,
1259
+ }));
1260
+ const headRoomWithStyle: IExcelJsCell[] = headRoom.map((v) => ({ v, t: 'n', z: '0.00%' }));
1261
+ const headRoomRows = [
1262
+ [{ v: 'Total Revolver Availability', t: 's' }, ...totalRevolverWithStyle],
1263
+ [{ v: 'Revolver Loan Balance', t: 's' }, ...totalRevolverBalanceWithStyle],
1264
+ [{ v: 'Headroom %', t: 's' }, ...headRoomWithStyle],
1265
+ ];
1266
+ return [headRoomHeader, ...headRoomRows];
1267
+ };
1268
+
1269
+ const headRoomData = await getHeadRoomRows();
1270
+
1271
+ // TOP 5
1272
+ const getTop5Data = async () => {
1273
+ const top5SKUs_new = await this.borrowerSummaryService.getTop5SKUsData(borrowerId);
1274
+ const top5CustomerConcentration_new = await this.borrowerSummaryService.getTop5CustomersConcentrationData(borrowerId);
1275
+ const gap = 1;
1276
+
1277
+ const titleStyleLeft = {
1278
+ font: { bold: true, name: 'Calibri' },
1279
+ alignment: { horizontal: 'left' },
1280
+ };
1281
+
1282
+ const titleStyleRight = {
1283
+ font: { bold: true, name: 'Calibri' },
1284
+ alignment: { horizontal: 'right' },
1285
+ };
1286
+
1287
+ const getTop5SummaryTable = (
1288
+ title: string,
1289
+ headers: string[],
1290
+ rows: { _id: string, totalAmount: number }[],
1291
+ startColIndex: number,
1292
+ ): IExcelJsCell[][] => {
1293
+ const result: IExcelJsCell[][] = [];
1294
+
1295
+ const total = rows.reduce((acc, row) => new Decimal(acc).add(row.totalAmount).toNumber(), 0);
1296
+
1297
+ // Title
1298
+ result.push(
1299
+ Array(startColIndex).fill({ v: '' }).concat([
1300
+ { v: title, t: 's', s: titleStyleLeft },
1301
+ { v: '' },
1302
+ { v: '' },
1303
+ ]),
1304
+ );
1305
+
1306
+ // Headers
1307
+ result.push(
1308
+ Array(startColIndex).fill({ v: '' }).concat(
1309
+ headers.map((header, index) => ({ v: header, t: 's', s: index > 0 ? titleStyleRight : titleStyleLeft })),
1310
+ ),
1311
+ );
1312
+
1313
+ // Data rows
1314
+ for (const row of rows.slice(0, 5)) {
1315
+ result.push(
1316
+ Array(startColIndex).fill({ v: '' }).concat([
1317
+ { v: row._id, t: 's' },
1318
+ {
1319
+ v: row.totalAmount,
1320
+ t: 'n',
1321
+ z: numberFormat.thousands,
1322
+ },
1323
+ {
1324
+ v: new Decimal(row.totalAmount).div(total).toDP(4).toNumber(),
1325
+ t: 'n',
1326
+ z: numberFormat.percent,
1327
+ },
1328
+ ]),
1329
+ );
1330
+ }
1331
+ return result;
1332
+ };
1333
+
1334
+ const leftTable = getTop5SummaryTable(
1335
+ 'Top 5 SKU',
1336
+ ['SKU', 'Value', '%'],
1337
+ top5SKUs_new,
1338
+ 1,
1339
+ );
1340
+
1341
+ const rightTable = getTop5SummaryTable(
1342
+ 'Top 5 customers',
1343
+ ['Customer', 'Value', '%'],
1344
+ top5CustomerConcentration_new,
1345
+ gap,
1346
+ );
1347
+
1348
+ const maxRows = Math.max(leftTable.length, rightTable.length);
1349
+ const combined: IExcelJsCell[][] = [];
1350
+
1351
+ for (let i = 0; i < maxRows; i++) {
1352
+ combined.push([
1353
+ ...(leftTable[i] || []),
1354
+ ...(rightTable[i] || []),
1355
+ ]);
1356
+ }
1357
+ return combined;
1358
+ };
1359
+ const top5Data = await getTop5Data();
1360
+
1361
+ // COMPLIANCE REPORT
1362
+ const complianceItemsHeader: ComplianceItemRow = {
1363
+ name: 'Item',
1364
+ dueDate: 'Due date',
1365
+ progress: 'Progress',
1366
+ submittedDate: 'Submitted date',
1367
+ status: 'Status',
1368
+ };
1369
+ const complianceItemsHeaderKeys = Object.keys(complianceItemsHeader);
1370
+ const complianceItemsHeaderAsArray = reorderObject(complianceItemsHeader, complianceItemsHeaderKeys);
1371
+ const complianceItemsHeaderAsArrayWithStyles = Object.values(complianceItemsHeaderAsArray).reduce((acc, v) => [...acc, {
1372
+ v,
1373
+ t: 's',
1374
+ }], []);
1375
+
1376
+ const complianceBorrower = complianceBorrowersMap.get(borrowerId);
1377
+ const fullComplianceBorrower = await this.complianceBorrowersService.getFullComplianceBorrowerById(complianceBorrower._id.toString());
1378
+ const dueItems = [];
1379
+ const acceptedItems = [];
1380
+ const previousMonthStart = dayjs(start).utcOffset(0).subtract(1, 'months').startOf('month');
1381
+
1382
+ if (fullComplianceBorrower) {
1383
+ fullComplianceBorrower.items.forEach((item) => {
1384
+ item.instances.forEach((instance) => {
1385
+ const row: ComplianceItemRow = {
1386
+ name: `${item.item?.name} (${dayjs(instance.nextDate).format('MMM.D, YYYY')})`,
1387
+ dueDate: instance.nextDate ? dayjs(instance.nextDate).format('YYYY/MM/DD') : '-',
1388
+ progress: instance.progress?.text ?? '-',
1389
+ submittedDate: instance.submittedDate ? dayjs(instance.submittedDate).format('YYYY/MM/DD') : '-',
1390
+ status: instance.status,
1391
+ };
1392
+ if (instance.status === EComplianceItemStatus.ACCEPTED) {
1393
+ if (previousMonthStart.isBefore(dayjs(instance.submittedDate))) {
1394
+ acceptedItems.push(row);
1395
+ }
1396
+ } else {
1397
+ dueItems.push(row);
1398
+ }
1399
+ });
1400
+ });
1401
+ }
1402
+ const dueItemsDataAsArrays = dueItems.map((row) => reorderObject(row, complianceItemsHeaderKeys));
1403
+ const dueItemsDataAsArraysWithStyles = Object.values([...dueItemsDataAsArrays]).map((dataRow) => {
1404
+ return Object.values(dataRow).map((v) => ({ v, t: 's' }));
1405
+ });
1406
+ const acceptedItemsDataAsArrays = acceptedItems.map((row) => reorderObject(row, complianceItemsHeaderKeys));
1407
+ const acceptedItemsDataAsArraysWithStyles = Object.values([...acceptedItemsDataAsArrays]).map((dataRow) => {
1408
+ return Object.values(dataRow).map((v) => ({ v, t: 's' }));
1409
+ });
1410
+
1411
+ return [
1412
+ borrowerTitle,
1413
+ borrowerSubTitle,
1414
+ [{ v: 'ALL VALUES $\'000' }],
1415
+ emptyRow,
1416
+
1417
+ getSegment(`REVOLVER LOAN BALANCE & COLLATERAL - AS AT ${dayjs(lastBBC.bbcDate).utcOffset(0).format(dateFormat)} (LAST SIGNED BBC)`),
1418
+ headerWithStyles,
1419
+ ...inventoryDataWithStyles,
1420
+ emptyRow,
1421
+ ...receivableDataWithStyles,
1422
+ emptyRow,
1423
+ ...reserveDataWithStyles,
1424
+ emptyRow,
1425
+ ...loanBalanceTotalDataAsArraysWithStyles,
1426
+
1427
+ ...termLoanData,
1428
+
1429
+ getSegment('LOAN ECONOMICS'),
1430
+ ...loanEconomicsData,
1431
+ emptyRow,
1432
+
1433
+ getSegment('FINANCIALS'),
1434
+ emptyRow,
1435
+ ...financialData,
1436
+ emptyRow,
1437
+
1438
+ getSegment('HEADROOM'),
1439
+ emptyRow,
1440
+ ...headRoomData,
1441
+ emptyRow,
1442
+
1443
+ getSegment('TOP 5'),
1444
+ emptyRow,
1445
+ ...top5Data,
1446
+ emptyRow,
1447
+
1448
+ getSegment('COMPLIANCE REPORT'),
1449
+ complianceItemsHeaderAsArrayWithStyles,
1450
+ emptyRow,
1451
+ ...dueItemsDataAsArraysWithStyles,
1452
+ emptyRow,
1453
+ ...acceptedItemsDataAsArraysWithStyles,
1454
+ ];
1455
+ };
1456
+
1457
+ private async getPaidAmount(productId: string) {
1458
+ const loanCharges = await this.loanChargesService.getLoanChargeForProduct(productId);
1459
+ const yieldLoanCharges = loanCharges.filter((charge) => charge.includeInYield);
1460
+
1461
+ const totals = await LoanStatementTransactionModel.aggregate<{ totalAmountPaid: number }>([
1462
+ {
1463
+ '$match': {
1464
+ 'chargeId': {
1465
+ '$in': yieldLoanCharges.map((charge) => charge._id),
1466
+ },
1467
+ },
1468
+ }, {
1469
+ '$group': {
1470
+ '_id': null,
1471
+ 'totalAmountPaid': {
1472
+ '$sum': '$amountPaid',
1473
+ },
1474
+ },
1475
+ }, {
1476
+ '$project': {
1477
+ '_id': 0,
1478
+ 'totalAmountPaid': {
1479
+ '$round': [
1480
+ '$totalAmountPaid', 2,
1481
+ ],
1482
+ },
1483
+ },
1484
+ },
1485
+ ]);
1486
+ if (totals.length > 0) {
1487
+ return totals[0].totalAmountPaid;
1488
+ }
1489
+ return 0;
1490
+ };
1491
+
1492
+ async getNewSummaryData(borrowerIds: string[], start: Date, end: Date): Promise<{ [p: string]: IExcelJsCell[][] }> {
1493
+ const allBorrowers = await BorrowerModel.find().lean();
1494
+ allBorrowers.forEach((borrower) => borrowersMap.set(borrower._id.toString(), borrower));
1495
+
1496
+ const allProducts = await LoanProduct.find().lean();
1497
+ allProducts.forEach((product) => productsMap.set(product._id.toString(), product));
1498
+
1499
+ const allCharges = await LoanCharge.find().lean();
1500
+ allCharges.forEach((charge) => chargesMap.set(charge._id.toString(), charge));
1501
+
1502
+ const allComplianceBorrowers = await BorrowerCompliance.find().lean();
1503
+ allComplianceBorrowers.forEach((borrower) => complianceBorrowersMap.set(borrower.borrower.toString(), borrower));
1504
+
1505
+ const mainData = await this.getMainData(borrowerIds, start, end);
1506
+ const borrowersSummary = await this.getAllBorrowersSummary(borrowerIds, start, end);
1507
+ return { ...mainData, ...borrowersSummary };
1508
+ };
1509
+ }