@hed-hog/finance 0.0.318 → 0.0.319

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 (66) hide show
  1. package/dist/dto/create-bank-account.dto.d.ts +1 -0
  2. package/dist/dto/create-bank-account.dto.d.ts.map +1 -1
  3. package/dist/dto/create-bank-account.dto.js +7 -0
  4. package/dist/dto/create-bank-account.dto.js.map +1 -1
  5. package/dist/dto/create-bank-statement-entry.dto.d.ts +8 -0
  6. package/dist/dto/create-bank-statement-entry.dto.d.ts.map +1 -0
  7. package/dist/dto/create-bank-statement-entry.dto.js +54 -0
  8. package/dist/dto/create-bank-statement-entry.dto.js.map +1 -0
  9. package/dist/dto/update-bank-account.dto.d.ts +1 -0
  10. package/dist/dto/update-bank-account.dto.d.ts.map +1 -1
  11. package/dist/dto/update-bank-account.dto.js +7 -0
  12. package/dist/dto/update-bank-account.dto.js.map +1 -1
  13. package/dist/dto/update-bank-statement-entry.dto.d.ts +6 -0
  14. package/dist/dto/update-bank-statement-entry.dto.d.ts.map +1 -0
  15. package/dist/dto/update-bank-statement-entry.dto.js +42 -0
  16. package/dist/dto/update-bank-statement-entry.dto.js.map +1 -0
  17. package/dist/finance-bank-accounts.controller.d.ts +25 -13
  18. package/dist/finance-bank-accounts.controller.d.ts.map +1 -1
  19. package/dist/finance-bank-accounts.controller.js +5 -3
  20. package/dist/finance-bank-accounts.controller.js.map +1 -1
  21. package/dist/finance-data.controller.d.ts +4 -0
  22. package/dist/finance-data.controller.d.ts.map +1 -1
  23. package/dist/finance-installments.controller.d.ts +3 -2
  24. package/dist/finance-installments.controller.d.ts.map +1 -1
  25. package/dist/finance-installments.controller.js +10 -6
  26. package/dist/finance-installments.controller.js.map +1 -1
  27. package/dist/finance-statements.controller.d.ts +61 -12
  28. package/dist/finance-statements.controller.d.ts.map +1 -1
  29. package/dist/finance-statements.controller.js +50 -8
  30. package/dist/finance-statements.controller.js.map +1 -1
  31. package/dist/finance-transfers.controller.d.ts +13 -8
  32. package/dist/finance-transfers.controller.d.ts.map +1 -1
  33. package/dist/finance-transfers.controller.js +11 -5
  34. package/dist/finance-transfers.controller.js.map +1 -1
  35. package/dist/finance.service.d.ts +124 -35
  36. package/dist/finance.service.d.ts.map +1 -1
  37. package/dist/finance.service.js +389 -55
  38. package/dist/finance.service.js.map +1 -1
  39. package/hedhog/data/role.yaml +9 -1
  40. package/hedhog/data/route.yaml +42 -0
  41. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +87 -72
  42. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +53 -25
  43. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +8 -0
  44. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +60 -24
  45. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +114 -31
  46. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +25 -3
  47. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +732 -61
  48. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +101 -15
  49. package/hedhog/table/bank_statement_line.yaml +1 -1
  50. package/hedhog/table/cashflow_projection.yaml +1 -1
  51. package/hedhog/table/financial_installment.yaml +2 -2
  52. package/hedhog/table/financial_title.yaml +1 -1
  53. package/hedhog/table/installment_allocation.yaml +1 -1
  54. package/hedhog/table/receivable_schedule.yaml +1 -1
  55. package/hedhog/table/settlement.yaml +1 -1
  56. package/hedhog/table/settlement_allocation.yaml +5 -5
  57. package/package.json +7 -7
  58. package/src/dto/create-bank-account.dto.ts +18 -1
  59. package/src/dto/create-bank-statement-entry.dto.ts +50 -0
  60. package/src/dto/update-bank-account.dto.ts +11 -1
  61. package/src/dto/update-bank-statement-entry.dto.ts +31 -0
  62. package/src/finance-bank-accounts.controller.ts +3 -2
  63. package/src/finance-installments.controller.ts +9 -3
  64. package/src/finance-statements.controller.ts +40 -0
  65. package/src/finance-transfers.controller.ts +7 -1
  66. package/src/finance.service.ts +543 -61
@@ -20,6 +20,7 @@ import { readFile } from 'node:fs/promises';
20
20
  import { CreateBankAccountDto } from './dto/create-bank-account.dto';
21
21
  import { CreateBankReconciliationDto } from './dto/create-bank-reconciliation.dto';
22
22
  import { CreateBankStatementAdjustmentDto } from './dto/create-bank-statement-adjustment.dto';
23
+ import { CreateBankStatementEntryDto } from './dto/create-bank-statement-entry.dto';
23
24
  import { CreateCostCenterDto } from './dto/create-cost-center.dto';
24
25
  import { CreateFinanceCategoryDto } from './dto/create-finance-category.dto';
25
26
  import { CreateFinanceTagDto } from './dto/create-finance-tag.dto';
@@ -33,6 +34,7 @@ import { ReverseSettlementDto } from './dto/reverse-settlement.dto';
33
34
  import { SendCollectionDto } from './dto/send-collection.dto';
34
35
  import { SettleInstallmentDto } from './dto/settle-installment.dto';
35
36
  import { UpdateBankAccountDto } from './dto/update-bank-account.dto';
37
+ import { UpdateBankStatementEntryDto } from './dto/update-bank-statement-entry.dto';
36
38
  import { UpdateCostCenterDto } from './dto/update-cost-center.dto';
