@hed-hog/finance 0.0.238 → 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/dto/reject-title.dto.d.ts +4 -0
- package/dist/dto/reject-title.dto.d.ts.map +1 -0
- package/dist/dto/reject-title.dto.js +22 -0
- package/dist/dto/reject-title.dto.js.map +1 -0
- package/dist/dto/reverse-settlement.dto.d.ts +4 -0
- package/dist/dto/reverse-settlement.dto.d.ts.map +1 -0
- package/dist/dto/reverse-settlement.dto.js +22 -0
- package/dist/dto/reverse-settlement.dto.js.map +1 -0
- package/dist/dto/settle-installment.dto.d.ts +12 -0
- package/dist/dto/settle-installment.dto.d.ts.map +1 -0
- package/dist/dto/settle-installment.dto.js +71 -0
- package/dist/dto/settle-installment.dto.js.map +1 -0
- package/dist/finance-data.controller.d.ts +13 -5
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.d.ts +380 -12
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +144 -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 +435 -19
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +1286 -80
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +117 -0
- package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +92 -12
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +434 -7
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1172 -25
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +396 -49
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +430 -14
- 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/dto/reject-title.dto.ts +7 -0
- package/src/dto/reverse-settlement.dto.ts +7 -0
- package/src/dto/settle-installment.dto.ts +55 -0
- package/src/finance-installments.controller.ts +172 -10
- package/src/finance-statements.controller.ts +61 -2
- package/src/finance.module.ts +2 -1
- package/src/finance.service.ts +1887 -106
- 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') {
|
|
@@ -540,6 +546,17 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
540
546
|
if (auditLogsResult.status === 'rejected') {
|
|
541
547
|
this.logger.error('Failed to load finance audit logs', auditLogsResult.reason);
|
|
542
548
|
}
|
|
549
|
+
const aprovacoesPendentes = payables
|
|
550
|
+
.filter((title) => title.status === 'rascunho')
|
|
551
|
+
.map((title) => ({
|
|
552
|
+
id: String(title.id),
|
|
553
|
+
tituloId: String(title.id),
|
|
554
|
+
solicitante: '-',
|
|
555
|
+
valor: Number(title.valorTotal || 0),
|
|
556
|
+
politica: 'Aprovação financeira',
|
|
557
|
+
urgencia: 'media',
|
|
558
|
+
dataSolicitacao: title.criadoEm,
|
|
559
|
+
}));
|
|
543
560
|
return {
|
|
544
561
|
kpis: {
|
|
545
562
|
saldoCaixa: 0,
|
|
@@ -557,7 +574,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
557
574
|
pessoas: people,
|
|
558
575
|
categorias: categories,
|
|
559
576
|
centrosCusto: costCenters,
|
|
560
|
-
aprovacoesPendentes
|
|
577
|
+
aprovacoesPendentes,
|
|
561
578
|
agingInadimplencia: [],
|
|
562
579
|
cenarios: [],
|
|
563
580
|
transferencias: [],
|
|
@@ -587,9 +604,42 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
587
604
|
async createAccountsPayableTitle(data, locale, userId) {
|
|
588
605
|
return this.createTitle(data, 'payable', locale, userId);
|
|
589
606
|
}
|
|
607
|
+
async updateAccountsPayableTitle(id, data, locale, userId) {
|
|
608
|
+
return this.updateDraftTitle(id, data, 'payable', locale, userId);
|
|
609
|
+
}
|
|
610
|
+
async approveAccountsPayableTitle(id, locale, userId) {
|
|
611
|
+
return this.approveTitle(id, 'payable', locale, userId);
|
|
612
|
+
}
|
|
613
|
+
async rejectAccountsPayableTitle(id, data, locale, userId) {
|
|
614
|
+
return this.rejectTitle(id, data, 'payable', locale, userId);
|
|
615
|
+
}
|
|
616
|
+
async cancelAccountsPayableTitle(id, data, locale, userId) {
|
|
617
|
+
return this.cancelTitle(id, data, 'payable', locale, userId);
|
|
618
|
+
}
|
|
619
|
+
async settleAccountsPayableInstallment(id, data, locale, userId) {
|
|
620
|
+
return this.settleTitleInstallment(id, data, 'payable', locale, userId);
|
|
621
|
+
}
|
|
622
|
+
async reverseAccountsPayableSettlement(id, settlementId, data, locale, userId) {
|
|
623
|
+
return this.reverseTitleSettlement(id, settlementId, data, 'payable', locale, userId);
|
|
624
|
+
}
|
|
590
625
|
async createAccountsReceivableTitle(data, locale, userId) {
|
|
591
626
|
return this.createTitle(data, 'receivable', locale, userId);
|
|
592
627
|
}
|
|
628
|
+
async updateAccountsReceivableTitle(id, data, locale, userId) {
|
|
629
|
+
return this.updateDraftTitle(id, data, 'receivable', locale, userId);
|
|
630
|
+
}
|
|
631
|
+
async approveAccountsReceivableTitle(id, locale, userId) {
|
|
632
|
+
return this.approveTitle(id, 'receivable', locale, userId);
|
|
633
|
+
}
|
|
634
|
+
async cancelAccountsReceivableTitle(id, data, locale, userId) {
|
|
635
|
+
return this.cancelTitle(id, data, 'receivable', locale, userId);
|
|
636
|
+
}
|
|
637
|
+
async settleAccountsReceivableInstallment(id, data, locale, userId) {
|
|
638
|
+
return this.settleTitleInstallment(id, data, 'receivable', locale, userId);
|
|
639
|
+
}
|
|
640
|
+
async reverseAccountsReceivableSettlement(id, settlementId, data, locale, userId) {
|
|
641
|
+
return this.reverseTitleSettlement(id, settlementId, data, 'receivable', locale, userId);
|
|
642
|
+
}
|
|
593
643
|
async createTag(data) {
|
|
594
644
|
const slug = this.normalizeTagSlug(data.name);
|
|
595
645
|
if (!slug) {
|
|
@@ -825,6 +875,368 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
825
875
|
statusConciliacao: this.mapStatementStatusToPt(statement.status),
|
|
826
876
|
}));
|
|
827
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
|
+
}
|
|
828
1240
|
async createBankAccount(data, userId) {
|
|
829
1241
|
var _a;
|
|
830
1242
|
const accountType = this.mapAccountTypeFromPt(data.type);
|
|
@@ -1124,110 +1536,759 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1124
1536
|
return title;
|
|
1125
1537
|
}
|
|
1126
1538
|
async createTitle(data, titleType, locale, userId) {
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (!person) {
|
|
1132
|
-
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, 'Person not found'));
|
|
1133
|
-
}
|
|
1134
|
-
if (data.finance_category_id) {
|
|
1135
|
-
const category = await this.prisma.finance_category.findUnique({
|
|
1136
|
-
where: { id: data.finance_category_id },
|
|
1539
|
+
const installments = this.normalizeAndValidateInstallments(data, locale);
|
|
1540
|
+
const createdTitleId = await this.prisma.$transaction(async (tx) => {
|
|
1541
|
+
const person = await tx.person.findUnique({
|
|
1542
|
+
where: { id: data.person_id },
|
|
1137
1543
|
select: { id: true },
|
|
1138
1544
|
});
|
|
1139
|
-
if (!
|
|
1140
|
-
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('
|
|
1545
|
+
if (!person) {
|
|
1546
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, 'Person not found'));
|
|
1141
1547
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1548
|
+
if (data.finance_category_id) {
|
|
1549
|
+
const category = await tx.finance_category.findUnique({
|
|
1550
|
+
where: { id: data.finance_category_id },
|
|
1551
|
+
select: { id: true },
|
|
1552
|
+
});
|
|
1553
|
+
if (!category) {
|
|
1554
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('categoryNotFound', locale, 'Category not found'));
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (data.cost_center_id) {
|
|
1558
|
+
const costCenter = await tx.cost_center.findUnique({
|
|
1559
|
+
where: { id: data.cost_center_id },
|
|
1560
|
+
select: { id: true },
|
|
1561
|
+
});
|
|
1562
|
+
if (!costCenter) {
|
|
1563
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('costCenterNotFound', locale, 'Cost center not found'));
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
const attachmentFileIds = [
|
|
1567
|
+
...new Set((data.attachment_file_ids || []).filter((fileId) => fileId > 0)),
|
|
1568
|
+
];
|
|
1569
|
+
if (attachmentFileIds.length > 0) {
|
|
1570
|
+
const existingFiles = await tx.file.findMany({
|
|
1571
|
+
where: {
|
|
1572
|
+
id: { in: attachmentFileIds },
|
|
1573
|
+
},
|
|
1574
|
+
select: {
|
|
1575
|
+
id: true,
|
|
1576
|
+
},
|
|
1577
|
+
});
|
|
1578
|
+
const existingFileIds = new Set(existingFiles.map((file) => file.id));
|
|
1579
|
+
const invalidFileIds = attachmentFileIds.filter((fileId) => !existingFileIds.has(fileId));
|
|
1580
|
+
if (invalidFileIds.length > 0) {
|
|
1581
|
+
throw new common_1.BadRequestException(`Invalid attachment file IDs: ${invalidFileIds.join(', ')}`);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
await this.assertDateNotInClosedPeriod(tx, data.competence_date
|
|
1585
|
+
? new Date(data.competence_date)
|
|
1586
|
+
: new Date(installments[0].due_date), 'create title');
|
|
1587
|
+
const title = await tx.financial_title.create({
|
|
1588
|
+
data: {
|
|
1589
|
+
person_id: data.person_id,
|
|
1590
|
+
title_type: titleType,
|
|
1591
|
+
status: 'draft',
|
|
1592
|
+
document_number: data.document_number,
|
|
1593
|
+
description: data.description,
|
|
1594
|
+
competence_date: data.competence_date
|
|
1595
|
+
? new Date(data.competence_date)
|
|
1596
|
+
: null,
|
|
1597
|
+
issue_date: data.issue_date ? new Date(data.issue_date) : null,
|
|
1598
|
+
total_amount_cents: this.toCents(data.total_amount),
|
|
1599
|
+
finance_category_id: data.finance_category_id,
|
|
1600
|
+
created_by_user_id: userId,
|
|
1601
|
+
},
|
|
1602
|
+
});
|
|
1603
|
+
if (attachmentFileIds.length > 0) {
|
|
1604
|
+
await tx.financial_title_attachment.createMany({
|
|
1605
|
+
data: attachmentFileIds.map((fileId) => ({
|
|
1606
|
+
title_id: title.id,
|
|
1607
|
+
file_id: fileId,
|
|
1608
|
+
uploaded_by_user_id: userId,
|
|
1609
|
+
})),
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
for (let index = 0; index < installments.length; index++) {
|
|
1613
|
+
const installment = installments[index];
|
|
1614
|
+
const amountCents = installment.amount_cents;
|
|
1615
|
+
const createdInstallment = await tx.financial_installment.create({
|
|
1616
|
+
data: {
|
|
1617
|
+
title_id: title.id,
|
|
1618
|
+
installment_number: installment.installment_number,
|
|
1619
|
+
competence_date: data.competence_date
|
|
1620
|
+
? new Date(data.competence_date)
|
|
1621
|
+
: new Date(installment.due_date),
|
|
1622
|
+
due_date: new Date(installment.due_date),
|
|
1623
|
+
amount_cents: amountCents,
|
|
1624
|
+
open_amount_cents: amountCents,
|
|
1625
|
+
status: this.resolveInstallmentStatus(amountCents, amountCents, new Date(installment.due_date)),
|
|
1626
|
+
notes: data.description,
|
|
1627
|
+
},
|
|
1628
|
+
});
|
|
1629
|
+
if (data.cost_center_id) {
|
|
1630
|
+
await tx.installment_allocation.create({
|
|
1631
|
+
data: {
|
|
1632
|
+
installment_id: createdInstallment.id,
|
|
1633
|
+
cost_center_id: data.cost_center_id,
|
|
1634
|
+
allocated_amount_cents: amountCents,
|
|
1635
|
+
},
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
await this.createAuditLog(tx, {
|
|
1640
|
+
action: 'CREATE_TITLE',
|
|
1641
|
+
entityTable: 'financial_title',
|
|
1642
|
+
entityId: String(title.id),
|
|
1643
|
+
actorUserId: userId,
|
|
1644
|
+
summary: `Created ${titleType} title ${title.id} in draft`,
|
|
1645
|
+
afterData: JSON.stringify({
|
|
1646
|
+
status: 'draft',
|
|
1647
|
+
total_amount_cents: this.toCents(data.total_amount),
|
|
1648
|
+
}),
|
|
1649
|
+
});
|
|
1650
|
+
return title.id;
|
|
1651
|
+
});
|
|
1652
|
+
const createdTitle = await this.getTitleById(createdTitleId, titleType, locale);
|
|
1653
|
+
return this.mapTitleToFront(createdTitle, data.payment_channel);
|
|
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 },
|
|
1146
1679
|
select: { id: true },
|
|
1147
1680
|
});
|
|
1148
|
-
if (!
|
|
1149
|
-
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('
|
|
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
|
+
}
|
|
1150
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');
|
|
1151
1838
|
}
|
|
1152
|
-
|
|
1839
|
+
return this.mapTitleToFront(updatedTitle, data.payment_channel);
|
|
1840
|
+
}
|
|
1841
|
+
normalizeAndValidateInstallments(data, locale) {
|
|
1842
|
+
const fallbackDueDate = data.due_date;
|
|
1843
|
+
const totalAmountCents = this.toCents(data.total_amount);
|
|
1844
|
+
const sourceInstallments = data.installments && data.installments.length > 0
|
|
1153
1845
|
? data.installments
|
|
1154
1846
|
: [
|
|
1155
1847
|
{
|
|
1156
1848
|
installment_number: 1,
|
|
1157
|
-
due_date:
|
|
1849
|
+
due_date: fallbackDueDate,
|
|
1158
1850
|
amount: data.total_amount,
|
|
1159
1851
|
},
|
|
1160
1852
|
];
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
},
|
|
1853
|
+
const normalizedInstallments = sourceInstallments.map((installment, index) => {
|
|
1854
|
+
const installmentDueDate = installment.due_date || fallbackDueDate;
|
|
1855
|
+
if (!installmentDueDate) {
|
|
1856
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('installmentDueDateRequired', locale, 'Installment due date is required'));
|
|
1857
|
+
}
|
|
1858
|
+
const amountCents = this.toCents(installment.amount);
|
|
1859
|
+
if (amountCents <= 0) {
|
|
1860
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('installmentAmountInvalid', locale, 'Installment amount must be greater than zero'));
|
|
1861
|
+
}
|
|
1862
|
+
return {
|
|
1863
|
+
installment_number: installment.installment_number || index + 1,
|
|
1864
|
+
due_date: installmentDueDate,
|
|
1865
|
+
amount_cents: amountCents,
|
|
1866
|
+
};
|
|
1176
1867
|
});
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1868
|
+
const installmentsTotalCents = normalizedInstallments.reduce((acc, installment) => acc + installment.amount_cents, 0);
|
|
1869
|
+
if (installmentsTotalCents !== totalAmountCents) {
|
|
1870
|
+
throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('installmentsTotalMismatch', locale, 'Installments total must be equal to title total amount'));
|
|
1871
|
+
}
|
|
1872
|
+
return normalizedInstallments;
|
|
1873
|
+
}
|
|
1874
|
+
async approveTitle(titleId, titleType, locale, userId) {
|
|
1875
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
1876
|
+
const title = await tx.financial_title.findFirst({
|
|
1182
1877
|
where: {
|
|
1183
|
-
id:
|
|
1878
|
+
id: titleId,
|
|
1879
|
+
title_type: titleType,
|
|
1184
1880
|
},
|
|
1185
1881
|
select: {
|
|
1186
1882
|
id: true,
|
|
1883
|
+
status: true,
|
|
1884
|
+
competence_date: true,
|
|
1187
1885
|
},
|
|
1188
1886
|
});
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
if (invalidFileIds.length > 0) {
|
|
1192
|
-
throw new common_1.BadRequestException(`Invalid attachment file IDs: ${invalidFileIds.join(', ')}`);
|
|
1887
|
+
if (!title) {
|
|
1888
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
1193
1889
|
}
|
|
1194
|
-
await this.
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}
|
|
1890
|
+
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'approve title');
|
|
1891
|
+
if (title.status !== 'draft') {
|
|
1892
|
+
throw new common_1.BadRequestException('Only draft titles can be approved');
|
|
1893
|
+
}
|
|
1894
|
+
await tx.financial_title.update({
|
|
1895
|
+
where: { id: title.id },
|
|
1896
|
+
data: {
|
|
1897
|
+
status: 'open',
|
|
1898
|
+
},
|
|
1200
1899
|
});
|
|
1900
|
+
await this.createAuditLog(tx, {
|
|
1901
|
+
action: 'APPROVE_TITLE',
|
|
1902
|
+
entityTable: 'financial_title',
|
|
1903
|
+
entityId: String(title.id),
|
|
1904
|
+
actorUserId: userId,
|
|
1905
|
+
summary: `Approved ${titleType} title ${title.id}`,
|
|
1906
|
+
beforeData: JSON.stringify({ status: title.status }),
|
|
1907
|
+
afterData: JSON.stringify({ status: 'open' }),
|
|
1908
|
+
});
|
|
1909
|
+
return tx.financial_title.findFirst({
|
|
1910
|
+
where: {
|
|
1911
|
+
id: title.id,
|
|
1912
|
+
title_type: titleType,
|
|
1913
|
+
},
|
|
1914
|
+
include: this.defaultTitleInclude(),
|
|
1915
|
+
});
|
|
1916
|
+
});
|
|
1917
|
+
if (!updatedTitle) {
|
|
1918
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
1201
1919
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1920
|
+
return this.mapTitleToFront(updatedTitle);
|
|
1921
|
+
}
|
|
1922
|
+
async rejectTitle(titleId, data, titleType, locale, userId) {
|
|
1923
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
1924
|
+
const title = await tx.financial_title.findFirst({
|
|
1925
|
+
where: {
|
|
1926
|
+
id: titleId,
|
|
1927
|
+
title_type: titleType,
|
|
1928
|
+
},
|
|
1929
|
+
select: {
|
|
1930
|
+
id: true,
|
|
1931
|
+
status: true,
|
|
1932
|
+
competence_date: true,
|
|
1933
|
+
},
|
|
1934
|
+
});
|
|
1935
|
+
if (!title) {
|
|
1936
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
1937
|
+
}
|
|
1938
|
+
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'reject title');
|
|
1939
|
+
if (title.status !== 'draft') {
|
|
1940
|
+
throw new common_1.BadRequestException('Only draft titles can be rejected');
|
|
1941
|
+
}
|
|
1942
|
+
await tx.financial_title.update({
|
|
1943
|
+
where: { id: title.id },
|
|
1944
|
+
data: {
|
|
1945
|
+
status: 'canceled',
|
|
1946
|
+
},
|
|
1947
|
+
});
|
|
1948
|
+
await this.createAuditLog(tx, {
|
|
1949
|
+
action: 'REJECT_TITLE',
|
|
1950
|
+
entityTable: 'financial_title',
|
|
1951
|
+
entityId: String(title.id),
|
|
1952
|
+
actorUserId: userId,
|
|
1953
|
+
summary: `Rejected ${titleType} title ${title.id}`,
|
|
1954
|
+
beforeData: JSON.stringify({ status: title.status }),
|
|
1955
|
+
afterData: JSON.stringify({
|
|
1956
|
+
status: 'canceled',
|
|
1957
|
+
reason: (data === null || data === void 0 ? void 0 : data.reason) || null,
|
|
1958
|
+
}),
|
|
1959
|
+
});
|
|
1960
|
+
return tx.financial_title.findFirst({
|
|
1961
|
+
where: {
|
|
1962
|
+
id: title.id,
|
|
1963
|
+
title_type: titleType,
|
|
1964
|
+
},
|
|
1965
|
+
include: this.defaultTitleInclude(),
|
|
1966
|
+
});
|
|
1967
|
+
});
|
|
1968
|
+
if (!updatedTitle) {
|
|
1969
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
1970
|
+
}
|
|
1971
|
+
return this.mapTitleToFront(updatedTitle);
|
|
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 },
|
|
1206
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
|
+
}
|
|
2024
|
+
async settleTitleInstallment(titleId, data, titleType, locale, userId) {
|
|
2025
|
+
const amountCents = this.toCents(data.amount);
|
|
2026
|
+
if (amountCents <= 0) {
|
|
2027
|
+
throw new common_1.BadRequestException('Settlement amount must be greater than zero');
|
|
2028
|
+
}
|
|
2029
|
+
const settledAt = data.settled_at ? new Date(data.settled_at) : new Date();
|
|
2030
|
+
if (Number.isNaN(settledAt.getTime())) {
|
|
2031
|
+
throw new common_1.BadRequestException('Invalid settlement date');
|
|
2032
|
+
}
|
|
2033
|
+
const result = await this.prisma.$transaction(async (tx) => {
|
|
2034
|
+
var _a;
|
|
2035
|
+
const title = await tx.financial_title.findFirst({
|
|
2036
|
+
where: {
|
|
2037
|
+
id: titleId,
|
|
2038
|
+
title_type: titleType,
|
|
2039
|
+
},
|
|
2040
|
+
select: {
|
|
2041
|
+
id: true,
|
|
2042
|
+
person_id: true,
|
|
2043
|
+
status: true,
|
|
2044
|
+
competence_date: true,
|
|
2045
|
+
},
|
|
2046
|
+
});
|
|
2047
|
+
if (!title) {
|
|
2048
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
2049
|
+
}
|
|
2050
|
+
if (!['open', 'partial'].includes(title.status)) {
|
|
2051
|
+
throw new common_1.BadRequestException('Only open/partial titles can be settled');
|
|
2052
|
+
}
|
|
2053
|
+
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'settle installment');
|
|
2054
|
+
const installment = await tx.financial_installment.findFirst({
|
|
2055
|
+
where: {
|
|
2056
|
+
id: data.installment_id,
|
|
1207
2057
|
title_id: title.id,
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
2058
|
+
},
|
|
2059
|
+
select: {
|
|
2060
|
+
id: true,
|
|
2061
|
+
title_id: true,
|
|
2062
|
+
amount_cents: true,
|
|
2063
|
+
open_amount_cents: true,
|
|
2064
|
+
due_date: true,
|
|
2065
|
+
status: true,
|
|
2066
|
+
},
|
|
2067
|
+
});
|
|
2068
|
+
if (!installment) {
|
|
2069
|
+
throw new common_1.BadRequestException('Installment not found for this title');
|
|
2070
|
+
}
|
|
2071
|
+
if (installment.status === 'settled' || installment.status === 'canceled') {
|
|
2072
|
+
throw new common_1.BadRequestException('This installment cannot be settled');
|
|
2073
|
+
}
|
|
2074
|
+
if (amountCents > installment.open_amount_cents) {
|
|
2075
|
+
throw new common_1.BadRequestException('Settlement amount exceeds open amount');
|
|
2076
|
+
}
|
|
2077
|
+
const paymentMethodId = await this.resolvePaymentMethodId(tx, data.payment_channel);
|
|
2078
|
+
const settlement = await tx.settlement.create({
|
|
2079
|
+
data: {
|
|
2080
|
+
person_id: title.person_id,
|
|
2081
|
+
bank_account_id: data.bank_account_id || null,
|
|
2082
|
+
payment_method_id: paymentMethodId,
|
|
2083
|
+
settlement_type: titleType,
|
|
2084
|
+
status: 'confirmed',
|
|
2085
|
+
settled_at: settledAt,
|
|
1213
2086
|
amount_cents: amountCents,
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
notes: data.description,
|
|
2087
|
+
description: ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || null,
|
|
2088
|
+
created_by_user_id: userId,
|
|
1217
2089
|
},
|
|
1218
2090
|
});
|
|
1219
|
-
|
|
1220
|
-
|
|
2091
|
+
await tx.settlement_allocation.create({
|
|
2092
|
+
data: {
|
|
2093
|
+
settlement_id: settlement.id,
|
|
2094
|
+
installment_id: installment.id,
|
|
2095
|
+
allocated_amount_cents: amountCents,
|
|
2096
|
+
discount_cents: this.toCents(data.discount || 0),
|
|
2097
|
+
interest_cents: this.toCents(data.interest || 0),
|
|
2098
|
+
penalty_cents: this.toCents(data.penalty || 0),
|
|
2099
|
+
},
|
|
2100
|
+
});
|
|
2101
|
+
const decrementResult = await tx.financial_installment.updateMany({
|
|
2102
|
+
where: {
|
|
2103
|
+
id: installment.id,
|
|
2104
|
+
open_amount_cents: {
|
|
2105
|
+
gte: amountCents,
|
|
2106
|
+
},
|
|
2107
|
+
},
|
|
2108
|
+
data: {
|
|
2109
|
+
open_amount_cents: {
|
|
2110
|
+
decrement: amountCents,
|
|
2111
|
+
},
|
|
2112
|
+
},
|
|
2113
|
+
});
|
|
2114
|
+
if (decrementResult.count !== 1) {
|
|
2115
|
+
throw new common_1.BadRequestException('Installment was updated concurrently, please try again');
|
|
2116
|
+
}
|
|
2117
|
+
const updatedInstallment = await tx.financial_installment.findUnique({
|
|
2118
|
+
where: {
|
|
2119
|
+
id: installment.id,
|
|
2120
|
+
},
|
|
2121
|
+
select: {
|
|
2122
|
+
id: true,
|
|
2123
|
+
amount_cents: true,
|
|
2124
|
+
open_amount_cents: true,
|
|
2125
|
+
due_date: true,
|
|
2126
|
+
status: true,
|
|
2127
|
+
},
|
|
2128
|
+
});
|
|
2129
|
+
if (!updatedInstallment) {
|
|
2130
|
+
throw new common_1.NotFoundException('Installment not found');
|
|
2131
|
+
}
|
|
2132
|
+
const nextInstallmentStatus = this.resolveInstallmentStatus(updatedInstallment.amount_cents, updatedInstallment.open_amount_cents, updatedInstallment.due_date);
|
|
2133
|
+
if (updatedInstallment.status !== nextInstallmentStatus) {
|
|
2134
|
+
await tx.financial_installment.update({
|
|
2135
|
+
where: {
|
|
2136
|
+
id: updatedInstallment.id,
|
|
2137
|
+
},
|
|
2138
|
+
data: {
|
|
2139
|
+
status: nextInstallmentStatus,
|
|
2140
|
+
},
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
const previousTitleStatus = title.status;
|
|
2144
|
+
const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
|
|
2145
|
+
await this.createAuditLog(tx, {
|
|
2146
|
+
action: 'SETTLE_INSTALLMENT',
|
|
2147
|
+
entityTable: 'financial_title',
|
|
2148
|
+
entityId: String(title.id),
|
|
2149
|
+
actorUserId: userId,
|
|
2150
|
+
summary: `Settled installment ${installment.id} of title ${title.id}`,
|
|
2151
|
+
beforeData: JSON.stringify({
|
|
2152
|
+
title_status: previousTitleStatus,
|
|
2153
|
+
installment_open_amount_cents: installment.open_amount_cents,
|
|
2154
|
+
}),
|
|
2155
|
+
afterData: JSON.stringify({
|
|
2156
|
+
title_status: nextTitleStatus,
|
|
2157
|
+
installment_open_amount_cents: updatedInstallment.open_amount_cents,
|
|
2158
|
+
settlement_id: settlement.id,
|
|
2159
|
+
}),
|
|
2160
|
+
});
|
|
2161
|
+
const updatedTitle = await tx.financial_title.findFirst({
|
|
2162
|
+
where: {
|
|
2163
|
+
id: title.id,
|
|
2164
|
+
title_type: titleType,
|
|
2165
|
+
},
|
|
2166
|
+
include: this.defaultTitleInclude(),
|
|
2167
|
+
});
|
|
2168
|
+
if (!updatedTitle) {
|
|
2169
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
2170
|
+
}
|
|
2171
|
+
return {
|
|
2172
|
+
title: updatedTitle,
|
|
2173
|
+
settlementId: settlement.id,
|
|
2174
|
+
};
|
|
2175
|
+
});
|
|
2176
|
+
return Object.assign(Object.assign({}, this.mapTitleToFront(result.title)), { settlementId: String(result.settlementId) });
|
|
2177
|
+
}
|
|
2178
|
+
async reverseTitleSettlement(titleId, settlementId, data, titleType, locale, userId) {
|
|
2179
|
+
const updatedTitle = await this.prisma.$transaction(async (tx) => {
|
|
2180
|
+
const title = await tx.financial_title.findFirst({
|
|
2181
|
+
where: {
|
|
2182
|
+
id: titleId,
|
|
2183
|
+
title_type: titleType,
|
|
2184
|
+
},
|
|
2185
|
+
select: {
|
|
2186
|
+
id: true,
|
|
2187
|
+
status: true,
|
|
2188
|
+
competence_date: true,
|
|
2189
|
+
},
|
|
2190
|
+
});
|
|
2191
|
+
if (!title) {
|
|
2192
|
+
throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
|
|
2193
|
+
}
|
|
2194
|
+
await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'reverse settlement');
|
|
2195
|
+
const settlement = await tx.settlement.findFirst({
|
|
2196
|
+
where: {
|
|
2197
|
+
id: settlementId,
|
|
2198
|
+
settlement_type: titleType,
|
|
2199
|
+
settlement_allocation: {
|
|
2200
|
+
some: {
|
|
2201
|
+
financial_installment: {
|
|
2202
|
+
title_id: title.id,
|
|
2203
|
+
},
|
|
2204
|
+
},
|
|
2205
|
+
},
|
|
2206
|
+
},
|
|
2207
|
+
include: {
|
|
2208
|
+
settlement_allocation: {
|
|
2209
|
+
include: {
|
|
2210
|
+
financial_installment: {
|
|
2211
|
+
select: {
|
|
2212
|
+
id: true,
|
|
2213
|
+
amount_cents: true,
|
|
2214
|
+
open_amount_cents: true,
|
|
2215
|
+
due_date: true,
|
|
2216
|
+
status: true,
|
|
2217
|
+
},
|
|
2218
|
+
},
|
|
2219
|
+
},
|
|
2220
|
+
},
|
|
2221
|
+
},
|
|
2222
|
+
});
|
|
2223
|
+
if (!settlement) {
|
|
2224
|
+
throw new common_1.NotFoundException('Settlement not found for this title');
|
|
2225
|
+
}
|
|
2226
|
+
if (settlement.status === 'reversed') {
|
|
2227
|
+
throw new common_1.BadRequestException('This settlement is already reversed');
|
|
2228
|
+
}
|
|
2229
|
+
for (const allocation of settlement.settlement_allocation) {
|
|
2230
|
+
const installment = allocation.financial_installment;
|
|
2231
|
+
if (!installment) {
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
const nextOpenAmountCents = installment.open_amount_cents + allocation.allocated_amount_cents;
|
|
2235
|
+
if (nextOpenAmountCents > installment.amount_cents) {
|
|
2236
|
+
throw new common_1.BadRequestException(`Reverse would exceed installment amount for installment ${installment.id}`);
|
|
2237
|
+
}
|
|
2238
|
+
const nextInstallmentStatus = this.resolveInstallmentStatus(installment.amount_cents, nextOpenAmountCents, installment.due_date);
|
|
2239
|
+
await tx.financial_installment.update({
|
|
2240
|
+
where: {
|
|
2241
|
+
id: installment.id,
|
|
2242
|
+
},
|
|
1221
2243
|
data: {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
allocated_amount_cents: amountCents,
|
|
2244
|
+
open_amount_cents: nextOpenAmountCents,
|
|
2245
|
+
status: nextInstallmentStatus,
|
|
1225
2246
|
},
|
|
1226
2247
|
});
|
|
1227
2248
|
}
|
|
2249
|
+
await tx.settlement.update({
|
|
2250
|
+
where: {
|
|
2251
|
+
id: settlement.id,
|
|
2252
|
+
},
|
|
2253
|
+
data: {
|
|
2254
|
+
status: 'reversed',
|
|
2255
|
+
description: [
|
|
2256
|
+
settlement.description,
|
|
2257
|
+
data.reason ? `Reversed: ${data.reason.trim()}` : 'Reversed',
|
|
2258
|
+
]
|
|
2259
|
+
.filter(Boolean)
|
|
2260
|
+
.join(' | '),
|
|
2261
|
+
},
|
|
2262
|
+
});
|
|
2263
|
+
const previousTitleStatus = title.status;
|
|
2264
|
+
const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
|
|
2265
|
+
await this.createAuditLog(tx, {
|
|
2266
|
+
action: 'REVERSE_SETTLEMENT',
|
|
2267
|
+
entityTable: 'financial_title',
|
|
2268
|
+
entityId: String(title.id),
|
|
2269
|
+
actorUserId: userId,
|
|
2270
|
+
summary: `Reversed settlement ${settlement.id} from title ${title.id}`,
|
|
2271
|
+
beforeData: JSON.stringify({
|
|
2272
|
+
title_status: previousTitleStatus,
|
|
2273
|
+
settlement_status: settlement.status,
|
|
2274
|
+
}),
|
|
2275
|
+
afterData: JSON.stringify({
|
|
2276
|
+
title_status: nextTitleStatus,
|
|
2277
|
+
settlement_status: 'reversed',
|
|
2278
|
+
}),
|
|
2279
|
+
});
|
|
2280
|
+
return tx.financial_title.findFirst({
|
|
2281
|
+
where: {
|
|
2282
|
+
id: title.id,
|
|
2283
|
+
title_type: titleType,
|
|
2284
|
+
},
|
|
2285
|
+
include: this.defaultTitleInclude(),
|
|
2286
|
+
});
|
|
2287
|
+
});
|
|
2288
|
+
if (!updatedTitle) {
|
|
2289
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
1228
2290
|
}
|
|
1229
|
-
|
|
1230
|
-
return this.mapTitleToFront(createdTitle, data.payment_channel);
|
|
2291
|
+
return this.mapTitleToFront(updatedTitle);
|
|
1231
2292
|
}
|
|
1232
2293
|
async updateTitleTags(titleId, titleType, tagIds, locale) {
|
|
1233
2294
|
const title = await this.getTitleById(titleId, titleType, locale);
|
|
@@ -1377,6 +2438,124 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1377
2438
|
data: log.created_at.toISOString(),
|
|
1378
2439
|
}));
|
|
1379
2440
|
}
|
|
2441
|
+
async resolvePaymentMethodId(tx, paymentChannel) {
|
|
2442
|
+
const paymentType = this.mapPaymentMethodFromPt(paymentChannel);
|
|
2443
|
+
if (!paymentType) {
|
|
2444
|
+
return null;
|
|
2445
|
+
}
|
|
2446
|
+
const paymentMethod = await tx.payment_method.findFirst({
|
|
2447
|
+
where: {
|
|
2448
|
+
type: paymentType,
|
|
2449
|
+
status: 'active',
|
|
2450
|
+
},
|
|
2451
|
+
select: {
|
|
2452
|
+
id: true,
|
|
2453
|
+
},
|
|
2454
|
+
});
|
|
2455
|
+
return (paymentMethod === null || paymentMethod === void 0 ? void 0 : paymentMethod.id) || null;
|
|
2456
|
+
}
|
|
2457
|
+
async assertDateNotInClosedPeriod(tx, competenceDate, operation) {
|
|
2458
|
+
if (!competenceDate) {
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
const closedPeriod = await tx.period_close.findFirst({
|
|
2462
|
+
where: {
|
|
2463
|
+
status: 'closed',
|
|
2464
|
+
period_start: {
|
|
2465
|
+
lte: competenceDate,
|
|
2466
|
+
},
|
|
2467
|
+
period_end: {
|
|
2468
|
+
gte: competenceDate,
|
|
2469
|
+
},
|
|
2470
|
+
},
|
|
2471
|
+
select: {
|
|
2472
|
+
id: true,
|
|
2473
|
+
},
|
|
2474
|
+
});
|
|
2475
|
+
if (closedPeriod) {
|
|
2476
|
+
throw new common_1.BadRequestException(`Cannot ${operation}: competence is in a closed period`);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
resolveInstallmentStatus(amountCents, openAmountCents, dueDate, currentStatus) {
|
|
2480
|
+
if (currentStatus === 'canceled') {
|
|
2481
|
+
return 'canceled';
|
|
2482
|
+
}
|
|
2483
|
+
if (openAmountCents <= 0) {
|
|
2484
|
+
return 'settled';
|
|
2485
|
+
}
|
|
2486
|
+
if (openAmountCents < amountCents) {
|
|
2487
|
+
return 'partial';
|
|
2488
|
+
}
|
|
2489
|
+
const today = new Date();
|
|
2490
|
+
today.setHours(0, 0, 0, 0);
|
|
2491
|
+
const dueDateOnly = new Date(dueDate);
|
|
2492
|
+
dueDateOnly.setHours(0, 0, 0, 0);
|
|
2493
|
+
if (dueDateOnly < today) {
|
|
2494
|
+
return 'overdue';
|
|
2495
|
+
}
|
|
2496
|
+
return 'open';
|
|
2497
|
+
}
|
|
2498
|
+
deriveTitleStatusFromInstallments(installments) {
|
|
2499
|
+
if (installments.length === 0) {
|
|
2500
|
+
return 'open';
|
|
2501
|
+
}
|
|
2502
|
+
const effectiveStatuses = installments.map((installment) => this.resolveInstallmentStatus(installment.amount_cents, installment.open_amount_cents, installment.due_date, installment.status));
|
|
2503
|
+
if (effectiveStatuses.every((status) => status === 'settled')) {
|
|
2504
|
+
return 'settled';
|
|
2505
|
+
}
|
|
2506
|
+
const hasPayment = installments.some((installment) => installment.open_amount_cents < installment.amount_cents);
|
|
2507
|
+
if (hasPayment) {
|
|
2508
|
+
return 'partial';
|
|
2509
|
+
}
|
|
2510
|
+
return 'open';
|
|
2511
|
+
}
|
|
2512
|
+
async recalculateTitleStatus(tx, titleId) {
|
|
2513
|
+
const title = await tx.financial_title.findUnique({
|
|
2514
|
+
where: {
|
|
2515
|
+
id: titleId,
|
|
2516
|
+
},
|
|
2517
|
+
select: {
|
|
2518
|
+
id: true,
|
|
2519
|
+
status: true,
|
|
2520
|
+
financial_installment: {
|
|
2521
|
+
select: {
|
|
2522
|
+
amount_cents: true,
|
|
2523
|
+
open_amount_cents: true,
|
|
2524
|
+
due_date: true,
|
|
2525
|
+
status: true,
|
|
2526
|
+
},
|
|
2527
|
+
},
|
|
2528
|
+
},
|
|
2529
|
+
});
|
|
2530
|
+
if (!title) {
|
|
2531
|
+
throw new common_1.NotFoundException('Financial title not found');
|
|
2532
|
+
}
|
|
2533
|
+
const nextStatus = this.deriveTitleStatusFromInstallments(title.financial_installment);
|
|
2534
|
+
if (title.status !== nextStatus) {
|
|
2535
|
+
await tx.financial_title.update({
|
|
2536
|
+
where: {
|
|
2537
|
+
id: title.id,
|
|
2538
|
+
},
|
|
2539
|
+
data: {
|
|
2540
|
+
status: nextStatus,
|
|
2541
|
+
},
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
return nextStatus;
|
|
2545
|
+
}
|
|
2546
|
+
async createAuditLog(tx, data) {
|
|
2547
|
+
await tx.audit_log.create({
|
|
2548
|
+
data: {
|
|
2549
|
+
actor_user_id: data.actorUserId || null,
|
|
2550
|
+
action: data.action,
|
|
2551
|
+
entity_table: data.entityTable,
|
|
2552
|
+
entity_id: data.entityId,
|
|
2553
|
+
summary: data.summary || null,
|
|
2554
|
+
before_data: data.beforeData || null,
|
|
2555
|
+
after_data: data.afterData || null,
|
|
2556
|
+
},
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
1380
2559
|
mapTitleToFront(title, paymentChannelOverride) {
|
|
1381
2560
|
var _a;
|
|
1382
2561
|
const allocations = title.financial_installment.flatMap((installment) => installment.installment_allocation);
|
|
@@ -1397,21 +2576,26 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1397
2576
|
numero: installment.installment_number,
|
|
1398
2577
|
vencimento: installment.due_date.toISOString(),
|
|
1399
2578
|
valor: this.fromCents(installment.amount_cents),
|
|
1400
|
-
|
|
2579
|
+
valorAberto: this.fromCents(installment.open_amount_cents),
|
|
2580
|
+
status: this.mapStatusToPt(this.resolveInstallmentStatus(installment.amount_cents, installment.open_amount_cents, installment.due_date, installment.status)),
|
|
1401
2581
|
metodoPagamento: this.mapPaymentMethodToPt((_c = (_b = (_a = installment.settlement_allocation[0]) === null || _a === void 0 ? void 0 : _a.settlement) === null || _b === void 0 ? void 0 : _b.payment_method) === null || _c === void 0 ? void 0 : _c.type) || paymentChannelOverride || 'transferencia',
|
|
1402
2582
|
liquidacoes: installment.settlement_allocation.map((allocation) => {
|
|
1403
|
-
var _a, _b, _c, _d, _e;
|
|
2583
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1404
2584
|
return ({
|
|
1405
2585
|
id: String(allocation.id),
|
|
1406
|
-
|
|
2586
|
+
settlementId: ((_a = allocation.settlement) === null || _a === void 0 ? void 0 : _a.id)
|
|
2587
|
+
? String(allocation.settlement.id)
|
|
2588
|
+
: null,
|
|
2589
|
+
data: (_c = (_b = allocation.settlement) === null || _b === void 0 ? void 0 : _b.settled_at) === null || _c === void 0 ? void 0 : _c.toISOString(),
|
|
1407
2590
|
valor: this.fromCents(allocation.allocated_amount_cents),
|
|
1408
2591
|
juros: this.fromCents(allocation.interest_cents || 0),
|
|
1409
2592
|
desconto: this.fromCents(allocation.discount_cents || 0),
|
|
1410
2593
|
multa: this.fromCents(allocation.penalty_cents || 0),
|
|
1411
|
-
contaBancariaId: ((
|
|
2594
|
+
contaBancariaId: ((_d = allocation.settlement) === null || _d === void 0 ? void 0 : _d.bank_account_id)
|
|
1412
2595
|
? String(allocation.settlement.bank_account_id)
|
|
1413
2596
|
: null,
|
|
1414
|
-
|
|
2597
|
+
status: ((_e = allocation.settlement) === null || _e === void 0 ? void 0 : _e.status) || null,
|
|
2598
|
+
metodo: this.mapPaymentMethodToPt((_g = (_f = allocation.settlement) === null || _f === void 0 ? void 0 : _f.payment_method) === null || _g === void 0 ? void 0 : _g.type) || 'transferencia',
|
|
1415
2599
|
});
|
|
1416
2600
|
}),
|
|
1417
2601
|
});
|
|
@@ -1462,7 +2646,6 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1462
2646
|
mapStatusToPt(status) {
|
|
1463
2647
|
const statusMap = {
|
|
1464
2648
|
draft: 'rascunho',
|
|
1465
|
-
approved: 'aprovado',
|
|
1466
2649
|
open: 'aberto',
|
|
1467
2650
|
partial: 'parcial',
|
|
1468
2651
|
settled: 'liquidado',
|
|
@@ -1489,7 +2672,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1489
2672
|
}
|
|
1490
2673
|
const statusMap = {
|
|
1491
2674
|
rascunho: 'draft',
|
|
1492
|
-
aprovado: '
|
|
2675
|
+
aprovado: 'open',
|
|
1493
2676
|
aberto: 'open',
|
|
1494
2677
|
parcial: 'partial',
|
|
1495
2678
|
liquidado: 'settled',
|
|
@@ -1510,6 +2693,27 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1510
2693
|
};
|
|
1511
2694
|
return paymentMethodMap[paymentMethodType] || undefined;
|
|
1512
2695
|
}
|
|
2696
|
+
mapPaymentMethodFromPt(paymentMethodType) {
|
|
2697
|
+
if (!paymentMethodType) {
|
|
2698
|
+
return undefined;
|
|
2699
|
+
}
|
|
2700
|
+
const paymentMethodMap = {
|
|
2701
|
+
boleto: 'boleto',
|
|
2702
|
+
pix: 'pix',
|
|
2703
|
+
transferencia: 'ted',
|
|
2704
|
+
transferência: 'ted',
|
|
2705
|
+
ted: 'ted',
|
|
2706
|
+
doc: 'doc',
|
|
2707
|
+
cartao: 'card',
|
|
2708
|
+
cartão: 'card',
|
|
2709
|
+
dinheiro: 'cash',
|
|
2710
|
+
cheque: 'other',
|
|
2711
|
+
cash: 'cash',
|
|
2712
|
+
card: 'card',
|
|
2713
|
+
other: 'other',
|
|
2714
|
+
};
|
|
2715
|
+
return paymentMethodMap[(paymentMethodType || '').toLowerCase()];
|
|
2716
|
+
}
|
|
1513
2717
|
mapAccountTypeToPt(accountType) {
|
|
1514
2718
|
const accountTypeMap = {
|
|
1515
2719
|
checking: 'corrente',
|
|
@@ -1689,8 +2893,10 @@ let FinanceService = FinanceService_1 = class FinanceService {
|
|
|
1689
2893
|
exports.FinanceService = FinanceService;
|
|
1690
2894
|
exports.FinanceService = FinanceService = FinanceService_1 = __decorate([
|
|
1691
2895
|
(0, common_1.Injectable)(),
|
|
2896
|
+
__param(3, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.FileService))),
|
|
1692
2897
|
__metadata("design:paramtypes", [api_prisma_1.PrismaService,
|
|
1693
2898
|
api_pagination_1.PaginationService,
|
|
1694
|
-
core_1.AiService
|
|
2899
|
+
core_1.AiService,
|
|
2900
|
+
core_1.FileService])
|
|
1695
2901
|
], FinanceService);
|
|
1696
2902
|
//# sourceMappingURL=finance.service.js.map
|