@hed-hog/finance 0.0.239 → 0.0.240
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/README.md +1 -22
- package/dist/finance-installments.controller.d.ts +132 -0
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +52 -0
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance-statements.controller.d.ts +8 -0
- package/dist/finance-statements.controller.d.ts.map +1 -1
- package/dist/finance-statements.controller.js +40 -0
- package/dist/finance-statements.controller.js.map +1 -1
- package/dist/finance.module.d.ts.map +1 -1
- package/dist/finance.module.js +1 -0
- package/dist/finance.module.js.map +1 -1
- package/dist/finance.service.d.ts +160 -2
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +626 -8
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +54 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +80 -4
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +736 -13
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1 -3
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +212 -60
- package/hedhog/frontend/messages/en.json +1 -0
- package/hedhog/frontend/messages/pt.json +1 -0
- package/hedhog/query/0_constraints.sql +2 -0
- package/hedhog/query/constraints.sql +86 -0
- package/hedhog/table/bank_account.yaml +0 -8
- package/hedhog/table/financial_title.yaml +1 -9
- package/hedhog/table/settlement.yaml +0 -8
- package/package.json +6 -6
- package/src/finance-installments.controller.ts +70 -10
- package/src/finance-statements.controller.ts +61 -2
- package/src/finance.module.ts +2 -1
- package/src/finance.service.ts +868 -12
- package/hedhog/table/branch.yaml +0 -18
package/dist/finance.service.js
CHANGED
|
@@ -8,6 +8,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
11
14
|
var FinanceService_1;
|
|
12
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
16
|
exports.FinanceService = void 0;
|
|
@@ -16,11 +19,14 @@ const api_pagination_1 = require("@hed-hog/api-pagination");
|
|
|
16
19
|
const api_prisma_1 = require("@hed-hog/api-prisma");
|
|
17
20
|
const core_1 = require("@hed-hog/core");
|
|
18
21
|
const common_1 = require("@nestjs/common");
|
|
22
|
+
const node_crypto_1 = require("node:crypto");
|
|
23
|
+
const promises_1 = require("node:fs/promises");
|
|
19
24
|
let FinanceService = FinanceService_1 = class FinanceService {
|
|
20
|
-
constructor(prisma, paginationService, ai) {
|
|
25
|
+
constructor(prisma, paginationService, ai, fileService) {
|
|
21
26
|
this.prisma = prisma;
|
|
22
27
|
this.paginationService = paginationService;
|
|
23
28
|
this.ai = ai;
|
|
29
|
+
this.fileService = fileService;
|
|
24
30
|
this.logger = new common_1.Logger(FinanceService_1.name);
|
|
25
31
|
}
|
|
26
32
|
async getAgentExtractInfoFromFile(file, fileId, titleType = 'payable') {
|
|
@@ -598,12 +604,18 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
598
604
|
async createAccountsPayableTitle(data, locale, userId) {
|
|
599
605
|
return this.createTitle(data, 'payable', locale, userId);
|
|
600
606
|
}
|
|
607
|
+
async updateAccountsPayableTitle(id, data, locale, userId) {
|
|
608
|
+
return this.updateDraftTitle(id, data, 'payable', locale, userId);
|
|
609
|
+
}
|
|
601
610
|
async approveAccountsPayableTitle(id, locale, userId) {
|
|
602
611
|
return this.approveTitle(id, 'payable', locale, userId);
|
|
603
612
|
}
|
|
604
613
|
async rejectAccountsPayableTitle(id, data, locale, userId) {
|
|
605
614
|
return this.rejectTitle(id, data, 'payable', locale, userId);
|
|
606
615
|
}
|
|
616
|
+
async cancelAccountsPayableTitle(id, data, locale, userId) {
|
|
617
|
+
return this.cancelTitle(id, data, 'payable', locale, userId);
|
|
618
|
+
}
|
|
607
619
|
async settleAccountsPayableInstallment(id, data, locale, userId) {
|
|
608
620
|
return this.settleTitleInstallment(id, data, 'payable', locale, userId);
|
|
609
621
|
}
|
|
@@ -613,9 +625,15 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
613
625
|
async createAccountsReceivableTitle(data, locale, userId) {
|
|
614
626
|
return this.createTitle(data, 'receivable', locale, userId);
|
|
615
627
|
}
|
|
628
|
+
async updateAccountsReceivableTitle(id, data, locale, userId) {
|
|
629
|
+
return this.updateDraftTitle(id, data, 'receivable', locale, userId);
|
|
630
|
+
}
|
|
616
631
|
async approveAccountsReceivableTitle(id, locale, userId) {
|
|
617
632
|
return this.approveTitle(id, 'receivable', locale, userId);
|
|
618
633
|
}
|
|
634
|
+
async cancelAccountsReceivableTitle(id, data, locale, userId) {
|
|
635
|
+
return this.cancelTitle(id, data, 'receivable', locale, userId);
|
|
636
|
+
}
|
|
619
637
|
async settleAccountsReceivableInstallment(id, data, locale, userId) {
|
|
620
638
|
return this.settleTitleInstallment(id, data, 'receivable', locale, userId);
|
|
621
639
|
}
|
|
@@ -857,6 +875,368 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
857
875
|
statusConciliacao: this.mapStatementStatusToPt(statement.status),
|
|
858
876
|
}));
|
|
859
877
|
}
|
|
878
|
+
async exportBankStatementsCsv(bankAccountId) {
|
|
879
|
+
const statements = await this.listBankStatements(bankAccountId);
|
|
880
|
+
const headers = [
|
|
881
|
+
'id',
|
|
882
|
+
'data',
|
|
883
|
+
'descricao',
|
|
884
|
+
'valor',
|
|
885
|
+
'tipo',
|
|
886
|
+
'status_conciliacao',
|
|
887
|
+
'conta_bancaria_id',
|
|
888
|
+
];
|
|
889
|
+
const lines = statements.map((statement) => {
|
|
890
|
+
const columns = [
|
|
891
|
+
statement.id,
|
|
892
|
+
statement.data,
|
|
893
|
+
statement.descricao,
|
|
894
|
+
String(statement.valor),
|
|
895
|
+
statement.tipo,
|
|
896
|
+
statement.statusConciliacao,
|
|
897
|
+
statement.contaBancariaId,
|
|
898
|
+
];
|
|
899
|
+
return columns.map((column) => this.escapeCsvCell(column)).join(';');
|
|
900
|
+
});
|
|
901
|
+
const csv = ['\uFEFF' + headers.join(';'), ...lines].join('\n');
|
|
902
|
+
const fileName = `extrato-bancario-${bankAccountId}-${new Date()
|
|
903
|
+
.toISOString()
|
|
904
|
+
.slice(0, 10)}.csv`;
|
|
905
|
+
return {
|
|
906
|
+
csv,
|
|
907
|
+
fileName,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
async importBankStatements(bankAccountId, file, locale, userId) {
|
|
911
|
+
if (!file) {
|
|
912
|
+
throw new common_1.BadRequestException('File is required');
|
|
913
|
+
}
|
|
914
|
+
const bankAccount = await this.prisma.bank_account.findUnique({
|
|
915
|
+
where: { id: bankAccountId },
|
|
916
|
+
select: { id: true },
|
|
917
|
+
});
|
|
918
|
+
if (!bankAccount) {
|
|
919
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, 'Bank account not found').replace('{{item}}', 'Bank account'));
|
|
920
|
+
}
|
|
921
|
+
const sourceType = this.detectStatementSourceType(file);
|
|
922
|
+
const uploadedFile = await this.fileService.upload('finance/statements', file);
|
|
923
|
+
const rawContent = await this.getUploadedFileText(file);
|
|
924
|
+
const parsedEntries = sourceType === 'ofx'
|
|
925
|
+
? this.parseOfxStatements(rawContent)
|
|
926
|
+
: this.parseCsvStatements(rawContent);
|
|
927
|
+
if (parsedEntries.length === 0) {
|
|
928
|
+
throw new common_1.BadRequestException('No valid statement rows were found in the uploaded file');
|
|
929
|
+
}
|
|
930
|
+
const normalizedEntries = parsedEntries
|
|
931
|
+
.filter((entry) => entry.postedDate && Number.isFinite(entry.amount))
|
|
932
|
+
.map((entry, index) => {
|
|
933
|
+
const postedDate = new Date(entry.postedDate);
|
|
934
|
+
if (Number.isNaN(postedDate.getTime())) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
const amountCents = this.toCents(entry.amount);
|
|
938
|
+
if (!Number.isFinite(amountCents) || amountCents === 0) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
const dedupeBase = [
|
|
942
|
+
bankAccountId,
|
|
943
|
+
postedDate.toISOString().slice(0, 10),
|
|
944
|
+
amountCents,
|
|
945
|
+
entry.externalId || '',
|
|
946
|
+
entry.description || '',
|
|
947
|
+
index,
|
|
948
|
+
].join('|');
|
|
949
|
+
return {
|
|
950
|
+
postedDate,
|
|
951
|
+
amountCents,
|
|
952
|
+
externalId: entry.externalId || null,
|
|
953
|
+
description: (entry.description || 'Lançamento importado').slice(0, 1000),
|
|
954
|
+
dedupeKey: this.createShortHash(`statement-line:${dedupeBase}`, 64),
|
|
955
|
+
};
|
|
956
|
+
})
|
|
957
|
+
.filter(Boolean);
|
|
958
|
+
if (normalizedEntries.length === 0) {
|
|
959
|
+
throw new common_1.BadRequestException('No valid financial entries were parsed');
|
|
960
|
+
}
|
|
961
|
+
const statementFingerprint = this.createShortHash([
|
|
962
|
+
bankAccountId,
|
|
963
|
+
sourceType,
|
|
964
|
+
uploadedFile.id,
|
|
965
|
+
file.originalname,
|
|
966
|
+
file.size,
|
|
967
|
+
normalizedEntries.length,
|
|
968
|
+
].join('|'), 24);
|
|
969
|
+
const statement = await this.prisma.bank_statement.create({
|
|
970
|
+
data: {
|
|
971
|
+
bank_account_id: bankAccountId,
|
|
972
|
+
source_type: sourceType,
|
|
973
|
+
idempotency_key: `import-${uploadedFile.id}-${statementFingerprint}`,
|
|
974
|
+
period_start: normalizedEntries
|
|
975
|
+
.map((entry) => entry.postedDate)
|
|
976
|
+
.sort((a, b) => a.getTime() - b.getTime())[0],
|
|
977
|
+
period_end: normalizedEntries
|
|
978
|
+
.map((entry) => entry.postedDate)
|
|
979
|
+
.sort((a, b) => b.getTime() - a.getTime())[0],
|
|
980
|
+
imported_at: new Date(),
|
|
981
|
+
imported_by_user_id: userId || null,
|
|
982
|
+
},
|
|
983
|
+
select: { id: true },
|
|
984
|
+
});
|
|
985
|
+
await this.prisma.bank_statement_line.createMany({
|
|
986
|
+
data: normalizedEntries.map((entry) => ({
|
|
987
|
+
bank_statement_id: statement.id,
|
|
988
|
+
bank_account_id: bankAccountId,
|
|
989
|
+
external_id: entry.externalId,
|
|
990
|
+
posted_date: entry.postedDate,
|
|
991
|
+
amount_cents: entry.amountCents,
|
|
992
|
+
description: entry.description,
|
|
993
|
+
status: 'imported',
|
|
994
|
+
dedupe_key: entry.dedupeKey,
|
|
995
|
+
})),
|
|
996
|
+
skipDuplicates: true,
|
|
997
|
+
});
|
|
998
|
+
return {
|
|
999
|
+
statementId: String(statement.id),
|
|
1000
|
+
fileId: String(uploadedFile.id),
|
|
1001
|
+
importedRows: normalizedEntries.length,
|
|
1002
|
+
sourceType,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
detectStatementSourceType(file) {
|
|
1006
|
+
const filename = (file.originalname || '').toLowerCase();
|
|
1007
|
+
const mimetype = (file.mimetype || '').toLowerCase();
|
|
1008
|
+
if (filename.endsWith('.ofx') ||
|
|
1009
|
+
mimetype.includes('ofx') ||
|
|
1010
|
+
mimetype.includes('x-ofx')) {
|
|
1011
|
+
return 'ofx';
|
|
1012
|
+
}
|
|
1013
|
+
if (filename.endsWith('.csv') ||
|
|
1014
|
+
mimetype.includes('csv') ||
|
|
1015
|
+
mimetype.includes('comma-separated-values')) {
|
|
1016
|
+
return 'csv';
|
|
1017
|
+
}
|
|
1018
|
+
throw new common_1.BadRequestException('Only CSV and OFX files are supported');
|
|
1019
|
+
}
|
|
1020
|
+
async getUploadedFileText(file) {
|
|
1021
|
+
let buffer = file.buffer;
|
|
1022
|
+
if (!buffer && file.path) {
|
|
1023
|
+
buffer = await (0, promises_1.readFile)(file.path);
|
|
1024
|
+
}
|
|
1025
|
+
if (!buffer || buffer.length === 0) {
|
|
1026
|
+
throw new common_1.BadRequestException('Uploaded file is empty');
|
|
1027
|
+
}
|
|
1028
|
+
const utf8 = buffer.toString('utf8');
|
|
1029
|
+
if (utf8.includes('�')) {
|
|
1030
|
+
return buffer.toString('latin1');
|
|
1031
|
+
}
|
|
1032
|
+
return utf8;
|
|
1033
|
+
}
|
|
1034
|
+
parseCsvStatements(content) {
|
|
1035
|
+
const normalizedContent = content.replace(/^\uFEFF/, '').trim();
|
|
1036
|
+
if (!normalizedContent) {
|
|
1037
|
+
return [];
|
|
1038
|
+
}
|
|
1039
|
+
const rows = normalizedContent
|
|
1040
|
+
.split(/\r?\n/)
|
|
1041
|
+
.map((line) => line.trim())
|
|
1042
|
+
.filter((line) => !!line);
|
|
1043
|
+
if (rows.length === 0) {
|
|
1044
|
+
return [];
|
|
1045
|
+
}
|
|
1046
|
+
const delimiter = this.detectCsvDelimiter(rows[0]);
|
|
1047
|
+
const headerCells = this.parseCsvLine(rows[0], delimiter);
|
|
1048
|
+
const normalizedHeaders = headerCells.map((cell) => this.normalizeCsvHeader(cell));
|
|
1049
|
+
const headerIndexes = {
|
|
1050
|
+
date: this.findFirstHeaderIndex(normalizedHeaders, [
|
|
1051
|
+
'data',
|
|
1052
|
+
'dt',
|
|
1053
|
+
'posteddate',
|
|
1054
|
+
'lancamento',
|
|
1055
|
+
'movimento',
|
|
1056
|
+
]),
|
|
1057
|
+
description: this.findFirstHeaderIndex(normalizedHeaders, [
|
|
1058
|
+
'descricao',
|
|
1059
|
+
'historico',
|
|
1060
|
+
'memo',
|
|
1061
|
+
'complemento',
|
|
1062
|
+
]),
|
|
1063
|
+
amount: this.findFirstHeaderIndex(normalizedHeaders, [
|
|
1064
|
+
'valor',
|
|
1065
|
+
'amount',
|
|
1066
|
+
'montante',
|
|
1067
|
+
]),
|
|
1068
|
+
debit: this.findFirstHeaderIndex(normalizedHeaders, ['debito', 'debit']),
|
|
1069
|
+
credit: this.findFirstHeaderIndex(normalizedHeaders, [
|
|
1070
|
+
'credito',
|
|
1071
|
+
'credit',
|
|
1072
|
+
]),
|
|
1073
|
+
externalId: this.findFirstHeaderIndex(normalizedHeaders, [
|
|
1074
|
+
'id',
|
|
1075
|
+
'documento',
|
|
1076
|
+
'numero',
|
|
1077
|
+
'fitid',
|
|
1078
|
+
]),
|
|
1079
|
+
};
|
|
1080
|
+
const hasHeader = headerIndexes.date >= 0 ||
|
|
1081
|
+
headerIndexes.description >= 0 ||
|
|
1082
|
+
headerIndexes.amount >= 0 ||
|
|
1083
|
+
headerIndexes.debit >= 0 ||
|
|
1084
|
+
headerIndexes.credit >= 0;
|
|
1085
|
+
const dataRows = hasHeader ? rows.slice(1) : rows;
|
|
1086
|
+
return dataRows
|
|
1087
|
+
.map((line) => this.parseCsvLine(line, delimiter))
|
|
1088
|
+
.map((columns) => {
|
|
1089
|
+
const dateRaw = (headerIndexes.date >= 0
|
|
1090
|
+
? columns[headerIndexes.date]
|
|
1091
|
+
: columns[0]) || '';
|
|
1092
|
+
const descriptionRaw = (headerIndexes.description >= 0
|
|
1093
|
+
? columns[headerIndexes.description]
|
|
1094
|
+
: columns[1]) || '';
|
|
1095
|
+
const amountRaw = headerIndexes.amount >= 0 ? columns[headerIndexes.amount] : '';
|
|
1096
|
+
const debitRaw = headerIndexes.debit >= 0 ? columns[headerIndexes.debit] : '';
|
|
1097
|
+
const creditRaw = headerIndexes.credit >= 0 ? columns[headerIndexes.credit] : '';
|
|
1098
|
+
const fallbackAmountRaw = columns[2] || '';
|
|
1099
|
+
const externalIdRaw = headerIndexes.externalId >= 0 ? columns[headerIndexes.externalId] : '';
|
|
1100
|
+
const postedDate = this.parseStatementDate(dateRaw);
|
|
1101
|
+
if (!postedDate) {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
const amount = this.resolveCsvAmount(amountRaw || fallbackAmountRaw, debitRaw, creditRaw);
|
|
1105
|
+
if (amount === null) {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
return {
|
|
1109
|
+
postedDate,
|
|
1110
|
+
amount,
|
|
1111
|
+
description: (descriptionRaw === null || descriptionRaw === void 0 ? void 0 : descriptionRaw.trim()) || 'Movimentação importada',
|
|
1112
|
+
externalId: (externalIdRaw === null || externalIdRaw === void 0 ? void 0 : externalIdRaw.trim()) || null,
|
|
1113
|
+
};
|
|
1114
|
+
})
|
|
1115
|
+
.filter(Boolean);
|
|
1116
|
+
}
|
|
1117
|
+
parseOfxStatements(content) {
|
|
1118
|
+
const normalized = content.replace(/\r/g, '');
|
|
1119
|
+
const segments = normalized
|
|
1120
|
+
.split(/<STMTTRN>/i)
|
|
1121
|
+
.slice(1)
|
|
1122
|
+
.map((segment) => segment.split(/<\/STMTTRN>/i)[0]);
|
|
1123
|
+
return segments
|
|
1124
|
+
.map((segment) => {
|
|
1125
|
+
var _a;
|
|
1126
|
+
const dateRaw = this.extractOfxTag(segment, 'DTPOSTED');
|
|
1127
|
+
const amountRaw = this.extractOfxTag(segment, 'TRNAMT');
|
|
1128
|
+
const fitId = this.extractOfxTag(segment, 'FITID');
|
|
1129
|
+
const memo = this.extractOfxTag(segment, 'MEMO');
|
|
1130
|
+
const name = this.extractOfxTag(segment, 'NAME');
|
|
1131
|
+
const trnType = (_a = this.extractOfxTag(segment, 'TRNTYPE')) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
1132
|
+
const postedDate = this.parseStatementDate(dateRaw);
|
|
1133
|
+
const amount = this.toNullableNumber(amountRaw);
|
|
1134
|
+
if (!postedDate || amount === null) {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
let normalizedAmount = amount;
|
|
1138
|
+
if (['debit', 'payment', 'withdrawal'].includes(trnType || '') &&
|
|
1139
|
+
normalizedAmount > 0) {
|
|
1140
|
+
normalizedAmount *= -1;
|
|
1141
|
+
}
|
|
1142
|
+
const description = [memo, name].filter(Boolean).join(' - ').trim();
|
|
1143
|
+
return {
|
|
1144
|
+
postedDate,
|
|
1145
|
+
amount: normalizedAmount,
|
|
1146
|
+
description: description || 'Movimentação OFX',
|
|
1147
|
+
externalId: fitId || null,
|
|
1148
|
+
};
|
|
1149
|
+
})
|
|
1150
|
+
.filter(Boolean);
|
|
1151
|
+
}
|
|
1152
|
+
detectCsvDelimiter(line) {
|
|
1153
|
+
var _a;
|
|
1154
|
+
const delimiters = [';', ',', '\t'];
|
|
1155
|
+
const counts = delimiters.map((delimiter) => ({
|
|
1156
|
+
delimiter,
|
|
1157
|
+
count: (line.match(new RegExp(`\\${delimiter}`, 'g')) || []).length,
|
|
1158
|
+
}));
|
|
1159
|
+
return ((_a = counts.sort((a, b) => b.count - a.count)[0]) === null || _a === void 0 ? void 0 : _a.delimiter) || ';';
|
|
1160
|
+
}
|
|
1161
|
+
parseCsvLine(line, delimiter) {
|
|
1162
|
+
const result = [];
|
|
1163
|
+
let current = '';
|
|
1164
|
+
let inQuotes = false;
|
|
1165
|
+
for (let i = 0; i < line.length; i++) {
|
|
1166
|
+
const char = line[i];
|
|
1167
|
+
const next = line[i + 1];
|
|
1168
|
+
if (char === '"') {
|
|
1169
|
+
if (inQuotes && next === '"') {
|
|
1170
|
+
current += '"';
|
|
1171
|
+
i++;
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
inQuotes = !inQuotes;
|
|
1175
|
+
}
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (char === delimiter && !inQuotes) {
|
|
1179
|
+
result.push(current.trim());
|
|
1180
|
+
current = '';
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
current += char;
|
|
1184
|
+
}
|
|
1185
|
+
result.push(current.trim());
|
|
1186
|
+
return result;
|
|
1187
|
+
}
|
|
1188
|
+
normalizeCsvHeader(value) {
|
|
1189
|
+
return String(value || '')
|
|
1190
|
+
.normalize('NFD')
|
|
1191
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
1192
|
+
.toLowerCase()
|
|
1193
|
+
.replace(/[^a-z0-9]/g, '');
|
|
1194
|
+
}
|
|
1195
|
+
findFirstHeaderIndex(headers, candidates) {
|
|
1196
|
+
return headers.findIndex((header) => candidates.some((candidate) => header.includes(candidate)));
|
|
1197
|
+
}
|
|
1198
|
+
parseStatementDate(value) {
|
|
1199
|
+
const raw = String(value || '').trim();
|
|
1200
|
+
if (!raw) {
|
|
1201
|
+
return '';
|
|
1202
|
+
}
|
|
1203
|
+
const onlyDateDigits = raw.match(/^(\d{8})/);
|
|
1204
|
+
if (onlyDateDigits === null || onlyDateDigits === void 0 ? void 0 : onlyDateDigits[1]) {
|
|
1205
|
+
const v = onlyDateDigits[1];
|
|
1206
|
+
return `${v.slice(0, 4)}-${v.slice(4, 6)}-${v.slice(6, 8)}`;
|
|
1207
|
+
}
|
|
1208
|
+
const normalized = this.normalizeDate(raw);
|
|
1209
|
+
if (normalized) {
|
|
1210
|
+
return normalized;
|
|
1211
|
+
}
|
|
1212
|
+
return '';
|
|
1213
|
+
}
|
|
1214
|
+
resolveCsvAmount(amountRaw, debitRaw, creditRaw) {
|
|
1215
|
+
const amount = this.toNullableNumber(amountRaw);
|
|
1216
|
+
const debit = this.toNullableNumber(debitRaw);
|
|
1217
|
+
const credit = this.toNullableNumber(creditRaw);
|
|
1218
|
+
if (credit !== null || debit !== null) {
|
|
1219
|
+
return (credit || 0) - (debit || 0);
|
|
1220
|
+
}
|
|
1221
|
+
if (amount === null) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
return amount;
|
|
1225
|
+
}
|
|
1226
|
+
extractOfxTag(segment, tag) {
|
|
1227
|
+
var _a;
|
|
1228
|
+
const regex = new RegExp(`<${tag}>([^\n\r<]*)`, 'i');
|
|
1229
|
+
const match = segment.match(regex);
|
|
1230
|
+
return ((_a = match === null || match === void 0 ? void 0 : match[1]) === null || _a === void 0 ? void 0 : _a.trim()) || '';
|
|
1231
|
+
}
|
|
1232
|
+
createShortHash(input, length = 40) {
|
|
1233
|
+
return (0, node_crypto_1.createHash)('sha256').update(input).digest('hex').slice(0, length);
|
|
1234
|
+
}
|
|
1235
|
+
escapeCsvCell(value) {
|
|
1236
|
+
const normalizedValue = String(value !== null && value !== void 0 ? value : '');
|
|
1237
|
+
const escapedValue = normalizedValue.replace(/"/g, '""');
|
|
1238
|
+
return `"${escapedValue}"`;
|
|
1239
|
+
}
|
|
860
1240
|
async createBankAccount(data, userId) {
|
|
861
1241
|
var _a;
|
|
862
1242
|
const accountType = this.mapAccountTypeFromPt(data.type);
|
|
@@ -1272,6 +1652,192 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1272
1652
|
const createdTitle = await this.getTitleById(createdTitleId, titleType, locale);
|
|
1273
1653
|
return this.mapTitleToFront(createdTitle, data.payment_channel);
|
|
1274
1654
|
}
|
|
1655
|
+
async updateDraftTitle(titleId, data, titleType, locale, userId) {
|
|
1656
|
+
const installments = this.normalizeAndValidateInstallments(data, locale);
|
|
1657
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
1658
|
+
const title = await tx.financial_title.findFirst({
|
|
1659
|
+
where: {
|
|
1660
|
+
id: titleId,
|
|
1661
|
+
title_type: titleType,
|
|
1662
|
+
},
|
|
1663
|
+
include: {
|
|
1664
|
+
financial_installment: {
|
|
1665
|
+
select: {
|
|
1666
|
+
id: true,
|
|
1667
|
+
},
|
|
1668
|
+
},
|
|
1669
|
+
},
|
|
1670
|
+
});
|
|
1671
|
+
if (!title) {
|
|
1672
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
1673
|
+
}
|
|
1674
|
+
if (title.status !== 'draft') {
|
|
1675
|
+
throw new common_1.BadRequestException('Only draft titles can be edited');
|
|
1676
|
+
}
|
|
1677
|
+
const person = await tx.person.findUnique({
|
|
1678
|
+
where: { id: data.person_id },
|
|
1679
|
+
select: { id: true },
|
|
1680
|
+
});
|
|
1681
|
+
if (!person) {
|
|
1682
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, 'Person not found'));
|
|
1683
|
+
}
|
|
1684
|
+
if (data.finance_category_id) {
|
|
1685
|
+
const category = await tx.finance_category.findUnique({
|
|
1686
|
+
where: { id: data.finance_category_id },
|
|
1687
|
+
select: { id: true },
|
|
1688
|
+
});
|
|
1689
|
+
if (!category) {
|
|
1690
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('categoryNotFound', locale, 'Category not found'));
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
if (data.cost_center_id) {
|
|
1694
|
+
const costCenter = await tx.cost_center.findUnique({
|
|
1695
|
+
where: { id: data.cost_center_id },
|
|
1696
|
+
select: { id: true },
|
|
1697
|
+
});
|
|
1698
|
+
if (!costCenter) {
|
|
1699
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('costCenterNotFound', locale, 'Cost center not found'));
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
const attachmentFileIds = [
|
|
1703
|
+
...new Set((data.attachment_file_ids || []).filter((fileId) => fileId > 0)),
|
|
1704
|
+
];
|
|
1705
|
+
if (attachmentFileIds.length > 0) {
|
|
1706
|
+
const existingFiles = await tx.file.findMany({
|
|
1707
|
+
where: {
|
|
1708
|
+
id: { in: attachmentFileIds },
|
|
1709
|
+
},
|
|
1710
|
+
select: {
|
|
1711
|
+
id: true,
|
|
1712
|
+
},
|
|
1713
|
+
});
|
|
1714
|
+
const existingFileIds = new Set(existingFiles.map((file) => file.id));
|
|
1715
|
+
const invalidFileIds = attachmentFileIds.filter((fileId) => !existingFileIds.has(fileId));
|
|
1716
|
+
if (invalidFileIds.length > 0) {
|
|
1717
|
+
throw new common_1.BadRequestException(`Invalid attachment file IDs: ${invalidFileIds.join(', ')}`);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
await this.assertDateNotInClosedPeriod(tx, data.competence_date
|
|
1721
|
+
? new Date(data.competence_date)
|
|
1722
|
+
: new Date(installments[0].due_date), 'update title');
|
|
1723
|
+
const installmentIds = title.financial_installment.map((item) => item.id);
|
|
1724
|
+
if (installmentIds.length > 0) {
|
|
1725
|
+
const hasSettlements = await tx.settlement_allocation.findFirst({
|
|
1726
|
+
where: {
|
|
1727
|
+
installment_id: {
|
|
1728
|
+
in: installmentIds,
|
|
1729
|
+
},
|
|
1730
|
+
},
|
|
1731
|
+
select: {
|
|
1732
|
+
id: true,
|
|
1733
|
+
},
|
|
1734
|
+
});
|
|
1735
|
+
if (hasSettlements) {
|
|
1736
|
+
throw new common_1.BadRequestException('Cannot edit title with settled installments');
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
await tx.financial_title.update({
|
|
1740
|
+
where: { id: title.id },
|
|
1741
|
+
data: {
|
|
1742
|
+
person_id: data.person_id,
|
|
1743
|
+
document_number: data.document_number,
|
|
1744
|
+
description: data.description,
|
|
1745
|
+
competence_date: data.competence_date
|
|
1746
|
+
? new Date(data.competence_date)
|
|
1747
|
+
: null,
|
|
1748
|
+
issue_date: data.issue_date ? new Date(data.issue_date) : null,
|
|
1749
|
+
total_amount_cents: this.toCents(data.total_amount),
|
|
1750
|
+
finance_category_id: data.finance_category_id,
|
|
1751
|
+
},
|
|
1752
|
+
});
|
|
1753
|
+
if (installmentIds.length > 0) {
|
|
1754
|
+
await tx.installment_allocation.deleteMany({
|
|
1755
|
+
where: {
|
|
1756
|
+
installment_id: {
|
|
1757
|
+
in: installmentIds,
|
|
1758
|
+
},
|
|
1759
|
+
},
|
|
1760
|
+
});
|
|
1761
|
+
await tx.financial_installment_tag.deleteMany({
|
|
1762
|
+
where: {
|
|
1763
|
+
installment_id: {
|
|
1764
|
+
in: installmentIds,
|
|
1765
|
+
},
|
|
1766
|
+
},
|
|
1767
|
+
});
|
|
1768
|
+
await tx.financial_installment.deleteMany({
|
|
1769
|
+
where: {
|
|
1770
|
+
title_id: title.id,
|
|
1771
|
+
},
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
for (const installment of installments) {
|
|
1775
|
+
const amountCents = installment.amount_cents;
|
|
1776
|
+
const createdInstallment = await tx.financial_installment.create({
|
|
1777
|
+
data: {
|
|
1778
|
+
title_id: title.id,
|
|
1779
|
+
installment_number: installment.installment_number,
|
|
1780
|
+
competence_date: data.competence_date
|
|
1781
|
+
? new Date(data.competence_date)
|
|
1782
|
+
: new Date(installment.due_date),
|
|
1783
|
+
due_date: new Date(installment.due_date),
|
|
1784
|
+
amount_cents: amountCents,
|
|
1785
|
+
open_amount_cents: amountCents,
|
|
1786
|
+
status: this.resolveInstallmentStatus(amountCents, amountCents, new Date(installment.due_date)),
|
|
1787
|
+
notes: data.description,
|
|
1788
|
+
},
|
|
1789
|
+
});
|
|
1790
|
+
if (data.cost_center_id) {
|
|
1791
|
+
await tx.installment_allocation.create({
|
|
1792
|
+
data: {
|
|
1793
|
+
installment_id: createdInstallment.id,
|
|
1794
|
+
cost_center_id: data.cost_center_id,
|
|
1795
|
+
allocated_amount_cents: amountCents,
|
|
1796
|
+
},
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
if (data.attachment_file_ids) {
|
|
1801
|
+
await tx.financial_title_attachment.deleteMany({
|
|
1802
|
+
where: {
|
|
1803
|
+
title_id: title.id,
|
|
1804
|
+
},
|
|
1805
|
+
});
|
|
1806
|
+
if (attachmentFileIds.length > 0) {
|
|
1807
|
+
await tx.financial_title_attachment.createMany({
|
|
1808
|
+
data: attachmentFileIds.map((fileId) => ({
|
|
1809
|
+
title_id: title.id,
|
|
1810
|
+
file_id: fileId,
|
|
1811
|
+
uploaded_by_user_id: userId,
|
|
1812
|
+
})),
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
await this.createAuditLog(tx, {
|
|
1817
|
+
action: 'UPDATE_TITLE',
|
|
1818
|
+
entityTable: 'financial_title',
|
|
1819
|
+
entityId: String(title.id),
|
|
1820
|
+
actorUserId: userId,
|
|
1821
|
+
summary: `Updated draft ${titleType} title ${title.id}`,
|
|
1822
|
+
beforeData: JSON.stringify({ status: title.status }),
|
|
1823
|
+
afterData: JSON.stringify({
|
|
1824
|
+
status: title.status,
|
|
1825
|
+
total_amount_cents: this.toCents(data.total_amount),
|
|
1826
|
+
}),
|
|
1827
|
+
});
|
|
1828
|
+
return tx.financial_title.findFirst({
|
|
1829
|
+
where: {
|
|
1830
|
+
id: title.id,
|
|
1831
|
+
title_type: titleType,
|
|
1832
|
+
},
|
|
1833
|
+
include: this.defaultTitleInclude(),
|
|
1834
|
+
});
|
|
1835
|
+
});
|
|
1836
|
+
if (!updatedTitle) {
|
|
1837
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
1838
|
+
}
|
|
1839
|
+
return this.mapTitleToFront(updatedTitle, data.payment_channel);
|
|
1840
|
+
}
|
|
1275
1841
|
normalizeAndValidateInstallments(data, locale) {
|
|
1276
1842
|
const fallbackDueDate = data.due_date;
|
|
1277
1843
|
const totalAmountCents = this.toCents(data.total_amount);
|
|
@@ -1328,7 +1894,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1328
1894
|
await tx.financial_title.update({
|
|
1329
1895
|
where: { id: title.id },
|
|
1330
1896
|
data: {
|
|
1331
|
-
status: '
|
|
1897
|
+
status: 'open',
|
|
1332
1898
|
},
|
|
1333
1899
|
});
|
|
1334
1900
|
await this.createAuditLog(tx, {
|
|
@@ -1338,7 +1904,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1338
1904
|
actorUserId: userId,
|
|
1339
1905
|
summary: `Approved ${titleType} title ${title.id}`,
|
|
1340
1906
|
beforeData: JSON.stringify({ status: title.status }),
|
|
1341
|
-
afterData: JSON.stringify({ status: '
|
|
1907
|
+
afterData: JSON.stringify({ status: 'open' }),
|
|
1342
1908
|
});
|
|
1343
1909
|
return tx.financial_title.findFirst({
|
|
1344
1910
|
where: {
|
|
@@ -1404,6 +1970,57 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1404
1970
|
}
|
|
1405
1971
|
return this.mapTitleToFront(updatedTitle);
|
|
1406
1972
|
}
|
|
1973
|
+
async cancelTitle(titleId, data, titleType, locale, userId) {
|
|
1974
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
1975
|
+
const title = await tx.financial_title.findFirst({
|
|
1976
|
+
where: {
|
|
1977
|
+
id: titleId,
|
|
1978
|
+
title_type: titleType,
|
|
1979
|
+
},
|
|
1980
|
+
select: {
|
|
1981
|
+
id: true,
|
|
1982
|
+
status: true,
|
|
1983
|
+
competence_date: true,
|
|
1984
|
+
},
|
|
1985
|
+
});
|
|
1986
|
+
if (!title) {
|
|
1987
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
1988
|
+
}
|
|
1989
|
+
if (title.status === 'settled' || title.status === 'canceled') {
|
|
1990
|
+
throw new common_1.BadRequestException('Title cannot be canceled in current status');
|
|
1991
|
+
}
|
|
1992
|
+
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'cancel title');
|
|
1993
|
+
await tx.financial_title.update({
|
|
1994
|
+
where: { id: title.id },
|
|
1995
|
+
data: {
|
|
1996
|
+
status: 'canceled',
|
|
1997
|
+
},
|
|
1998
|
+
});
|
|
1999
|
+
await this.createAuditLog(tx, {
|
|
2000
|
+
action: 'CANCEL_TITLE',
|
|
2001
|
+
entityTable: 'financial_title',
|
|
2002
|
+
entityId: String(title.id),
|
|
2003
|
+
actorUserId: userId,
|
|
2004
|
+
summary: `Canceled ${titleType} title ${title.id}`,
|
|
2005
|
+
beforeData: JSON.stringify({ status: title.status }),
|
|
2006
|
+
afterData: JSON.stringify({
|
|
2007
|
+
status: 'canceled',
|
|
2008
|
+
reason: (data === null || data === void 0 ? void 0 : data.reason) || null,
|
|
2009
|
+
}),
|
|
2010
|
+
});
|
|
2011
|
+
return tx.financial_title.findFirst({
|
|
2012
|
+
where: {
|
|
2013
|
+
id: title.id,
|
|
2014
|
+
title_type: titleType,
|
|
2015
|
+
},
|
|
2016
|
+
include: this.defaultTitleInclude(),
|
|
2017
|
+
});
|
|
2018
|
+
});
|
|
2019
|
+
if (!updatedTitle) {
|
|
2020
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
2021
|
+
}
|
|
2022
|
+
return this.mapTitleToFront(updatedTitle);
|
|
2023
|
+
}
|
|
1407
2024
|
async settleTitleInstallment(titleId, data, titleType, locale, userId) {
|
|
1408
2025
|
const amountCents = this.toCents(data.amount);
|
|
1409
2026
|
if (amountCents <= 0) {
|
|
@@ -1430,8 +2047,8 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1430
2047
|
if (!title) {
|
|
1431
2048
|
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
1432
2049
|
}
|
|
1433
|
-
if (!['
|
|
1434
|
-
throw new common_1.BadRequestException('Only
|
|
2050
|
+
if (!['open', 'partial'].includes(title.status)) {
|
|
2051
|
+
throw new common_1.BadRequestException('Only open/partial titles can be settled');
|
|
1435
2052
|
}
|
|
1436
2053
|
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'settle installment');
|
|
1437
2054
|
const installment = await tx.financial_installment.findFirst({
|
|
@@ -2029,7 +2646,6 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2029
2646
|
mapStatusToPt(status) {
|
|
2030
2647
|
const statusMap = {
|
|
2031
2648
|
draft: 'rascunho',
|
|
2032
|
-
approved: 'aprovado',
|
|
2033
2649
|
open: 'aberto',
|
|
2034
2650
|
partial: 'parcial',
|
|
2035
2651
|
settled: 'liquidado',
|
|
@@ -2056,7 +2672,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2056
2672
|
}
|
|
2057
2673
|
const statusMap = {
|
|
2058
2674
|
rascunho: 'draft',
|
|
2059
|
-
aprovado: '
|
|
2675
|
+
aprovado: 'open',
|
|
2060
2676
|
aberto: 'open',
|
|
2061
2677
|
parcial: 'partial',
|
|
2062
2678
|
liquidado: 'settled',
|
|
@@ -2277,8 +2893,10 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
2277
2893
|
exports.FinanceService = FinanceService;
|
|
2278
2894
|
exports.FinanceService = FinanceService = FinanceService_1 = __decorate([
|
|
2279
2895
|
(0, common_1.Injectable)(),
|
|
2896
|
+
__param(3, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.FileService))),
|
|
2280
2897
|
__metadata("design:paramtypes", [api_prisma_1.PrismaService,
|
|
2281
2898
|
api_pagination_1.PaginationService,
|
|
2282
|
-
core_1.AiService
|
|
2899
|
+
core_1.AiService,
|
|
2900
|
+
core_1.FileService])
|
|
2283
2901
|
], FinanceService);
|
|
2284
2902
|
//# sourceMappingURL=finance.service.js.map
|