@hed-hog/finance 0.0.237 → 0.0.239

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 (47) hide show
  1. package/dist/dto/create-finance-tag.dto.d.ts +5 -0
  2. package/dist/dto/create-finance-tag.dto.d.ts.map +1 -0
  3. package/dist/dto/create-finance-tag.dto.js +29 -0
  4. package/dist/dto/create-finance-tag.dto.js.map +1 -0
  5. package/dist/dto/reject-title.dto.d.ts +4 -0
  6. package/dist/dto/reject-title.dto.d.ts.map +1 -0
  7. package/dist/dto/reject-title.dto.js +22 -0
  8. package/dist/dto/reject-title.dto.js.map +1 -0
  9. package/dist/dto/reverse-settlement.dto.d.ts +4 -0
  10. package/dist/dto/reverse-settlement.dto.d.ts.map +1 -0
  11. package/dist/dto/reverse-settlement.dto.js +22 -0
  12. package/dist/dto/reverse-settlement.dto.js.map +1 -0
  13. package/dist/dto/settle-installment.dto.d.ts +12 -0
  14. package/dist/dto/settle-installment.dto.d.ts.map +1 -0
  15. package/dist/dto/settle-installment.dto.js +71 -0
  16. package/dist/dto/settle-installment.dto.js.map +1 -0
  17. package/dist/dto/update-installment-tags.dto.d.ts +4 -0
  18. package/dist/dto/update-installment-tags.dto.d.ts.map +1 -0
  19. package/dist/dto/update-installment-tags.dto.js +27 -0
  20. package/dist/dto/update-installment-tags.dto.js.map +1 -0
  21. package/dist/finance-data.controller.d.ts +17 -5
  22. package/dist/finance-data.controller.d.ts.map +1 -1
  23. package/dist/finance-installments.controller.d.ts +325 -8
  24. package/dist/finance-installments.controller.d.ts.map +1 -1
  25. package/dist/finance-installments.controller.js +128 -0
  26. package/dist/finance-installments.controller.js.map +1 -1
  27. package/dist/finance.service.d.ts +357 -13
  28. package/dist/finance.service.d.ts.map +1 -1
  29. package/dist/finance.service.js +835 -64
  30. package/dist/finance.service.js.map +1 -1
  31. package/hedhog/data/route.yaml +90 -0
  32. package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +2 -0
  33. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +92 -12
  34. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +601 -79
  35. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +481 -19
  36. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +598 -69
  37. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +472 -15
  38. package/hedhog/frontend/messages/en.json +38 -0
  39. package/hedhog/frontend/messages/pt.json +38 -0
  40. package/package.json +5 -5
  41. package/src/dto/create-finance-tag.dto.ts +15 -0
  42. package/src/dto/reject-title.dto.ts +7 -0
  43. package/src/dto/reverse-settlement.dto.ts +7 -0
  44. package/src/dto/settle-installment.dto.ts +55 -0
  45. package/src/dto/update-installment-tags.dto.ts +12 -0
  46. package/src/finance-installments.controller.ts +145 -9
  47. package/src/finance.service.ts +1333 -165
@@ -498,7 +498,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
498
498
  return Number.isFinite(num) ? num : null;
499
499
  }
