@hed-hog/finance 0.0.256 → 0.0.260

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 (64) hide show
  1. package/dist/dto/create-bank-statement-adjustment.dto.d.ts +8 -0
  2. package/dist/dto/create-bank-statement-adjustment.dto.d.ts.map +1 -0
  3. package/dist/dto/create-bank-statement-adjustment.dto.js +50 -0
  4. package/dist/dto/create-bank-statement-adjustment.dto.js.map +1 -0
  5. package/dist/dto/create-transfer.dto.d.ts +8 -0
  6. package/dist/dto/create-transfer.dto.d.ts.map +1 -0
  7. package/dist/dto/create-transfer.dto.js +52 -0
  8. package/dist/dto/create-transfer.dto.js.map +1 -0
  9. package/dist/dto/register-collection-agreement.dto.d.ts +7 -0
  10. package/dist/dto/register-collection-agreement.dto.d.ts.map +1 -0
  11. package/dist/dto/register-collection-agreement.dto.js +37 -0
  12. package/dist/dto/register-collection-agreement.dto.js.map +1 -0
  13. package/dist/dto/send-collection.dto.d.ts +5 -0
  14. package/dist/dto/send-collection.dto.d.ts.map +1 -0
  15. package/dist/dto/send-collection.dto.js +29 -0
  16. package/dist/dto/send-collection.dto.js.map +1 -0
  17. package/dist/dto/settle-installment.dto.d.ts +1 -0
  18. package/dist/dto/settle-installment.dto.d.ts.map +1 -1
  19. package/dist/dto/settle-installment.dto.js +6 -0
  20. package/dist/dto/settle-installment.dto.js.map +1 -1
  21. package/dist/finance-collections.controller.d.ts +35 -0
  22. package/dist/finance-collections.controller.d.ts.map +1 -0
  23. package/dist/finance-collections.controller.js +65 -0
  24. package/dist/finance-collections.controller.js.map +1 -0
  25. package/dist/finance-data.controller.d.ts +4 -0
  26. package/dist/finance-data.controller.d.ts.map +1 -1
  27. package/dist/finance-installments.controller.d.ts +44 -0
  28. package/dist/finance-installments.controller.d.ts.map +1 -1
  29. package/dist/finance-statements.controller.d.ts +16 -2
  30. package/dist/finance-statements.controller.d.ts.map +1 -1
  31. package/dist/finance-statements.controller.js +34 -6
  32. package/dist/finance-statements.controller.js.map +1 -1
  33. package/dist/finance-transfers.controller.d.ts +23 -0
  34. package/dist/finance-transfers.controller.d.ts.map +1 -0
  35. package/dist/finance-transfers.controller.js +56 -0
  36. package/dist/finance-transfers.controller.js.map +1 -0
  37. package/dist/finance.module.d.ts.map +1 -1
  38. package/dist/finance.module.js +4 -0
  39. package/dist/finance.module.js.map +1 -1
  40. package/dist/finance.service.d.ts +115 -2
  41. package/dist/finance.service.d.ts.map +1 -1
  42. package/dist/finance.service.js +632 -8
  43. package/dist/finance.service.js.map +1 -1
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -0
  47. package/dist/index.js.map +1 -1
  48. package/hedhog/data/route.yaml +63 -0
  49. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +643 -440
  50. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +825 -477
  51. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +367 -43
  52. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +315 -75
  53. package/package.json +6 -6
  54. package/src/dto/create-bank-statement-adjustment.dto.ts +38 -0
  55. package/src/dto/create-transfer.dto.ts +46 -0
  56. package/src/dto/register-collection-agreement.dto.ts +27 -0
  57. package/src/dto/send-collection.dto.ts +14 -0
  58. package/src/dto/settle-installment.dto.ts +5 -0
  59. package/src/finance-collections.controller.ts +34 -0
  60. package/src/finance-statements.controller.ts +29 -1
  61. package/src/finance-transfers.controller.ts +26 -0
  62. package/src/finance.module.ts +4 -0
  63. package/src/finance.service.ts +775 -5
  64. package/src/index.ts +2 -0
@@ -587,6 +587,242 @@ let FinanceService = FinanceService_1 = class FinanceService {
587
587
  periodoAberto: openPeriod,
588
588
  };
589
589
  }
