@hed-hog/finance 0.0.318 → 0.0.321

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 (92) 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/create-currency.dto.d.ts +6 -0
  10. package/dist/dto/create-currency.dto.d.ts.map +1 -0
  11. package/dist/dto/create-currency.dto.js +37 -0
  12. package/dist/dto/create-currency.dto.js.map +1 -0
  13. package/dist/dto/update-bank-account.dto.d.ts +1 -0
  14. package/dist/dto/update-bank-account.dto.d.ts.map +1 -1
  15. package/dist/dto/update-bank-account.dto.js +7 -0
  16. package/dist/dto/update-bank-account.dto.js.map +1 -1
  17. package/dist/dto/update-bank-statement-entry.dto.d.ts +6 -0
  18. package/dist/dto/update-bank-statement-entry.dto.d.ts.map +1 -0
  19. package/dist/dto/update-bank-statement-entry.dto.js +42 -0
  20. package/dist/dto/update-bank-statement-entry.dto.js.map +1 -0
  21. package/dist/dto/update-currency.dto.d.ts +7 -0
  22. package/dist/dto/update-currency.dto.d.ts.map +1 -0
  23. package/dist/dto/update-currency.dto.js +47 -0
  24. package/dist/dto/update-currency.dto.js.map +1 -0
  25. package/dist/finance-bank-accounts.controller.d.ts +25 -13
  26. package/dist/finance-bank-accounts.controller.d.ts.map +1 -1
  27. package/dist/finance-bank-accounts.controller.js +5 -3
  28. package/dist/finance-bank-accounts.controller.js.map +1 -1
  29. package/dist/finance-currencies.controller.d.ts +36 -0
  30. package/dist/finance-currencies.controller.d.ts.map +1 -0
  31. package/dist/finance-currencies.controller.js +74 -0
  32. package/dist/finance-currencies.controller.js.map +1 -0
  33. package/dist/finance-data.controller.d.ts +4 -0
  34. package/dist/finance-data.controller.d.ts.map +1 -1
  35. package/dist/finance-installments.controller.d.ts +3 -2
  36. package/dist/finance-installments.controller.d.ts.map +1 -1
  37. package/dist/finance-installments.controller.js +10 -6
  38. package/dist/finance-installments.controller.js.map +1 -1
  39. package/dist/finance-statements.controller.d.ts +61 -12
  40. package/dist/finance-statements.controller.d.ts.map +1 -1
  41. package/dist/finance-statements.controller.js +50 -8
  42. package/dist/finance-statements.controller.js.map +1 -1
  43. package/dist/finance-transfers.controller.d.ts +13 -8
  44. package/dist/finance-transfers.controller.d.ts.map +1 -1
  45. package/dist/finance-transfers.controller.js +11 -5
  46. package/dist/finance-transfers.controller.js.map +1 -1
  47. package/dist/finance.module.d.ts.map +1 -1
  48. package/dist/finance.module.js +2 -0
  49. package/dist/finance.module.js.map +1 -1
  50. package/dist/finance.service.d.ts +153 -35
  51. package/dist/finance.service.d.ts.map +1 -1
  52. package/dist/finance.service.js +468 -55
  53. package/dist/finance.service.js.map +1 -1
  54. package/hedhog/data/currency.yaml +14 -0
  55. package/hedhog/data/menu.yaml +16 -0
  56. package/hedhog/data/role.yaml +9 -1
  57. package/hedhog/data/route.yaml +78 -0
  58. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +87 -72
  59. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +53 -25
  60. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +8 -0
  61. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +60 -24
  62. package/hedhog/frontend/app/administration/currencies/page.tsx.ejs +490 -0
  63. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +243 -65
  64. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +25 -3
  65. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +732 -61
  66. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +101 -15
  67. package/hedhog/frontend/messages/en.json +58 -0
  68. package/hedhog/frontend/messages/pt.json +58 -0
  69. package/hedhog/table/bank_account.yaml +8 -0
  70. package/hedhog/table/bank_statement_line.yaml +1 -1
  71. package/hedhog/table/cashflow_projection.yaml +1 -1
  72. package/hedhog/table/currency.yaml +21 -0
  73. package/hedhog/table/financial_installment.yaml +2 -2
  74. package/hedhog/table/financial_title.yaml +1 -1
  75. package/hedhog/table/installment_allocation.yaml +1 -1
  76. package/hedhog/table/receivable_schedule.yaml +1 -1
  77. package/hedhog/table/settlement.yaml +1 -1
  78. package/hedhog/table/settlement_allocation.yaml +5 -5
  79. package/package.json +6 -6
  80. package/src/dto/create-bank-account.dto.ts +18 -1
  81. package/src/dto/create-bank-statement-entry.dto.ts +50 -0
  82. package/src/dto/create-currency.dto.ts +21 -0
  83. package/src/dto/update-bank-account.dto.ts +11 -1
  84. package/src/dto/update-bank-statement-entry.dto.ts +31 -0
  85. package/src/dto/update-currency.dto.ts +31 -0
  86. package/src/finance-bank-accounts.controller.ts +3 -2
  87. package/src/finance-currencies.controller.ts +44 -0
  88. package/src/finance-installments.controller.ts +9 -3
  89. package/src/finance-statements.controller.ts +40 -0
  90. package/src/finance-transfers.controller.ts +7 -1
  91. package/src/finance.module.ts +2 -0
  92. package/src/finance.service.ts +633 -55
@@ -20,7 +20,9 @@ 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';
25
+ import { CreateCurrencyDto } from './dto/create-currency.dto';
24
26
  import { CreateFinanceCategoryDto } from './dto/create-finance-category.dto';
25
27
  import { CreateFinanceTagDto } from './dto/create-finance-tag.dto';
26
28
  import { CreateFinancialTitleDto } from './dto/create-financial-title.dto';
@@ -33,7 +35,9 @@ import { ReverseSettlementDto } from './dto/reverse-settlement.dto';
33
35
  import { SendCollectionDto } from './dto/send-collection.dto';
34
36
  import { SettleInstallmentDto } from './dto/settle-installment.dto';
