@hed-hog/finance 0.0.261 → 0.0.266

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 (28) hide show
  1. package/dist/dto/update-finance-scenario-settings.dto.d.ts +7 -0
  2. package/dist/dto/update-finance-scenario-settings.dto.d.ts.map +1 -0
  3. package/dist/dto/update-finance-scenario-settings.dto.js +39 -0
  4. package/dist/dto/update-finance-scenario-settings.dto.js.map +1 -0
  5. package/dist/finance-data.controller.d.ts +61 -7
  6. package/dist/finance-data.controller.d.ts.map +1 -1
  7. package/dist/finance-data.controller.js +23 -3
  8. package/dist/finance-data.controller.js.map +1 -1
  9. package/dist/finance.service.d.ts +79 -9
  10. package/dist/finance.service.d.ts.map +1 -1
  11. package/dist/finance.service.js +471 -70
  12. package/dist/finance.service.js.map +1 -1
  13. package/hedhog/data/route.yaml +9 -0
  14. package/hedhog/data/setting_group.yaml +152 -0
  15. package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +31 -3
  16. package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +38 -7
  17. package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +3 -1
  18. package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +74 -4
  19. package/hedhog/frontend/app/reports/actual-vs-forecast/page.tsx.ejs +361 -0
  20. package/hedhog/frontend/app/reports/aging-default/page.tsx.ejs +368 -0
  21. package/hedhog/frontend/app/reports/cash-position/page.tsx.ejs +432 -0
  22. package/hedhog/frontend/messages/en.json +182 -0
  23. package/hedhog/frontend/messages/pt.json +182 -0
  24. package/hedhog/query/triggers-period-close.sql +361 -0
  25. package/package.json +8 -8
  26. package/src/dto/update-finance-scenario-settings.dto.ts +21 -0
  27. package/src/finance-data.controller.ts +18 -3
  28. package/src/finance.service.ts +781 -79
@@ -22,10 +22,11 @@ const common_1 = require("@nestjs/common");
22
22
  const node_crypto_1 = require("node:crypto");
23
23
  const promises_1 = require("node:fs/promises");