37
39
  import { UpdateFinanceCategoryDto } from './dto/update-finance-category.dto';
38
40
 
@@ -654,6 +656,28 @@ export class FinanceService {
654
656
  return dt.toISOString().slice(0, 10);
655
657
  }
656
658
 
659
+ private parseFilterDate(value?: string, endOfDay = false): Date | undefined {
660
+ if (!value) {
661
+ return undefined;
662
+ }
663
+
664
+ const raw = String(value).trim();
665
+ if (!raw) {
666
+ return undefined;
667
+ }
668
+
669
+ const normalized = /^\d{4}-\d{2}-\d{2}$/.test(raw)
670
+ ? `${raw}T${endOfDay ? '23:59:59.999' : '00:00:00.000'}`
671
+ : raw;
672
+ const parsed = new Date(normalized);
673
+
674
+ if (Number.isNaN(parsed.getTime())) {
675
+ return undefined;
676
+ }
677
+
678
+ return parsed;
679
+ }
680
+
657
681
  private normalizeMonth(value: any): string {
658
682
  if (!value) return '';
659
683
  const raw = String(value).trim();
@@ -695,6 +719,14 @@ export class FinanceService {
695
719
  card: 'cartao',
696
720
  credito: 'cartao',
697
721
  debito: 'cartao',
722
+ 'debito automatico': 'debito_automatico',
723
+ debito_automatico: 'debito_automatico',
724
+ 'debito-automatico': 'debito_automatico',
725
+ 'débito automático': 'debito_automatico',
726
+ 'debito em conta': 'debito_em_conta',
727
+ debito_em_conta: 'debito_em_conta',
728
+ 'debito-em-conta': 'debito_em_conta',
729
+ 'débito em conta': 'debito_em_conta',
698
730
  dinheiro: 'dinheiro',
699
731
  cash: 'dinheiro',
700
732
  cheque: 'cheque',
@@ -774,7 +806,14 @@ export class FinanceService {
774
806
  this.loadCategories(),
775
807
  this.loadCostCenters(),
776
808
  this.loadBankAccounts(),
777
- this.listBankStatements(),
809
+ this.listBankStatements({
810
+ page: 1,
811
+ pageSize: 1000,
812
+ search: undefined,
813
+ sortField: undefined,
814
+ sortOrder: undefined,
815
+ fields: undefined,
816
+ }),
778
817
  this.getAccountsReceivableCollectionsDefault(),
779
818
  this.loadTags(),
780
819
  this.loadAuditLogs(),
@@ -793,10 +832,10 @@ export class FinanceService {
793
832
  costCentersResult.status === 'fulfilled' ? costCentersResult.value : [];
794
833
  const bankAccounts =
795
834
  bankAccountsResult.status === 'fulfilled' ? bankAccountsResult.value : [];
796
- const bankStatements =
797
- bankStatementsResult.status === 'fulfilled'
798
- ? bankStatementsResult.value
799
- : [];
835
+ const bankStatements =
836
+ bankStatementsResult.status === 'fulfilled'
837
+ ? bankStatementsResult.value.data
838
+ : [];
800
839
  const collectionsDefault =
801
840
  collectionsDefaultResult.status === 'fulfilled'
802
841
  ? collectionsDefaultResult.value
@@ -2114,7 +2153,7 @@ export class FinanceService {
2114
2153
  throw new NotFoundException('Person not found');
2115
2154
  }
2116
2155
 
2117
- const firstDueDate = new Date(data.first_due_date);
2156
+ const firstDueDate = this.parseLocalDate(data.first_due_date);
2118
2157
 
2119
2158
  if (Number.isNaN(firstDueDate.getTime())) {
2120
2159
  throw new BadRequestException('Invalid first due date');
@@ -2333,15 +2372,23 @@ export class FinanceService {
2333
2372
  async listAccountsPayableInstallments(
2334
2373
  paginationParams: PaginationDTO,
2335
2374
  status?: string,
2375
+ filters?: {
2376
+ from?: string;
2377
+ to?: string;
2378
+ },
2336
2379
  ) {
2337
- return this.listTitles('payable', paginationParams, status);
2380
+ return this.listTitles('payable', paginationParams, status, filters);
2338
2381
  }
2339
2382
 
2340
2383
  async listAccountsReceivableInstallments(
2341
2384
  paginationParams: PaginationDTO,
2342
2385
  status?: string,
2386
+ filters?: {
2387
+ from?: string;
2388
+ to?: string;
2389
+ },
2343
2390
  ) {
2344
- return this.listTitles('receivable', paginationParams, status);
2391
+ return this.listTitles('receivable', paginationParams, status, filters);
2345
2392
  }
2346
2393
 
2347
2394
  async getAccountsPayableInstallment(id: number, locale: string) {
@@ -2727,27 +2774,49 @@ export class FinanceService {
2727
2774
  return this.updateTitleTags(id, 'receivable', tagIds, locale);
2728
2775
  }
2729
2776
 
2730
- async listBankAccounts() {
2731
- const bankAccounts = await this.prisma.bank_account.findMany({
2732
- include: {
2733
- bank_statement_line: {
2734
- select: {
2735
- amount_cents: true,
2736
- status: true,
2777
+ async listBankAccounts(paginationParams?: PaginationDTO) {
2778
+ const paginated = await this.paginationService.paginate(
2779
+ this.prisma.bank_account,
2780
+ {
2781
+ ...paginationParams,
2782
+ sortField: paginationParams?.sortField || 'code',
2783
+ sortOrder: paginationParams?.sortOrder || PageOrderDirection.Asc,
2784
+ },
2785
+ {
2786
+ include: {
2787
+ bank_statement_line: {
2788
+ select: {
2789
+ amount_cents: true,
2790
+ status: true,
2791
+ posted_date: true,
2792
+ description: true,
2793
+ },
2737
2794
  },
2738
2795
  },
2796
+ orderBy: [{ code: 'asc' }, { name: 'asc' }],
2739
2797
  },
2740
- orderBy: [{ code: 'asc' }, { name: 'asc' }],
2741
- });
2798
+ );
2742
2799
 
2743
- return bankAccounts.map((bankAccount) => this.mapBankAccountToFront(bankAccount));
2800
+ return {
2801
+ ...paginated,
2802
+ data: (paginated.data || []).map((bankAccount) =>
2803
+ this.mapBankAccountToFront(bankAccount),
2804
+ ),
2805
+ };
2744
2806
  }
2745
2807
 
2746
- async listTransfers(filters?: {
2747
- search?: string;
2748
- bank_account_id?: string;
2749
- }) {
2808
+ async listTransfers(
2809
+ paginationParams: PaginationDTO,
2810
+ filters?: {
2811
+ search?: string;
2812
+ bank_account_id?: string;
2813
+ from?: string;
2814
+ to?: string;
2815
+ },
2816
+ ) {
2750
2817
  const search = filters?.search?.trim();
2818
+ const fromDate = this.parseFilterDate(filters?.from);
2819
+ const toDate = this.parseFilterDate(filters?.to, true);
2751
2820
  const parsedBankAccountId = filters?.bank_account_id
2752
2821
  ? Number.parseInt(filters.bank_account_id, 10)
2753
2822
  : undefined;
@@ -2773,6 +2842,14 @@ export class FinanceService {
2773
2842
  }
2774
2843
  : {}),
2775
2844
  ...(bankAccountId ? { bank_account_id: bankAccountId } : {}),
2845
+ ...((fromDate || toDate)
2846
+ ? {
2847
+ posted_date: {
2848
+ ...(fromDate ? { gte: fromDate } : {}),
2849
+ ...(toDate ? { lte: toDate } : {}),
2850
+ },
2851
+ }
2852
+ : {}),
2776
2853
  },
2777
2854
  select: {
2778
2855
  external_id: true,
@@ -2788,7 +2865,12 @@ export class FinanceService {
2788
2865
  );
2789
2866
 
2790
2867
  if (transferKeys.length === 0) {
2791
- return [];
2868
+ return {
2869
+ ...this.paginateCollection([], paginationParams),
2870
+ summary: {
2871
+ totalTransferido: 0,
2872
+ },
2873
+ };
2792
2874
  }
2793
2875
  }
2794
2876
 
@@ -2805,6 +2887,14 @@ export class FinanceService {
2805
2887
  startsWith: 'transfer:',
2806
2888
  },
2807
2889
  }),
2890
+ ...((fromDate || toDate)
2891
+ ? {
2892
+ posted_date: {
2893
+ ...(fromDate ? { gte: fromDate } : {}),
2894
+ ...(toDate ? { lte: toDate } : {}),
2895
+ },
2896
+ }
2897
+ : {}),
2808
2898
  },
2809
2899
  select: {
2810
2900
  id: true,
@@ -2844,20 +2934,28 @@ export class FinanceService {
2844
2934
  contaOrigemId: String(sourceLine.bank_account_id),
2845
2935
  contaDestinoId: String(destinationLine.bank_account_id),
2846
2936
  data: sourceLine.posted_date.toISOString(),
2847
- valor: this.fromCents(Math.abs(sourceLine.amount_cents)),
2937
+ valor: Math.abs(this.fromCents(sourceLine.amount_cents)),
2848
2938
  descricao: sourceLine.description || destinationLine.description || '',
2849
2939
  };
2850
2940
  })
2851
2941
  .filter(Boolean);
2852
2942
 
2853
- return transfers;
2943
+ return {
2944
+ ...this.paginateCollection(transfers, paginationParams),
2945
+ summary: {
2946
+ totalTransferido: transfers.reduce(
2947
+ (acc, transfer) => acc + Number(transfer?.valor || 0),
2948
+ 0,
2949
+ ),
2950
+ },
2951
+ };
2854
2952
  }
2855
2953
 
2856
2954
  async createTransfer(data: CreateTransferDto, userId?: number) {
2857
2955
  const sourceAccountId = Number(data.source_account_id);
2858
2956
  const destinationAccountId = Number(data.destination_account_id);
2859
2957
  const amount = Number(data.amount);
2860
- const postedDate = new Date(data.date);
2958
+ const postedDate = this.parseLocalDate(data.date);
2861
2959
 
2862
2960
  if (
2863
2961
  Number.isNaN(sourceAccountId) ||
@@ -3182,8 +3280,18 @@ export class FinanceService {
3182
3280
  };
3183
3281
  }
3184
3282
 
3185
- async listBankStatements(bankAccountId?: number, search?: string) {
3283
+ async listBankStatements(
3284
+ paginationParams: PaginationDTO,
3285
+ bankAccountId?: number,
3286
+ search?: string,
3287
+ filters?: {
3288
+ from?: string;
3289
+ to?: string;
3290
+ },
3291
+ ) {
3186
3292
  const trimmedSearch = search?.trim();
3293
+ const fromDate = this.parseFilterDate(filters?.from);
3294
+ const toDate = this.parseFilterDate(filters?.to, true);
3187
3295
 
3188
3296
  const statements = await this.prisma.bank_statement_line.findMany({
3189
3297
  where: {
@@ -3196,6 +3304,14 @@ export class FinanceService {
3196
3304
  },
3197
3305
  }
3198
3306
  : {}),
3307
+ ...((fromDate || toDate)
3308
+ ? {
3309
+ posted_date: {
3310
+ ...(fromDate ? { gte: fromDate } : {}),
3311
+ ...(toDate ? { lte: toDate } : {}),
3312
+ },
3313
+ }
3314
+ : {}),
3199
3315
  },
3200
3316
  include: {
3201
3317
  bank_account: {
@@ -3222,7 +3338,7 @@ export class FinanceService {
3222
3338
  orderBy: [{ posted_date: 'desc' }, { id: 'desc' }],
3223
3339
  });
3224
3340
 
3225
- return statements.map((statement) => ({
3341
+ const mappedStatements = statements.map((statement) => ({
3226
3342
  id: String(statement.id),
3227
3343
  contaBancariaId: String(statement.bank_account_id),
3228
3344
  data: statement.posted_date.toISOString(),
@@ -3230,6 +3346,13 @@ export class FinanceService {
3230
3346
  valor: this.fromCents(statement.amount_cents),
3231
3347
  tipo: statement.amount_cents >= 0 ? 'entrada' : 'saida',
3232
3348
  statusConciliacao: this.mapStatementStatusToPt(statement.status),
3349
+ isTransfer: statement.external_id?.startsWith('transfer:') || false,
3350
+ canEdit:
3351
+ !statement.external_id?.startsWith('transfer:') &&
3352
+ statement.bank_reconciliation.length === 0,
3353
+ canDelete:
3354
+ !statement.external_id?.startsWith('transfer:') &&
3355
+ statement.bank_reconciliation.length === 0,
3233
3356
  reconciliationId: statement.bank_reconciliation[0]?.id
3234
3357
  ? String(statement.bank_reconciliation[0].id)
3235
3358
  : null,
@@ -3237,6 +3360,18 @@ export class FinanceService {
3237
3360
  ? String(statement.bank_reconciliation[0].settlement_id)
3238
3361
  : null,
3239
3362
  }));
3363
+
3364
+ return {
3365
+ ...this.paginateCollection(mappedStatements, paginationParams),
3366
+ summary: {
3367
+ totalEntradas: mappedStatements
3368
+ .filter((statement) => statement.tipo === 'entrada')
3369
+ .reduce((acc, statement) => acc + Number(statement.valor || 0), 0),
3370
+ totalSaidas: mappedStatements
3371
+ .filter((statement) => statement.tipo === 'saida')
3372
+ .reduce((acc, statement) => acc + Number(statement.valor || 0), 0),
3373
+ },
3374
+ };
3240
3375
  }
3241
3376
 
3242
3377
  async createBankReconciliation(
@@ -3322,7 +3457,7 @@ export class FinanceService {
3322
3457
  title.id,
3323
3458
  {
3324
3459
  installment_id: installment.id,
3325
- amount: this.fromCents(Math.abs(statementLine.amount_cents)),
3460
+ amount: Math.abs(this.fromCents(statementLine.amount_cents)),
3326
3461
  settled_at: statementLine.posted_date.toISOString(),
3327
3462
  bank_account_id: statementLine.bank_account_id,
3328
3463
  bank_statement_line_id: statementLine.id,
@@ -3379,7 +3514,7 @@ export class FinanceService {
3379
3514
  const receivableAmounts = new Set<number>();
3380
3515
 
3381
3516
  for (const installment of openInstallments) {
3382
- const amount = Math.abs(installment.open_amount_cents);
3517
+ const amount = Number(installment.open_amount_cents);
3383
3518
  if (installment.financial_title.title_type === 'payable') {
3384
3519
  payableAmounts.add(amount);
3385
3520
  } else if (installment.financial_title.title_type === 'receivable') {
@@ -3391,8 +3526,8 @@ export class FinanceService {
3391
3526
  let differenceCents = 0;
3392
3527
 
3393
3528
  for (const statement of pendingStatements) {
3394
- const normalizedAmount = Math.abs(statement.amount_cents);
3395
- differenceCents += statement.amount_cents;
3529
+ const normalizedAmount = Math.abs(Number(statement.amount_cents));
3530
+ differenceCents += Number(statement.amount_cents);
3396
3531
 
3397
3532
  const hasPossibleMatch =
3398
3533
  statement.amount_cents < 0
@@ -3410,8 +3545,28 @@ export class FinanceService {
3410
3545
  };
3411
3546
  }
3412
3547
 
3413
- async exportBankStatementsCsv(bankAccountId: number, search?: string) {
3414
- const statements = await this.listBankStatements(bankAccountId, search);
3548
+ async exportBankStatementsCsv(
3549
+ bankAccountId: number,
3550
+ search?: string,
3551
+ filters?: {
3552
+ from?: string;
3553
+ to?: string;
3554
+ },
3555
+ ) {
3556
+ const statementsResult = await this.listBankStatements(
3557
+ {
3558
+ page: 1,
3559
+ pageSize: 100000,
3560
+ search: undefined,
3561
+ sortField: undefined,
3562
+ sortOrder: undefined,
3563
+ fields: undefined,
3564
+ },
3565
+ bankAccountId,
3566
+ search,
3567
+ filters,
3568
+ );
3569
+ const statements = statementsResult.data;
3415
3570
 
3416
3571
  const headers = [
3417
3572
  'id',
@@ -3454,7 +3609,7 @@ export class FinanceService {
3454
3609
  ) {
3455
3610
  const bankAccountId = Number(data.bank_account_id);
3456
3611
  const amount = Number(data.amount);
3457
- const postedAt = data.date ? new Date(data.date) : new Date();
3612
+ const postedAt = data.date ? this.parseLocalDate(data.date) : new Date();
3458
3613
 
3459
3614
  if (Number.isNaN(bankAccountId) || bankAccountId <= 0) {
3460
3615
  throw new BadRequestException('bank_account_id is required');
@@ -3527,6 +3682,264 @@ export class FinanceService {
3527
3682
  };
3528
3683
  }
3529
3684
 
3685
+ async createBankStatementEntry(
3686
+ data: CreateBankStatementEntryDto,
3687
+ userId?: number,
3688
+ ) {
3689
+ const bankAccountId = Number(data.bank_account_id);
3690
+ const amount = Number(data.amount);
3691
+ const postedDate = new Date(`${data.date}T00:00:00`);
3692
+
3693
+ if (Number.isNaN(bankAccountId) || bankAccountId <= 0) {
3694
+ throw new BadRequestException('bank_account_id is required');
3695
+ }
3696
+
3697
+ if (Number.isNaN(amount) || amount <= 0) {
3698
+ throw new BadRequestException('amount must be greater than zero');
3699
+ }
3700
+
3701
+ if (Number.isNaN(postedDate.getTime())) {
3702
+ throw new BadRequestException('Invalid statement date');
3703
+ }
3704
+
3705
+ const description = data.description?.trim();
3706
+ if (!description) {
3707
+ throw new BadRequestException('description is required');
3708
+ }
3709
+
3710
+ const bankAccount = await this.prisma.bank_account.findUnique({
3711
+ where: { id: bankAccountId },
3712
+ select: { id: true },
3713
+ });
3714
+
3715
+ if (!bankAccount) {
3716
+ throw new NotFoundException('Bank account not found');
3717
+ }
3718
+
3719
+ const signedAmountCents =
3720
+ data.type === 'saida'
3721
+ ? -Math.abs(this.toCents(amount))
3722
+ : Math.abs(this.toCents(amount));
3723
+
3724
+ const created = await this.prisma.$transaction(async (tx) => {
3725
+ await this.assertDateNotInClosedPeriod(
3726
+ tx,
3727
+ postedDate,
3728
+ 'create bank statement entry',
3729
+ );
3730
+
3731
+ const reference = `manual-entry:${bankAccountId}:${Date.now()}`;
3732
+ const statement = await tx.bank_statement.create({
3733
+ data: {
3734
+ bank_account_id: bankAccountId,
3735
+ source_type: 'manual',
3736
+ imported_at: postedDate,
3737
+ imported_by_user_id: userId,
3738
+ idempotency_key: reference,
3739
+ period_start: postedDate,
3740
+ period_end: postedDate,
3741
+ },
3742
+ });
3743
+
3744
+ return tx.bank_statement_line.create({
3745
+ data: {
3746
+ bank_statement_id: statement.id,
3747
+ bank_account_id: bankAccountId,
3748
+ posted_date: postedDate,
3749
+ amount_cents: signedAmountCents,
3750
+ description,
3751
+ status: 'pending',
3752
+ dedupe_key: `${reference}:${Math.round(Math.random() * 1_000_000)}`,
3753
+ },
3754
+ });
3755
+ });
3756
+
3757
+ return {
3758
+ id: String(created.id),
3759
+ contaBancariaId: String(created.bank_account_id),
3760
+ data: created.posted_date.toISOString(),
3761
+ descricao: created.description,
3762
+ valor: this.fromCents(created.amount_cents),
3763
+ tipo: created.amount_cents >= 0 ? 'entrada' : 'saida',
3764
+ statusConciliacao: this.mapStatementStatusToPt(created.status),
3765
+ isTransfer: false,
3766
+ canEdit: true,
3767
+ canDelete: true,
3768
+ reconciliationId: null,
3769
+ settlementId: null,
3770
+ };
3771
+ }
3772
+
3773
+ async updateBankStatementEntry(
3774
+ id: number,
3775
+ data: UpdateBankStatementEntryDto,
3776
+ userId?: number,
3777
+ ) {
3778
+ if (data.amount !== undefined) {
3779
+ const amount = Number(data.amount);
3780
+ if (Number.isNaN(amount) || amount <= 0) {
3781
+ throw new BadRequestException('amount must be greater than zero');
3782
+ }
3783
+ }
3784
+
3785
+ if (data.amount === undefined && data.description === undefined && data.date === undefined) {
3786
+ throw new BadRequestException('At least one field must be provided for update');
3787
+ }
3788
+
3789
+ const updated = await this.prisma.$transaction(async (tx) => {
3790
+ const statementLine = await tx.bank_statement_line.findUnique({
3791
+ where: { id },
3792
+ include: {
3793
+ bank_reconciliation: {
3794
+ where: {
3795
+ status: {
3796
+ in: ['pending', 'reconciled', 'adjusted'],
3797
+ },
3798
+ },
3799
+ select: {
3800
+ id: true,
3801
+ },
3802
+ take: 1,
3803
+ },
3804
+ },
3805
+ });
3806
+
3807
+ if (!statementLine) {
3808
+ throw new NotFoundException('Bank statement line not found');
3809
+ }
3810
+
3811
+ if (statementLine.external_id?.startsWith('transfer:')) {
3812
+ throw new ConflictException('Transfer movements cannot be edited');
3813
+ }
3814
+
3815
+ if (statementLine.bank_reconciliation.length > 0) {
3816
+ throw new ConflictException('Reconciled movements cannot be edited');
3817
+ }
3818
+
3819
+ const targetDate = data.date ? this.parseLocalDate(data.date) : statementLine.posted_date;
3820
+ await this.assertDateNotInClosedPeriod(
3821
+ tx,
3822
+ targetDate,
3823
+ 'update bank statement entry',
3824
+ );
3825
+
3826
+ const updateData: Record<string, unknown> = {};
3827
+
3828
+ if (data.amount !== undefined) {
3829
+ const amount = Number(data.amount);
3830
+ updateData.amount_cents =
3831
+ statementLine.amount_cents < 0
3832
+ ? -Math.abs(this.toCents(amount))
3833
+ : Math.abs(this.toCents(amount));
3834
+ }
3835
+
3836
+ if (data.description !== undefined) {
3837
+ updateData.description = data.description.trim();
3838
+ }
3839
+
3840
+ if (data.date !== undefined) {
3841
+ updateData.posted_date = this.parseLocalDate(data.date);
3842
+ }
3843
+
3844
+ const result = await tx.bank_statement_line.update({
3845
+ where: { id },
3846
+ data: updateData,
3847
+ });
3848
+
3849
+ await this.createAuditLog(tx, {
3850
+ action: 'UPDATE_BANK_STATEMENT_LINE',
3851
+ entityTable: 'bank_statement_line',
3852
+ entityId: String(result.id),
3853
+ actorUserId: userId,
3854
+ summary: `Updated bank statement line ${result.id}`,
3855
+ });
3856
+
3857
+ return result;
3858
+ });
3859
+
3860
+ return {
3861
+ id: String(updated.id),
3862
+ contaBancariaId: String(updated.bank_account_id),
3863
+ data: updated.posted_date.toISOString(),
3864
+ descricao: updated.description,
3865
+ valor: this.fromCents(updated.amount_cents),
3866
+ tipo: updated.amount_cents >= 0 ? 'entrada' : 'saida',
3867
+ statusConciliacao: this.mapStatementStatusToPt(updated.status),
3868
+ isTransfer: false,
3869
+ canEdit: true,
3870
+ canDelete: true,
3871
+ reconciliationId: null,
3872
+ settlementId: null,
3873
+ };
3874
+ }
3875
+
3876
+ async deleteBankStatementEntry(id: number, userId?: number) {
3877
+ return this.prisma.$transaction(async (tx) => {
3878
+ const statementLine = await tx.bank_statement_line.findUnique({
3879
+ where: { id },
3880
+ include: {
3881
+ bank_reconciliation: {
3882
+ where: {
3883
+ status: {
3884
+ in: ['pending', 'reconciled', 'adjusted'],
3885
+ },
3886
+ },
3887
+ select: {
3888
+ id: true,
3889
+ },
3890
+ take: 1,
3891
+ },
3892
+ },
3893
+ });
3894
+
3895
+ if (!statementLine) {
3896
+ throw new NotFoundException('Bank statement line not found');
3897
+ }
3898
+
3899
+ if (statementLine.external_id?.startsWith('transfer:')) {
3900
+ throw new ConflictException('Transfer movements cannot be deleted');
3901
+ }
3902
+
3903
+ if (statementLine.bank_reconciliation.length > 0) {
3904
+ throw new ConflictException('Reconciled movements cannot be deleted');
3905
+ }
3906
+
3907
+ await this.assertDateNotInClosedPeriod(
3908
+ tx,
3909
+ statementLine.posted_date,
3910
+ 'delete bank statement entry',
3911
+ );
3912
+
3913
+ await tx.bank_statement_line.delete({
3914
+ where: { id },
3915
+ });
3916
+
3917
+ const remainingLines = await tx.bank_statement_line.count({
3918
+ where: {
3919
+ bank_statement_id: statementLine.bank_statement_id,
3920
+ },
3921
+ });
3922
+
3923
+ if (remainingLines === 0) {
3924
+ await tx.bank_statement.delete({
3925
+ where: {
3926
+ id: statementLine.bank_statement_id,
3927
+ },
3928
+ });
3929
+ }
3930
+
3931
+ await this.createAuditLog(tx, {
3932
+ action: 'DELETE_BANK_STATEMENT_LINE',
3933
+ entityTable: 'bank_statement_line',
3934
+ entityId: String(statementLine.id),
3935
+ actorUserId: userId,
3936
+ summary: `Deleted bank statement line ${statementLine.id}`,
3937
+ });
3938
+
3939
+ return { success: true };
3940
+ });
3941
+ }
3942
+
3530
3943
  async importBankStatements(
3531
3944
  bankAccountId: number,
3532
3945
  file: MulterFile,
@@ -4003,6 +4416,7 @@ export class FinanceService {
4003
4416
 
4004
4417
  const code = this.generateBankAccountCode(data.bank, data.account);
4005
4418
  const name = data.description?.trim() || data.bank;
4419
+ const startDate = this.parseFilterDate(data.start_date) || new Date();
4006
4420
 
4007
4421
  const createdAccount = await this.prisma.$transaction(async (tx) => {
4008
4422
  const account = await tx.bank_account.create({
@@ -4019,7 +4433,7 @@ export class FinanceService {
4019
4433
  });
4020
4434
 
4021
4435
  if (data.initial_balance && data.initial_balance > 0) {
4022
- const postedDate = new Date();
4436
+ const postedDate = startDate;
4023
4437
  await this.assertDateNotInClosedPeriod(
4024
4438
  tx,
4025
4439
  postedDate,
@@ -4058,6 +4472,8 @@ export class FinanceService {
4058
4472
  select: {
4059
4473
  amount_cents: true,
4060
4474
  status: true,
4475
+ posted_date: true,
4476
+ description: true,
4061
4477
  },
4062
4478
  },
4063
4479
  },
@@ -4099,8 +4515,8 @@ export class FinanceService {
4099
4515
  }
4100
4516
 
4101
4517
  async createPeriodClose(data: CreatePeriodCloseDto, userId?: number) {
4102
- const periodStart = new Date(data.period_start);
4103
- const periodEnd = new Date(data.period_end);
4518
+ const periodStart = this.parseLocalDate(data.period_start);
4519
+ const periodEnd = this.parseLocalDate(data.period_end);
4104
4520
 
4105
4521
  if (
4106
4522
  Number.isNaN(periodStart.getTime()) ||
@@ -4176,6 +4592,8 @@ export class FinanceService {
4176
4592
  select: {
4177
4593
  amount_cents: true,
4178
4594
  status: true,
4595
+ posted_date: true,
4596
+ description: true,
4179
4597
  },
4180
4598
  },
4181
4599
  },
@@ -4346,9 +4764,15 @@ export class FinanceService {
4346
4764
  titleType: TitleType,
4347
4765
  paginationParams: PaginationDTO,
4348
4766
  status?: string,
4767
+ filters?: {
4768
+ from?: string;
4769
+ to?: string;
4770
+ },
4349
4771
  ) {
4350
4772
  const prismaStatus = this.mapStatusFromPt(status);
4351
4773
  const search = paginationParams?.search?.trim();
4774
+ const fromDate = this.parseFilterDate(filters?.from);
4775
+ const toDate = this.parseFilterDate(filters?.to, true);
4352
4776
  const where: any = {
4353
4777
  title_type: titleType,
4354
4778
  };
@@ -4376,6 +4800,17 @@ export class FinanceService {
4376
4800
  ];
4377
4801
  }
4378
4802
 
4803
+ if (fromDate || toDate) {
4804
+ where.financial_installment = {
4805
+ some: {
4806
+ due_date: {
4807
+ ...(fromDate ? { gte: fromDate } : {}),
4808
+ ...(toDate ? { lte: toDate } : {}),
4809
+ },
4810
+ },
4811
+ };
4812
+ }
4813
+
4379
4814
  const normalizedPaginationParams: PaginationDTO = {
4380
4815
  ...paginationParams,
4381
4816
  sortField: paginationParams?.sortField || 'created_at',
@@ -4495,8 +4930,8 @@ export class FinanceService {
4495
4930
  await this.assertDateNotInClosedPeriod(
4496
4931
  tx,
4497
4932
  data.competence_date
4498
- ? new Date(data.competence_date)
4499
- : new Date(installments[0].due_date),
4933
+ ? this.parseLocalDate(data.competence_date)
4934
+ : this.parseLocalDate(installments[0].due_date),
4500
4935
  'create title',
4501
4936
  );
4502
4937
 
@@ -4508,9 +4943,9 @@ export class FinanceService {
4508
4943
  document_number: data.document_number,
4509
4944
  description: data.description,
4510
4945
  competence_date: data.competence_date
4511
- ? new Date(data.competence_date)
4946
+ ? this.parseLocalDate(data.competence_date)
4512
4947
  : null,
4513
- issue_date: data.issue_date ? new Date(data.issue_date) : null,
4948
+ issue_date: data.issue_date ? this.parseLocalDate(data.issue_date) : null,
4514
4949
  total_amount_cents: this.toCents(data.total_amount),
4515
4950
  finance_category_id: data.finance_category_id,
4516
4951
  created_by_user_id: userId,
@@ -4536,15 +4971,15 @@ export class FinanceService {
4536
4971
  title_id: title.id,
4537
4972
  installment_number: installment.installment_number,
4538
4973
  competence_date: data.competence_date
4539
- ? new Date(data.competence_date)
4540
- : new Date(installment.due_date),
4541
- due_date: new Date(installment.due_date),
4974
+ ? this.parseLocalDate(data.competence_date)
4975
+ : this.parseLocalDate(installment.due_date),
4976
+ due_date: this.parseLocalDate(installment.due_date),
4542
4977
  amount_cents: amountCents,
4543
4978
  open_amount_cents: amountCents,
4544
4979
  status: this.resolveInstallmentStatus(
4545
4980
  amountCents,
4546
4981
  amountCents,
4547
- new Date(installment.due_date),
4982
+ this.parseLocalDate(installment.due_date),
4548
4983
  ),
4549
4984
  notes: data.description,
4550
4985
  },
@@ -4684,7 +5119,7 @@ export class FinanceService {
4684
5119
  await this.assertDateNotInClosedPeriod(
4685
5120
  tx,
4686
5121
  data.competence_date
4687
- ? new Date(data.competence_date)
5122
+ ? this.parseLocalDate(data.competence_date)
4688
5123
  : new Date(installments[0].due_date),
4689
5124
  'update title',
4690
5125
  );
@@ -4717,9 +5152,9 @@ export class FinanceService {
4717
5152
  document_number: data.document_number,
4718
5153
  description: data.description,
4719
5154
  competence_date: data.competence_date
4720
- ? new Date(data.competence_date)
5155
+ ? this.parseLocalDate(data.competence_date)
4721
5156
  : null,
4722
- issue_date: data.issue_date ? new Date(data.issue_date) : null,
5157
+ issue_date: data.issue_date ? this.parseLocalDate(data.issue_date) : null,
4723
5158
  total_amount_cents: this.toCents(data.total_amount),
4724
5159
  finance_category_id: data.finance_category_id,
4725
5160
  },
@@ -4757,15 +5192,15 @@ export class FinanceService {
4757
5192
  title_id: title.id,
4758
5193
  installment_number: installment.installment_number,
4759
5194
  competence_date: data.competence_date
4760
- ? new Date(data.competence_date)
4761
- : new Date(installment.due_date),
4762
- due_date: new Date(installment.due_date),
5195
+ ? this.parseLocalDate(data.competence_date)
5196
+ : this.parseLocalDate(installment.due_date),
5197
+ due_date: this.parseLocalDate(installment.due_date),
4763
5198
  amount_cents: amountCents,
4764
5199
  open_amount_cents: amountCents,
4765
5200
  status: this.resolveInstallmentStatus(
4766
5201
  amountCents,
4767
5202
  amountCents,
4768
- new Date(installment.due_date),
5203
+ this.parseLocalDate(installment.due_date),
4769
5204
  ),
4770
5205
  notes: data.description,
4771
5206
  },
@@ -5292,7 +5727,7 @@ export class FinanceService {
5292
5727
  );
5293
5728
  }
5294
5729
 
5295
- if (Math.abs(statementLine.amount_cents) !== amountCents) {
5730
+ if (Math.abs(Number(statementLine.amount_cents)) !== amountCents) {
5296
5731
  throw new ConflictException(
5297
5732
  'Bank statement amount and settlement amount must match',
5298
5733
  );
@@ -5398,11 +5833,11 @@ export class FinanceService {
5398
5833
  throw new NotFoundException('Installment not found');
5399
5834
  }
5400
5835
 
5401
- const nextInstallmentStatus = this.resolveInstallmentStatus(
5402
- updatedInstallment.amount_cents,
5403
- updatedInstallment.open_amount_cents,
5404
- updatedInstallment.due_date,
5405
- );
5836
+ const nextInstallmentStatus = this.resolveInstallmentStatus(
5837
+ Number(updatedInstallment.amount_cents),
5838
+ Number(updatedInstallment.open_amount_cents),
5839
+ updatedInstallment.due_date,
5840
+ );
5406
5841
 
5407
5842
  if (updatedInstallment.status !== nextInstallmentStatus) {
5408
5843
  await tx.financial_installment.update({
@@ -5930,6 +6365,8 @@ export class FinanceService {
5930
6365
  select: {
5931
6366
  amount_cents: true,
5932
6367
  status: true,
6368
+ posted_date: true,
6369
+ description: true,
5933
6370
  },
5934
6371
  },
5935
6372
  },
@@ -6334,6 +6771,31 @@ export class FinanceService {
6334
6771
  return statusMap[status] || 'aberto';
6335
6772
  }
6336
6773
 
6774
+ private paginateCollection<T>(items: T[], paginationParams?: PaginationDTO) {
6775
+ const requestedPage = Number(paginationParams?.page || 1);
6776
+ const requestedPageSize = Number(paginationParams?.pageSize || 10);
6777
+ const page = Number.isNaN(requestedPage) || requestedPage < 1
6778
+ ? 1
6779
+ : requestedPage;
6780
+ const pageSize = Number.isNaN(requestedPageSize) || requestedPageSize < 1
6781
+ ? 10
6782
+ : requestedPageSize;
6783
+ const total = items.length;
6784
+ const lastPage = Math.max(1, Math.ceil(total / pageSize));
6785
+ const currentPage = Math.min(page, lastPage);
6786
+ const start = (currentPage - 1) * pageSize;
6787
+
6788
+ return {
6789
+ data: items.slice(start, start + pageSize),
6790
+ total,
6791
+ page: currentPage,
6792
+ pageSize,
6793
+ prev: currentPage > 1 ? currentPage - 1 : null,
6794
+ next: currentPage < lastPage ? currentPage + 1 : null,
6795
+ lastPage,
6796
+ };
6797
+ }
6798
+
6337
6799
  private normalizeTagSlug(value?: string | null) {
6338
6800
  if (!value) {
6339
6801
  return '';
@@ -6430,18 +6892,25 @@ export class FinanceService {
6430
6892
 
6431
6893
  private mapBankAccountToFront(bankAccount: any) {
6432
6894
  const currentCents = (bankAccount.bank_statement_line || []).reduce(
6433
- (acc, line) => acc + line.amount_cents,
6895
+ (acc, line) => acc + Number(line.amount_cents),
6434
6896
  0,
6435
6897
  );
6436
6898
 
6437
6899
  const reconciledCents = (bankAccount.bank_statement_line || []).reduce(
6438
6900
  (acc, line) =>
6439
6901
  line.status === 'reconciled' || line.status === 'adjusted'
6440
- ? acc + line.amount_cents
6902
+ ? acc + Number(line.amount_cents)
6441
6903
  : acc,
6442
6904
  0,
6443
6905
  );
6444
6906
 
6907
+ const initialLine = (bankAccount.bank_statement_line || []).find(
6908
+ (line: any) => line.description === 'Saldo inicial',
6909
+ );
6910
+ const dataInicial = initialLine?.posted_date
6911
+ ? new Date(initialLine.posted_date).toISOString().slice(0, 10)
6912
+ : null;
6913
+
6445
6914
  return {
6446
6915
  id: String(bankAccount.id),
6447
6916
  codigo: bankAccount.code,
@@ -6454,6 +6923,7 @@ export class FinanceService {
6454
6923
  saldoAtual: this.fromCents(currentCents),
6455
6924
  saldoConciliado: this.fromCents(reconciledCents),
6456
6925
  ativo: bankAccount.status === 'active',
6926
+ dataInicial,
6457
6927
  };
6458
6928
  }
6459
6929
 
@@ -6796,6 +7266,18 @@ export class FinanceService {
6796
7266
  return Math.round(value * 100);
6797
7267
  }
6798
7268
 
7269
+ /**
7270
+ * Parses a date-only string (YYYY-MM-DD) as local noon to avoid
7271
+ * timezone shifts that would cause the date to appear as the previous day
7272
+ * in UTC-negative timezones.
7273
+ */
7274
+ private parseLocalDate(dateString: string): Date {
7275
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
7276
+ return new Date(dateString + 'T12:00:00.000Z');
7277
+ }
7278
+ return new Date(dateString);
7279
+ }
7280
+
6799
7281
  private fromCents(value: number | bigint | string | null | undefined) {
6800
7282
  if (value === null || value === undefined) {
6801
7283
  return 0;