@hed-hog/finance 0.0.238 → 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 (34) hide show
  1. package/dist/dto/reject-title.dto.d.ts +4 -0
  2. package/dist/dto/reject-title.dto.d.ts.map +1 -0
  3. package/dist/dto/reject-title.dto.js +22 -0
  4. package/dist/dto/reject-title.dto.js.map +1 -0
  5. package/dist/dto/reverse-settlement.dto.d.ts +4 -0
  6. package/dist/dto/reverse-settlement.dto.d.ts.map +1 -0
  7. package/dist/dto/reverse-settlement.dto.js +22 -0
  8. package/dist/dto/reverse-settlement.dto.js.map +1 -0
  9. package/dist/dto/settle-installment.dto.d.ts +12 -0
  10. package/dist/dto/settle-installment.dto.d.ts.map +1 -0
  11. package/dist/dto/settle-installment.dto.js +71 -0
  12. package/dist/dto/settle-installment.dto.js.map +1 -0
  13. package/dist/finance-data.controller.d.ts +13 -5
  14. package/dist/finance-data.controller.d.ts.map +1 -1
  15. package/dist/finance-installments.controller.d.ts +248 -12
  16. package/dist/finance-installments.controller.d.ts.map +1 -1
  17. package/dist/finance-installments.controller.js +92 -0
  18. package/dist/finance-installments.controller.js.map +1 -1
  19. package/dist/finance.service.d.ts +275 -17
  20. package/dist/finance.service.d.ts.map +1 -1
  21. package/dist/finance.service.js +666 -78
  22. package/dist/finance.service.js.map +1 -1
  23. package/hedhog/data/route.yaml +63 -0
  24. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +92 -12
  25. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +355 -4
  26. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +440 -16
  27. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +396 -49
  28. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +432 -14
  29. package/package.json +5 -5
  30. package/src/dto/reject-title.dto.ts +7 -0
  31. package/src/dto/reverse-settlement.dto.ts +7 -0
  32. package/src/dto/settle-installment.dto.ts +55 -0
  33. package/src/finance-installments.controller.ts +102 -0
  34. package/src/finance.service.ts +1007 -82
@@ -540,6 +540,17 @@ let FinanceService = FinanceService_1 = class FinanceService {
540
540
  if (auditLogsResult.status === 'rejected') {
541
541
  this.logger.error('Failed to load finance audit logs', auditLogsResult.reason);
542
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
+ }));
543
554
  return {
544
555
  kpis: {
545
556
  saldoCaixa: 0,
@@ -557,7 +568,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
557
568
  pessoas: people,
558
569
  categorias: categories,
559
570
  centrosCusto: costCenters,
560
- aprovacoesPendentes: [],
571
+ aprovacoesPendentes,
561
572
  agingInadimplencia: [],
562
573
  cenarios: [],
563
574
  transferencias: [],
@@ -587,9 +598,30 @@ let FinanceService = FinanceService_1 = class FinanceService {
587
598
  async createAccountsPayableTitle(data, locale, userId) {
588
599
  return this.createTitle(data, 'payable', locale, userId);
589
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
+ }
590
613
  async createAccountsReceivableTitle(data, locale, userId) {
591
614
  return this.createTitle(data, 'receivable', locale, userId);
592
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
+ }
593
625
  async createTag(data) {
594
626
  const slug = this.normalizeTagSlug(data.name);
595
627
  if (!slug) {
@@ -1124,110 +1156,522 @@ let FinanceService = FinanceService_1 = class FinanceService {
1124
1156
  return title;
1125
1157
  }
1126
1158
  async createTitle(data, titleType, locale, userId) {
1127
- const person = await this.prisma.person.findUnique({
1128
- where: { id: data.person_id },
1129
- select: { id: true },
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 },
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 },
1137
1163
  select: { id: true },
1138
1164
  });
1139
- if (!category) {
1140
- 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'));
1141
1167
  }
1142
- }
1143
- if (data.cost_center_id) {
1144
- const costCenter = await this.prisma.cost_center.findUnique({
1145
- where: { id: data.cost_center_id },
1146
- 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
+ },
1147
1222
  });
1148
- if (!costCenter) {
1149
- 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
+ });
1150
1231
  }
1151
- }
1152
- 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
1153
1279
  ? data.installments
1154
1280
  : [
1155
1281
  {
1156
1282
  installment_number: 1,
1157
- due_date: data.due_date,
1283
+ due_date: fallbackDueDate,
1158
1284
  amount: data.total_amount,
1159
1285
  },
1160
1286
  ];
1161
- const title = await this.prisma.financial_title.create({
1162
- data: {
1163
- person_id: data.person_id,
1164
- title_type: titleType,
1165
- status: 'open',
1166
- document_number: data.document_number,
1167
- description: data.description,
1168
- competence_date: data.competence_date
1169
- ? new Date(data.competence_date)
1170
- : null,
1171
- issue_date: data.issue_date ? new Date(data.issue_date) : null,
1172
- total_amount_cents: this.toCents(data.total_amount),
1173
- finance_category_id: data.finance_category_id,
1174
- created_by_user_id: userId,
1175
- },
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
+ }
1292
+ const amountCents = this.toCents(installment.amount);
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
+ };
1176
1301
  });
