@hed-hog/finance 0.0.279 → 0.0.285
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/dist/dto/finance-report-query.dto.d.ts +16 -0
- package/dist/dto/finance-report-query.dto.d.ts.map +1 -0
- package/dist/dto/finance-report-query.dto.js +59 -0
- package/dist/dto/finance-report-query.dto.js.map +1 -0
- package/dist/finance-reports.controller.d.ts +71 -0
- package/dist/finance-reports.controller.d.ts.map +1 -0
- package/dist/finance-reports.controller.js +61 -0
- package/dist/finance-reports.controller.js.map +1 -0
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +2 -0
- package/dist/finance.module.js.map +1 -1
- package/dist/finance.service.d.ts +93 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +456 -0
- package/dist/finance.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/hedhog/data/route.yaml +27 -0
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +158 -125
- package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +102 -88
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +113 -89
- package/hedhog/frontend/app/reports/_lib/use-finance-reports.ts.ejs +233 -0
- package/hedhog/frontend/app/reports/overview-results/page.tsx.ejs +96 -78
- package/hedhog/frontend/app/reports/top-customers/page.tsx.ejs +247 -130
- package/hedhog/frontend/app/reports/top-operational-expenses/page.tsx.ejs +250 -135
- package/hedhog/frontend/messages/en.json +33 -2
- package/hedhog/frontend/messages/pt.json +33 -2
- package/package.json +6 -6
- package/src/dto/finance-report-query.dto.ts +49 -0
- package/src/finance-reports.controller.ts +28 -0
- package/src/finance.module.ts +2 -0
- package/src/finance.service.ts +645 -10
- package/src/index.ts +1 -0
package/src/finance.service.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
PageOrderDirection,
|
|
4
|
+
PaginationDTO,
|
|
5
|
+
PaginationService,
|
|
6
6
|
} from '@hed-hog/api-pagination';
|
|
7
7
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
8
8
|
import { AiService, FileService, SettingService } from '@hed-hog/core';
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
BadRequestException,
|
|
11
|
+
ConflictException,
|
|
12
|
+
forwardRef,
|
|
13
|
+
Inject,
|
|
14
|
+
Injectable,
|
|
15
|
+
Logger,
|
|
16
|
+
NotFoundException,
|
|
17
17
|
} from '@nestjs/common';
|
|
18
18
|
import { createHash } from 'node:crypto';
|
|
19
19
|
import { readFile } from 'node:fs/promises';
|
|
@@ -52,6 +52,7 @@ type TitleStatus =
|
|
|
52
52
|
| 'overdue';
|
|
53
53
|
|
|
54
54
|
type ForecastScenario = 'base' | 'pessimista' | 'otimista';
|
|
55
|
+
type FinanceReportGroupBy = 'day' | 'week' | 'month' | 'year';
|
|
55
56
|
|
|
56
57
|
type FinancialScenarioConfig = {
|
|
57
58
|
id: ForecastScenario;
|
|
@@ -932,6 +933,465 @@ export class FinanceService {
|
|
|
932
933
|
};
|
|
933
934
|
}
|
|
934
935
|
|
|
936
|
+
async getOverviewResultsReport(filters?: {
|
|
937
|
+
from?: string;
|
|
938
|
+
to?: string;
|
|
939
|
+
groupBy?: FinanceReportGroupBy;
|
|
940
|
+
}) {
|
|
941
|
+
const { fromDate, toDate } = this.resolveReportDateRange(
|
|
942
|
+
filters?.from,
|
|
943
|
+
filters?.to,
|
|
944
|
+
);
|
|
945
|
+
const groupBy = this.resolveReportGroupBy(filters?.groupBy);
|
|
946
|
+
const installments = await this.prisma.financial_installment.findMany({
|
|
947
|
+
where: {
|
|
948
|
+
competence_date: {
|
|
949
|
+
gte: fromDate,
|
|
950
|
+
lte: toDate,
|
|
951
|
+
},
|
|
952
|
+
status: {
|
|
953
|
+
not: 'canceled',
|
|
954
|
+
},
|
|
955
|
+
financial_title: {
|
|
956
|
+
status: {
|
|
957
|
+
notIn: ['draft', 'canceled'],
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
select: {
|
|
962
|
+
competence_date: true,
|
|
963
|
+
amount_cents: true,
|
|
964
|
+
financial_title: {
|
|
965
|
+
select: {
|
|
966
|
+
title_type: true,
|
|
967
|
+
finance_category: {
|
|
968
|
+
select: {
|
|
969
|
+
code: true,
|
|
970
|
+
name: true,
|
|
971
|
+
kind: true,
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
orderBy: {
|
|
978
|
+
competence_date: 'asc',
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const grouped = new Map<
|
|
983
|
+
string,
|
|
984
|
+
{
|
|
985
|
+
period: string;
|
|
986
|
+
faturamento: number;
|
|
987
|
+
despesasEmprestimos: number;
|
|
988
|
+
diferenca: number;
|
|
989
|
+
aporteInvestidor: number;
|
|
990
|
+
emprestimoBanco: number;
|
|
991
|
+
despesas: number;
|
|
992
|
+
}
|
|
993
|
+
>();
|
|
994
|
+
|
|
995
|
+
for (const installment of installments) {
|
|
996
|
+
const category = installment.financial_title.finance_category;
|
|
997
|
+
|
|
998
|
+
if (this.isTransferReportCategory(category)) {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const period = this.getReportBucketKey(
|
|
1003
|
+
installment.competence_date,
|
|
1004
|
+
groupBy,
|
|
1005
|
+
);
|
|
1006
|
+
const current = grouped.get(period) || {
|
|
1007
|
+
period,
|
|
1008
|
+
faturamento: 0,
|
|
1009
|
+
despesasEmprestimos: 0,
|
|
1010
|
+
diferenca: 0,
|
|
1011
|
+
aporteInvestidor: 0,
|
|
1012
|
+
emprestimoBanco: 0,
|
|
1013
|
+
despesas: 0,
|
|
1014
|
+
};
|
|
1015
|
+
const amount = this.fromCents(installment.amount_cents);
|
|
1016
|
+
|
|
1017
|
+
if (installment.financial_title.title_type === 'receivable') {
|
|
1018
|
+
if (this.isInvestorContributionReportCategory(category)) {
|
|
1019
|
+
current.aporteInvestidor += amount;
|
|
1020
|
+
} else if (this.isLoanReportCategory(category)) {
|
|
1021
|
+
current.emprestimoBanco += amount;
|
|
1022
|
+
} else {
|
|
1023
|
+
current.faturamento += amount;
|
|
1024
|
+
}
|
|
1025
|
+
} else if (this.isLoanReportCategory(category)) {
|
|
1026
|
+
current.emprestimoBanco += amount;
|
|
1027
|
+
} else {
|
|
1028
|
+
current.despesas += amount;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
current.despesasEmprestimos =
|
|
1032
|
+
current.despesas + current.emprestimoBanco;
|
|
1033
|
+
current.diferenca = current.faturamento - current.despesasEmprestimos;
|
|
1034
|
+
|
|
1035
|
+
grouped.set(period, current);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const rows = Array.from(grouped.values())
|
|
1039
|
+
.sort((a, b) => this.sortReportBuckets(a.period, b.period, groupBy))
|
|
1040
|
+
.map((row) => ({
|
|
1041
|
+
period: row.period,
|
|
1042
|
+
faturamento: this.roundCurrency(row.faturamento),
|
|
1043
|
+
despesasEmprestimos: this.roundCurrency(row.despesasEmprestimos),
|
|
1044
|
+
diferenca: this.roundCurrency(row.diferenca),
|
|
1045
|
+
aporteInvestidor: this.roundCurrency(row.aporteInvestidor),
|
|
1046
|
+
emprestimoBanco: this.roundCurrency(row.emprestimoBanco),
|
|
1047
|
+
despesas: this.roundCurrency(row.despesas),
|
|
1048
|
+
}));
|
|
1049
|
+
|
|
1050
|
+
const totals = rows.reduce(
|
|
1051
|
+
(acc, row) => {
|
|
1052
|
+
acc.faturamento += row.faturamento;
|
|
1053
|
+
acc.despesasEmprestimos += row.despesasEmprestimos;
|
|
1054
|
+
acc.diferenca += row.diferenca;
|
|
1055
|
+
acc.aporteInvestidor += row.aporteInvestidor;
|
|
1056
|
+
acc.emprestimoBanco += row.emprestimoBanco;
|
|
1057
|
+
acc.despesas += row.despesas;
|
|
1058
|
+
return acc;
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
faturamento: 0,
|
|
1062
|
+
despesasEmprestimos: 0,
|
|
1063
|
+
diferenca: 0,
|
|
1064
|
+
aporteInvestidor: 0,
|
|
1065
|
+
emprestimoBanco: 0,
|
|
1066
|
+
despesas: 0,
|
|
1067
|
+
},
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
return {
|
|
1071
|
+
rows,
|
|
1072
|
+
totals: {
|
|
1073
|
+
faturamento: this.roundCurrency(totals.faturamento),
|
|
1074
|
+
despesasEmprestimos: this.roundCurrency(totals.despesasEmprestimos),
|
|
1075
|
+
diferenca: this.roundCurrency(totals.diferenca),
|
|
1076
|
+
aporteInvestidor: this.roundCurrency(totals.aporteInvestidor),
|
|
1077
|
+
emprestimoBanco: this.roundCurrency(totals.emprestimoBanco),
|
|
1078
|
+
despesas: this.roundCurrency(totals.despesas),
|
|
1079
|
+
margem:
|
|
1080
|
+
totals.faturamento > 0
|
|
1081
|
+
? this.roundCurrency(
|
|
1082
|
+
(totals.diferenca / Math.abs(totals.faturamento)) * 100,
|
|
1083
|
+
)
|
|
1084
|
+
: 0,
|
|
1085
|
+
},
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
async getTopCustomersReport(filters?: {
|
|
1090
|
+
from?: string;
|
|
1091
|
+
to?: string;
|
|
1092
|
+
groupBy?: FinanceReportGroupBy;
|
|
1093
|
+
topN?: number;
|
|
1094
|
+
search?: string;
|
|
1095
|
+
}) {
|
|
1096
|
+
const { fromDate, toDate } = this.resolveReportDateRange(
|
|
1097
|
+
filters?.from,
|
|
1098
|
+
filters?.to,
|
|
1099
|
+
);
|
|
1100
|
+
const groupBy = this.resolveReportGroupBy(filters?.groupBy);
|
|
1101
|
+
const topN = this.resolveTopN(filters?.topN);
|
|
1102
|
+
const normalizedSearch = this.normalizeReportText(filters?.search || '');
|
|
1103
|
+
const installments = await this.prisma.financial_installment.findMany({
|
|
1104
|
+
where: {
|
|
1105
|
+
competence_date: {
|
|
1106
|
+
gte: fromDate,
|
|
1107
|
+
lte: toDate,
|
|
1108
|
+
},
|
|
1109
|
+
status: {
|
|
1110
|
+
not: 'canceled',
|
|
1111
|
+
},
|
|
1112
|
+
financial_title: {
|
|
1113
|
+
title_type: 'receivable',
|
|
1114
|
+
status: {
|
|
1115
|
+
notIn: ['draft', 'canceled'],
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
select: {
|
|
1120
|
+
competence_date: true,
|
|
1121
|
+
amount_cents: true,
|
|
1122
|
+
financial_title: {
|
|
1123
|
+
select: {
|
|
1124
|
+
person_id: true,
|
|
1125
|
+
person: {
|
|
1126
|
+
select: {
|
|
1127
|
+
name: true,
|
|
1128
|
+
},
|
|
1129
|
+
},
|
|
1130
|
+
finance_category: {
|
|
1131
|
+
select: {
|
|
1132
|
+
code: true,
|
|
1133
|
+
name: true,
|
|
1134
|
+
kind: true,
|
|
1135
|
+
},
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
},
|
|
1139
|
+
},
|
|
1140
|
+
orderBy: {
|
|
1141
|
+
competence_date: 'asc',
|
|
1142
|
+
},
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
const byCustomer = new Map<string, { customer: string; value: number }>();
|
|
1146
|
+
const byBucket = new Map<string, number>();
|
|
1147
|
+
|
|
1148
|
+
for (const installment of installments) {
|
|
1149
|
+
const category = installment.financial_title.finance_category;
|
|
1150
|
+
|
|
1151
|
+
if (
|
|
1152
|
+
this.isTransferReportCategory(category) ||
|
|
1153
|
+
this.isLoanReportCategory(category) ||
|
|
1154
|
+
this.isInvestorContributionReportCategory(category)
|
|
1155
|
+
) {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const customer =
|
|
1160
|
+
installment.financial_title.person?.name ||
|
|
1161
|
+
`Customer ${installment.financial_title.person_id}`;
|
|
1162
|
+
const amount = this.fromCents(installment.amount_cents);
|
|
1163
|
+
const current = byCustomer.get(customer) || { customer, value: 0 };
|
|
1164
|
+
|
|
1165
|
+
current.value += amount;
|
|
1166
|
+
byCustomer.set(customer, current);
|
|
1167
|
+
|
|
1168
|
+
const period = this.getReportBucketKey(
|
|
1169
|
+
installment.competence_date,
|
|
1170
|
+
groupBy,
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
byBucket.set(period, (byBucket.get(period) || 0) + amount);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const filteredCustomers = Array.from(byCustomer.values())
|
|
1177
|
+
.filter((item) =>
|
|
1178
|
+
normalizedSearch.length === 0
|
|
1179
|
+
? true
|
|
1180
|
+
: this.normalizeReportText(item.customer).includes(normalizedSearch),
|
|
1181
|
+
)
|
|
1182
|
+
.sort((a, b) => b.value - a.value);
|
|
1183
|
+
|
|
1184
|
+
const topCustomers = filteredCustomers.slice(0, topN).map((item) => ({
|
|
1185
|
+
customer: item.customer,
|
|
1186
|
+
value: this.roundCurrency(item.value),
|
|
1187
|
+
}));
|
|
1188
|
+
const total = this.roundCurrency(
|
|
1189
|
+
topCustomers.reduce((acc, item) => acc + item.value, 0),
|
|
1190
|
+
);
|
|
1191
|
+
const top5 = this.roundCurrency(
|
|
1192
|
+
topCustomers.slice(0, 5).reduce((acc, item) => acc + item.value, 0),
|
|
1193
|
+
);
|
|
1194
|
+
const pieData = topCustomers.slice(0, 9).map((item) => ({
|
|
1195
|
+
customer: item.customer,
|
|
1196
|
+
value: item.value,
|
|
1197
|
+
}));
|
|
1198
|
+
const othersValue = this.roundCurrency(
|
|
1199
|
+
topCustomers.slice(9).reduce((acc, item) => acc + item.value, 0),
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
if (othersValue > 0) {
|
|
1203
|
+
pieData.push({
|
|
1204
|
+
customer: 'Outros',
|
|
1205
|
+
value: othersValue,
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const groupedPeriods = Array.from(byBucket.entries())
|
|
1210
|
+
.map(([period, value]) => ({
|
|
1211
|
+
period,
|
|
1212
|
+
value: this.roundCurrency(value),
|
|
1213
|
+
}))
|
|
1214
|
+
.sort((a, b) => this.sortReportBuckets(a.period, b.period, groupBy));
|
|
1215
|
+
|
|
1216
|
+
return {
|
|
1217
|
+
total,
|
|
1218
|
+
top5Percent: total > 0 ? this.roundCurrency((top5 / total) * 100) : 0,
|
|
1219
|
+
topCustomers,
|
|
1220
|
+
pieData,
|
|
1221
|
+
groupedPeriods,
|
|
1222
|
+
leader: topCustomers[0] || null,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async getTopOperationalExpensesReport(filters?: {
|
|
1227
|
+
from?: string;
|
|
1228
|
+
to?: string;
|
|
1229
|
+
groupBy?: FinanceReportGroupBy;
|
|
1230
|
+
topN?: number;
|
|
1231
|
+
search?: string;
|
|
1232
|
+
}) {
|
|
1233
|
+
const { fromDate, toDate } = this.resolveReportDateRange(
|
|
1234
|
+
filters?.from,
|
|
1235
|
+
filters?.to,
|
|
1236
|
+
);
|
|
1237
|
+
const groupBy = this.resolveReportGroupBy(filters?.groupBy);
|
|
1238
|
+
const topN = this.resolveTopN(filters?.topN);
|
|
1239
|
+
const normalizedSearch = this.normalizeReportText(filters?.search || '');
|
|
1240
|
+
const installments = await this.prisma.financial_installment.findMany({
|
|
1241
|
+
where: {
|
|
1242
|
+
competence_date: {
|
|
1243
|
+
gte: fromDate,
|
|
1244
|
+
lte: toDate,
|
|
1245
|
+
},
|
|
1246
|
+
status: {
|
|
1247
|
+
not: 'canceled',
|
|
1248
|
+
},
|
|
1249
|
+
financial_title: {
|
|
1250
|
+
title_type: 'payable',
|
|
1251
|
+
status: {
|
|
1252
|
+
notIn: ['draft', 'canceled'],
|
|
1253
|
+
},
|
|
1254
|
+
},
|
|
1255
|
+
},
|
|
1256
|
+
select: {
|
|
1257
|
+
competence_date: true,
|
|
1258
|
+
amount_cents: true,
|
|
1259
|
+
installment_allocation: {
|
|
1260
|
+
select: {
|
|
1261
|
+
allocated_amount_cents: true,
|
|
1262
|
+
cost_center: {
|
|
1263
|
+
select: {
|
|
1264
|
+
name: true,
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
financial_title: {
|
|
1270
|
+
select: {
|
|
1271
|
+
finance_category: {
|
|
1272
|
+
select: {
|
|
1273
|
+
code: true,
|
|
1274
|
+
name: true,
|
|
1275
|
+
kind: true,
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
},
|
|
1281
|
+
orderBy: {
|
|
1282
|
+
competence_date: 'asc',
|
|
1283
|
+
},
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
const byExpense = new Map<
|
|
1287
|
+
string,
|
|
1288
|
+
{ category: string; costCenter: string; label: string; value: number }
|
|
1289
|
+
>();
|
|
1290
|
+
const byCostCenter = new Map<string, number>();
|
|
1291
|
+
const byBucket = new Map<string, number>();
|
|
1292
|
+
|
|
1293
|
+
for (const installment of installments) {
|
|
1294
|
+
const category = installment.financial_title.finance_category;
|
|
1295
|
+
|
|
1296
|
+
if (
|
|
1297
|
+
this.isTransferReportCategory(category) ||
|
|
1298
|
+
this.isLoanReportCategory(category)
|
|
1299
|
+
) {
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
const allocations =
|
|
1304
|
+
installment.installment_allocation.length > 0
|
|
1305
|
+
? installment.installment_allocation.map((allocation) => ({
|
|
1306
|
+
costCenter: allocation.cost_center?.name || 'N/A',
|
|
1307
|
+
amount: this.fromCents(allocation.allocated_amount_cents),
|
|
1308
|
+
}))
|
|
1309
|
+
: [
|
|
1310
|
+
{
|
|
1311
|
+
costCenter: 'N/A',
|
|
1312
|
+
amount: this.fromCents(installment.amount_cents),
|
|
1313
|
+
},
|
|
1314
|
+
];
|
|
1315
|
+
|
|
1316
|
+
const period = this.getReportBucketKey(
|
|
1317
|
+
installment.competence_date,
|
|
1318
|
+
groupBy,
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
for (const allocation of allocations) {
|
|
1322
|
+
const categoryName = category?.name || 'Sem categoria';
|
|
1323
|
+
const key = `${categoryName}::${allocation.costCenter}`;
|
|
1324
|
+
const current = byExpense.get(key) || {
|
|
1325
|
+
category: categoryName,
|
|
1326
|
+
costCenter: allocation.costCenter,
|
|
1327
|
+
label: `${categoryName} - ${allocation.costCenter}`,
|
|
1328
|
+
value: 0,
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
current.value += allocation.amount;
|
|
1332
|
+
byExpense.set(key, current);
|
|
1333
|
+
byCostCenter.set(
|
|
1334
|
+
allocation.costCenter,
|
|
1335
|
+
(byCostCenter.get(allocation.costCenter) || 0) + allocation.amount,
|
|
1336
|
+
);
|
|
1337
|
+
byBucket.set(period, (byBucket.get(period) || 0) + allocation.amount);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
const filteredExpenses = Array.from(byExpense.values())
|
|
1342
|
+
.filter((item) =>
|
|
1343
|
+
normalizedSearch.length === 0
|
|
1344
|
+
? true
|
|
1345
|
+
: this.normalizeReportText(
|
|
1346
|
+
`${item.category} ${item.costCenter}`,
|
|
1347
|
+
).includes(normalizedSearch),
|
|
1348
|
+
)
|
|
1349
|
+
.sort((a, b) => b.value - a.value);
|
|
1350
|
+
|
|
1351
|
+
const topExpenses = filteredExpenses.slice(0, topN).map((item) => ({
|
|
1352
|
+
category: item.category,
|
|
1353
|
+
costCenter: item.costCenter,
|
|
1354
|
+
label: item.label,
|
|
1355
|
+
value: this.roundCurrency(item.value),
|
|
1356
|
+
}));
|
|
1357
|
+
const total = this.roundCurrency(
|
|
1358
|
+
topExpenses.reduce((acc, item) => acc + item.value, 0),
|
|
1359
|
+
);
|
|
1360
|
+
const pieByCostCenter = new Map<string, number>();
|
|
1361
|
+
|
|
1362
|
+
for (const item of topExpenses) {
|
|
1363
|
+
pieByCostCenter.set(
|
|
1364
|
+
item.costCenter,
|
|
1365
|
+
(pieByCostCenter.get(item.costCenter) || 0) + item.value,
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const pieData = Array.from(pieByCostCenter.entries())
|
|
1370
|
+
.map(([name, value]) => ({
|
|
1371
|
+
name,
|
|
1372
|
+
value: this.roundCurrency(value),
|
|
1373
|
+
}))
|
|
1374
|
+
.sort((a, b) => b.value - a.value);
|
|
1375
|
+
const groupedPeriods = Array.from(byBucket.entries())
|
|
1376
|
+
.map(([period, value]) => ({
|
|
1377
|
+
period,
|
|
1378
|
+
value: this.roundCurrency(value),
|
|
1379
|
+
}))
|
|
1380
|
+
.sort((a, b) => this.sortReportBuckets(a.period, b.period, groupBy));
|
|
1381
|
+
|
|
1382
|
+
return {
|
|
1383
|
+
total,
|
|
1384
|
+
average:
|
|
1385
|
+
topExpenses.length > 0
|
|
1386
|
+
? this.roundCurrency(total / topExpenses.length)
|
|
1387
|
+
: 0,
|
|
1388
|
+
topExpenses,
|
|
1389
|
+
pieData,
|
|
1390
|
+
groupedPeriods,
|
|
1391
|
+
highest: topExpenses[0] || null,
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
|
|
935
1395
|
async updateScenarioSettings(
|
|
936
1396
|
scenario: string,
|
|
937
1397
|
data: {
|
|
@@ -6153,6 +6613,181 @@ export class FinanceService {
|
|
|
6153
6613
|
}
|
|
6154
6614
|
}
|
|
6155
6615
|
|
|
6616
|
+
private resolveReportDateRange(from?: string, to?: string) {
|
|
6617
|
+
const fromDate = from
|
|
6618
|
+
? new Date(`${from}T00:00:00.000`)
|
|
6619
|
+
: new Date('2021-01-01T00:00:00.000');
|
|
6620
|
+
const toDate = to
|
|
6621
|
+
? new Date(`${to}T23:59:59.999`)
|
|
6622
|
+
: new Date('2026-12-31T23:59:59.999');
|
|
6623
|
+
|
|
6624
|
+
if (
|
|
6625
|
+
Number.isNaN(fromDate.getTime()) ||
|
|
6626
|
+
Number.isNaN(toDate.getTime()) ||
|
|
6627
|
+
fromDate > toDate
|
|
6628
|
+
) {
|
|
6629
|
+
throw new BadRequestException('Invalid report date range');
|
|
6630
|
+
}
|
|
6631
|
+
|
|
6632
|
+
return { fromDate, toDate };
|
|
6633
|
+
}
|
|
6634
|
+
|
|
6635
|
+
private resolveReportGroupBy(groupBy?: string): FinanceReportGroupBy {
|
|
6636
|
+
if (groupBy === 'day' || groupBy === 'week' || groupBy === 'month') {
|
|
6637
|
+
return groupBy;
|
|
6638
|
+
}
|
|
6639
|
+
|
|
6640
|
+
return 'year';
|
|
6641
|
+
}
|
|
6642
|
+
|
|
6643
|
+
private resolveTopN(topN?: number) {
|
|
6644
|
+
if (!topN || !Number.isFinite(topN)) {
|
|
6645
|
+
return 20;
|
|
6646
|
+
}
|
|
6647
|
+
|
|
6648
|
+
return Math.max(1, Math.min(100, Math.trunc(topN)));
|
|
6649
|
+
}
|
|
6650
|
+
|
|
6651
|
+
private getReportBucketKey(date: Date, groupBy: FinanceReportGroupBy) {
|
|
6652
|
+
const baseDate = this.startOfDay(date);
|
|
6653
|
+
|
|
6654
|
+
if (groupBy === 'day') {
|
|
6655
|
+
return baseDate.toISOString().slice(0, 10);
|
|
6656
|
+
}
|
|
6657
|
+
|
|
6658
|
+
if (groupBy === 'week') {
|
|
6659
|
+
const weekStart = this.startOfWeek(baseDate);
|
|
6660
|
+
const isoWeek = this.getIsoWeek(weekStart);
|
|
6661
|
+
|
|
6662
|
+
return `${weekStart.getFullYear()}-W${String(isoWeek).padStart(2, '0')}`;
|
|
6663
|
+
}
|
|
6664
|
+
|
|
6665
|
+
if (groupBy === 'month') {
|
|
6666
|
+
return `${baseDate.getFullYear()}-${String(baseDate.getMonth() + 1).padStart(2, '0')}`;
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
return String(baseDate.getFullYear());
|
|
6670
|
+
}
|
|
6671
|
+
|
|
6672
|
+
private sortReportBuckets(
|
|
6673
|
+
a: string,
|
|
6674
|
+
b: string,
|
|
6675
|
+
groupBy: FinanceReportGroupBy,
|
|
6676
|
+
) {
|
|
6677
|
+
if (groupBy === 'year' || groupBy === 'month' || groupBy === 'day') {
|
|
6678
|
+
return a.localeCompare(b);
|
|
6679
|
+
}
|
|
6680
|
+
|
|
6681
|
+
const [aYear, aWeek] = a.split('-W');
|
|
6682
|
+
const [bYear, bWeek] = b.split('-W');
|
|
6683
|
+
const yearDiff = Number(aYear) - Number(bYear);
|
|
6684
|
+
|
|
6685
|
+
if (yearDiff !== 0) {
|
|
6686
|
+
return yearDiff;
|
|
6687
|
+
}
|
|
6688
|
+
|
|
6689
|
+
return Number(aWeek) - Number(bWeek);
|
|
6690
|
+
}
|
|
6691
|
+
|
|
6692
|
+
private startOfWeek(date: Date) {
|
|
6693
|
+
const current = new Date(date);
|
|
6694
|
+
const day = current.getDay();
|
|
6695
|
+
const diff = day === 0 ? -6 : 1 - day;
|
|
6696
|
+
|
|
6697
|
+
current.setDate(current.getDate() + diff);
|
|
6698
|
+
current.setHours(0, 0, 0, 0);
|
|
6699
|
+
|
|
6700
|
+
return current;
|
|
6701
|
+
}
|
|
6702
|
+
|
|
6703
|
+
private getIsoWeek(date: Date) {
|
|
6704
|
+
const current = new Date(
|
|
6705
|
+
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
|
|
6706
|
+
);
|
|
6707
|
+
const dayNum = current.getUTCDay() || 7;
|
|
6708
|
+
|
|
6709
|
+
current.setUTCDate(current.getUTCDate() + 4 - dayNum);
|
|
6710
|
+
|
|
6711
|
+
const yearStart = new Date(Date.UTC(current.getUTCFullYear(), 0, 1));
|
|
6712
|
+
|
|
6713
|
+
return Math.ceil(
|
|
6714
|
+
((current.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7,
|
|
6715
|
+
);
|
|
6716
|
+
}
|
|
6717
|
+
|
|
6718
|
+
private normalizeReportText(value: string) {
|
|
6719
|
+
return value
|
|
6720
|
+
.normalize('NFD')
|
|
6721
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
6722
|
+
.toLowerCase()
|
|
6723
|
+
.trim();
|
|
6724
|
+
}
|
|
6725
|
+
|
|
6726
|
+
private isTransferReportCategory(category?: {
|
|
6727
|
+
code?: string | null;
|
|
6728
|
+
name?: string | null;
|
|
6729
|
+
kind?: string | null;
|
|
6730
|
+
} | null) {
|
|
6731
|
+
if (!category) {
|
|
6732
|
+
return false;
|
|
6733
|
+
}
|
|
6734
|
+
|
|
6735
|
+
if (category.kind === 'transfer') {
|
|
6736
|
+
return true;
|
|
6737
|
+
}
|
|
6738
|
+
|
|
6739
|
+
const haystack = this.normalizeReportText(
|
|
6740
|
+
`${category.code || ''} ${category.name || ''}`,
|
|
6741
|
+
);
|
|
6742
|
+
|
|
6743
|
+
return haystack.includes('transfer');
|
|
6744
|
+
}
|
|
6745
|
+
|
|
6746
|
+
private isLoanReportCategory(category?: {
|
|
6747
|
+
code?: string | null;
|
|
6748
|
+
name?: string | null;
|
|
6749
|
+
kind?: string | null;
|
|
6750
|
+
} | null) {
|
|
6751
|
+
if (!category) {
|
|
6752
|
+
return false;
|
|
6753
|
+
}
|
|
6754
|
+
|
|
6755
|
+
const haystack = this.normalizeReportText(
|
|
6756
|
+
`${category.code || ''} ${category.name || ''}`,
|
|
6757
|
+
);
|
|
6758
|
+
|
|
6759
|
+
return (
|
|
6760
|
+
haystack.includes('emprestimo') ||
|
|
6761
|
+
haystack.includes('financiamento') ||
|
|
6762
|
+
haystack.includes('loan')
|
|
6763
|
+
);
|
|
6764
|
+
}
|
|
6765
|
+
|
|
6766
|
+
private isInvestorContributionReportCategory(category?: {
|
|
6767
|
+
code?: string | null;
|
|
6768
|
+
name?: string | null;
|
|
6769
|
+
kind?: string | null;
|
|
6770
|
+
} | null) {
|
|
6771
|
+
if (!category) {
|
|
6772
|
+
return false;
|
|
6773
|
+
}
|
|
6774
|
+
|
|
6775
|
+
const haystack = this.normalizeReportText(
|
|
6776
|
+
`${category.code || ''} ${category.name || ''}`,
|
|
6777
|
+
);
|
|
6778
|
+
|
|
6779
|
+
return (
|
|
6780
|
+
haystack.includes('aporte') ||
|
|
6781
|
+
haystack.includes('investidor') ||
|
|
6782
|
+
haystack.includes('capital') ||
|
|
6783
|
+
haystack.includes('socio')
|
|
6784
|
+
);
|
|
6785
|
+
}
|
|
6786
|
+
|
|
6787
|
+
private roundCurrency(value: number) {
|
|
6788
|
+
return Number(value.toFixed(2));
|
|
6789
|
+
}
|
|
6790
|
+
|
|
6156
6791
|
private toCents(value: number) {
|
|
6157
6792
|
return Math.round(value * 100);
|
|
6158
6793
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './finance-cost-centers.controller';
|
|
|
7
7
|
export * from './finance-data.controller';
|
|
8
8
|
export * from './finance-installments.controller';
|
|
9
9
|
export * from './finance-period-close.controller';
|
|
10
|
+
export * from './finance-reports.controller';
|
|
10
11
|
export * from './finance-statements.controller';
|
|
11
12
|
export * from './finance-transfers.controller';
|
|
12
13
|
export * from './finance.module';
|