590
+ async getAccountsReceivableCollectionsDefault() {
591
+ var _a;
592
+ const today = this.startOfDay(new Date());
593
+ const overdueInstallments = await this.prisma.financial_installment.findMany({
594
+ where: {
595
+ open_amount_cents: {
596
+ gt: 0,
597
+ },
598
+ due_date: {
599
+ lt: today,
600
+ },
601
+ financial_title: {
602
+ title_type: 'receivable',
603
+ },
604
+ },
605
+ select: {
606
+ due_date: true,
607
+ open_amount_cents: true,
608
+ amount_cents: true,
609
+ financial_title: {
610
+ select: {
611
+ person_id: true,
612
+ person: {
613
+ select: {
614
+ name: true,
615
+ },
616
+ },
617
+ },
618
+ },
619
+ },
620
+ });
621
+ const byCustomer = new Map();
622
+ for (const installment of overdueInstallments) {
623
+ const personId = installment.financial_title.person_id;
624
+ const personName = ((_a = installment.financial_title.person) === null || _a === void 0 ? void 0 : _a.name) || `Cliente ${personId}`;
625
+ const openAmount = installment.open_amount_cents > 0
626
+ ? installment.open_amount_cents
627
+ : installment.amount_cents;
628
+ const amount = this.fromCents(openAmount);
629
+ const diffDays = Math.floor((today.getTime() - this.startOfDay(installment.due_date).getTime()) /
630
+ 86400000);
631
+ if (!byCustomer.has(personId)) {
632
+ byCustomer.set(personId, {
633
+ clienteId: String(personId),
634
+ cliente: personName,
635
+ bucket0_30: 0,
636
+ bucket31_60: 0,
637
+ bucket61_90: 0,
638
+ bucket90plus: 0,
639
+ total: 0,
640
+ });
641
+ }
642
+ const customer = byCustomer.get(personId);
643
+ if (diffDays <= 30) {
644
+ customer.bucket0_30 += amount;
645
+ }
646
+ else if (diffDays <= 60) {
647
+ customer.bucket31_60 += amount;
648
+ }
649
+ else if (diffDays <= 90) {
650
+ customer.bucket61_90 += amount;
651
+ }
652
+ else {
653
+ customer.bucket90plus += amount;
654
+ }
655
+ customer.total += amount;
656
+ }
657
+ const agingInadimplencia = Array.from(byCustomer.values())
658
+ .map((item) => (Object.assign(Object.assign({}, item), { bucket0_30: Number(item.bucket0_30.toFixed(2)), bucket31_60: Number(item.bucket31_60.toFixed(2)), bucket61_90: Number(item.bucket61_90.toFixed(2)), bucket90plus: Number(item.bucket90plus.toFixed(2)), total: Number(item.total.toFixed(2)) })))
659
+ .sort((a, b) => b.total - a.total);
660
+ const contactHistory = await this.prisma.audit_log.findMany({
661
+ where: {
662
+ entity_table: {
663
+ in: ['financial_collection_contact', 'financial_collection_agreement'],
664
+ },
665
+ },
666
+ include: {
667
+ user: {
668
+ select: {
669
+ name: true,
670
+ },
671
+ },
672
+ },
673
+ orderBy: {
674
+ created_at: 'desc',
675
+ },
676
+ take: 500,
677
+ });
678
+ const historicoContatos = contactHistory.map((log) => {
679
+ var _a;
680
+ const afterData = this.parseAiJson(log.after_data || '{}');
681
+ return {
682
+ clienteId: log.entity_id,
683
+ tipo: this.mapCollectionActionToType(log.action, afterData === null || afterData === void 0 ? void 0 : afterData.channel),
684
+ data: log.created_at.toISOString(),
685
+ descricao: log.summary || '',
686
+ responsavel: ((_a = log.user) === null || _a === void 0 ? void 0 : _a.name) || 'Sistema',
687
+ };
688
+ });
689
+ return {
690
+ agingInadimplencia,
691
+ historicoContatos,
692
+ };
693
+ }
694
+ async sendCollection(personId, data, actorUserId) {
695
+ const person = await this.prisma.person.findUnique({
696
+ where: {
697
+ id: personId,
698
+ },
699
+ select: {
700
+ id: true,
701
+ name: true,
702
+ },
703
+ });
704
+ if (!person) {
705
+ throw new common_1.NotFoundException('Person not found');
706
+ }
707
+ const log = await this.prisma.audit_log.create({
708
+ data: {
709
+ actor_user_id: actorUserId || null,
710
+ action: 'collection_sent',
711
+ entity_table: 'financial_collection_contact',
712
+ entity_id: String(person.id),
713
+ summary: `Cobrança enviada via e-mail para ${person.name}`,
714
+ after_data: JSON.stringify({
715
+ message: data.message,
716
+ subject: data.subject || null,
717
+ }),
718
+ },
719
+ });
720
+ return {
721
+ id: String(log.id),
722
+ success: true,
723
+ };
724
+ }
725
+ async registerCollectionAgreement(personId, data, actorUserId) {
726
+ const person = await this.prisma.person.findUnique({
727
+ where: {
728
+ id: personId,
729
+ },
730
+ select: {
731
+ id: true,
732
+ name: true,
733
+ },
734
+ });
735
+ if (!person) {
736
+ throw new common_1.NotFoundException('Person not found');
737
+ }
738
+ const firstDueDate = new Date(data.first_due_date);
739
+ if (Number.isNaN(firstDueDate.getTime())) {
740
+ throw new common_1.BadRequestException('Invalid first due date');
741
+ }
742
+ const totalAmountCents = this.toCents(Number(data.amount));
743
+ if (totalAmountCents <= 0) {
744
+ throw new common_1.BadRequestException('Invalid agreement amount');
745
+ }
746
+ const baseInstallmentCents = Math.floor(totalAmountCents / data.installments);
747
+ const remainder = totalAmountCents % data.installments;
748
+ const created = await this.prisma.$transaction(async (tx) => {
749
+ const title = await tx.financial_title.create({
750
+ data: {
751
+ person_id: person.id,
752
+ title_type: 'receivable',
753
+ status: 'open',
754
+ document_number: `ACD-${Date.now()}`,
755
+ description: data.notes || 'Acordo de cobrança',
756
+ competence_date: firstDueDate,
757
+ issue_date: new Date(),
758
+ total_amount_cents: totalAmountCents,
759
+ created_by_user_id: actorUserId || null,
760
+ },
761
+ select: {
762
+ id: true,
763
+ },
764
+ });
765
+ for (let index = 0; index < data.installments; index += 1) {
766
+ const dueDate = this.addMonths(firstDueDate, index);
767
+ const amountCents = baseInstallmentCents + (index === data.installments - 1 ? remainder : 0);
768
+ await tx.financial_installment.create({
769
+ data: {
770
+ title_id: title.id,
771
+ installment_number: index + 1,
772
+ competence_date: dueDate,
773
+ due_date: dueDate,
774
+ amount_cents: amountCents,
775
+ open_amount_cents: amountCents,
776
+ status: this.resolveInstallmentStatus(amountCents, amountCents, dueDate, 'open'),
777
+ notes: data.notes || null,
778
+ },
779
+ });
780
+ }
781
+ const log = await tx.audit_log.create({
782
+ data: {
783
+ actor_user_id: actorUserId || null,
784
+ action: 'collection_agreement_registered',
785
+ entity_table: 'financial_collection_agreement',
786
+ entity_id: String(person.id),
787
+ summary: `Acordo registrado para ${person.name}: ${data.installments}x`,
788
+ after_data: JSON.stringify({
789
+ title_id: title.id,
790
+ installments: data.installments,
791
+ amount: data.amount,
792
+ first_due_date: data.first_due_date,
793
+ notes: data.notes || null,
794
+ }),
795
+ },
796
+ select: {
797
+ id: true,
798
+ },
799
+ });
800
+ return {
801
+ titleId: title.id,
802
+ auditLogId: log.id,
803
+ };
804
+ });
805
+ return {
806
+ success: true,
807
+ titleId: String(created.titleId),
808
+ auditLogId: String(created.auditLogId),
809
+ };
810
+ }
811
+ mapCollectionActionToType(action, channel) {
812
+ if (action === 'collection_agreement_registered') {
813
+ return 'Acordo';
814
+ }
815
+ if (channel) {
816
+ const normalized = String(channel).toLowerCase();
817
+ if (normalized === 'email')
818
+ return 'E-mail';
819
+ if (normalized === 'whatsapp')
820
+ return 'WhatsApp';
821
+ if (normalized === 'sms')
822
+ return 'SMS';
823
+ }
824
+ return 'Contato';
825
+ }
590
826
  calculateDashboardKpis(payables, receivables, bankAccounts) {
591
827
  const today = this.startOfDay(new Date());
592
828
  const day7 = this.addDays(today, 7);
@@ -637,6 +873,11 @@ let FinanceService = FinanceService_1 = class FinanceService {
637
873
  next.setDate(next.getDate() + days);
638
874
  return next;
639
875
  }
876
+ addMonths(date, months) {
877
+ const next = new Date(date);
878
+ next.setMonth(next.getMonth() + months);
879
+ return next;
880
+ }
640
881
  async listAccountsPayableInstallments(paginationParams, status) {
641
882
  return this.listTitles('payable', paginationParams, status);
642
883
  }
@@ -882,6 +1123,184 @@ let FinanceService = FinanceService_1 = class FinanceService {
882
1123
  });