500
500
  async getData() {
501
- const [payables, receivables, people, categories, costCenters, bankAccounts, tags, auditLogs,] = await Promise.all([
501
+ const [payablesResult, receivablesResult, peopleResult, categoriesResult, costCentersResult, bankAccountsResult, tagsResult, auditLogsResult,] = await Promise.allSettled([
502
502
  this.loadTitles('payable'),
503
503
  this.loadTitles('receivable'),
504
504
  this.loadPeople(),
@@ -508,6 +508,49 @@ let FinanceService = FinanceService_1 = class FinanceService {
508
508
  this.loadTags(),
509
509
  this.loadAuditLogs(),
510
510
  ]);
511
+ const payables = payablesResult.status === 'fulfilled' ? payablesResult.value : [];
512
+ const receivables = receivablesResult.status === 'fulfilled' ? receivablesResult.value : [];
513
+ const people = peopleResult.status === 'fulfilled' ? peopleResult.value : [];
514
+ const categories = categoriesResult.status === 'fulfilled' ? categoriesResult.value : [];
515
+ const costCenters = costCentersResult.status === 'fulfilled' ? costCentersResult.value : [];
516
+ const bankAccounts = bankAccountsResult.status === 'fulfilled' ? bankAccountsResult.value : [];
517
+ const tags = tagsResult.status === 'fulfilled' ? tagsResult.value : [];
518
+ const auditLogs = auditLogsResult.status === 'fulfilled' ? auditLogsResult.value : [];
519
+ if (payablesResult.status === 'rejected') {
520
+ this.logger.error('Failed to load finance payables', payablesResult.reason);
521
+ }
522
+ if (receivablesResult.status === 'rejected') {
523
+ this.logger.error('Failed to load finance receivables', receivablesResult.reason);
524
+ }
525
+ if (peopleResult.status === 'rejected') {
526
+ this.logger.error('Failed to load finance people', peopleResult.reason);
527
+ }
528
+ if (categoriesResult.status === 'rejected') {
529
+ this.logger.error('Failed to load finance categories', categoriesResult.reason);
530
+ }
531
+ if (costCentersResult.status === 'rejected') {
532
+ this.logger.error('Failed to load finance cost centers', costCentersResult.reason);
533
+ }
534
+ if (bankAccountsResult.status === 'rejected') {
535
+ this.logger.error('Failed to load finance bank accounts', bankAccountsResult.reason);
536
+ }
537
+ if (tagsResult.status === 'rejected') {
538
+ this.logger.error('Failed to load finance tags', tagsResult.reason);
539
+ }
540
+ if (auditLogsResult.status === 'rejected') {
541
+ this.logger.error('Failed to load finance audit logs', auditLogsResult.reason);
542
+ }
543
+ const aprovacoesPendentes = payables
544
+ .filter((title) => title.status === 'rascunho')
545
+ .map((title) => ({
546
+ id: String(title.id),
547
+ tituloId: String(title.id),
548
+ solicitante: '-',
549
+ valor: Number(title.valorTotal || 0),
550
+ politica: 'Aprovação financeira',
551
+ urgencia: 'media',
552
+ dataSolicitacao: title.criadoEm,
553
+ }));
511
554
  return {
512
555
  kpis: {
513
556
  saldoCaixa: 0,
@@ -525,7 +568,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
525
568
  pessoas: people,
526
569
  categorias: categories,
527
570
  centrosCusto: costCenters,
528
- aprovacoesPendentes: [],
571
+ aprovacoesPendentes,
529
572
  agingInadimplencia: [],
530
573
  cenarios: [],
531
574
  transferencias: [],
@@ -555,9 +598,76 @@ let FinanceService = FinanceService_1 = class FinanceService {
555
598
  async createAccountsPayableTitle(data, locale, userId) {
556
599
  return this.createTitle(data, 'payable', locale, userId);
557
600
  }
601
+ async approveAccountsPayableTitle(id, locale, userId) {
602
+ return this.approveTitle(id, 'payable', locale, userId);
603
+ }
604
+ async rejectAccountsPayableTitle(id, data, locale, userId) {
605
+ return this.rejectTitle(id, data, 'payable', locale, userId);
606
+ }
607
+ async settleAccountsPayableInstallment(id, data, locale, userId) {
608
+ return this.settleTitleInstallment(id, data, 'payable', locale, userId);
609
+ }
610
+ async reverseAccountsPayableSettlement(id, settlementId, data, locale, userId) {
611
+ return this.reverseTitleSettlement(id, settlementId, data, 'payable', locale, userId);
612
+ }
558
613
  async createAccountsReceivableTitle(data, locale, userId) {
559
614
  return this.createTitle(data, 'receivable', locale, userId);
560
615
  }
616
+ async approveAccountsReceivableTitle(id, locale, userId) {
617
+ return this.approveTitle(id, 'receivable', locale, userId);
618
+ }
619
+ async settleAccountsReceivableInstallment(id, data, locale, userId) {
620
+ return this.settleTitleInstallment(id, data, 'receivable', locale, userId);
621
+ }
622
+ async reverseAccountsReceivableSettlement(id, settlementId, data, locale, userId) {
623
+ return this.reverseTitleSettlement(id, settlementId, data, 'receivable', locale, userId);
624
+ }
625
+ async createTag(data) {
626
+ const slug = this.normalizeTagSlug(data.name);
627
+ if (!slug) {
628
+ throw new common_1.BadRequestException('Tag name is required');
629
+ }
630
+ const existingTag = await this.prisma.tag.findFirst({
631
+ where: {
632
+ slug,
633
+ },
634
+ select: {
635
+ id: true,
636
+ slug: true,
637
+ color: true,
638
+ },
639
+ });
640
+ if (existingTag) {
641
+ return {
642
+ id: String(existingTag.id),
643
+ nome: existingTag.slug,
644
+ cor: existingTag.color,
645
+ };
646
+ }
647
+ const createdTag = await this.prisma.tag.create({
648
+ data: {
649
+ slug,
650
+ color: data.color || '#000000',
651
+ status: 'active',
652
+ },
653
+ select: {
654
+ id: true,
655
+ slug: true,
656
+ color: true,
657
+ },
658
+ });
659
+ return {
660
+ id: String(createdTag.id),
661
+ nome: createdTag.slug,
662
+ cor: createdTag.color,
663
+ };
664
+ }
665
+ async updateAccountsPayableInstallmentTags(id, tagIds, locale) {
666
+ return this.updateTitleTags(id, 'payable', tagIds, locale);
667
+ }
668
+ async updateAccountsReceivableInstallmentTags(id, tagIds, locale) {
669
+ return this.updateTitleTags(id, 'receivable', tagIds, locale);
670
+ }
561
671
  async listBankAccounts() {
562
672
  const bankAccounts = await this.prisma.bank_account.findMany({
563
673
  include: {
@@ -1046,85 +1156,583 @@ let FinanceService = FinanceService_1 = class FinanceService {
1046
1156
  return title;
1047
1157
  }
1048
1158
  async createTitle(data, titleType, locale, userId) {
1049
- const person = await this.prisma.person.findUnique({
1050
- where: { id: data.person_id },
1051
- select: { id: true },
1052
- });
1053
- if (!person) {
1054
- throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, 'Person not found'));
1055
- }
1056
- if (data.finance_category_id) {
1057
- const category = await this.prisma.finance_category.findUnique({
1058
- where: { id: data.finance_category_id },
1159
+ const installments = this.normalizeAndValidateInstallments(data, locale);
1160
+ const createdTitleId = await this.prisma.$transaction(async (tx) => {
1161
+ const person = await tx.person.findUnique({
1162
+ where: { id: data.person_id },
1059
1163
  select: { id: true },
1060
1164
  });
1061
- if (!category) {
1062
- throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('categoryNotFound', locale, 'Category not found'));
1165
+ if (!person) {
1166
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('personNotFound', locale, 'Person not found'));
1063
1167
  }
1064
- }
1065
- if (data.cost_center_id) {
1066
- const costCenter = await this.prisma.cost_center.findUnique({
1067
- where: { id: data.cost_center_id },
1068
- select: { id: true },
1168
+ if (data.finance_category_id) {
1169
+ const category = await tx.finance_category.findUnique({
1170
+ where: { id: data.finance_category_id },
1171
+ select: { id: true },
1172
+ });
1173
+ if (!category) {
1174
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('categoryNotFound', locale, 'Category not found'));
1175
+ }
1176
+ }
1177
+ if (data.cost_center_id) {
1178
+ const costCenter = await tx.cost_center.findUnique({
1179
+ where: { id: data.cost_center_id },
1180
+ select: { id: true },
1181
+ });
1182
+ if (!costCenter) {
1183
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('costCenterNotFound', locale, 'Cost center not found'));
1184
+ }
1185
+ }
1186
+ const attachmentFileIds = [
1187
+ ...new Set((data.attachment_file_ids || []).filter((fileId) => fileId > 0)),
1188
+ ];
1189
+ if (attachmentFileIds.length > 0) {
1190
+ const existingFiles = await tx.file.findMany({
1191
+ where: {
1192
+ id: { in: attachmentFileIds },
1193
+ },
1194
+ select: {
1195
+ id: true,
1196
+ },
1197
+ });
1198
+ const existingFileIds = new Set(existingFiles.map((file) => file.id));
1199
+ const invalidFileIds = attachmentFileIds.filter((fileId) => !existingFileIds.has(fileId));
1200
+ if (invalidFileIds.length > 0) {
1201
+ throw new common_1.BadRequestException(`Invalid attachment file IDs: ${invalidFileIds.join(', ')}`);
1202
+ }
1203
+ }
1204
+ await this.assertDateNotInClosedPeriod(tx, data.competence_date
1205
+ ? new Date(data.competence_date)
1206
+ : new Date(installments[0].due_date), 'create title');
1207
+ const title = await tx.financial_title.create({
1208
+ data: {
1209
+ person_id: data.person_id,
1210
+ title_type: titleType,
1211
+ status: 'draft',
1212
+ document_number: data.document_number,
1213
+ description: data.description,
1214
+ competence_date: data.competence_date
1215
+ ? new Date(data.competence_date)
1216
+ : null,
1217
+ issue_date: data.issue_date ? new Date(data.issue_date) : null,
1218
+ total_amount_cents: this.toCents(data.total_amount),
1219
+ finance_category_id: data.finance_category_id,
1220
+ created_by_user_id: userId,
1221
+ },
1069
1222
  });
1070
- if (!costCenter) {
1071
- throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('costCenterNotFound', locale, 'Cost center not found'));
1223
+ if (attachmentFileIds.length > 0) {
1224
+ await tx.financial_title_attachment.createMany({
1225
+ data: attachmentFileIds.map((fileId) => ({
1226
+ title_id: title.id,
1227
+ file_id: fileId,
1228
+ uploaded_by_user_id: userId,
1229
+ })),
1230
+ });
1072
1231
  }
1073
- }
1074
- const installments = data.installments && data.installments.length > 0
1232
+ for (let index = 0; index < installments.length; index++) {
1233
+ const installment = installments[index];
1234
+ const amountCents = installment.amount_cents;
1235
+ const createdInstallment = await tx.financial_installment.create({
1236
+ data: {
1237
+ title_id: title.id,
1238
+ installment_number: installment.installment_number,
1239
+ competence_date: data.competence_date
1240
+ ? new Date(data.competence_date)
1241
+ : new Date(installment.due_date),
1242
+ due_date: new Date(installment.due_date),
1243
+ amount_cents: amountCents,
1244
+ open_amount_cents: amountCents,
1245
+ status: this.resolveInstallmentStatus(amountCents, amountCents, new Date(installment.due_date)),
1246
+ notes: data.description,
1247
+ },
1248
+ });
1249
+ if (data.cost_center_id) {
1250
+ await tx.installment_allocation.create({
1251
+ data: {
1252
+ installment_id: createdInstallment.id,
1253
+ cost_center_id: data.cost_center_id,
1254
+ allocated_amount_cents: amountCents,
1255
+ },
1256
+ });
1257
+ }
1258
+ }
1259
+ await this.createAuditLog(tx, {
1260
+ action: 'CREATE_TITLE',
1261
+ entityTable: 'financial_title',
1262
+ entityId: String(title.id),
1263
+ actorUserId: userId,
1264
+ summary: `Created ${titleType} title ${title.id} in draft`,
1265
+ afterData: JSON.stringify({
1266
+ status: 'draft',
1267
+ total_amount_cents: this.toCents(data.total_amount),
1268
+ }),
1269
+ });
1270
+ return title.id;
1271
+ });
1272
+ const createdTitle = await this.getTitleById(createdTitleId, titleType, locale);
1273
+ return this.mapTitleToFront(createdTitle, data.payment_channel);
1274
+ }
1275
+ normalizeAndValidateInstallments(data, locale) {
1276
+ const fallbackDueDate = data.due_date;
1277
+ const totalAmountCents = this.toCents(data.total_amount);
1278
+ const sourceInstallments = data.installments && data.installments.length > 0
1075
1279
  ? data.installments
1076
1280
  : [
1077
1281
  {
1078
1282
  installment_number: 1,
1079
- due_date: data.due_date,
1283
+ due_date: fallbackDueDate,
1080
1284
  amount: data.total_amount,
1081
1285
  },
1082
1286
  ];
1083
- const title = await this.prisma.financial_title.create({
1084
- data: {
1085
- person_id: data.person_id,
1086
- title_type: titleType,
1087
- status: 'open',
1088
- document_number: data.document_number,
1089
- description: data.description,
1090
- competence_date: data.competence_date
1091
- ? new Date(data.competence_date)
1092
- : null,
1093
- issue_date: data.issue_date ? new Date(data.issue_date) : null,
1094
- total_amount_cents: this.toCents(data.total_amount),
1095
- finance_category_id: data.finance_category_id,
1096
- created_by_user_id: userId,
1097
- },
1098
- });
1099
- for (let index = 0; index < installments.length; index++) {
1100
- const installment = installments[index];
1287
+ const normalizedInstallments = sourceInstallments.map((installment, index) => {
1288
+ const installmentDueDate = installment.due_date || fallbackDueDate;
1289
+ if (!installmentDueDate) {
1290
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('installmentDueDateRequired', locale, 'Installment due date is required'));
1291
+ }
1101
1292
  const amountCents = this.toCents(installment.amount);
1102
- const createdInstallment = await this.prisma.financial_installment.create({
1293
+ if (amountCents <= 0) {
1294
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('installmentAmountInvalid', locale, 'Installment amount must be greater than zero'));
1295
+ }
1296
+ return {
1297
+ installment_number: installment.installment_number || index + 1,
1298
+ due_date: installmentDueDate,
1299
+ amount_cents: amountCents,
1300
+ };
1301
+ });
1302
+ const installmentsTotalCents = normalizedInstallments.reduce((acc, installment) => acc + installment.amount_cents, 0);
1303
+ if (installmentsTotalCents !== totalAmountCents) {
1304
+ throw new common_1.BadRequestException((0, api_locale_1.getLocaleText)('installmentsTotalMismatch', locale, 'Installments total must be equal to title total amount'));
1305
+ }
1306
+ return normalizedInstallments;
1307
+ }
1308
+ async approveTitle(titleId, titleType, locale, userId) {
1309
+ const updatedTitle = await this.prisma.$transaction(async (tx) => {
1310
+ const title = await tx.financial_title.findFirst({
1311
+ where: {
1312
+ id: titleId,
1313
+ title_type: titleType,
1314
+ },
1315
+ select: {
1316
+ id: true,
1317
+ status: true,
1318
+ competence_date: true,
1319
+ },
1320
+ });
1321
+ if (!title) {
1322
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
1323
+ }
1324
+ await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'approve title');
1325
+ if (title.status !== 'draft') {
1326
+ throw new common_1.BadRequestException('Only draft titles can be approved');
1327
+ }
1328
+ await tx.financial_title.update({
1329
+ where: { id: title.id },
1103
1330
  data: {
1331
+ status: 'approved',
1332
+ },
1333
+ });
1334
+ await this.createAuditLog(tx, {
1335
+ action: 'APPROVE_TITLE',
1336
+ entityTable: 'financial_title',
1337
+ entityId: String(title.id),
1338
+ actorUserId: userId,
1339
+ summary: `Approved ${titleType} title ${title.id}`,
1340
+ beforeData: JSON.stringify({ status: title.status }),
1341
+ afterData: JSON.stringify({ status: 'approved' }),
1342
+ });
1343
+ return tx.financial_title.findFirst({
1344
+ where: {
1345
+ id: title.id,
1346
+ title_type: titleType,
1347
+ },
1348
+ include: this.defaultTitleInclude(),
1349
+ });
1350
+ });
1351
+ if (!updatedTitle) {
1352
+ throw new common_1.NotFoundException('Financial title not found');
1353
+ }
1354
+ return this.mapTitleToFront(updatedTitle);
1355
+ }
1356
+ async rejectTitle(titleId, data, titleType, locale, userId) {
1357
+ const updatedTitle = await this.prisma.$transaction(async (tx) => {
1358
+ const title = await tx.financial_title.findFirst({
1359
+ where: {
1360
+ id: titleId,
1361
+ title_type: titleType,
1362
+ },
1363
+ select: {
1364
+ id: true,
1365
+ status: true,
1366
+ competence_date: true,
1367
+ },
1368
+ });
1369
+ if (!title) {
1370
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
1371
+ }
1372
+ await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'reject title');
1373
+ if (title.status !== 'draft') {
1374
+ throw new common_1.BadRequestException('Only draft titles can be rejected');
1375
+ }
1376
+ await tx.financial_title.update({
1377
+ where: { id: title.id },
1378
+ data: {
1379
+ status: 'canceled',
1380
+ },
1381
+ });
1382
+ await this.createAuditLog(tx, {
1383
+ action: 'REJECT_TITLE',
1384
+ entityTable: 'financial_title',
1385
+ entityId: String(title.id),
1386
+ actorUserId: userId,
1387
+ summary: `Rejected ${titleType} title ${title.id}`,
1388
+ beforeData: JSON.stringify({ status: title.status }),
1389
+ afterData: JSON.stringify({
1390
+ status: 'canceled',
1391
+ reason: (data === null || data === void 0 ? void 0 : data.reason) || null,
1392
+ }),
1393
+ });
1394
+ return tx.financial_title.findFirst({
1395
+ where: {
1396
+ id: title.id,
1397
+ title_type: titleType,
1398
+ },
1399
+ include: this.defaultTitleInclude(),
1400
+ });
1401
+ });
1402
+ if (!updatedTitle) {
1403
+ throw new common_1.NotFoundException('Financial title not found');
1404
+ }
1405
+ return this.mapTitleToFront(updatedTitle);
1406
+ }
1407
+ async settleTitleInstallment(titleId, data, titleType, locale, userId) {
1408
+ const amountCents = this.toCents(data.amount);
1409
+ if (amountCents <= 0) {
1410
+ throw new common_1.BadRequestException('Settlement amount must be greater than zero');
1411
+ }
1412
+ const settledAt = data.settled_at ? new Date(data.settled_at) : new Date();
1413
+ if (Number.isNaN(settledAt.getTime())) {
1414
+ throw new common_1.BadRequestException('Invalid settlement date');
1415
+ }
1416
+ const result = await this.prisma.$transaction(async (tx) => {
1417
+ var _a;
1418
+ const title = await tx.financial_title.findFirst({
1419
+ where: {
1420
+ id: titleId,
1421
+ title_type: titleType,
1422
+ },
1423
+ select: {
1424
+ id: true,
1425
+ person_id: true,
1426
+ status: true,
1427
+ competence_date: true,
1428
+ },
1429
+ });
1430
+ if (!title) {
1431
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
1432
+ }
1433
+ if (!['approved', 'open', 'partial'].includes(title.status)) {
1434
+ throw new common_1.BadRequestException('Only approved/open/partial titles can be settled');
1435
+ }
1436
+ await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'settle installment');
1437
+ const installment = await tx.financial_installment.findFirst({
1438
+ where: {
1439
+ id: data.installment_id,
1104
1440
  title_id: title.id,
1105
- installment_number: installment.installment_number || index + 1,
1106
- competence_date: data.competence_date
1107
- ? new Date(data.competence_date)
1108
- : new Date(installment.due_date),
1109
- due_date: new Date(installment.due_date),
1441
+ },
1442
+ select: {
1443
+ id: true,
1444
+ title_id: true,
1445
+ amount_cents: true,
1446
+ open_amount_cents: true,
1447
+ due_date: true,
1448
+ status: true,
1449
+ },
1450
+ });
1451
+ if (!installment) {
1452
+ throw new common_1.BadRequestException('Installment not found for this title');
1453
+ }
1454
+ if (installment.status === 'settled' || installment.status === 'canceled') {
1455
+ throw new common_1.BadRequestException('This installment cannot be settled');
1456
+ }
1457
+ if (amountCents > installment.open_amount_cents) {
1458
+ throw new common_1.BadRequestException('Settlement amount exceeds open amount');
1459
+ }
1460
+ const paymentMethodId = await this.resolvePaymentMethodId(tx, data.payment_channel);
1461
+ const settlement = await tx.settlement.create({
1462
+ data: {
1463
+ person_id: title.person_id,
1464
+ bank_account_id: data.bank_account_id || null,
1465
+ payment_method_id: paymentMethodId,
1466
+ settlement_type: titleType,
1467
+ status: 'confirmed',
1468
+ settled_at: settledAt,
1110
1469
  amount_cents: amountCents,
1111
- open_amount_cents: amountCents,
1112
- status: 'open',
1113
- notes: data.description,
1470
+ description: ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || null,
1471
+ created_by_user_id: userId,
1114
1472
  },
1115
1473
  });
1116
- if (data.cost_center_id) {
1117
- await this.prisma.installment_allocation.create({
1474
+ await tx.settlement_allocation.create({
1475
+ data: {
1476
+ settlement_id: settlement.id,
1477
+ installment_id: installment.id,
1478
+ allocated_amount_cents: amountCents,
1479
+ discount_cents: this.toCents(data.discount || 0),
1480
+ interest_cents: this.toCents(data.interest || 0),
1481
+ penalty_cents: this.toCents(data.penalty || 0),
1482
+ },
1483
+ });
1484
+ const decrementResult = await tx.financial_installment.updateMany({
1485
+ where: {
1486
+ id: installment.id,
1487
+ open_amount_cents: {
1488
+ gte: amountCents,
1489
+ },
1490
+ },
1491
+ data: {
1492
+ open_amount_cents: {
1493
+ decrement: amountCents,
1494
+ },
1495
+ },
1496
+ });
1497
+ if (decrementResult.count !== 1) {
1498
+ throw new common_1.BadRequestException('Installment was updated concurrently, please try again');
1499
+ }
1500
+ const updatedInstallment = await tx.financial_installment.findUnique({
1501
+ where: {
1502
+ id: installment.id,
1503
+ },
1504
+ select: {
1505
+ id: true,
1506
+ amount_cents: true,
1507
+ open_amount_cents: true,
1508
+ due_date: true,
1509
+ status: true,
1510
+ },
1511
+ });
1512
+ if (!updatedInstallment) {
1513
+ throw new common_1.NotFoundException('Installment not found');
1514
+ }
1515
+ const nextInstallmentStatus = this.resolveInstallmentStatus(updatedInstallment.amount_cents, updatedInstallment.open_amount_cents, updatedInstallment.due_date);
1516
+ if (updatedInstallment.status !== nextInstallmentStatus) {
1517
+ await tx.financial_installment.update({
1518
+ where: {
1519
+ id: updatedInstallment.id,
1520
+ },
1118
1521
  data: {
1119
- installment_id: createdInstallment.id,
1120
- cost_center_id: data.cost_center_id,
1121
- allocated_amount_cents: amountCents,
1522
+ status: nextInstallmentStatus,
1122
1523
  },
1123
1524
  });
1124
1525
  }
1526
+ const previousTitleStatus = title.status;
1527
+ const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
1528
+ await this.createAuditLog(tx, {
1529
+ action: 'SETTLE_INSTALLMENT',
1530
+ entityTable: 'financial_title',
1531
+ entityId: String(title.id),
1532
+ actorUserId: userId,
1533
+ summary: `Settled installment ${installment.id} of title ${title.id}`,
1534
+ beforeData: JSON.stringify({
1535
+ title_status: previousTitleStatus,
1536
+ installment_open_amount_cents: installment.open_amount_cents,
1537
+ }),
1538
+ afterData: JSON.stringify({
1539
+ title_status: nextTitleStatus,
1540
+ installment_open_amount_cents: updatedInstallment.open_amount_cents,
1541
+ settlement_id: settlement.id,
1542
+ }),
1543
+ });
1544
+ const updatedTitle = await tx.financial_title.findFirst({
1545
+ where: {
1546
+ id: title.id,
1547
+ title_type: titleType,
1548
+ },
1549
+ include: this.defaultTitleInclude(),
1550
+ });
1551
+ if (!updatedTitle) {
1552
+ throw new common_1.NotFoundException('Financial title not found');
1553
+ }
1554
+ return {
1555
+ title: updatedTitle,
1556
+ settlementId: settlement.id,
1557
+ };
1558
+ });
1559
+ return Object.assign(Object.assign({}, this.mapTitleToFront(result.title)), { settlementId: String(result.settlementId) });
1560
+ }
1561
+ async reverseTitleSettlement(titleId, settlementId, data, titleType, locale, userId) {
1562
+ const updatedTitle = await this.prisma.$transaction(async (tx) => {
1563
+ const title = await tx.financial_title.findFirst({
1564
+ where: {
1565
+ id: titleId,
1566
+ title_type: titleType,
1567
+ },
1568
+ select: {
1569
+ id: true,
1570
+ status: true,
1571
+ competence_date: true,
1572
+ },
1573
+ });
1574
+ if (!title) {
1575
+ throw new common_1.NotFoundException((0, api_locale_1.getLocaleText)('itemNotFound', locale, `Financial title with ID ${titleId} not found`).replace('{{item}}', 'Financial title'));
1576
+ }
1577
+ await this.assertDateNotInClosedPeriod(tx, title.competence_date, 'reverse settlement');
1578
+ const settlement = await tx.settlement.findFirst({
1579
+ where: {
1580
+ id: settlementId,
1581
+ settlement_type: titleType,
1582
+ settlement_allocation: {
1583
+ some: {
1584
+ financial_installment: {
1585
+ title_id: title.id,
1586
+ },
1587
+ },
1588
+ },
1589
+ },
1590
+ include: {
1591
+ settlement_allocation: {
1592
+ include: {
1593
+ financial_installment: {
1594
+ select: {
1595
+ id: true,
1596
+ amount_cents: true,
1597
+ open_amount_cents: true,
1598
+ due_date: true,
1599
+ status: true,
1600
+ },
1601
+ },
1602
+ },
1603
+ },
1604
+ },
1605
+ });
1606
+ if (!settlement) {
1607
+ throw new common_1.NotFoundException('Settlement not found for this title');
1608
+ }
1609
+ if (settlement.status === 'reversed') {
1610
+ throw new common_1.BadRequestException('This settlement is already reversed');
1611
+ }
1612
+ for (const allocation of settlement.settlement_allocation) {
1613
+ const installment = allocation.financial_installment;
1614
+ if (!installment) {
1615
+ continue;
1616
+ }
1617
+ const nextOpenAmountCents = installment.open_amount_cents + allocation.allocated_amount_cents;
1618
+ if (nextOpenAmountCents > installment.amount_cents) {
1619
+ throw new common_1.BadRequestException(`Reverse would exceed installment amount for installment ${installment.id}`);
1620
+ }
1621
+ const nextInstallmentStatus = this.resolveInstallmentStatus(installment.amount_cents, nextOpenAmountCents, installment.due_date);
1622
+ await tx.financial_installment.update({
1623
+ where: {
1624
+ id: installment.id,
1625
+ },
1626
+ data: {
1627
+ open_amount_cents: nextOpenAmountCents,
1628
+ status: nextInstallmentStatus,
1629
+ },
1630
+ });
1631
+ }
1632
+ await tx.settlement.update({
1633
+ where: {
1634
+ id: settlement.id,
1635
+ },
1636
+ data: {
1637
+ status: 'reversed',
1638
+ description: [
1639
+ settlement.description,
1640
+ data.reason ? `Reversed: ${data.reason.trim()}` : 'Reversed',
1641
+ ]
1642
+ .filter(Boolean)
1643
+ .join(' | '),
1644
+ },
1645
+ });
1646
+ const previousTitleStatus = title.status;
1647
+ const nextTitleStatus = await this.recalculateTitleStatus(tx, title.id);
1648
+ await this.createAuditLog(tx, {
1649
+ action: 'REVERSE_SETTLEMENT',
1650
+ entityTable: 'financial_title',
1651
+ entityId: String(title.id),
1652
+ actorUserId: userId,
1653
+ summary: `Reversed settlement ${settlement.id} from title ${title.id}`,
1654
+ beforeData: JSON.stringify({
1655
+ title_status: previousTitleStatus,
1656
+ settlement_status: settlement.status,
1657
+ }),
1658
+ afterData: JSON.stringify({
1659
+ title_status: nextTitleStatus,
1660
+ settlement_status: 'reversed',
1661
+ }),
1662
+ });
1663
+ return tx.financial_title.findFirst({
1664
+ where: {
1665
+ id: title.id,
1666
+ title_type: titleType,
1667
+ },
1668
+ include: this.defaultTitleInclude(),
1669
+ });
1670
+ });
1671
+ if (!updatedTitle) {
1672
+ throw new common_1.NotFoundException('Financial title not found');
1125
1673
  }
1126
- const createdTitle = await this.getTitleById(title.id, titleType, locale);
1127
- return this.mapTitleToFront(createdTitle, data.payment_channel);
1674
+ return this.mapTitleToFront(updatedTitle);
1675
+ }
1676
+ async updateTitleTags(titleId, titleType, tagIds, locale) {
1677
+ const title = await this.getTitleById(titleId, titleType, locale);
1678
+ const installmentIds = (title.financial_installment || []).map((installment) => installment.id);
1679
+ if (installmentIds.length === 0) {
1680
+ throw new common_1.BadRequestException('Financial title has no installments');
1681
+ }
1682
+ const normalizedTagIds = [
1683
+ ...new Set((tagIds || [])
1684
+ .map((tagId) => Number(tagId))
1685
+ .filter((tagId) => Number.isInteger(tagId) && tagId > 0)),
1686
+ ];
1687
+ if (normalizedTagIds.length > 0) {
1688
+ const existingTags = await this.prisma.tag.findMany({
1689
+ where: {
1690
+ id: {
1691
+ in: normalizedTagIds,
1692
+ },
1693
+ },
1694
+ select: {
1695
+ id: true,
1696
+ },
1697
+ });
1698
+ const existingTagIds = new Set(existingTags.map((tag) => tag.id));
1699
+ const invalidTagIds = normalizedTagIds.filter((tagId) => !existingTagIds.has(tagId));
1700
+ if (invalidTagIds.length > 0) {
1701
+ throw new common_1.BadRequestException(`Invalid tag IDs: ${invalidTagIds.join(', ')}`);
1702
+ }
1703
+ }
1704
+ await this.prisma.$transaction(async (tx) => {
1705
+ if (normalizedTagIds.length === 0) {
1706
+ await tx.financial_installment_tag.deleteMany({
1707
+ where: {
1708
+ installment_id: {
1709
+ in: installmentIds,
1710
+ },
1711
+ },
1712
+ });
1713
+ return;
1714
+ }
1715
+ await tx.financial_installment_tag.deleteMany({
1716
+ where: {
1717
+ installment_id: {
1718
+ in: installmentIds,
1719
+ },
1720
+ tag_id: {
1721
+ notIn: normalizedTagIds,
1722
+ },
1723
+ },
1724
+ });
1725
+ const newRelations = installmentIds.flatMap((installmentId) => normalizedTagIds.map((tagId) => ({
1726
+ installment_id: installmentId,
1727
+ tag_id: tagId,
1728
+ })));
1729
+ await tx.financial_installment_tag.createMany({
1730
+ data: newRelations,
1731
+ skipDuplicates: true,
1732
+ });
1733
+ });
1734
+ const updatedTitle = await this.getTitleById(titleId, titleType, locale);
1735
+ return this.mapTitleToFront(updatedTitle);
1128
1736
  }
1129
1737
  async loadTitles(type) {
1130
1738
  const titles = await this.prisma.financial_title.findMany({
@@ -1213,6 +1821,124 @@ let FinanceService = FinanceService_1 = class FinanceService {
1213
1821
  data: log.created_at.toISOString(),
1214
1822
  }));
1215
1823
  }
1824
+ async resolvePaymentMethodId(tx, paymentChannel) {
1825
+ const paymentType = this.mapPaymentMethodFromPt(paymentChannel);
1826
+ if (!paymentType) {
1827
+ return null;
1828
+ }
1829
+ const paymentMethod = await tx.payment_method.findFirst({
1830
+ where: {
1831
+ type: paymentType,
1832
+ status: 'active',
1833
+ },
1834
+ select: {
1835
+ id: true,
1836
+ },
1837
+ });
1838
+ return (paymentMethod === null || paymentMethod === void 0 ? void 0 : paymentMethod.id) || null;
1839
+ }
1840
+ async assertDateNotInClosedPeriod(tx, competenceDate, operation) {
1841
+ if (!competenceDate) {
1842
+ return;
1843
+ }
1844
+ const closedPeriod = await tx.period_close.findFirst({
1845
+ where: {
1846
+ status: 'closed',
1847
+ period_start: {
1848
+ lte: competenceDate,
1849
+ },
1850
+ period_end: {
1851
+ gte: competenceDate,
1852
+ },
1853
+ },
1854
+ select: {
1855
+ id: true,
1856
+ },
1857
+ });
1858
+ if (closedPeriod) {
1859
+ throw new common_1.BadRequestException(`Cannot ${operation}: competence is in a closed period`);
1860
+ }
1861
+ }
1862
+ resolveInstallmentStatus(amountCents, openAmountCents, dueDate, currentStatus) {
1863
+ if (currentStatus === 'canceled') {
1864
+ return 'canceled';
1865
+ }
1866
+ if (openAmountCents <= 0) {
1867
+ return 'settled';
1868
+ }
1869
+ if (openAmountCents < amountCents) {
1870
+ return 'partial';
1871
+ }
1872
+ const today = new Date();
1873
+ today.setHours(0, 0, 0, 0);
1874
+ const dueDateOnly = new Date(dueDate);
1875
+ dueDateOnly.setHours(0, 0, 0, 0);
1876
+ if (dueDateOnly < today) {
1877
+ return 'overdue';
1878
+ }
1879
+ return 'open';
1880
+ }
1881
+ deriveTitleStatusFromInstallments(installments) {
1882
+ if (installments.length === 0) {
1883
+ return 'open';
1884
+ }
1885
+ const effectiveStatuses = installments.map((installment) => this.resolveInstallmentStatus(installment.amount_cents, installment.open_amount_cents, installment.due_date, installment.status));
1886
+ if (effectiveStatuses.every((status) => status === 'settled')) {
1887
+ return 'settled';
1888
+ }
1889
+ const hasPayment = installments.some((installment) => installment.open_amount_cents < installment.amount_cents);
1890
+ if (hasPayment) {
1891
+ return 'partial';
1892
+ }
1893
+ return 'open';
1894
+ }
1895
+ async recalculateTitleStatus(tx, titleId) {
1896
+ const title = await tx.financial_title.findUnique({
1897
+ where: {
1898
+ id: titleId,
1899
+ },
1900
+ select: {
1901
+ id: true,
1902
+ status: true,
1903
+ financial_installment: {
1904
+ select: {
1905
+ amount_cents: true,
1906
+ open_amount_cents: true,
1907
+ due_date: true,
1908
+ status: true,
1909
+ },
1910
+ },
1911
+ },
1912
+ });
1913
+ if (!title) {
1914
+ throw new common_1.NotFoundException('Financial title not found');
1915
+ }
1916
+ const nextStatus = this.deriveTitleStatusFromInstallments(title.financial_installment);
1917
+ if (title.status !== nextStatus) {
1918
+ await tx.financial_title.update({
1919
+ where: {
1920
+ id: title.id,
1921
+ },
1922
+ data: {
1923
+ status: nextStatus,
1924
+ },
1925
+ });
1926
+ }
1927
+ return nextStatus;
1928
+ }
1929
+ async createAuditLog(tx, data) {
1930
+ await tx.audit_log.create({
1931
+ data: {
1932
+ actor_user_id: data.actorUserId || null,
1933
+ action: data.action,
1934
+ entity_table: data.entityTable,
1935
+ entity_id: data.entityId,
1936
+ summary: data.summary || null,
1937
+ before_data: data.beforeData || null,
1938
+ after_data: data.afterData || null,
1939
+ },
1940
+ });
1941
+ }
1216
1942
  mapTitleToFront(title, paymentChannelOverride) {
1217
1943
  var _a;
1218
1944
  const allocations = title.financial_installment.flatMap((installment) => installment.installment_allocation);
@@ -1233,28 +1959,40 @@ let FinanceService = FinanceService_1 = class FinanceService {
1233
1959
  numero: installment.installment_number,
1234
1960
  vencimento: installment.due_date.toISOString(),
1235
1961
  valor: this.fromCents(installment.amount_cents),
1236
- status: this.mapStatusToPt(installment.status),
1962
+ valorAberto: this.fromCents(installment.open_amount_cents),
1963
+ status: this.mapStatusToPt(this.resolveInstallmentStatus(installment.amount_cents, installment.open_amount_cents, installment.due_date, installment.status)),
1237
1964
  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',
1238
1965
  liquidacoes: installment.settlement_allocation.map((allocation) => {
1239
- var _a, _b, _c, _d, _e;
1966
+ var _a, _b, _c, _d, _e, _f, _g;
1240
1967
  return ({
1241
1968
  id: String(allocation.id),
1242
- data: (_b = (_a = allocation.settlement) === null || _a === void 0 ? void 0 : _a.settled_at) === null || _b === void 0 ? void 0 : _b.toISOString(),
1969
+ settlementId: ((_a = allocation.settlement) === null || _a === void 0 ? void 0 : _a.id)
1970
+ ? String(allocation.settlement.id)
1971
+ : null,
1972
+ data: (_c = (_b = allocation.settlement) === null || _b === void 0 ? void 0 : _b.settled_at) === null || _c === void 0 ? void 0 : _c.toISOString(),
1243
1973
  valor: this.fromCents(allocation.allocated_amount_cents),
1244
1974
  juros: this.fromCents(allocation.interest_cents || 0),
1245
1975
  desconto: this.fromCents(allocation.discount_cents || 0),
1246
1976
  multa: this.fromCents(allocation.penalty_cents || 0),
1247
- contaBancariaId: ((_c = allocation.settlement) === null || _c === void 0 ? void 0 : _c.bank_account_id)
1977
+ contaBancariaId: ((_d = allocation.settlement) === null || _d === void 0 ? void 0 : _d.bank_account_id)
1248
1978
  ? String(allocation.settlement.bank_account_id)
1249
1979
  : null,
1250
- metodo: this.mapPaymentMethodToPt((_e = (_d = allocation.settlement) === null || _d === void 0 ? void 0 : _d.payment_method) === null || _e === void 0 ? void 0 : _e.type) || 'transferencia',
1980
+ status: ((_e = allocation.settlement) === null || _e === void 0 ? void 0 : _e.status) || null,
1981
+ 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',
1251
1982
  });
1252
1983
  }),
1253
1984
  });
1254
1985
  });
1986
+ const attachmentDetails = title.financial_title_attachment.map((attachment) => {
1987
+ var _a, _b;
1988
+ return ({
1989
+ id: String(attachment.file_id),
1990
+ nome: ((_a = attachment.file) === null || _a === void 0 ? void 0 : _a.filename) || ((_b = attachment.file) === null || _b === void 0 ? void 0 : _b.path),
1991
+ });
1992
+ });
1255
1993
  return Object.assign({ id: String(title.id), documento: title.document_number || `TIT-${title.id}`, descricao: title.description || '', competencia: title.competence_date
1256
1994
  ? title.competence_date.toISOString().slice(0, 7)
1257
- : '', valorTotal: this.fromCents(title.total_amount_cents), status: this.mapStatusToPt(title.status), criadoEm: title.created_at.toISOString(), categoriaId: title.finance_category_id ? String(title.finance_category_id) : null, centroCustoId: firstCostCenter ? String(firstCostCenter) : null, anexos: title.financial_title_attachment.map((attachment) => { var _a, _b; return ((_a = attachment.file) === null || _a === void 0 ? void 0 : _a.filename) || ((_b = attachment.file) === null || _b === void 0 ? void 0 : _b.path); }), tags, parcelas: mappedInstallments, canal: this.mapPaymentMethodToPt(channelFromSettlement) ||
1995
+ : '', valorTotal: this.fromCents(title.total_amount_cents), status: this.mapStatusToPt(title.status), criadoEm: title.created_at.toISOString(), categoriaId: title.finance_category_id ? String(title.finance_category_id) : null, centroCustoId: firstCostCenter ? String(firstCostCenter) : null, anexos: attachmentDetails.map((attachment) => attachment.nome), anexosDetalhes: attachmentDetails, tags, parcelas: mappedInstallments, canal: this.mapPaymentMethodToPt(channelFromSettlement) ||
1258
1996
  paymentChannelOverride ||
1259
1997
  'transferencia' }, (title.title_type === 'payable'
1260
1998
  ? { fornecedorId: String(title.person_id) }
@@ -1300,6 +2038,18 @@ let FinanceService = FinanceService_1 = class FinanceService {
1300
2038
  };
1301
2039
  return statusMap[status] || 'aberto';
1302
2040
  }
2041
+ normalizeTagSlug(value) {
2042
+ if (!value) {
2043
+ return '';
2044
+ }
2045
+ return value
2046
+ .normalize('NFD')
2047
+ .replace(/[\u0300-\u036f]/g, '')
2048
+ .toLowerCase()
2049
+ .trim()
2050
+ .replace(/[^a-z0-9]+/g, '-')
2051
+ .replace(/(^-|-$)+/g, '');
2052
+ }
1303
2053
  mapStatusFromPt(status) {
1304
2054
  if (!status || status === 'all') {
1305
2055
  return undefined;
@@ -1327,6 +2077,27 @@ let FinanceService = FinanceService_1 = class FinanceService {
1327
2077
  };
1328
2078
  return paymentMethodMap[paymentMethodType] || undefined;
1329
2079
  }
2080
+ mapPaymentMethodFromPt(paymentMethodType) {
2081
+ if (!paymentMethodType) {
2082
+ return undefined;
2083
+ }
2084
+ const paymentMethodMap = {
2085
+ boleto: 'boleto',
2086
+ pix: 'pix',
2087
+ transferencia: 'ted',
2088
+ transferência: 'ted',
2089
+ ted: 'ted',
2090
+ doc: 'doc',
2091
+ cartao: 'card',
2092
+ cartão: 'card',
2093
+ dinheiro: 'cash',
2094
+ cheque: 'other',
2095
+ cash: 'cash',
2096
+ card: 'card',
2097
+ other: 'other',
2098
+ };
2099
+ return paymentMethodMap[(paymentMethodType || '').toLowerCase()];
2100
+ }
1330
2101
  mapAccountTypeToPt(accountType) {
1331
2102
  const accountTypeMap = {
1332
2103
  checking: 'corrente',