35
37
  import { UpdateBankAccountDto } from './dto/update-bank-account.dto';
38
+ import { UpdateBankStatementEntryDto } from './dto/update-bank-statement-entry.dto';
36
39
  import { UpdateCostCenterDto } from './dto/update-cost-center.dto';
40
+ import { UpdateCurrencyDto } from './dto/update-currency.dto';
37
41
  import { UpdateFinanceCategoryDto } from './dto/update-finance-category.dto';
38
42
 
39
43
  type TitleType = 'payable' | 'receivable';
@@ -654,6 +658,28 @@ export class FinanceService {
654
658
  return dt.toISOString().slice(0, 10);
655
659
  }
656
660
 
661
+ private parseFilterDate(value?: string, endOfDay = false): Date | undefined {
662
+ if (!value) {
663
+ return undefined;
664
+ }
665
+
666
+ const raw = String(value).trim();
667
+ if (!raw) {
668
+ return undefined;
669
+ }
670
+
671
+ const normalized = /^\d{4}-\d{2}-\d{2}$/.test(raw)
672
+ ? `${raw}T${endOfDay ? '23:59:59.999' : '00:00:00.000'}`
673
+ : raw;
674
+ const parsed = new Date(normalized);
675
+
676
+ if (Number.isNaN(parsed.getTime())) {
677
+ return undefined;
678
+ }
679
+
680
+ return parsed;
681
+ }
682
+
657
683
  private normalizeMonth(value: any): string {
658
684
  if (!value) return '';
659
685
  const raw = String(value).trim();
@@ -695,6 +721,14 @@ export class FinanceService {
695
721
  card: 'cartao',
696
722
  credito: 'cartao',
697
723
  debito: 'cartao',
724
+ 'debito automatico': 'debito_automatico',
725
+ debito_automatico: 'debito_automatico',
726
+ 'debito-automatico': 'debito_automatico',
727
+ 'débito automático': 'debito_automatico',
728
+ 'debito em conta': 'debito_em_conta',
729
+ debito_em_conta: 'debito_em_conta',
730
+ 'debito-em-conta': 'debito_em_conta',
731
+ 'débito em conta': 'debito_em_conta',
698
732
  dinheiro: 'dinheiro',
699
733
  cash: 'dinheiro',
700
734
  cheque: 'cheque',
@@ -774,7 +808,14 @@ export class FinanceService {
774
808
  this.loadCategories(),
775
809
  this.loadCostCenters(),
776
810
  this.loadBankAccounts(),
777
- this.listBankStatements(),
811
+ this.listBankStatements({
812
+ page: 1,
813
+ pageSize: 1000,
814
+ search: undefined,
815
+ sortField: undefined,
816
+ sortOrder: undefined,
817
+ fields: undefined,
818
+ }),
778
819
  this.getAccountsReceivableCollectionsDefault(),
779
820
  this.loadTags(),
780
821
  this.loadAuditLogs(),
@@ -795,7 +836,7 @@ export class FinanceService {
795
836
  bankAccountsResult.status === 'fulfilled' ? bankAccountsResult.value : [];
796
837
  const bankStatements =
797
838
  bankStatementsResult.status === 'fulfilled'
798
- ? bankStatementsResult.value
839
+ ? bankStatementsResult.value.data
799
840
  : [];
800
841
  const collectionsDefault =
801
842
  collectionsDefaultResult.status === 'fulfilled'
@@ -2114,7 +2155,7 @@ export class FinanceService {
2114
2155
  throw new NotFoundException('Person not found');
2115
2156
  }
2116
2157
 
2117
- const firstDueDate = new Date(data.first_due_date);
2158
+ const firstDueDate = this.parseLocalDate(data.first_due_date);
2118
2159
 
2119
2160
  if (Number.isNaN(firstDueDate.getTime())) {
2120
2161
  throw new BadRequestException('Invalid first due date');
@@ -2333,15 +2374,23 @@ export class FinanceService {
2333
2374
  async listAccountsPayableInstallments(
2334
2375
  paginationParams: PaginationDTO,
2335
2376
  status?: string,
2377
+ filters?: {
2378
+ from?: string;
2379
+ to?: string;
2380
+ },
2336
2381
  ) {
2337
- return this.listTitles('payable', paginationParams, status);
2382
+ return this.listTitles('payable', paginationParams, status, filters);
2338
2383
  }
2339
2384
 
2340
2385
  async listAccountsReceivableInstallments(
2341
2386
  paginationParams: PaginationDTO,
2342
2387
  status?: string,
2388
+ filters?: {
2389
+ from?: string;
2390
+ to?: string;
2391
+ },
2343
2392
  ) {
2344
- return this.listTitles('receivable', paginationParams, status);
2393
+ return this.listTitles('receivable', paginationParams, status, filters);
2345
2394
  }
2346
2395
 
2347
2396
  async getAccountsPayableInstallment(id: number, locale: string) {
@@ -2727,27 +2776,49 @@ export class FinanceService {
2727
2776
  return this.updateTitleTags(id, 'receivable', tagIds, locale);
2728
2777
  }
2729
2778
 
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,
2779
+ async listBankAccounts(paginationParams?: PaginationDTO) {
2780
+ const paginated = await this.paginationService.paginate(
2781
+ this.prisma.bank_account,
2782
+ {
2783
+ ...paginationParams,
2784
+ sortField: paginationParams?.sortField || 'code',
2785
+ sortOrder: paginationParams?.sortOrder || PageOrderDirection.Asc,
2786
+ },
2787
+ {
2788
+ include: {
2789
+ bank_statement_line: {
2790
+ select: {
2791
+ amount_cents: true,
2792
+ status: true,
2793
+ posted_date: true,
2794
+ description: true,
2795
+ },
2737
2796
  },
2738
2797
  },
2798
+ orderBy: [{ code: 'asc' }, { name: 'asc' }],
2739
2799
  },
2740
- orderBy: [{ code: 'asc' }, { name: 'asc' }],
2741
- });
2800
+ );
2742
2801
 
2743
- return bankAccounts.map((bankAccount) => this.mapBankAccountToFront(bankAccount));
2802
+ return {
2803
+ ...paginated,
2804
+ data: (paginated.data || []).map((bankAccount) =>
2805
+ this.mapBankAccountToFront(bankAccount),
2806
+ ),
2807
+ };
2744
2808
  }
2745
2809
 
2746
- async listTransfers(filters?: {
2747
- search?: string;
2748
- bank_account_id?: string;
2749
- }) {
2810
+ async listTransfers(
2811
+ paginationParams: PaginationDTO,
2812
+ filters?: {
2813
+ search?: string;
2814
+ bank_account_id?: string;
2815
+ from?: string;
2816
+ to?: string;
2817
+ },
2818
+ ) {
2750
2819
  const search = filters?.search?.trim();
2820
+ const fromDate = this.parseFilterDate(filters?.from);
2821
+ const toDate = this.parseFilterDate(filters?.to, true);
2751
2822
  const parsedBankAccountId = filters?.bank_account_id
2752
2823
  ? Number.parseInt(filters.bank_account_id, 10)
2753
2824
  : undefined;
@@ -2773,6 +2844,14 @@ export class FinanceService {
2773
2844
  }
2774
2845
  : {}),
2775
2846
  ...(bankAccountId ? { bank_account_id: bankAccountId } : {}),
2847
+ ...((fromDate || toDate)
2848
+ ? {
2849
+ posted_date: {
2850
+ ...(fromDate ? { gte: fromDate } : {}),
2851
+ ...(toDate ? { lte: toDate } : {}),
2852
+ },
2853
+ }
2854
+ : {}),
2776
2855
  },
2777
2856
  select: {
2778
2857
  external_id: true,
@@ -2788,7 +2867,12 @@ export class FinanceService {
2788
2867
  );
2789
2868
 
2790
2869
  if (transferKeys.length === 0) {
2791
- return [];
2870
+ return {
2871
+ ...this.paginateCollection([], paginationParams),
2872
+ summary: {
2873
+ totalTransferido: 0,
2874
+ },
2875
+ };
2792
2876
  }
2793
2877
  }
2794
2878
 
@@ -2805,6 +2889,14 @@ export class FinanceService {
2805
2889
  startsWith: 'transfer:',
2806
2890
  },
2807
2891
  }),
2892
+ ...((fromDate || toDate)
2893
+ ? {
2894
+ posted_date: {
2895
+ ...(fromDate ? { gte: fromDate } : {}),
2896
+ ...(toDate ? { lte: toDate } : {}),
2897
+ },
2898
+ }
2899
+ : {}),
2808
2900
  },
2809
2901
  select: {
2810
2902
  id: true,
@@ -2844,20 +2936,28 @@ export class FinanceService {
2844
2936
  contaOrigemId: String(sourceLine.bank_account_id),
2845
2937
  contaDestinoId: String(destinationLine.bank_account_id),
2846
2938
  data: sourceLine.posted_date.toISOString(),
2847
- valor: this.fromCents(Math.abs(sourceLine.amount_cents)),
2939
+ valor: Math.abs(this.fromCents(sourceLine.amount_cents)),
2848
2940
  descricao: sourceLine.description || destinationLine.description || '',
2849
2941
  };
2850
2942
  })
2851
2943
  .filter(Boolean);
2852
2944
 
2853
- return transfers;
2945
+ return {
2946
+ ...this.paginateCollection(transfers, paginationParams),
2947
+ summary: {
2948
+ totalTransferido: transfers.reduce(
2949
+ (acc, transfer) => acc + Number(transfer?.valor || 0),
2950
+ 0,
2951
+ ),
2952
+ },
2953
+ };
2854
2954
  }