24
24
  let FinanceService = FinanceService_1 = class FinanceService {
25
- constructor(prisma, paginationService, ai, fileService) {
25
+ constructor(prisma, paginationService, ai, settingService, fileService) {
26
26
  this.prisma = prisma;
27
27
  this.paginationService = paginationService;
28
28
  this.ai = ai;
29
+ this.settingService = settingService;
29
30
  this.fileService = fileService;
30
31
  this.logger = new common_1.Logger(FinanceService_1.name);
31
32
  }
@@ -503,17 +504,22 @@ let FinanceService = FinanceService_1 = class FinanceService {
503
504
  const num = Number(normalized);
504
505
  return Number.isFinite(num) ? num : null;
505
506
  }
506
- async getData() {
507
- const [payablesResult, receivablesResult, peopleResult, categoriesResult, costCentersResult, bankAccountsResult, tagsResult, auditLogsResult, openPeriodResult,] = await Promise.allSettled([
507
+ async getData(filters) {
508
+ const today = this.startOfDay(new Date());
509
+ const [payablesResult, receivablesResult, peopleResult, categoriesResult, costCentersResult, bankAccountsResult, bankStatementsResult, collectionsDefaultResult, tagsResult, auditLogsResult, openPeriodResult, scenarioSettingsResult, confirmedSettlementsResult,] = await Promise.allSettled([
508
510
  this.loadTitles('payable'),
509
511
  this.loadTitles('receivable'),
510
512
  this.loadPeople(),
511
513
  this.loadCategories(),
512
514
  this.loadCostCenters(),
513
515
  this.loadBankAccounts(),
516
+ this.listBankStatements(),
517
+ this.getAccountsReceivableCollectionsDefault(),
514
518
  this.loadTags(),
515
519
  this.loadAuditLogs(),
516
520
  this.loadOpenPeriod(),
521
+ this.loadFinancialScenarioSettings(),
522
+ this.loadConfirmedSettlements(today, this.addDays(today, 365)),
517
523
  ]);
518
524
  const payables = payablesResult.status === 'fulfilled' ? payablesResult.value : [];
519
525
  const receivables = receivablesResult.status === 'fulfilled' ? receivablesResult.value : [];
@@ -521,9 +527,27 @@ let FinanceService = FinanceService_1 = class FinanceService {
521
527
  const categories = categoriesResult.status === 'fulfilled' ? categoriesResult.value : [];
522
528
  const costCenters = costCentersResult.status === 'fulfilled' ? costCentersResult.value : [];
523
529
  const bankAccounts = bankAccountsResult.status === 'fulfilled' ? bankAccountsResult.value : [];
530
+ const bankStatements = bankStatementsResult.status === 'fulfilled'
531
+ ? bankStatementsResult.value
532
+ : [];
533
+ const collectionsDefault = collectionsDefaultResult.status === 'fulfilled'
534
+ ? collectionsDefaultResult.value
535
+ : {
536
+ agingInadimplencia: [],
537
+ historicoContatos: [],
538
+ };
524
539
  const tags = tagsResult.status === 'fulfilled' ? tagsResult.value : [];
525
540
  const auditLogs = auditLogsResult.status === 'fulfilled' ? auditLogsResult.value : [];
526
541
  const openPeriod = openPeriodResult.status === 'fulfilled' ? openPeriodResult.value : null;
542
+ const confirmedSettlements = confirmedSettlementsResult.status === 'fulfilled'
543
+ ? confirmedSettlementsResult.value
544
+ : [];
545
+ const scenarioSettings = scenarioSettingsResult.status === 'fulfilled'
546
+ ? scenarioSettingsResult.value
547
+ : this.getDefaultFinancialScenarioSettings();
548
+ const horizonteDias = this.resolveHorizonDays(filters === null || filters === void 0 ? void 0 : filters.horizonteDias, scenarioSettings.defaultHorizonDays);
549
+ const cenario = this.resolveForecastScenario(filters === null || filters === void 0 ? void 0 : filters.cenario, scenarioSettings.defaultScenario);
550
+ const forecastEnd = this.addDays(today, horizonteDias);
527
551
  if (payablesResult.status === 'rejected') {
528
552
  this.logger.error('Failed to load finance payables', payablesResult.reason);
529
553
  }
@@ -542,6 +566,12 @@ let FinanceService = FinanceService_1 = class FinanceService {
542
566
  if (bankAccountsResult.status === 'rejected') {
543
567
  this.logger.error('Failed to load finance bank accounts', bankAccountsResult.reason);
544
568
  }
569
+ if (bankStatementsResult.status === 'rejected') {
570
+ this.logger.error('Failed to load finance bank statements', bankStatementsResult.reason);
571
+ }
572
+ if (collectionsDefaultResult.status === 'rejected') {
573
+ this.logger.error('Failed to load finance collections default', collectionsDefaultResult.reason);
574
+ }
545
575
  if (tagsResult.status === 'rejected') {
546
576
  this.logger.error('Failed to load finance tags', tagsResult.reason);
547
577
  }
@@ -551,6 +581,12 @@ let FinanceService = FinanceService_1 = class FinanceService {
551
581
  if (openPeriodResult.status === 'rejected') {
552
582
  this.logger.error('Failed to load finance open period', openPeriodResult.reason);
553
583
  }
584
+ if (confirmedSettlementsResult.status === 'rejected') {
585
+ this.logger.error('Failed to load finance confirmed settlements', confirmedSettlementsResult.reason);
586
+ }
587
+ if (scenarioSettingsResult.status === 'rejected') {
588
+ this.logger.error('Failed to load finance scenario settings', scenarioSettingsResult.reason);
589
+ }
554
590
  const aprovacoesPendentes = payables
555
591
  .filter((title) => title.status === 'rascunho')
556
592
  .map((title) => ({
@@ -563,30 +599,356 @@ let FinanceService = FinanceService_1 = class FinanceService {
563
599
  dataSolicitacao: title.criadoEm,
564
600
  }));
565
601
  const kpis = this.calculateDashboardKpis(payables, receivables, bankAccounts);
602
+ const { recebiveis, adquirentes } = this.buildReceivablesCalendarData(receivables);
603
+ const { fluxoCaixaPrevisto, entradasPrevistas, saidasPrevistas } = this.buildCashFlowForecastData(payables, receivables, confirmedSettlements, kpis.saldoCaixa, today, forecastEnd, scenarioSettings.map[cenario]);
566
604
  return {
567
605
  kpis,
568
- fluxoCaixaPrevisto: [],
606
+ defaultScenario: scenarioSettings.defaultScenario,
607
+ defaultHorizonDays: scenarioSettings.defaultHorizonDays,
608
+ fluxoCaixaPrevisto,
569
609
  titulosPagar: payables,
570
610
  titulosReceber: receivables,
571
- extratos: [],
611
+ extratos: bankStatements,
572
612
  contasBancarias: bankAccounts,
573
613
  pessoas: people,
574
614
  categorias: categories,
575
615
  centrosCusto: costCenters,
576
616
  aprovacoesPendentes,
577
- agingInadimplencia: [],
578
- cenarios: [],
617
+ agingInadimplencia: collectionsDefault.agingInadimplencia,
618
+ cenarios: scenarioSettings.cenarios,
579
619
  transferencias: [],
580
620
  tags,
581
621
  logsAuditoria: auditLogs,
582
- recebiveis: [],
583
- adquirentes: [],
584
- historicoContatos: [],
585
- entradasPrevistas: [],
586
- saidasPrevistas: [],
622
+ recebiveis,
623
+ adquirentes,
624
+ historicoContatos: collectionsDefault.historicoContatos,
625
+ entradasPrevistas,
626
+ saidasPrevistas,
587
627
  periodoAberto: openPeriod,
588
628
  };
589
629
  }
630
+ async updateScenarioSettings(scenario, data) {
631
+ const scenarioSlug = this.resolveForecastScenarioStrict(scenario);
632
+ if (data.atrasoMedio < 0 || data.atrasoMedio > 365) {
633
+ throw new common_1.BadRequestException('Invalid average delay value');
634
+ }
635
+ if (data.taxaInadimplencia < 0 || data.taxaInadimplencia > 100) {
636
+ throw new common_1.BadRequestException('Invalid default rate value');
637
+ }
638
+ if (data.crescimentoReceita < -100 || data.crescimentoReceita > 1000) {
639
+ throw new common_1.BadRequestException('Invalid revenue growth value');
640
+ }
641
+ const prefix = `finance-scenario-${scenarioSlug === 'pessimista'
642
+ ? 'pessimistic'
643
+ : scenarioSlug === 'otimista'
644
+ ? 'optimistic'
645
+ : 'base'}`;
646
+ const settingUpdates = [
647
+ {
648
+ slug: `${prefix}-average-delay-days`,
649
+ value: String(data.atrasoMedio),
650
+ },
651
+ {
652
+ slug: `${prefix}-default-rate-percent`,
653
+ value: String(data.taxaInadimplencia),
654
+ },
655
+ {
656
+ slug: `${prefix}-revenue-growth-percent`,
657
+ value: String(data.crescimentoReceita),
658
+ },
659
+ ];
660
+ if (data.setAsDefault !== false) {
661
+ settingUpdates.push({
662
+ slug: 'finance-default-scenario',
663
+ value: scenarioSlug,
664
+ });
665
+ }
666
+ await this.settingService.setManySettings({
667
+ setting: settingUpdates,
668
+ });
669
+ const settings = await this.loadFinancialScenarioSettings();
670
+ return {
671
+ success: true,
672
+ cenarios: settings.cenarios,
673
+ defaultScenario: settings.defaultScenario,
674
+ };
675
+ }
676
+ resolveHorizonDays(value, fallback = 90) {
677
+ const parsed = Number(value);
678
+ const allowed = new Set([30, 60, 90, 180, 365]);
679
+ if (!Number.isFinite(parsed)) {
680
+ return allowed.has(fallback) ? fallback : 90;
681
+ }
682
+ return allowed.has(parsed) ? parsed : allowed.has(fallback) ? fallback : 90;
683
+ }
684
+ resolveForecastScenario(value, fallback = 'base') {
685
+ if (value === 'pessimista') {
686
+ return 'pessimista';
687
+ }
688
+ if (value === 'otimista') {
689
+ return 'otimista';
690
+ }
691
+ return fallback;
692
+ }
693
+ resolveForecastScenarioStrict(value) {
694
+ if (value === 'base' || value === 'pessimista' || value === 'otimista') {
695
+ return value;
696
+ }
697
+ throw new common_1.BadRequestException('Invalid scenario');
698
+ }
699
+ buildCashFlowForecastData(payables, receivables, confirmedSettlements, initialBalance, fromDate, toDate, scenarioConfig) {
700
+ const scenarioMultiplier = this.resolveScenarioMultiplier(scenarioConfig);
701
+ const entradasPrevistas = this.buildExpectedCashFlowEntries(receivables, fromDate, toDate, 'inflow').map((item) => (Object.assign(Object.assign({}, item), { valor: Number((item.valor * scenarioMultiplier.inflow).toFixed(2)) })));
702
+ const saidasPrevistas = this.buildExpectedCashFlowEntries(payables, fromDate, toDate, 'outflow').map((item) => (Object.assign(Object.assign({}, item), { valor: Number((item.valor * scenarioMultiplier.outflow).toFixed(2)) })));
703
+ const expectedByDate = new Map();
704
+ for (const entry of entradasPrevistas) {
705
+ const dateKey = entry.vencimento.slice(0, 10);
706
+ const current = expectedByDate.get(dateKey) || {
707
+ entradas: 0,
708
+ saidas: 0,
709
+ realizado: 0,
710
+ };
711
+ current.entradas = Number((current.entradas + Number(entry.valor || 0)).toFixed(2));
712
+ expectedByDate.set(dateKey, current);
713
+ }
714
+ for (const outflow of saidasPrevistas) {
715
+ const dateKey = outflow.vencimento.slice(0, 10);
716
+ const current = expectedByDate.get(dateKey) || {
717
+ entradas: 0,
718
+ saidas: 0,
719
+ realizado: 0,
720
+ };
721
+ current.saidas = Number((current.saidas + Number(outflow.valor || 0)).toFixed(2));
722
+ expectedByDate.set(dateKey, current);
723
+ }
724
+ for (const settlement of confirmedSettlements) {
725
+ const dateKey = settlement.settled_at.toISOString().slice(0, 10);
726
+ const current = expectedByDate.get(dateKey) || {
727
+ entradas: 0,
728
+ saidas: 0,
729
+ realizado: 0,
730
+ };
731
+ const amount = this.fromCents(settlement.amount_cents);
732
+ if (settlement.settlement_type === 'payable') {
733
+ current.realizado = Number((current.realizado - amount).toFixed(2));
734
+ }
735
+ else if (settlement.settlement_type === 'receivable') {
736
+ current.realizado = Number((current.realizado + amount).toFixed(2));
737
+ }
738
+ expectedByDate.set(dateKey, current);
739
+ }
740
+ const fluxoCaixaPrevisto = [];
741
+ let runningProjectedBalance = Number(initialBalance || 0);
742
+ let runningActualBalance = Number(initialBalance || 0);
743
+ for (let cursor = this.startOfDay(fromDate); cursor <= toDate; cursor = this.addDays(cursor, 1)) {
744
+ const key = cursor.toISOString().slice(0, 10);
745
+ const dayData = expectedByDate.get(key) || {
746
+ entradas: 0,
747
+ saidas: 0,
748
+ realizado: 0,
749
+ };
750
+ runningProjectedBalance = Number((runningProjectedBalance + dayData.entradas - dayData.saidas).toFixed(2));
751
+ runningActualBalance = Number((runningActualBalance + dayData.realizado).toFixed(2));
752
+ fluxoCaixaPrevisto.push({
753
+ data: cursor.toISOString(),
754
+ saldoPrevisto: runningProjectedBalance,
755
+ saldoRealizado: runningActualBalance,
756
+ });
757
+ }
758
+ return {
759
+ fluxoCaixaPrevisto,
760
+ entradasPrevistas,
761
+ saidasPrevistas,
762
+ };
763
+ }
764
+ resolveScenarioMultiplier(scenarioConfig) {
765
+ const growthRate = scenarioConfig.crescimentoReceita / 100;
766
+ const defaultRate = scenarioConfig.taxaInadimplencia / 100;
767
+ const delayImpact = Math.max(0.5, 1 - scenarioConfig.atrasoMedio / 100);
768
+ const inflow = this.clampNumber((1 + growthRate) * (1 - defaultRate) * delayImpact, 0.1, 3);
769
+ const outflow = this.clampNumber(1 + Math.max(0, -growthRate) * 0.4 + defaultRate * 0.2, 0.1, 3);
770
+ return {
771
+ inflow,
772
+ outflow,
773
+ };
774
+ }
775
+ clampNumber(value, min, max) {
776
+ if (!Number.isFinite(value)) {
777
+ return min;
778
+ }
779
+ return Math.min(max, Math.max(min, value));
780
+ }
781
+ async loadFinancialScenarioSettings() {
782
+ const defaultSettings = this.getDefaultFinancialScenarioSettings();
783
+ const values = await this.settingService.getSettingValues([
784
+ 'finance-default-scenario',
785
+ 'finance-default-horizon-days',
786
+ 'finance-scenario-base-average-delay-days',
787
+ 'finance-scenario-base-default-rate-percent',
788
+ 'finance-scenario-base-revenue-growth-percent',
789
+ 'finance-scenario-pessimistic-average-delay-days',
790
+ 'finance-scenario-pessimistic-default-rate-percent',
791
+ 'finance-scenario-pessimistic-revenue-growth-percent',
792
+ 'finance-scenario-optimistic-average-delay-days',
793
+ 'finance-scenario-optimistic-default-rate-percent',
794
+ 'finance-scenario-optimistic-revenue-growth-percent',
795
+ ]);
796
+ const base = Object.assign(Object.assign({}, defaultSettings.map.base), { atrasoMedio: this.numberSetting(values['finance-scenario-base-average-delay-days'], defaultSettings.map.base.atrasoMedio), taxaInadimplencia: this.numberSetting(values['finance-scenario-base-default-rate-percent'], defaultSettings.map.base.taxaInadimplencia), crescimentoReceita: this.numberSetting(values['finance-scenario-base-revenue-growth-percent'], defaultSettings.map.base.crescimentoReceita), padrao: false });
797
+ const pessimista = Object.assign(Object.assign({}, defaultSettings.map.pessimista), { atrasoMedio: this.numberSetting(values['finance-scenario-pessimistic-average-delay-days'], defaultSettings.map.pessimista.atrasoMedio), taxaInadimplencia: this.numberSetting(values['finance-scenario-pessimistic-default-rate-percent'], defaultSettings.map.pessimista.taxaInadimplencia), crescimentoReceita: this.numberSetting(values['finance-scenario-pessimistic-revenue-growth-percent'], defaultSettings.map.pessimista.crescimentoReceita), padrao: false });
798
+ const otimista = Object.assign(Object.assign({}, defaultSettings.map.otimista), { atrasoMedio: this.numberSetting(values['finance-scenario-optimistic-average-delay-days'], defaultSettings.map.otimista.atrasoMedio), taxaInadimplencia: this.numberSetting(values['finance-scenario-optimistic-default-rate-percent'], defaultSettings.map.otimista.taxaInadimplencia), crescimentoReceita: this.numberSetting(values['finance-scenario-optimistic-revenue-growth-percent'], defaultSettings.map.otimista.crescimentoReceita), padrao: false });
799
+ const defaultScenario = this.resolveForecastScenario(String(values['finance-default-scenario'] || ''), defaultSettings.defaultScenario);
800
+ const cenarios = [base, pessimista, otimista].map((item) => (Object.assign(Object.assign({}, item), { padrao: item.id === defaultScenario })));
801
+ const map = {
802
+ base: cenarios.find((item) => item.id === 'base') || defaultSettings.map.base,
803
+ pessimista: cenarios.find((item) => item.id === 'pessimista') ||
804
+ defaultSettings.map.pessimista,
805
+ otimista: cenarios.find((item) => item.id === 'otimista') ||
806
+ defaultSettings.map.otimista,
807
+ };
808
+ return {
809
+ defaultScenario,
810
+ defaultHorizonDays: this.resolveHorizonDays(values['finance-default-horizon-days'], defaultSettings.defaultHorizonDays),
811
+ cenarios,
812
+ map,
813
+ };
814
+ }
815
+ getDefaultFinancialScenarioSettings() {
816
+ const base = {
817
+ id: 'base',
818
+ nome: 'Base',
819
+ descricao: 'Cenário de referência para projeções',
820
+ atrasoMedio: 5,
821
+ taxaInadimplencia: 3,
822
+ crescimentoReceita: 5,
823
+ padrao: true,
824
+ };
825
+ const pessimista = {
826
+ id: 'pessimista',
827
+ nome: 'Pessimista',
828
+ descricao: 'Premissas conservadoras com maior risco',
829
+ atrasoMedio: 12,
830
+ taxaInadimplencia: 6,
831
+ crescimentoReceita: -5,
832
+ padrao: false,
833
+ };
834
+ const otimista = {
835
+ id: 'otimista',
836
+ nome: 'Otimista',
837
+ descricao: 'Premissas favoráveis de crescimento',
838
+ atrasoMedio: 2,
839
+ taxaInadimplencia: 1.5,
840
+ crescimentoReceita: 10,
841
+ padrao: false,
842
+ };
843
+ const cenarios = [base, pessimista, otimista];
844
+ return {
845
+ defaultScenario: 'base',
846
+ defaultHorizonDays: 90,
847
+ cenarios,
848
+ map: {
849
+ base,
850
+ pessimista,
851
+ otimista,
852
+ },
853
+ };
854
+ }
855
+ numberSetting(value, fallback) {
856
+ const parsed = Number(value);
857
+ return Number.isFinite(parsed) ? parsed : fallback;
858
+ }
859
+ buildExpectedCashFlowEntries(titles, fromDate, toDate, direction) {
860
+ return (titles || [])
861
+ .flatMap((title) => ((title === null || title === void 0 ? void 0 : title.parcelas) || [])
862
+ .filter((installment) => {
863
+ const dueDate = this.startOfDay(new Date(installment.vencimento));
864
+ if (Number.isNaN(dueDate.getTime()))
865
+ return false;
866
+ const openAmount = Number(installment.valorAberto || 0);
867
+ const status = String(installment.status || '').toLowerCase();
868
+ const isOpen = status === 'aberto' || status === 'vencido' || status === 'parcial';
869
+ return (dueDate >= fromDate &&
870
+ dueDate <= toDate &&
871
+ isOpen &&
872
+ openAmount > 0);
873
+ })
874
+ .map((installment) => ({
875
+ categoria: (title === null || title === void 0 ? void 0 : title.descricao) ||
876
+ (direction === 'inflow' ? title === null || title === void 0 ? void 0 : title.cliente : title === null || title === void 0 ? void 0 : title.fornecedor) ||
877
+ 'Sem categoria',
878
+ vencimento: installment.vencimento,
879
+ valor: Number(installment.valorAberto || installment.valor || 0),
880
+ })))
881
+ .sort((a, b) => new Date(a.vencimento).getTime() - new Date(b.vencimento).getTime());
882
+ }
883
+ buildReceivablesCalendarData(receivables) {
884
+ const recebiveis = receivables
885
+ .flatMap((title) => (title.parcelas || []).map((installment) => {
886
+ const bruto = Number(installment.valor || 0);
887
+ const canal = this.mapReceivableChannelLabel(installment.metodoPagamento || title.canal);
888
+ const taxaPercentual = canal === 'Cartão'
889
+ ? 0.03
890
+ : canal === 'Boleto'
891
+ ? 0.015
892
+ : canal === 'Pix'
893
+ ? 0.005
894
+ : 0.01;
895
+ const taxas = Number((bruto * taxaPercentual).toFixed(2));
896
+ const liquido = Number((bruto - taxas).toFixed(2));
897
+ return {
898
+ id: `${title.id}-${installment.id}`,
899
+ canal,
900
+ adquirente: title.cliente || '-',
901
+ dataPrevista: installment.vencimento,
902
+ bruto,
903
+ taxas,
904
+ liquido,
905
+ status: this.mapReceivableCalendarStatus(installment.status),
906
+ };
907
+ }))
908
+ .sort((a, b) => new Date(a.dataPrevista).getTime() - new Date(b.dataPrevista).getTime());
909
+ const adquirenteMap = new Map();
910
+ for (const recebivel of recebiveis) {
911
+ const current = adquirenteMap.get(recebivel.adquirente) || {
912
+ nome: recebivel.adquirente,
913
+ transacoes: 0,
914
+ total: 0,
915
+ };
916
+ current.transacoes += 1;
917
+ current.total = Number((current.total + recebivel.liquido).toFixed(2));
918
+ adquirenteMap.set(recebivel.adquirente, current);
919
+ }
920
+ const adquirentes = Array.from(adquirenteMap.values()).sort((a, b) => b.total - a.total);
921
+ return {
922
+ recebiveis,
923
+ adquirentes,
924
+ };
925
+ }
926
+ mapReceivableCalendarStatus(status) {
927
+ const normalized = (status || '').toLowerCase();
928
+ if (normalized === 'liquidado') {
929
+ return 'liquidado';
930
+ }
931
+ if (['aberto', 'aprovado', 'parcial'].includes(normalized)) {
932
+ return 'confirmado';
933
+ }
934
+ return 'pendente';
935
+ }
936
+ mapReceivableChannelLabel(channel) {
937
+ const normalized = (channel || '').toLowerCase();
938
+ const channelMap = {
939
+ cartao: 'Cartão',
940
+ cartão: 'Cartão',
941
+ pix: 'Pix',
942
+ boleto: 'Boleto',
943
+ transferencia: 'Transferência',
944
+ transferência: 'Transferência',
945
+ ted: 'Transferência',
946
+ doc: 'Transferência',
947
+ dinheiro: 'Dinheiro',
948
+ cheque: 'Cheque',
949
+ };
950
+ return channelMap[normalized] || 'Transferência';
951
+ }
590
952
  async getAccountsReceivableCollectionsDefault() {
591
953
  var _a;
592
954
  const today = this.startOfDay(new Date());
@@ -746,6 +1108,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
746
1108
  const baseInstallmentCents = Math.floor(totalAmountCents / data.installments);
747
1109
  const remainder = totalAmountCents % data.installments;
748
1110
  const created = await this.prisma.$transaction(async (tx) => {
1111
+ await this.assertDateNotInClosedPeriod(tx, firstDueDate, 'register collection agreement');
749
1112
  const title = await tx.financial_title.create({
750
1113
  data: {
751
1114
  person_id: person.id,
@@ -765,6 +1128,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
765
1128
  for (let index = 0; index < data.installments; index += 1) {
766
1129
  const dueDate = this.addMonths(firstDueDate, index);
767
1130
  const amountCents = baseInstallmentCents + (index === data.installments - 1 ? remainder : 0);
1131
+ await this.assertDateNotInClosedPeriod(tx, dueDate, 'register collection agreement installment');
768
1132
  await tx.financial_installment.create({
769
1133
  data: {
770
1134
  title_id: title.id,
@@ -1245,6 +1609,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
1245
1609
  const description = ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || 'Transferência bancária';
1246
1610
  const transferReference = `transfer:${Date.now()}-${Math.round(Math.random() * 1000000)}`;
1247
1611
  await this.prisma.$transaction(async (tx) => {
1612
+ await this.assertDateNotInClosedPeriod(tx, postedDate, 'create transfer');
1248
1613
  const sourceStatement = await tx.bank_statement.create({
1249
1614
  data: {
1250
1615
  bank_account_id: sourceAccountId,
@@ -1607,6 +1972,7 @@ let FinanceService = FinanceService_1 = class FinanceService {
1607
1972
  const description = ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || `Ajuste: ${data.type}`;
1608
1973
  const reference = `adjustment:${Date.now()}-${Math.round(Math.random() * 1000000)}`;
1609
1974
  const created = await this.prisma.$transaction(async (tx) => {
1975
+ await this.assertDateNotInClosedPeriod(tx, postedAt, 'create bank statement adjustment');
1610
1976
  const statement = await tx.bank_statement.create({
1611
1977
  data: {
1612
1978
  bank_account_id: bankAccountId,
@@ -1701,34 +2067,43 @@ let FinanceService = FinanceService_1 = class FinanceService {
1701
2067
  file.size,
1702
2068
  normalizedEntries.length,
1703
2069
  ].join('|'), 24);
1704
- const statement = await this.prisma.bank_statement.create({
1705
- data: {
1706
- bank_account_id: bankAccountId,
1707
- source_type: sourceType,
1708
- idempotency_key: `import-${uploadedFile.id}-${statementFingerprint}`,
1709
- period_start: normalizedEntries
1710
- .map((entry) => entry.postedDate)
1711
- .sort((a, b) => a.getTime() - b.getTime())[0],
1712
- period_end: normalizedEntries
1713
- .map((entry) => entry.postedDate)
1714
- .sort((a, b) => b.getTime() - a.getTime())[0],
1715
- imported_at: new Date(),
1716
- imported_by_user_id: userId || null,
1717
- },
1718
- select: { id: true },
1719
- });
1720
- await this.prisma.bank_statement_line.createMany({
1721
- data: normalizedEntries.map((entry) => ({
1722
- bank_statement_id: statement.id,
1723
- bank_account_id: bankAccountId,
1724
- external_id: entry.externalId,
1725
- posted_date: entry.postedDate,
1726
- amount_cents: entry.amountCents,
1727
- description: entry.description,
1728
- status: 'imported',
1729
- dedupe_key: entry.dedupeKey,
1730
- })),
1731
- skipDuplicates: true,
2070
+ const statement = await this.prisma.$transaction(async (tx) => {
2071
+ const uniquePostedDates = [
2072
+ ...new Set(normalizedEntries.map((entry) => entry.postedDate.toISOString().slice(0, 10))),
2073
+ ];
2074
+ for (const postedDateText of uniquePostedDates) {
2075
+ await this.assertDateNotInClosedPeriod(tx, new Date(`${postedDateText}T00:00:00.000Z`), 'import bank statements');
2076
+ }
2077
+ const createdStatement = await tx.bank_statement.create({
2078
+ data: {
2079
+ bank_account_id: bankAccountId,
2080
+ source_type: sourceType,
2081
+ idempotency_key: `import-${uploadedFile.id}-${statementFingerprint}`,
2082
+ period_start: normalizedEntries
2083
+ .map((entry) => entry.postedDate)
2084
+ .sort((a, b) => a.getTime() - b.getTime())[0],
2085
+ period_end: normalizedEntries
2086
+ .map((entry) => entry.postedDate)
2087
+ .sort((a, b) => b.getTime() - a.getTime())[0],
2088
+ imported_at: new Date(),
2089
+ imported_by_user_id: userId || null,
2090
+ },
2091
+ select: { id: true },
2092
+ });
2093
+ await tx.bank_statement_line.createMany({
2094
+ data: normalizedEntries.map((entry) => ({
2095
+ bank_statement_id: createdStatement.id,
2096
+ bank_account_id: bankAccountId,
2097
+ external_id: entry.externalId,
2098
+ posted_date: entry.postedDate,
2099
+ amount_cents: entry.amountCents,
2100
+ description: entry.description,
2101
+ status: 'imported',
2102
+ dedupe_key: entry.dedupeKey,
2103
+ })),
2104
+ skipDuplicates: true,
2105
+ });
2106
+ return createdStatement;
1732
2107
  });
1733
2108
  return {
1734
2109
  statementId: String(statement.id),
@@ -1977,38 +2352,43 @@ let FinanceService = FinanceService_1 = class FinanceService {
1977
2352
  const accountType = this.mapAccountTypeFromPt(data.type);
1978
2353
  const code = this.generateBankAccountCode(data.bank, data.account);
1979
2354
  const name = ((_a = data.description) === null || _a === void 0 ? void 0 : _a.trim()) || data.bank;
1980
- const createdAccount = await this.prisma.bank_account.create({
1981
- data: {
1982
- code,
1983
- name,
1984
- bank_name: data.bank,
1985
- agency: data.branch || null,
1986
- account_number: data.account || null,
1987
- account_type: accountType,
1988
- status: 'active',
1989
- },
1990
- });
1991
- if (data.initial_balance && data.initial_balance > 0) {
1992
- const statement = await this.prisma.bank_statement.create({
1993
- data: {
1994
- bank_account_id: createdAccount.id,
1995
- source_type: 'csv',
1996
- imported_at: new Date(),
1997
- imported_by_user_id: userId,
1998
- },
1999
- });
2000
- await this.prisma.bank_statement_line.create({
2355
+ const createdAccount = await this.prisma.$transaction(async (tx) => {
2356
+ const account = await tx.bank_account.create({
2001
2357
  data: {
2002
- bank_statement_id: statement.id,
2003
- bank_account_id: createdAccount.id,
2004
- posted_date: new Date(),
2005
- amount_cents: this.toCents(data.initial_balance),
2006
- description: 'Saldo inicial',
2007
- status: 'reconciled',
2008
- dedupe_key: `initial-balance-${createdAccount.id}-${Date.now()}`,
2358
+ code,
2359
+ name,
2360
+ bank_name: data.bank,
2361
+ agency: data.branch || null,
2362
+ account_number: data.account || null,
2363
+ account_type: accountType,
2364
+ status: 'active',
2009
2365
  },
2010
2366
  });
2011
- }
2367
+ if (data.initial_balance && data.initial_balance > 0) {
2368
+ const postedDate = new Date();
2369
+ await this.assertDateNotInClosedPeriod(tx, postedDate, 'create bank account initial balance');
2370
+ const statement = await tx.bank_statement.create({
2371
+ data: {
2372
+ bank_account_id: account.id,
2373
+ source_type: 'csv',
2374
+ imported_at: postedDate,
2375
+ imported_by_user_id: userId,
2376
+ },
2377
+ });
2378
+ await tx.bank_statement_line.create({
2379
+ data: {
2380
+ bank_statement_id: statement.id,
2381
+ bank_account_id: account.id,
2382
+ posted_date: postedDate,
2383
+ amount_cents: this.toCents(data.initial_balance),
2384
+ description: 'Saldo inicial',
2385
+ status: 'reconciled',
2386
+ dedupe_key: `initial-balance-${account.id}-${Date.now()}`,
2387
+ },
2388
+ });
2389
+ }
2390
+ return account;
2391
+ });
2012
2392
  const account = await this.prisma.bank_account.findUnique({
2013
2393
  where: { id: createdAccount.id },
2014
2394
  include: {
@@ -3400,6 +3780,25 @@ let FinanceService = FinanceService_1 = class FinanceService {
3400
3780
  data: log.created_at.toISOString(),
3401
3781
  }));
3402
3782
  }
3783
+ async loadConfirmedSettlements(fromDate, toDate) {
3784
+ return this.prisma.settlement.findMany({
3785
+ where: {
3786
+ status: 'confirmed',
3787
+ settled_at: {
3788
+ gte: fromDate,
3789
+ lte: toDate,
3790
+ },
3791
+ },
3792
+ select: {
3793
+ settled_at: true,
3794
+ amount_cents: true,
3795
+ settlement_type: true,
3796
+ },
3797
+ orderBy: {
3798
+ settled_at: 'asc',
3799
+ },
3800
+ });
3801
+ }
3403
3802
  async loadOpenPeriod() {
3404
3803
  var _a, _b, _c, _d;
3405
3804
  const openPeriod = await this.prisma.period_close.findFirst({
@@ -3898,10 +4297,12 @@ let FinanceService = FinanceService_1 = class FinanceService {
3898
4297
  exports.FinanceService = FinanceService;
3899
4298
  exports.FinanceService = FinanceService = FinanceService_1 = __decorate([
3900
4299
  (0, common_1.Injectable)(),
3901
- __param(3, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.FileService))),
4300
+ __param(3, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.SettingService))),
4301
+ __param(4, (0, common_1.Inject)((0, common_1.forwardRef)(() => core_1.FileService))),
3902
4302
  __metadata("design:paramtypes", [api_prisma_1.PrismaService,
3903
4303
  api_pagination_1.PaginationService,
3904
4304
  core_1.AiService,
4305
+ core_1.SettingService,
3905
4306
  core_1.FileService])
3906
4307
  ], FinanceService);
3907
4308
  //# sourceMappingURL=finance.service.js.map