@hed-hog/finance 0.0.239 → 0.0.244

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +1 -22
  2. package/dist/finance-installments.controller.d.ts +132 -0
  3. package/dist/finance-installments.controller.d.ts.map +1 -1
  4. package/dist/finance-installments.controller.js +52 -0
  5. package/dist/finance-installments.controller.js.map +1 -1
  6. package/dist/finance-statements.controller.d.ts +8 -0
  7. package/dist/finance-statements.controller.d.ts.map +1 -1
  8. package/dist/finance-statements.controller.js +40 -0
  9. package/dist/finance-statements.controller.js.map +1 -1
  10. package/dist/finance.module.d.ts.map +1 -1
  11. package/dist/finance.module.js +1 -0
  12. package/dist/finance.module.js.map +1 -1
  13. package/dist/finance.service.d.ts +160 -2
  14. package/dist/finance.service.d.ts.map +1 -1
  15. package/dist/finance.service.js +626 -8
  16. package/dist/finance.service.js.map +1 -1
  17. package/hedhog/data/route.yaml +54 -0
  18. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +80 -4
  19. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +736 -13
  20. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +1 -1
  21. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1 -3
  22. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +212 -60
  23. package/hedhog/frontend/messages/en.json +1 -0
  24. package/hedhog/frontend/messages/pt.json +1 -0
  25. package/hedhog/query/constraints.sql +86 -0
  26. package/hedhog/table/bank_account.yaml +0 -8
  27. package/hedhog/table/financial_title.yaml +1 -9
  28. package/hedhog/table/settlement.yaml +0 -8
  29. package/package.json +6 -6
  30. package/src/finance-installments.controller.ts +70 -10
  31. package/src/finance-statements.controller.ts +61 -2
  32. package/src/finance.module.ts +2 -1
  33. package/src/finance.service.ts +868 -12
  34. package/hedhog/table/branch.yaml +0 -18
@@ -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: 'approved',
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: 'approved' }),
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 (!['approved', 'open', 'partial'].includes(title.status)) {
1434
- throw new common_1.BadRequestException('Only approved/open/partial titles can be settled');
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: 'approved',
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