1177
- const attachmentFileIds = [
1178
- ...new Set((data.attachment_file_ids || []).filter((fileId) => fileId > 0)),
1179
- ];
1180
- if (attachmentFileIds.length > 0) {
1181
- const existingFiles = await this.prisma.file.findMany({
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({
1182
1311
  where: {
1183
- id: { in: attachmentFileIds },
1312
+ id: titleId,
1313
+ title_type: titleType,
1184
1314
  },
1185
1315
  select: {
1186
1316
  id: true,
1317
+ status: true,
1318
+ competence_date: true,
1187
1319
  },
1188
1320
  });
1189
- const existingFileIds = new Set(existingFiles.map((file) => file.id));
1190
- const invalidFileIds = attachmentFileIds.filter((fileId) => !existingFileIds.has(fileId));
1191
- if (invalidFileIds.length > 0) {
1192
- throw new common_1.BadRequestException(`Invalid attachment file IDs: ${invalidFileIds.join(', ')}`);
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'));
1193
1323
  }
1194
- await this.prisma.financial_title_attachment.createMany({
1195
- data: attachmentFileIds.map((fileId) => ({
1196
- title_id: title.id,
1197
- file_id: fileId,
1198
- uploaded_by_user_id: userId,
1199
- })),
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 },
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' }),
1200
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');
1201
1353
  }
1202
- for (let index = 0; index < installments.length; index++) {
1203
- const installment = installments[index];
1204
- const amountCents = this.toCents(installment.amount);
1205
- const createdInstallment = await this.prisma.financial_installment.create({
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 },
1206
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,
1207
1440
  title_id: title.id,
1208
- installment_number: installment.installment_number || index + 1,
1209
- competence_date: data.competence_date
1210
- ? new Date(data.competence_date)
1211
- : new Date(installment.due_date),
1212
- 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,
1213
1469
  amount_cents: amountCents,
1214
- open_amount_cents: amountCents,
1215
- status: 'open',
1216
- notes: data.description,
1470
+ description: ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || null,
1471
+ created_by_user_id: userId,
1217
1472
  },
1218
1473
  });
1219
- if (data.cost_center_id) {
1220
- 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
+ },
1221
1521
  data: {
1222
- installment_id: createdInstallment.id,
1223
- cost_center_id: data.cost_center_id,
1224
- allocated_amount_cents: amountCents,
1522
+ status: nextInstallmentStatus,
1225
1523
  },
1226
1524
  });
1227
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');
1228
1673
  }
1229
- const createdTitle = await this.getTitleById(title.id, titleType, locale);
1230
- return this.mapTitleToFront(createdTitle, data.payment_channel);
1674
+ return this.mapTitleToFront(updatedTitle);
1231
1675
  }
1232
1676
  async updateTitleTags(titleId, titleType, tagIds, locale) {
1233
1677
  const title = await this.getTitleById(titleId, titleType, locale);
@@ -1377,6 +1821,124 @@ let FinanceService = FinanceService_1 = class FinanceService {
1377
1821
  data: log.created_at.toISOString(),
1378
1822
  }));
1379
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
+ }
1380
1942
  mapTitleToFront(title, paymentChannelOverride) {
1381
1943
  var _a;
1382
1944
  const allocations = title.financial_installment.flatMap((installment) => installment.installment_allocation);
@@ -1397,21 +1959,26 @@ let FinanceService = FinanceService_1 = class FinanceService {
1397
1959
  numero: installment.installment_number,
1398
1960
  vencimento: installment.due_date.toISOString(),
1399
1961
  valor: this.fromCents(installment.amount_cents),
1400
- 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)),
1401
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',
1402
1965
  liquidacoes: installment.settlement_allocation.map((allocation) => {
1403
- var _a, _b, _c, _d, _e;
1966
+ var _a, _b, _c, _d, _e, _f, _g;
1404
1967
  return ({
1405
1968
  id: String(allocation.id),
1406
- 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(),
1407
1973
  valor: this.fromCents(allocation.allocated_amount_cents),
1408
1974
  juros: this.fromCents(allocation.interest_cents || 0),
1409
1975
  desconto: this.fromCents(allocation.discount_cents || 0),
1410
1976
  multa: this.fromCents(allocation.penalty_cents || 0),
1411
- 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)
1412
1978
  ? String(allocation.settlement.bank_account_id)
1413
1979
  : null,
1414
- 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',
1415
1982
  });
1416
1983
  }),
1417
1984
  });
@@ -1510,6 +2077,27 @@ let FinanceService = FinanceService_1 = class FinanceService {
1510
2077
  };
1511
2078
  return paymentMethodMap[paymentMethodType] || undefined;
1512
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
+ }
1513
2101
  mapAccountTypeToPt(accountType) {
1514
2102
  const accountTypeMap = {
1515
2103
  checking: 'corrente',