2855
2955
 
2856
2956
  async createTransfer(data: CreateTransferDto, userId?: number) {
2857
2957
  const sourceAccountId = Number(data.source_account_id);
2858
2958
  const destinationAccountId = Number(data.destination_account_id);
2859
2959
  const amount = Number(data.amount);
2860
- const postedDate = new Date(data.date);
2960
+ const postedDate = this.parseLocalDate(data.date);
2861
2961
 
2862
2962
  if (
2863
2963
  Number.isNaN(sourceAccountId) ||
@@ -3182,8 +3282,18 @@ export class FinanceService {
3182
3282
  };
3183
3283
  }
3184
3284
 
3185
- async listBankStatements(bankAccountId?: number, search?: string) {
3285
+ async listBankStatements(
3286
+ paginationParams: PaginationDTO,
3287
+ bankAccountId?: number,
3288
+ search?: string,
3289
+ filters?: {
3290
+ from?: string;
3291
+ to?: string;
3292
+ },
3293
+ ) {
3186
3294
  const trimmedSearch = search?.trim();
3295
+ const fromDate = this.parseFilterDate(filters?.from);
3296
+ const toDate = this.parseFilterDate(filters?.to, true);
3187
3297
 
3188
3298
  const statements = await this.prisma.bank_statement_line.findMany({
3189
3299
  where: {
@@ -3196,6 +3306,14 @@ export class FinanceService {
3196
3306
  },
3197
3307
  }
3198
3308
  : {}),
3309
+ ...((fromDate || toDate)
3310
+ ? {
3311
+ posted_date: {
3312
+ ...(fromDate ? { gte: fromDate } : {}),
3313
+ ...(toDate ? { lte: toDate } : {}),
3314
+ },
3315
+ }
3316
+ : {}),
3199
3317
  },
3200
3318
  include: {
3201
3319
  bank_account: {
@@ -3222,7 +3340,7 @@ export class FinanceService {
3222
3340
  orderBy: [{ posted_date: 'desc' }, { id: 'desc' }],
3223
3341
  });
3224
3342
 
3225
- return statements.map((statement) => ({
3343
+ const mappedStatements = statements.map((statement) => ({
3226
3344
  id: String(statement.id),
3227
3345
  contaBancariaId: String(statement.bank_account_id),
3228
3346
  data: statement.posted_date.toISOString(),
@@ -3230,6 +3348,13 @@ export class FinanceService {
3230
3348
  valor: this.fromCents(statement.amount_cents),
3231
3349
  tipo: statement.amount_cents >= 0 ? 'entrada' : 'saida',
3232
3350
  statusConciliacao: this.mapStatementStatusToPt(statement.status),
3351
+ isTransfer: statement.external_id?.startsWith('transfer:') || false,
3352
+ canEdit:
3353
+ !statement.external_id?.startsWith('transfer:') &&
3354
+ statement.bank_reconciliation.length === 0,
3355
+ canDelete:
3356
+ !statement.external_id?.startsWith('transfer:') &&
3357
+ statement.bank_reconciliation.length === 0,
3233
3358
  reconciliationId: statement.bank_reconciliation[0]?.id
3234
3359
  ? String(statement.bank_reconciliation[0].id)
3235
3360
  : null,
@@ -3237,6 +3362,18 @@ export class FinanceService {
3237
3362
  ? String(statement.bank_reconciliation[0].settlement_id)
3238
3363
  : null,
3239
3364
  }));
3365
+
3366
+ return {
3367
+ ...this.paginateCollection(mappedStatements, paginationParams),
3368
+ summary: {
3369
+ totalEntradas: mappedStatements
3370
+ .filter((statement) => statement.tipo === 'entrada')
3371
+ .reduce((acc, statement) => acc + Number(statement.valor || 0), 0),
3372
+ totalSaidas: mappedStatements
3373
+ .filter((statement) => statement.tipo === 'saida')
3374
+ .reduce((acc, statement) => acc + Number(statement.valor || 0), 0),
3375
+ },
3376
+ };
3240
3377
  }
3241
3378
 
3242
3379
  async createBankReconciliation(
@@ -3322,7 +3459,7 @@ export class FinanceService {
3322
3459
  title.id,
3323
3460
  {
3324
3461
  installment_id: installment.id,
3325
- amount: this.fromCents(Math.abs(statementLine.amount_cents)),
3462
+ amount: Math.abs(this.fromCents(statementLine.amount_cents)),
3326
3463
  settled_at: statementLine.posted_date.toISOString(),
3327
3464
  bank_account_id: statementLine.bank_account_id,
3328
3465
  bank_statement_line_id: statementLine.id,
@@ -3379,7 +3516,7 @@ export class FinanceService {
3379
3516
  const receivableAmounts = new Set<number>();
3380
3517
 
3381
3518
  for (const installment of openInstallments) {
3382
- const amount = Math.abs(installment.open_amount_cents);
3519
+ const amount = Number(installment.open_amount_cents);
3383
3520
  if (installment.financial_title.title_type === 'payable') {
3384
3521
  payableAmounts.add(amount);
3385
3522
  } else if (installment.financial_title.title_type === 'receivable') {
@@ -3391,8 +3528,8 @@ export class FinanceService {
3391
3528
  let differenceCents = 0;
3392
3529
 
3393
3530
  for (const statement of pendingStatements) {
3394
- const normalizedAmount = Math.abs(statement.amount_cents);
3395
- differenceCents += statement.amount_cents;
3531
+ const normalizedAmount = Math.abs(Number(statement.amount_cents));
3532
+ differenceCents += Number(statement.amount_cents);
3396
3533
 
3397
3534
  const hasPossibleMatch =
3398
3535
  statement.amount_cents < 0
@@ -3410,8 +3547,28 @@ export class FinanceService {
3410
3547
  };
3411
3548
  }
3412
3549
 
3413
- async exportBankStatementsCsv(bankAccountId: number, search?: string) {
3414
- const statements = await this.listBankStatements(bankAccountId, search);
3550
+ async exportBankStatementsCsv(
3551
+ bankAccountId: number,
3552
+ search?: string,
3553
+ filters?: {
3554
+ from?: string;
3555
+ to?: string;
3556
+ },
3557
+ ) {
3558
+ const statementsResult = await this.listBankStatements(
3559
+ {
3560
+ page: 1,
3561
+ pageSize: 100000,
3562
+ search: undefined,
3563
+ sortField: undefined,
3564
+ sortOrder: undefined,
3565
+ fields: undefined,
3566
+ },
3567
+ bankAccountId,
3568
+ search,
3569
+ filters,
3570
+ );
3571
+ const statements = statementsResult.data;
3415
3572
 
3416
3573
  const headers = [
3417
3574
  'id',
@@ -3454,7 +3611,7 @@ export class FinanceService {
3454
3611
  ) {
3455
3612
  const bankAccountId = Number(data.bank_account_id);
3456
3613
  const amount = Number(data.amount);
3457
- const postedAt = data.date ? new Date(data.date) : new Date();
3614
+ const postedAt = data.date ? this.parseLocalDate(data.date) : new Date();
3458
3615
 
3459
3616
  if (Number.isNaN(bankAccountId) || bankAccountId <= 0) {
3460
3617
  throw new BadRequestException('bank_account_id is required');
@@ -3527,6 +3684,264 @@ export class FinanceService {
3527
3684
  };
3528
3685
  }
3529
3686
 
3687
+ async createBankStatementEntry(
3688
+ data: CreateBankStatementEntryDto,
3689
+ userId?: number,
3690
+ ) {
3691
+ const bankAccountId = Number(data.bank_account_id);
3692
+ const amount = Number(data.amount);
3693
+ const postedDate = new Date(`${data.date}T00:00:00`);
3694
+
3695
+ if (Number.isNaN(bankAccountId) || bankAccountId <= 0) {
3696
+ throw new BadRequestException('bank_account_id is required');
3697
+ }
3698
+
3699
+ if (Number.isNaN(amount) || amount <= 0) {
3700
+ throw new BadRequestException('amount must be greater than zero');
3701
+ }
3702
+
3703
+ if (Number.isNaN(postedDate.getTime())) {
3704
+ throw new BadRequestException('Invalid statement date');
3705
+ }
3706
+
3707
+ const description = data.description?.trim();
3708
+ if (!description) {
3709
+ throw new BadRequestException('description is required');
3710
+ }
3711
+
3712
+ const bankAccount = await this.prisma.bank_account.findUnique({
3713
+ where: { id: bankAccountId },
3714
+ select: { id: true },
3715
+ });
3716
+
3717
+ if (!bankAccount) {
3718
+ throw new NotFoundException('Bank account not found');
3719
+ }
3720
+
3721
+ const signedAmountCents =
3722
+ data.type === 'saida'
3723
+ ? -Math.abs(this.toCents(amount))
3724
+ : Math.abs(this.toCents(amount));
3725
+
3726
+ const created = await this.prisma.$transaction(async (tx) => {
3727
+ await this.assertDateNotInClosedPeriod(
3728
+ tx,
3729
+ postedDate,
3730
+ 'create bank statement entry',
3731
+ );
3732
+
3733
+ const reference = `manual-entry:${bankAccountId}:${Date.now()}`;
3734
+ const statement = await tx.bank_statement.create({
3735
+ data: {
3736
+ bank_account_id: bankAccountId,
3737
+ source_type: 'manual',
3738
+ imported_at: postedDate,
3739
+ imported_by_user_id: userId,
3740
+ idempotency_key: reference,
3741
+ period_start: postedDate,
3742
+ period_end: postedDate,
3743
+ },
3744
+ });
3745
+
3746
+ return tx.bank_statement_line.create({
3747
+ data: {
3748
+ bank_statement_id: statement.id,
3749
+ bank_account_id: bankAccountId,
3750
+ posted_date: postedDate,
3751
+ amount_cents: signedAmountCents,
3752
+ description,
3753
+ status: 'pending',
3754
+ dedupe_key: `${reference}:${Math.round(Math.random() * 1_000_000)}`,
3755
+ },
3756
+ });
3757
+ });
3758
+
3759
+ return {
3760
+ id: String(created.id),
3761
+ contaBancariaId: String(created.bank_account_id),
3762
+ data: created.posted_date.toISOString(),
3763
+ descricao: created.description,
3764
+ valor: this.fromCents(created.amount_cents),
3765
+ tipo: created.amount_cents >= 0 ? 'entrada' : 'saida',
3766
+ statusConciliacao: this.mapStatementStatusToPt(created.status),
3767
+ isTransfer: false,
3768
+ canEdit: true,
3769
+ canDelete: true,
3770
+ reconciliationId: null,
3771
+ settlementId: null,
3772
+ };
3773
+ }
3774
+
3775
+ async updateBankStatementEntry(
3776
+ id: number,
3777
+ data: UpdateBankStatementEntryDto,
3778
+ userId?: number,
3779
+ ) {
3780
+ if (data.amount !== undefined) {
3781
+ const amount = Number(data.amount);
3782
+ if (Number.isNaN(amount) || amount <= 0) {
3783
+ throw new BadRequestException('amount must be greater than zero');
3784
+ }
3785
+ }
3786
+
3787
+ if (data.amount === undefined && data.description === undefined && data.date === undefined) {
3788
+ throw new BadRequestException('At least one field must be provided for update');
3789
+ }
3790
+
3791
+ const updated = await this.prisma.$transaction(async (tx) => {
3792
+ const statementLine = await tx.bank_statement_line.findUnique({
3793
+ where: { id },
3794
+ include: {
3795
+ bank_reconciliation: {
3796
+ where: {
3797
+ status: {
3798
+ in: ['pending', 'reconciled', 'adjusted'],
3799
+ },
3800
+ },
3801
+ select: {
3802
+ id: true,
3803
+ },
3804
+ take: 1,
3805
+ },
3806
+ },
3807
+ });
3808
+
3809
+ if (!statementLine) {
3810
+ throw new NotFoundException('Bank statement line not found');
3811
+ }
3812
+
3813
+ if (statementLine.external_id?.startsWith('transfer:')) {
3814
+ throw new ConflictException('Transfer movements cannot be edited');
3815
+ }
3816
+
3817
+ if (statementLine.bank_reconciliation.length > 0) {
3818
+ throw new ConflictException('Reconciled movements cannot be edited');
3819
+ }
3820
+
3821
+ const targetDate = data.date ? this.parseLocalDate(data.date) : statementLine.posted_date;
3822
+ await this.assertDateNotInClosedPeriod(
3823
+ tx,
3824
+ targetDate,
3825
+ 'update bank statement entry',
3826
+ );
3827
+
3828
+ const updateData: Record<string, unknown> = {};
3829
+
3830
+ if (data.amount !== undefined) {
3831
+ const amount = Number(data.amount);
3832
+ updateData.amount_cents =
3833
+ statementLine.amount_cents < 0
3834
+ ? -Math.abs(this.toCents(amount))
3835
+ : Math.abs(this.toCents(amount));
3836
+ }
3837
+
3838
+ if (data.description !== undefined) {
3839
+ updateData.description = data.description.trim();
3840
+ }
3841
+
3842
+ if (data.date !== undefined) {
3843
+ updateData.posted_date = this.parseLocalDate(data.date);
3844
+ }
3845
+
3846
+ const result = await tx.bank_statement_line.update({
3847
+ where: { id },
3848
+ data: updateData,
3849
+ });
3850
+
3851
+ await this.createAuditLog(tx, {
3852
+ action: 'UPDATE_BANK_STATEMENT_LINE',
3853
+ entityTable: 'bank_statement_line',
3854
+ entityId: String(result.id),
3855
+ actorUserId: userId,
3856
+ summary: `Updated bank statement line ${result.id}`,
3857
+ });
3858
+
3859
+ return result;
3860
+ });
3861
+
3862
+ return {
3863
+ id: String(updated.id),
3864
+ contaBancariaId: String(updated.bank_account_id),
3865
+ data: updated.posted_date.toISOString(),
3866
+ descricao: updated.description,
3867
+ valor: this.fromCents(updated.amount_cents),
3868
+ tipo: updated.amount_cents >= 0 ? 'entrada' : 'saida',
3869
+ statusConciliacao: this.mapStatementStatusToPt(updated.status),
3870
+ isTransfer: false,
3871
+ canEdit: true,
3872
+ canDelete: true,
3873
+ reconciliationId: null,
3874
+ settlementId: null,
3875
+ };
3876
+ }
3877
+
3878
+ async deleteBankStatementEntry(id: number, userId?: number) {
3879
+ return this.prisma.$transaction(async (tx) => {
3880
+ const statementLine = await tx.bank_statement_line.findUnique({
3881
+ where: { id },
3882
+ include: {
3883
+ bank_reconciliation: {
3884
+ where: {
3885
+ status: {
3886
+ in: ['pending', 'reconciled', 'adjusted'],
3887
+ },
3888
+ },
3889
+ select: {
3890
+ id: true,
3891
+ },
3892
+ take: 1,
3893
+ },
3894
+ },
3895
+ });
3896
+
3897
+ if (!statementLine) {
3898
+ throw new NotFoundException('Bank statement line not found');
3899
+ }
3900
+
3901
+ if (statementLine.external_id?.startsWith('transfer:')) {
3902
+ throw new ConflictException('Transfer movements cannot be deleted');
3903
+ }
3904
+
3905
+ if (statementLine.bank_reconciliation.length > 0) {
3906
+ throw new ConflictException('Reconciled movements cannot be deleted');
3907
+ }
3908
+
3909
+ await this.assertDateNotInClosedPeriod(
3910
+ tx,
3911
+ statementLine.posted_date,
3912
+ 'delete bank statement entry',
3913
+ );
3914
+
3915
+ await tx.bank_statement_line.delete({
3916
+ where: { id },
3917
+ });
3918
+
3919
+ const remainingLines = await tx.bank_statement_line.count({
3920
+ where: {
3921
+ bank_statement_id: statementLine.bank_statement_id,
3922
+ },
3923
+ });
3924
+
3925
+ if (remainingLines === 0) {
3926
+ await tx.bank_statement.delete({
3927
+ where: {
3928
+ id: statementLine.bank_statement_id,
3929
+ },
3930
+ });
3931
+ }
3932
+
3933
+ await this.createAuditLog(tx, {
3934
+ action: 'DELETE_BANK_STATEMENT_LINE',
3935
+ entityTable: 'bank_statement_line',
3936
+ entityId: String(statementLine.id),
3937
+ actorUserId: userId,
3938
+ summary: `Deleted bank statement line ${statementLine.id}`,
3939
+ });
3940
+
3941
+ return { success: true };
3942
+ });
3943
+ }
3944
+
3530
3945
  async importBankStatements(
3531
3946
  bankAccountId: number,
3532
3947
  file: MulterFile,
@@ -4003,6 +4418,7 @@ export class FinanceService {
4003
4418
 
4004
4419
  const code = this.generateBankAccountCode(data.bank, data.account);
4005
4420
  const name = data.description?.trim() || data.bank;
4421
+ const startDate = this.parseFilterDate(data.start_date) || new Date();
4006
4422
 
4007
4423
  const createdAccount = await this.prisma.$transaction(async (tx) => {
4008
4424
  const account = await tx.bank_account.create({
@@ -4019,7 +4435,7 @@ export class FinanceService {
4019
4435
  });
4020
4436
 
4021
4437
  if (data.initial_balance && data.initial_balance > 0) {
4022
- const postedDate = new Date();
4438
+ const postedDate = startDate;
4023
4439
  await this.assertDateNotInClosedPeriod(
4024
4440
  tx,
4025
4441
  postedDate,
@@ -4058,6 +4474,8 @@ export class FinanceService {
4058
4474
  select: {
4059
4475
  amount_cents: true,
4060
4476
  status: true,
4477
+ posted_date: true,
4478
+ description: true,
4061
4479
  },
4062
4480
  },
4063
4481
  },
@@ -4099,8 +4517,8 @@ export class FinanceService {
4099
4517
  }
4100
4518
 
4101
4519
  async createPeriodClose(data: CreatePeriodCloseDto, userId?: number) {
4102
- const periodStart = new Date(data.period_start);
4103
- const periodEnd = new Date(data.period_end);
4520
+ const periodStart = this.parseLocalDate(data.period_start);
4521
+ const periodEnd = this.parseLocalDate(data.period_end);
4104
4522
 
4105
4523
  if (
4106
4524
  Number.isNaN(periodStart.getTime()) ||
@@ -4176,6 +4594,8 @@ export class FinanceService {
4176
4594
  select: {
4177
4595
  amount_cents: true,
4178
4596
  status: true,
4597
+ posted_date: true,
4598
+ description: true,
4179
4599
  },
4180
4600
  },
4181
4601
  },
@@ -4322,6 +4742,100 @@ export class FinanceService {
4322
4742
  return { success: true };
4323
4743
  }
4324
4744
 
4745
+ async listCurrencies(paginationParams: PaginationDTO) {
4746
+ const currencies = await this.prisma.currency.findMany({
4747
+ orderBy: [{ code: 'asc' }],
4748
+ });
4749
+
4750
+ return currencies.map((c) => ({
4751
+ id: String(c.id),
4752
+ code: c.code,
4753
+ name: c.name,
4754
+ symbol: c.symbol,
4755
+ status: c.status,
4756
+ ativo: c.status === 'active',
4757
+ }));
4758
+ }
4759
+
4760
+ async createCurrency(data: CreateCurrencyDto) {
4761
+ const existing = await this.prisma.currency.findUnique({
4762
+ where: { code: data.code.toUpperCase() },
4763
+ select: { id: true },
4764
+ });
4765
+
4766
+ if (existing) {
4767
+ throw new ConflictException(
4768
+ `Currency with code ${data.code.toUpperCase()} already exists`,
4769
+ );
4770
+ }
4771
+
4772
+ const created = await this.prisma.currency.create({
4773
+ data: {
4774
+ code: data.code.toUpperCase(),
4775
+ name: data.name.trim(),
4776
+ symbol: data.symbol.trim(),
4777
+ status: 'active',
4778
+ },
4779
+ });
4780
+
4781
+ return {
4782
+ id: String(created.id),
4783
+ code: created.code,
4784
+ name: created.name,
4785
+ symbol: created.symbol,
4786
+ status: created.status,
4787
+ ativo: created.status === 'active',
4788
+ };
4789
+ }
4790
+
4791
+ async updateCurrency(id: number, data: UpdateCurrencyDto) {
4792
+ const current = await this.prisma.currency.findUnique({
4793
+ where: { id },
4794
+ select: { id: true },
4795
+ });
4796
+
4797
+ if (!current) {
4798
+ throw new NotFoundException('Currency not found');
4799
+ }
4800
+
4801
+ const updated = await this.prisma.currency.update({
4802
+ where: { id },
4803
+ data: {
4804
+ code: data.code?.toUpperCase(),
4805
+ name: data.name?.trim(),
4806
+ symbol: data.symbol?.trim(),
4807
+ status: data.status,
4808
+ },
4809
+ });
4810
+
4811
+ return {
4812
+ id: String(updated.id),
4813
+ code: updated.code,
4814
+ name: updated.name,
4815
+ symbol: updated.symbol,
4816
+ status: updated.status,
4817
+ ativo: updated.status === 'active',
4818
+ };
4819
+ }
4820
+
4821
+ async deleteCurrency(id: number) {
4822
+ const current = await this.prisma.currency.findUnique({
4823
+ where: { id },
4824
+ select: { id: true },
4825
+ });
4826
+
4827
+ if (!current) {
4828
+ throw new NotFoundException('Currency not found');
4829
+ }
4830
+
4831
+ await this.prisma.currency.update({
4832
+ where: { id },
4833
+ data: { status: 'inactive' },
4834
+ });
4835
+
4836
+ return { success: true };
4837
+ }
4838
+
4325
4839
  async deleteFinanceCategory(id: number) {
4326
4840
  const current = await this.prisma.finance_category.findUnique({
4327
4841
  where: { id },
@@ -4346,9 +4860,15 @@ export class FinanceService {
4346
4860
  titleType: TitleType,
4347
4861
  paginationParams: PaginationDTO,
4348
4862
  status?: string,
4863
+ filters?: {
4864
+ from?: string;
4865
+ to?: string;
4866
+ },
4349
4867
  ) {
4350
4868
  const prismaStatus = this.mapStatusFromPt(status);
4351
4869
  const search = paginationParams?.search?.trim();
4870
+ const fromDate = this.parseFilterDate(filters?.from);
4871
+ const toDate = this.parseFilterDate(filters?.to, true);
4352
4872
  const where: any = {
4353
4873
  title_type: titleType,
4354
4874
  };
@@ -4376,6 +4896,17 @@ export class FinanceService {
4376
4896
  ];
4377
4897
  }
4378
4898
 
4899
+ if (fromDate || toDate) {
4900
+ where.financial_installment = {
4901
+ some: {
4902
+ due_date: {
4903
+ ...(fromDate ? { gte: fromDate } : {}),
4904
+ ...(toDate ? { lte: toDate } : {}),
4905
+ },
4906
+ },
4907
+ };
4908
+ }
4909
+
4379
4910
  const normalizedPaginationParams: PaginationDTO = {
4380
4911
  ...paginationParams,
4381
4912
  sortField: paginationParams?.sortField || 'created_at',
@@ -4495,8 +5026,8 @@ export class FinanceService {
4495
5026
  await this.assertDateNotInClosedPeriod(
4496
5027
  tx,
4497
5028
  data.competence_date
4498
- ? new Date(data.competence_date)
4499
- : new Date(installments[0].due_date),
5029
+ ? this.parseLocalDate(data.competence_date)
5030
+ : this.parseLocalDate(installments[0].due_date),
4500
5031
  'create title',
4501
5032
  );
4502
5033
 
@@ -4508,9 +5039,9 @@ export class FinanceService {
4508
5039
  document_number: data.document_number,
4509
5040
  description: data.description,
4510
5041
  competence_date: data.competence_date
4511
- ? new Date(data.competence_date)
5042
+ ? this.parseLocalDate(data.competence_date)
4512
5043
  : null,
4513
- issue_date: data.issue_date ? new Date(data.issue_date) : null,
5044
+ issue_date: data.issue_date ? this.parseLocalDate(data.issue_date) : null,
4514
5045
  total_amount_cents: this.toCents(data.total_amount),
4515
5046
  finance_category_id: data.finance_category_id,
4516
5047
  created_by_user_id: userId,
@@ -4536,15 +5067,15 @@ export class FinanceService {
4536
5067
  title_id: title.id,
4537
5068
  installment_number: installment.installment_number,
4538
5069
  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),
5070
+ ? this.parseLocalDate(data.competence_date)
5071
+ : this.parseLocalDate(installment.due_date),
5072
+ due_date: this.parseLocalDate(installment.due_date),
4542
5073
  amount_cents: amountCents,
4543
5074
  open_amount_cents: amountCents,
4544
5075
  status: this.resolveInstallmentStatus(
4545
5076
  amountCents,
4546
5077
  amountCents,
4547
- new Date(installment.due_date),
5078
+ this.parseLocalDate(installment.due_date),
4548
5079
  ),
4549
5080
  notes: data.description,
4550
5081
  },
@@ -4684,7 +5215,7 @@ export class FinanceService {
4684
5215
  await this.assertDateNotInClosedPeriod(
4685
5216
  tx,
4686
5217
  data.competence_date
4687
- ? new Date(data.competence_date)
5218
+ ? this.parseLocalDate(data.competence_date)
4688
5219
  : new Date(installments[0].due_date),
4689
5220
  'update title',
4690
5221
  );
@@ -4717,9 +5248,9 @@ export class FinanceService {
4717
5248
  document_number: data.document_number,
4718
5249
  description: data.description,
4719
5250
  competence_date: data.competence_date
4720
- ? new Date(data.competence_date)
5251
+ ? this.parseLocalDate(data.competence_date)
4721
5252
  : null,
4722
- issue_date: data.issue_date ? new Date(data.issue_date) : null,
5253
+ issue_date: data.issue_date ? this.parseLocalDate(data.issue_date) : null,
4723
5254
  total_amount_cents: this.toCents(data.total_amount),
4724
5255
  finance_category_id: data.finance_category_id,
4725
5256
  },
@@ -4757,15 +5288,15 @@ export class FinanceService {
4757
5288
  title_id: title.id,
4758
5289
  installment_number: installment.installment_number,
4759
5290
  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),
5291
+ ? this.parseLocalDate(data.competence_date)
5292
+ : this.parseLocalDate(installment.due_date),
5293
+ due_date: this.parseLocalDate(installment.due_date),
4763
5294
  amount_cents: amountCents,
4764
5295
  open_amount_cents: amountCents,
4765
5296
  status: this.resolveInstallmentStatus(
4766
5297
  amountCents,
4767
5298
  amountCents,
4768
- new Date(installment.due_date),
5299
+ this.parseLocalDate(installment.due_date),
4769
5300
  ),
4770
5301
  notes: data.description,
4771
5302
  },
@@ -5292,7 +5823,7 @@ export class FinanceService {
5292
5823
  );
5293
5824
  }
5294
5825
 
5295
- if (Math.abs(statementLine.amount_cents) !== amountCents) {
5826
+ if (Math.abs(Number(statementLine.amount_cents)) !== amountCents) {
5296
5827
  throw new ConflictException(
5297
5828
  'Bank statement amount and settlement amount must match',
5298
5829
  );
@@ -5399,8 +5930,8 @@ export class FinanceService {
5399
5930
  }
5400
5931
 
5401
5932
  const nextInstallmentStatus = this.resolveInstallmentStatus(
5402
- updatedInstallment.amount_cents,
5403
- updatedInstallment.open_amount_cents,
5933
+ Number(updatedInstallment.amount_cents),
5934
+ Number(updatedInstallment.open_amount_cents),
5404
5935
  updatedInstallment.due_date,
5405
5936
  );
5406
5937
 
@@ -5930,6 +6461,8 @@ export class FinanceService {
5930
6461
  select: {
5931
6462
  amount_cents: true,
5932
6463
  status: true,
6464
+ posted_date: true,
6465
+ description: true,
5933
6466
  },
5934
6467
  },
5935
6468
  },
@@ -6334,6 +6867,31 @@ export class FinanceService {
6334
6867
  return statusMap[status] || 'aberto';
6335
6868
  }
6336
6869
 
6870
+ private paginateCollection<T>(items: T[], paginationParams?: PaginationDTO) {
6871
+ const requestedPage = Number(paginationParams?.page || 1);
6872
+ const requestedPageSize = Number(paginationParams?.pageSize || 10);
6873
+ const page = Number.isNaN(requestedPage) || requestedPage < 1
6874
+ ? 1
6875
+ : requestedPage;
6876
+ const pageSize = Number.isNaN(requestedPageSize) || requestedPageSize < 1
6877
+ ? 10
6878
+ : requestedPageSize;
6879
+ const total = items.length;
6880
+ const lastPage = Math.max(1, Math.ceil(total / pageSize));
6881
+ const currentPage = Math.min(page, lastPage);
6882
+ const start = (currentPage - 1) * pageSize;
6883
+
6884
+ return {
6885
+ data: items.slice(start, start + pageSize),
6886
+ total,
6887
+ page: currentPage,
6888
+ pageSize,
6889
+ prev: currentPage > 1 ? currentPage - 1 : null,
6890
+ next: currentPage < lastPage ? currentPage + 1 : null,
6891
+ lastPage,
6892
+ };
6893
+ }
6894
+
6337
6895
  private normalizeTagSlug(value?: string | null) {
6338
6896
  if (!value) {
6339
6897
  return '';
@@ -6430,18 +6988,25 @@ export class FinanceService {
6430
6988
 
6431
6989
  private mapBankAccountToFront(bankAccount: any) {
6432
6990
  const currentCents = (bankAccount.bank_statement_line || []).reduce(
6433
- (acc, line) => acc + line.amount_cents,
6991
+ (acc, line) => acc + Number(line.amount_cents),
6434
6992
  0,
6435
6993
  );
6436
6994
 
6437
6995
  const reconciledCents = (bankAccount.bank_statement_line || []).reduce(
6438
6996
  (acc, line) =>
6439
6997
  line.status === 'reconciled' || line.status === 'adjusted'
6440
- ? acc + line.amount_cents
6998
+ ? acc + Number(line.amount_cents)
6441
6999
  : acc,
6442
7000
  0,
6443
7001
  );
6444
7002
 
7003
+ const initialLine = (bankAccount.bank_statement_line || []).find(
7004
+ (line: any) => line.description === 'Saldo inicial',
7005
+ );
7006
+ const dataInicial = initialLine?.posted_date
7007
+ ? new Date(initialLine.posted_date).toISOString().slice(0, 10)
7008
+ : null;
7009
+
6445
7010
  return {
6446
7011
  id: String(bankAccount.id),
6447
7012
  codigo: bankAccount.code,
@@ -6454,6 +7019,7 @@ export class FinanceService {
6454
7019
  saldoAtual: this.fromCents(currentCents),
6455
7020
  saldoConciliado: this.fromCents(reconciledCents),
6456
7021
  ativo: bankAccount.status === 'active',
7022
+ dataInicial,
6457
7023
  };
6458
7024
  }
6459
7025
 
@@ -6796,6 +7362,18 @@ export class FinanceService {
6796
7362
  return Math.round(value * 100);
6797
7363
  }
6798
7364
 
7365
+ /**
7366
+ * Parses a date-only string (YYYY-MM-DD) as local noon to avoid
7367
+ * timezone shifts that would cause the date to appear as the previous day
7368
+ * in UTC-negative timezones.
7369
+ */
7370
+ private parseLocalDate(dateString: string): Date {
7371
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
7372
+ return new Date(dateString + 'T12:00:00.000Z');
7373
+ }
7374
+ return new Date(dateString);
7375
+ }
7376
+
6799
7377
  private fromCents(value: number | bigint | string | null | undefined) {
6800
7378
  if (value === null || value === undefined) {
6801
7379
  return 0;