@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.
- package/dist/dto/update-finance-scenario-settings.dto.d.ts +7 -0
- package/dist/dto/update-finance-scenario-settings.dto.d.ts.map +1 -0
- package/dist/dto/update-finance-scenario-settings.dto.js +39 -0
- package/dist/dto/update-finance-scenario-settings.dto.js.map +1 -0
- package/dist/finance-data.controller.d.ts +61 -7
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance-data.controller.js +23 -3
- package/dist/finance-data.controller.js.map +1 -1
- package/dist/finance.service.d.ts +79 -9
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +471 -70
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +9 -0
- package/hedhog/data/setting_group.yaml +152 -0
- package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +31 -3
- package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +38 -7
- package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +3 -1
- package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +74 -4
- package/hedhog/frontend/app/reports/actual-vs-forecast/page.tsx.ejs +361 -0
- package/hedhog/frontend/app/reports/aging-default/page.tsx.ejs +368 -0
- package/hedhog/frontend/app/reports/cash-position/page.tsx.ejs +432 -0
- package/hedhog/frontend/messages/en.json +182 -0
- package/hedhog/frontend/messages/pt.json +182 -0
- package/hedhog/query/triggers-period-close.sql +361 -0
- package/package.json +8 -8
- package/src/dto/update-finance-scenario-settings.dto.ts +21 -0
- package/src/finance-data.controller.ts +18 -3
- package/src/finance.service.ts +781 -79
package/dist/finance.service.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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
|
|
1981
|
-
|
|
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
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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.
|
|
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
|