883
1124
  return bankAccounts.map((bankAccount) => this.mapBankAccountToFront(bankAccount));
884
1125
  }
1126
+ async listTransfers(filters) {
1127
+ var _a;
1128
+ const search = (_a = filters === null || filters === void 0 ? void 0 : filters.search) === null || _a === void 0 ? void 0 : _a.trim();
1129
+ const parsedBankAccountId = (filters === null || filters === void 0 ? void 0 : filters.bank_account_id)
1130
+ ? Number.parseInt(filters.bank_account_id, 10)
1131
+ : undefined;
1132
+ const bankAccountId = parsedBankAccountId && !Number.isNaN(parsedBankAccountId)
1133
+ ? parsedBankAccountId
1134
+ : undefined;
1135
+ let transferKeys;
1136
+ if (search || bankAccountId) {
1137
+ const filteredLines = await this.prisma.bank_statement_line.findMany({
1138
+ where: Object.assign(Object.assign({ external_id: {
1139
+ startsWith: 'transfer:',
1140
+ } }, (search
1141
+ ? {
1142
+ description: {
1143
+ contains: search,
1144
+ mode: 'insensitive',
1145
+ },
1146
+ }
1147
+ : {})), (bankAccountId ? { bank_account_id: bankAccountId } : {})),
1148
+ select: {
1149
+ external_id: true,
1150
+ },
1151
+ });
1152
+ transferKeys = Array.from(new Set(filteredLines
1153
+ .map((line) => line.external_id)
1154
+ .filter((externalId) => !!externalId)));
1155
+ if (transferKeys.length === 0) {
1156
+ return [];
1157
+ }
1158
+ }
1159
+ const transferLines = await this.prisma.bank_statement_line.findMany({
1160
+ where: Object.assign({}, (transferKeys
1161
+ ? {
1162
+ external_id: {
1163
+ in: transferKeys,
1164
+ },
1165
+ }
1166
+ : {
1167
+ external_id: {
1168
+ startsWith: 'transfer:',
1169
+ },
1170
+ })),
1171
+ select: {
1172
+ id: true,
1173
+ external_id: true,
1174
+ bank_account_id: true,
1175
+ posted_date: true,
1176
+ amount_cents: true,
1177
+ description: true,
1178
+ },
1179
+ orderBy: [{ posted_date: 'desc' }, { id: 'desc' }],
1180
+ });
1181
+ const groupedByTransfer = new Map();
1182
+ for (const line of transferLines) {
1183
+ const transferKey = line.external_id;
1184
+ if (!transferKey) {
1185
+ continue;
1186
+ }
1187
+ const current = groupedByTransfer.get(transferKey) || [];
1188
+ current.push(line);
1189
+ groupedByTransfer.set(transferKey, current);
1190
+ }
1191
+ const transfers = Array.from(groupedByTransfer.entries())
1192
+ .map(([transferKey, lines]) => {
1193
+ const sourceLine = lines.find((line) => line.amount_cents < 0);
1194
+ const destinationLine = lines.find((line) => line.amount_cents > 0);
1195
+ if (!sourceLine || !destinationLine) {
1196
+ return null;
1197
+ }
1198
+ return {
1199
+ id: transferKey.replace('transfer:', ''),
1200
+ contaOrigemId: String(sourceLine.bank_account_id),
1201
+ contaDestinoId: String(destinationLine.bank_account_id),
1202
+ data: sourceLine.posted_date.toISOString(),
1203
+ valor: this.fromCents(Math.abs(sourceLine.amount_cents)),
1204
+ descricao: sourceLine.description || destinationLine.description || '',
1205
+ };
1206
+ })
1207
+ .filter(Boolean);
1208
+ return transfers;
1209
+ }
1210
+ async createTransfer(data, userId) {
1211
+ var _a;
1212
+ const sourceAccountId = Number(data.source_account_id);
1213
+ const destinationAccountId = Number(data.destination_account_id);
1214
+ const amount = Number(data.amount);
1215
+ const postedDate = new Date(data.date);
1216
+ if (Number.isNaN(sourceAccountId) ||
1217
+ Number.isNaN(destinationAccountId) ||
1218
+ sourceAccountId <= 0 ||
1219
+ destinationAccountId <= 0) {
1220
+ throw new common_1.BadRequestException('Invalid bank account ids');
1221
+ }
1222
+ if (sourceAccountId === destinationAccountId) {
1223
+ throw new common_1.BadRequestException('Source and destination accounts must be different');
1224
+ }
1225
+ if (Number.isNaN(amount) || amount <= 0) {
1226
+ throw new common_1.BadRequestException('amount must be greater than zero');
1227
+ }
1228
+ if (Number.isNaN(postedDate.getTime())) {
1229
+ throw new common_1.BadRequestException('Invalid transfer date');
1230
+ }
1231
+ const accounts = await this.prisma.bank_account.findMany({
1232
+ where: {
1233
+ id: {
1234
+ in: [sourceAccountId, destinationAccountId],
1235
+ },
1236
+ },
1237
+ select: {
1238
+ id: true,
1239
+ },
1240
+ });
1241
+ if (accounts.length !== 2) {
1242
+ throw new common_1.NotFoundException('Bank account not found');
1243
+ }
1244
+ const amountCents = this.toCents(amount);
1245
+ const description = ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || 'Transferência bancária';
1246
+ const transferReference = `transfer:${Date.now()}-${Math.round(Math.random() * 1000000)}`;
1247
+ await this.prisma.$transaction(async (tx) => {
1248
+ const sourceStatement = await tx.bank_statement.create({
1249
+ data: {
1250
+ bank_account_id: sourceAccountId,
1251
+ source_type: 'manual',
1252
+ imported_at: new Date(),
1253
+ imported_by_user_id: userId,
1254
+ idempotency_key: `${transferReference}:source`,
1255
+ period_start: postedDate,
1256
+ period_end: postedDate,
1257
+ },
1258
+ });
1259
+ const destinationStatement = await tx.bank_statement.create({
1260
+ data: {
1261
+ bank_account_id: destinationAccountId,
1262
+ source_type: 'manual',
1263
+ imported_at: new Date(),
1264
+ imported_by_user_id: userId,
1265
+ idempotency_key: `${transferReference}:destination`,
1266
+ period_start: postedDate,
1267
+ period_end: postedDate,
1268
+ },
1269
+ });
1270
+ await tx.bank_statement_line.create({
1271
+ data: {
1272
+ bank_statement_id: sourceStatement.id,
1273
+ bank_account_id: sourceAccountId,
1274
+ external_id: transferReference,
1275
+ posted_date: postedDate,
1276
+ amount_cents: -Math.abs(amountCents),
1277
+ description,
1278
+ status: 'reconciled',
1279
+ dedupe_key: `${transferReference}:source`,
1280
+ },
1281
+ });
1282
+ await tx.bank_statement_line.create({
1283
+ data: {
1284
+ bank_statement_id: destinationStatement.id,
1285
+ bank_account_id: destinationAccountId,
1286
+ external_id: transferReference,
1287
+ posted_date: postedDate,
1288
+ amount_cents: Math.abs(amountCents),
1289
+ description,
1290
+ status: 'reconciled',
1291
+ dedupe_key: `${transferReference}:destination`,
1292
+ },
1293
+ });
1294
+ });
1295
+ return {
1296
+ id: transferReference.replace('transfer:', ''),
1297
+ contaOrigemId: String(sourceAccountId),
1298
+ contaDestinoId: String(destinationAccountId),
1299
+ data: postedDate.toISOString(),
1300
+ valor: amount,
1301
+ descricao: description,
1302
+ };
1303
+ }
885
1304
  async listCostCenters() {
886
1305
  const costCenters = await this.prisma.cost_center.findMany({
887
1306
  orderBy: [{ code: 'asc' }, { name: 'asc' }],
@@ -1035,9 +1454,17 @@ let FinanceService = FinanceService_1 = class FinanceService {
1035
1454
  });
1036
1455
  return Object.assign(Object.assign({}, paginated), { data: (paginated.data || []).map((period) => this.mapPeriodCloseToFront(period)) });
1037
1456
  }
1038
- async listBankStatements(bankAccountId) {
1457
+ async listBankStatements(bankAccountId, search) {
1458
+ const trimmedSearch = search === null || search === void 0 ? void 0 : search.trim();
1039
1459
  const statements = await this.prisma.bank_statement_line.findMany({
1040
- where: Object.assign({}, (bankAccountId ? { bank_account_id: bankAccountId } : {})),
1460
+ where: Object.assign(Object.assign({}, (bankAccountId ? { bank_account_id: bankAccountId } : {})), (trimmedSearch
1461
+ ? {
1462
+ description: {
1463
+ contains: trimmedSearch,
1464
+ mode: 'insensitive',
1465
+ },
1466
+ }
1467
+ : {})),
1041
1468
  include: {
1042
1469
  bank_account: {
1043
1470
  select: {
@@ -1057,8 +1484,74 @@ let FinanceService = FinanceService_1 = class FinanceService {
1057
1484
  statusConciliacao: this.mapStatementStatusToPt(statement.status),
1058
1485
  }));
1059
1486
  }
1060
- async exportBankStatementsCsv(bankAccountId) {
1061
- const statements = await this.listBankStatements(bankAccountId);
1487
+ async getBankReconciliationSummary(bankAccountId) {
1488
+ const pendingStatements = await this.prisma.bank_statement_line.findMany({
1489
+ where: {
1490
+ bank_account_id: bankAccountId,
1491
+ status: {
1492
+ in: ['pending', 'imported'],
1493
+ },
1494
+ },
1495
+ select: {
1496
+ amount_cents: true,
1497
+ },
1498
+ });
1499
+ const openInstallments = await this.prisma.financial_installment.findMany({
1500
+ where: {
1501
+ open_amount_cents: {
1502
+ gt: 0,
1503
+ },
1504
+ status: {
1505
+ in: ['open', 'partial', 'overdue'],
1506
+ },
1507
+ financial_title: {
1508
+ status: {
1509
+ in: ['open', 'partial', 'overdue'],
1510
+ },
1511
+ title_type: {
1512
+ in: ['payable', 'receivable'],
1513
+ },
1514
+ },
1515
+ },
1516
+ select: {
1517
+ open_amount_cents: true,
1518
+ financial_title: {
1519
+ select: {
1520
+ title_type: true,
1521
+ },
1522
+ },
1523
+ },
1524
+ });
1525
+ const payableAmounts = new Set();
1526
+ const receivableAmounts = new Set();
1527
+ for (const installment of openInstallments) {
1528
+ const amount = Math.abs(installment.open_amount_cents);
1529
+ if (installment.financial_title.title_type === 'payable') {
1530
+ payableAmounts.add(amount);
1531
+ }
1532
+ else if (installment.financial_title.title_type === 'receivable') {
1533
+ receivableAmounts.add(amount);
1534
+ }
1535
+ }
1536
+ let discrepancyCount = 0;
1537
+ let differenceCents = 0;
1538
+ for (const statement of pendingStatements) {
1539
+ const normalizedAmount = Math.abs(statement.amount_cents);
1540
+ differenceCents += statement.amount_cents;
1541
+ const hasPossibleMatch = statement.amount_cents < 0
1542
+ ? payableAmounts.has(normalizedAmount)
1543
+ : receivableAmounts.has(normalizedAmount);
1544
+ if (!hasPossibleMatch) {
1545
+ discrepancyCount += 1;
1546
+ }
1547
+ }
1548
+ return {
1549
+ discrepancies: discrepancyCount,
1550
+ difference: this.fromCents(differenceCents),
1551
+ };
1552
+ }
1553
+ async exportBankStatementsCsv(bankAccountId, search) {
1554
+ const statements = await this.listBankStatements(bankAccountId, search);
1062
1555
  const headers = [
1063
1556
  'id',
1064
1557
  'data',
@@ -1089,6 +1582,66 @@ let FinanceService = FinanceService_1 = class FinanceService {
1089
1582
  fileName,
1090
1583
  };
1091
1584
  }
1585
+ async createBankStatementAdjustment(data, userId) {
1586
+ var _a;
1587
+ const bankAccountId = Number(data.bank_account_id);
1588
+ const amount = Number(data.amount);
1589
+ const postedAt = data.date ? new Date(data.date) : new Date();
1590
+ if (Number.isNaN(bankAccountId) || bankAccountId <= 0) {
1591
+ throw new common_1.BadRequestException('bank_account_id is required');
1592
+ }
1593
+ if (Number.isNaN(amount) || amount <= 0) {
1594
+ throw new common_1.BadRequestException('amount must be greater than zero');
1595
+ }
1596
+ if (Number.isNaN(postedAt.getTime())) {
1597
+ throw new common_1.BadRequestException('Invalid adjustment date');
1598
+ }
1599
+ const bankAccount = await this.prisma.bank_account.findUnique({
1600
+ where: { id: bankAccountId },
1601
+ select: { id: true },
1602
+ });
1603
+ if (!bankAccount) {
1604
+ throw new common_1.NotFoundException('Bank account not found');
1605
+ }
1606
+ const adjustedAmountCents = -Math.abs(this.toCents(amount));
1607
+ const description = ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || `Ajuste: ${data.type}`;
1608
+ const reference = `adjustment:${Date.now()}-${Math.round(Math.random() * 1000000)}`;
1609
+ const created = await this.prisma.$transaction(async (tx) => {
1610
+ const statement = await tx.bank_statement.create({
1611
+ data: {
1612
+ bank_account_id: bankAccountId,
1613
+ source_type: 'manual',
1614
+ imported_at: new Date(),
1615
+ imported_by_user_id: userId,
1616
+ idempotency_key: reference,
1617
+ period_start: postedAt,
1618
+ period_end: postedAt,
1619
+ },
1620
+ });
1621
+ const line = await tx.bank_statement_line.create({
1622
+ data: {
1623
+ bank_statement_id: statement.id,
1624
+ bank_account_id: bankAccountId,
1625
+ external_id: reference,
1626
+ posted_date: postedAt,
1627
+ amount_cents: adjustedAmountCents,
1628
+ description,
1629
+ status: 'adjusted',
1630
+ dedupe_key: reference,
1631
+ },
1632
+ });
1633
+ return line;
1634
+ });
1635
+ return {
1636
+ id: String(created.id),
1637
+ contaBancariaId: String(created.bank_account_id),
1638
+ data: created.posted_date.toISOString(),
1639
+ descricao: created.description,
1640
+ valor: this.fromCents(created.amount_cents),
1641
+ tipo: created.amount_cents >= 0 ? 'entrada' : 'saida',
1642
+ statusConciliacao: this.mapStatementStatusToPt(created.status),
1643
+ };
1644
+ }
1092
1645
  async importBankStatements(bankAccountId, file, locale, userId) {
1093
1646
  if (!file) {
1094
1647
  throw new common_1.BadRequestException('File is required');
@@ -2327,6 +2880,67 @@ let FinanceService = FinanceService_1 = class FinanceService {
2327
2880
  created_by_user_id: userId,
2328
2881
  },
2329
2882
  });
2883
+ let reconciliationId = null;
2884
+ if (data.bank_statement_line_id) {
2885
+ const statementLine = await tx.bank_statement_line.findUnique({
2886
+ where: {
2887
+ id: data.bank_statement_line_id,
2888
+ },
2889
+ select: {
2890
+ id: true,
2891
+ bank_account_id: true,
2892
+ amount_cents: true,
2893
+ status: true,
2894
+ },
2895
+ });
2896
+ if (!statementLine) {
2897
+ throw new common_1.NotFoundException('Bank statement line not found');
2898
+ }
2899
+ if (data.bank_account_id &&
2900
+ statementLine.bank_account_id !== data.bank_account_id) {
2901
+ throw new common_1.ConflictException('Bank statement line does not belong to informed bank account');
2902
+ }
2903
+ if (Math.abs(statementLine.amount_cents) !== amountCents) {
2904
+ throw new common_1.ConflictException('Bank statement amount and settlement amount must match');
2905
+ }
2906
+ const hasReconciliation = await tx.bank_reconciliation.findFirst({
2907
+ where: {
2908
+ bank_statement_line_id: statementLine.id,
2909
+ status: 'reconciled',
2910
+ },
2911
+ select: {
2912
+ id: true,
2913
+ },
2914
+ });
2915
+ if (hasReconciliation) {
2916
+ throw new common_1.ConflictException('Bank statement line already reconciled');
2917
+ }
2918
+ const createdReconciliation = await tx.bank_reconciliation.create({
2919
+ data: {
2920
+ bank_statement_line_id: statementLine.id,
2921
+ settlement_id: settlement.id,
2922
+ status: 'reconciled',
2923
+ matched_at: settledAt,
2924
+ reconciled_at: settledAt,
2925
+ matched_by_user_id: userId || null,
2926
+ reconciled_by_user_id: userId || null,
2927
+ },
2928
+ select: {
2929
+ id: true,
2930
+ },
2931
+ });
2932
+ reconciliationId = createdReconciliation.id;
2933
+ if (statementLine.status !== 'reconciled') {
2934
+ await tx.bank_statement_line.update({
2935
+ where: {
2936
+ id: statementLine.id,
2937
+ },
2938
+ data: {
2939
+ status: 'reconciled',
2940
+ },
2941
+ });
2942
+ }
2943
+ }
2330
2944
  await tx.settlement_allocation.create({
2331
2945
  data: {
2332
2946
  settlement: {
@@ -2404,6 +3018,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
2404
3018
  title_status: nextTitleStatus,
2405
3019
  installment_open_amount_cents: updatedInstallment.open_amount_cents,
2406
3020
  settlement_id: settlement.id,
3021
+ bank_reconciliation_id: reconciliationId,
2407
3022
  }),
2408
3023
  });
2409
3024
  const updatedTitle = await tx.financial_title.findFirst({
@@ -2419,9 +3034,12 @@ let FinanceService = FinanceService_1 = class FinanceService {
2419
3034
  return {
2420
3035
  title: updatedTitle,
2421
3036
  settlementId: settlement.id,
3037
+ reconciliationId,
2422
3038
  };
2423
3039
  });
2424
- return Object.assign(Object.assign({}, this.mapTitleToFront(result.title)), { settlementId: String(result.settlementId) });
3040
+ return Object.assign(Object.assign({}, this.mapTitleToFront(result.title)), { settlementId: String(result.settlementId), reconciliationId: result.reconciliationId
3041
+ ? String(result.reconciliationId)
3042
+ : null });
2425
3043
  }
2426
3044
  catch (error) {
2427
3045
  const message = String((error === null || error === void 0 ? void 0 : error.message) || '');
@@ -2923,7 +3541,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
2923
3541
  });
2924
3542
  }
2925
3543
  mapTitleToFront(title, paymentChannelOverride) {
2926
- var _a;
3544
+ var _a, _b, _c;
2927
3545
  const allocations = title.financial_installment.flatMap((installment) => installment.installment_allocation);
2928
3546
  const firstCostCenter = (_a = allocations[0]) === null || _a === void 0 ? void 0 : _a.cost_center_id;
2929
3547
  const tags = [
@@ -2978,8 +3596,14 @@ let FinanceService = FinanceService_1 = class FinanceService {
2978
3596
  : '', 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) ||
2979
3597
  paymentChannelOverride ||
2980
3598
  'transferencia' }, (title.title_type === 'payable'
2981
- ? { fornecedorId: String(title.person_id) }
2982
- : { clienteId: String(title.person_id) }));
3599
+ ? {
3600
+ fornecedorId: String(title.person_id),
3601
+ fornecedor: ((_b = title.person) === null || _b === void 0 ? void 0 : _b.name) || '',
3602
+ }
3603
+ : {
3604
+ clienteId: String(title.person_id),
3605
+ cliente: ((_c = title.person) === null || _c === void 0 ? void 0 : _c.name) || '',
3606
+ }));
2983
3607
  }
2984
3608
  defaultTitleInclude() {
2985
3609
  return {