@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/src/finance.service.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { getLocaleText } from '@hed-hog/api-locale';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
PageOrderDirection,
|
|
4
|
+
PaginationDTO,
|
|
5
|
+
PaginationService,
|
|
6
6
|
} from '@hed-hog/api-pagination';
|
|
7
7
|
import { PrismaService } from '@hed-hog/api-prisma';
|
|
8
|
-
import { AiService, FileService } from '@hed-hog/core';
|
|
8
|
+
import { AiService, FileService, SettingService } from '@hed-hog/core';
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
BadRequestException,
|
|
11
|
+
ConflictException,
|
|
12
|
+
forwardRef,
|
|
13
|
+
Inject,
|
|
14
|
+
Injectable,
|
|
15
|
+
Logger,
|
|
16
|
+
NotFoundException,
|
|
17
17
|
} from '@nestjs/common';
|
|
18
18
|
import { createHash } from 'node:crypto';
|
|
19
19
|
import { readFile } from 'node:fs/promises';
|
|
@@ -50,6 +50,25 @@ type TitleStatus =
|
|
|
50
50
|
| 'canceled'
|
|
51
51
|
| 'overdue';
|
|
52
52
|
|
|
53
|
+
type ForecastScenario = 'base' | 'pessimista' | 'otimista';
|
|
54
|
+
|
|
55
|
+
type FinancialScenarioConfig = {
|
|
56
|
+
id: ForecastScenario;
|
|
57
|
+
nome: string;
|
|
58
|
+
descricao: string;
|
|
59
|
+
atrasoMedio: number;
|
|
60
|
+
taxaInadimplencia: number;
|
|
61
|
+
crescimentoReceita: number;
|
|
62
|
+
padrao: boolean;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type FinancialScenarioSettings = {
|
|
66
|
+
defaultScenario: ForecastScenario;
|
|
67
|
+
defaultHorizonDays: number;
|
|
68
|
+
cenarios: FinancialScenarioConfig[];
|
|
69
|
+
map: Record<ForecastScenario, FinancialScenarioConfig>;
|
|
70
|
+
};
|
|
71
|
+
|
|
53
72
|
@Injectable()
|
|
54
73
|
export class FinanceService {
|
|
55
74
|
private readonly logger = new Logger(FinanceService.name);
|
|
@@ -58,6 +77,8 @@ export class FinanceService {
|
|
|
58
77
|
private readonly prisma: PrismaService,
|
|
59
78
|
private readonly paginationService: PaginationService,
|
|
60
79
|
private readonly ai: AiService,
|
|
80
|
+
@Inject(forwardRef(() => SettingService))
|
|
81
|
+
private readonly settingService: SettingService,
|
|
61
82
|
@Inject(forwardRef(() => FileService))
|
|
62
83
|
private readonly fileService: FileService,
|
|
63
84
|
) {}
|
|
@@ -727,7 +748,9 @@ export class FinanceService {
|
|
|
727
748
|
return Number.isFinite(num) ? num : null;
|
|
728
749
|
}
|
|
729
750
|
|
|
730
|
-
async getData() {
|
|
751
|
+
async getData(filters?: { horizonteDias?: string | number; cenario?: string }) {
|
|
752
|
+
const today = this.startOfDay(new Date());
|
|
753
|
+
|
|
731
754
|
const [
|
|
732
755
|
payablesResult,
|
|
733
756
|
receivablesResult,
|
|
@@ -735,9 +758,13 @@ export class FinanceService {
|
|
|
735
758
|
categoriesResult,
|
|
736
759
|
costCentersResult,
|
|
737
760
|
bankAccountsResult,
|
|
761
|
+
bankStatementsResult,
|
|
762
|
+
collectionsDefaultResult,
|
|
738
763
|
tagsResult,
|
|
739
764
|
auditLogsResult,
|
|
740
765
|
openPeriodResult,
|
|
766
|
+
scenarioSettingsResult,
|
|
767
|
+
confirmedSettlementsResult,
|
|
741
768
|
] = await Promise.allSettled([
|
|
742
769
|
this.loadTitles('payable'),
|
|
743
770
|
this.loadTitles('receivable'),
|
|
@@ -745,9 +772,13 @@ export class FinanceService {
|
|
|
745
772
|
this.loadCategories(),
|
|
746
773
|
this.loadCostCenters(),
|
|
747
774
|
this.loadBankAccounts(),
|
|
775
|
+
this.listBankStatements(),
|
|
776
|
+
this.getAccountsReceivableCollectionsDefault(),
|
|
748
777
|
this.loadTags(),
|
|
749
778
|
this.loadAuditLogs(),
|
|
750
779
|
this.loadOpenPeriod(),
|
|
780
|
+
this.loadFinancialScenarioSettings(),
|
|
781
|
+
this.loadConfirmedSettlements(today, this.addDays(today, 365)),
|
|
751
782
|
]);
|
|
752
783
|
|
|
753
784
|
const payables = payablesResult.status === 'fulfilled' ? payablesResult.value : [];
|
|
@@ -760,11 +791,40 @@ export class FinanceService {
|
|
|
760
791
|
costCentersResult.status === 'fulfilled' ? costCentersResult.value : [];
|
|
761
792
|
const bankAccounts =
|
|
762
793
|
bankAccountsResult.status === 'fulfilled' ? bankAccountsResult.value : [];
|
|
794
|
+
const bankStatements =
|
|
795
|
+
bankStatementsResult.status === 'fulfilled'
|
|
796
|
+
? bankStatementsResult.value
|
|
797
|
+
: [];
|
|
798
|
+
const collectionsDefault =
|
|
799
|
+
collectionsDefaultResult.status === 'fulfilled'
|
|
800
|
+
? collectionsDefaultResult.value
|
|
801
|
+
: {
|
|
802
|
+
agingInadimplencia: [],
|
|
803
|
+
historicoContatos: [],
|
|
804
|
+
};
|
|
763
805
|
const tags = tagsResult.status === 'fulfilled' ? tagsResult.value : [];
|
|
764
806
|
const auditLogs =
|
|
765
807
|
auditLogsResult.status === 'fulfilled' ? auditLogsResult.value : [];
|
|
766
808
|
const openPeriod =
|
|
767
809
|
openPeriodResult.status === 'fulfilled' ? openPeriodResult.value : null;
|
|
810
|
+
const confirmedSettlements =
|
|
811
|
+
confirmedSettlementsResult.status === 'fulfilled'
|
|
812
|
+
? confirmedSettlementsResult.value
|
|
813
|
+
: [];
|
|
814
|
+
const scenarioSettings =
|
|
815
|
+
scenarioSettingsResult.status === 'fulfilled'
|
|
816
|
+
? scenarioSettingsResult.value
|
|
817
|
+
: this.getDefaultFinancialScenarioSettings();
|
|
818
|
+
|
|
819
|
+
const horizonteDias = this.resolveHorizonDays(
|
|
820
|
+
filters?.horizonteDias,
|
|
821
|
+
scenarioSettings.defaultHorizonDays,
|
|
822
|
+
);
|
|
823
|
+
const cenario = this.resolveForecastScenario(
|
|
824
|
+
filters?.cenario,
|
|
825
|
+
scenarioSettings.defaultScenario,
|
|
826
|
+
);
|
|
827
|
+
const forecastEnd = this.addDays(today, horizonteDias);
|
|
768
828
|
|
|
769
829
|
if (payablesResult.status === 'rejected') {
|
|
770
830
|
this.logger.error('Failed to load finance payables', payablesResult.reason);
|
|
@@ -784,6 +844,18 @@ export class FinanceService {
|
|
|
784
844
|
if (bankAccountsResult.status === 'rejected') {
|
|
785
845
|
this.logger.error('Failed to load finance bank accounts', bankAccountsResult.reason);
|
|
786
846
|
}
|
|
847
|
+
if (bankStatementsResult.status === 'rejected') {
|
|
848
|
+
this.logger.error(
|
|
849
|
+
'Failed to load finance bank statements',
|
|
850
|
+
bankStatementsResult.reason,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
if (collectionsDefaultResult.status === 'rejected') {
|
|
854
|
+
this.logger.error(
|
|
855
|
+
'Failed to load finance collections default',
|
|
856
|
+
collectionsDefaultResult.reason,
|
|
857
|
+
);
|
|
858
|
+
}
|
|
787
859
|
if (tagsResult.status === 'rejected') {
|
|
788
860
|
this.logger.error('Failed to load finance tags', tagsResult.reason);
|
|
789
861
|
}
|
|
@@ -793,6 +865,18 @@ export class FinanceService {
|
|
|
793
865
|
if (openPeriodResult.status === 'rejected') {
|
|
794
866
|
this.logger.error('Failed to load finance open period', openPeriodResult.reason);
|
|
795
867
|
}
|
|
868
|
+
if (confirmedSettlementsResult.status === 'rejected') {
|
|
869
|
+
this.logger.error(
|
|
870
|
+
'Failed to load finance confirmed settlements',
|
|
871
|
+
confirmedSettlementsResult.reason,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
if (scenarioSettingsResult.status === 'rejected') {
|
|
875
|
+
this.logger.error(
|
|
876
|
+
'Failed to load finance scenario settings',
|
|
877
|
+
scenarioSettingsResult.reason,
|
|
878
|
+
);
|
|
879
|
+
}
|
|
796
880
|
|
|
797
881
|
const aprovacoesPendentes = payables
|
|
798
882
|
.filter((title: any) => title.status === 'rascunho')
|
|
@@ -807,32 +891,581 @@ export class FinanceService {
|
|
|
807
891
|
}));
|
|
808
892
|
|
|
809
893
|
const kpis = this.calculateDashboardKpis(payables, receivables, bankAccounts);
|
|
894
|
+
const { recebiveis, adquirentes } =
|
|
895
|
+
this.buildReceivablesCalendarData(receivables);
|
|
896
|
+
const { fluxoCaixaPrevisto, entradasPrevistas, saidasPrevistas } =
|
|
897
|
+
this.buildCashFlowForecastData(
|
|
898
|
+
payables,
|
|
899
|
+
receivables,
|
|
900
|
+
confirmedSettlements,
|
|
901
|
+
kpis.saldoCaixa,
|
|
902
|
+
today,
|
|
903
|
+
forecastEnd,
|
|
904
|
+
scenarioSettings.map[cenario],
|
|
905
|
+
);
|
|
810
906
|
|
|
811
907
|
return {
|
|
812
908
|
kpis,
|
|
813
|
-
|
|
909
|
+
defaultScenario: scenarioSettings.defaultScenario,
|
|
910
|
+
defaultHorizonDays: scenarioSettings.defaultHorizonDays,
|
|
911
|
+
fluxoCaixaPrevisto,
|
|
814
912
|
titulosPagar: payables,
|
|
815
913
|
titulosReceber: receivables,
|
|
816
|
-
extratos:
|
|
914
|
+
extratos: bankStatements,
|
|
817
915
|
contasBancarias: bankAccounts,
|
|
818
916
|
pessoas: people,
|
|
819
917
|
categorias: categories,
|
|
820
918
|
centrosCusto: costCenters,
|
|
821
919
|
aprovacoesPendentes,
|
|
822
|
-
agingInadimplencia:
|
|
823
|
-
cenarios:
|
|
920
|
+
agingInadimplencia: collectionsDefault.agingInadimplencia,
|
|
921
|
+
cenarios: scenarioSettings.cenarios,
|
|
824
922
|
transferencias: [],
|
|
825
923
|
tags,
|
|
826
924
|
logsAuditoria: auditLogs,
|
|
827
|
-
recebiveis
|
|
828
|
-
adquirentes
|
|
829
|
-
historicoContatos:
|
|
830
|
-
entradasPrevistas
|
|
831
|
-
saidasPrevistas
|
|
925
|
+
recebiveis,
|
|
926
|
+
adquirentes,
|
|
927
|
+
historicoContatos: collectionsDefault.historicoContatos,
|
|
928
|
+
entradasPrevistas,
|
|
929
|
+
saidasPrevistas,
|
|
832
930
|
periodoAberto: openPeriod,
|
|
833
931
|
};
|
|
834
932
|
}
|
|
835
933
|
|
|
934
|
+
async updateScenarioSettings(
|
|
935
|
+
scenario: string,
|
|
936
|
+
data: {
|
|
937
|
+
atrasoMedio: number;
|
|
938
|
+
taxaInadimplencia: number;
|
|
939
|
+
crescimentoReceita: number;
|
|
940
|
+
setAsDefault?: boolean;
|
|
941
|
+
},
|
|
942
|
+
) {
|
|
943
|
+
const scenarioSlug = this.resolveForecastScenarioStrict(scenario);
|
|
944
|
+
|
|
945
|
+
if (data.atrasoMedio < 0 || data.atrasoMedio > 365) {
|
|
946
|
+
throw new BadRequestException('Invalid average delay value');
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (data.taxaInadimplencia < 0 || data.taxaInadimplencia > 100) {
|
|
950
|
+
throw new BadRequestException('Invalid default rate value');
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (data.crescimentoReceita < -100 || data.crescimentoReceita > 1000) {
|
|
954
|
+
throw new BadRequestException('Invalid revenue growth value');
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const prefix = `finance-scenario-${
|
|
958
|
+
scenarioSlug === 'pessimista'
|
|
959
|
+
? 'pessimistic'
|
|
960
|
+
: scenarioSlug === 'otimista'
|
|
961
|
+
? 'optimistic'
|
|
962
|
+
: 'base'
|
|
963
|
+
}`;
|
|
964
|
+
|
|
965
|
+
const settingUpdates: Array<{ slug: string; value: string }> = [
|
|
966
|
+
{
|
|
967
|
+
slug: `${prefix}-average-delay-days`,
|
|
968
|
+
value: String(data.atrasoMedio),
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
slug: `${prefix}-default-rate-percent`,
|
|
972
|
+
value: String(data.taxaInadimplencia),
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
slug: `${prefix}-revenue-growth-percent`,
|
|
976
|
+
value: String(data.crescimentoReceita),
|
|
977
|
+
},
|
|
978
|
+
];
|
|
979
|
+
|
|
980
|
+
if (data.setAsDefault !== false) {
|
|
981
|
+
settingUpdates.push({
|
|
982
|
+
slug: 'finance-default-scenario',
|
|
983
|
+
value: scenarioSlug,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
await this.settingService.setManySettings({
|
|
988
|
+
setting: settingUpdates,
|
|
989
|
+
} as any);
|
|
990
|
+
|
|
991
|
+
const settings = await this.loadFinancialScenarioSettings();
|
|
992
|
+
return {
|
|
993
|
+
success: true,
|
|
994
|
+
cenarios: settings.cenarios,
|
|
995
|
+
defaultScenario: settings.defaultScenario,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private resolveHorizonDays(value?: string | number, fallback = 90) {
|
|
1000
|
+
const parsed = Number(value);
|
|
1001
|
+
const allowed = new Set([30, 60, 90, 180, 365]);
|
|
1002
|
+
|
|
1003
|
+
if (!Number.isFinite(parsed)) {
|
|
1004
|
+
return allowed.has(fallback) ? fallback : 90;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return allowed.has(parsed) ? parsed : allowed.has(fallback) ? fallback : 90;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
private resolveForecastScenario(
|
|
1011
|
+
value?: string,
|
|
1012
|
+
fallback: ForecastScenario = 'base',
|
|
1013
|
+
): ForecastScenario {
|
|
1014
|
+
if (value === 'pessimista') {
|
|
1015
|
+
return 'pessimista';
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (value === 'otimista') {
|
|
1019
|
+
return 'otimista';
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return fallback;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
private resolveForecastScenarioStrict(value?: string): ForecastScenario {
|
|
1026
|
+
if (value === 'base' || value === 'pessimista' || value === 'otimista') {
|
|
1027
|
+
return value;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
throw new BadRequestException('Invalid scenario');
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
private buildCashFlowForecastData(
|
|
1034
|
+
payables: any[],
|
|
1035
|
+
receivables: any[],
|
|
1036
|
+
confirmedSettlements: Array<{
|
|
1037
|
+
settled_at: Date;
|
|
1038
|
+
amount_cents: bigint;
|
|
1039
|
+
settlement_type: string;
|
|
1040
|
+
}>,
|
|
1041
|
+
initialBalance: number,
|
|
1042
|
+
fromDate: Date,
|
|
1043
|
+
toDate: Date,
|
|
1044
|
+
scenarioConfig: FinancialScenarioConfig,
|
|
1045
|
+
) {
|
|
1046
|
+
const scenarioMultiplier = this.resolveScenarioMultiplier(scenarioConfig);
|
|
1047
|
+
|
|
1048
|
+
const entradasPrevistas = this.buildExpectedCashFlowEntries(
|
|
1049
|
+
receivables,
|
|
1050
|
+
fromDate,
|
|
1051
|
+
toDate,
|
|
1052
|
+
'inflow',
|
|
1053
|
+
).map((item) => ({
|
|
1054
|
+
...item,
|
|
1055
|
+
valor: Number((item.valor * scenarioMultiplier.inflow).toFixed(2)),
|
|
1056
|
+
}));
|
|
1057
|
+
|
|
1058
|
+
const saidasPrevistas = this.buildExpectedCashFlowEntries(
|
|
1059
|
+
payables,
|
|
1060
|
+
fromDate,
|
|
1061
|
+
toDate,
|
|
1062
|
+
'outflow',
|
|
1063
|
+
).map((item) => ({
|
|
1064
|
+
...item,
|
|
1065
|
+
valor: Number((item.valor * scenarioMultiplier.outflow).toFixed(2)),
|
|
1066
|
+
}));
|
|
1067
|
+
|
|
1068
|
+
const expectedByDate = new Map<
|
|
1069
|
+
string,
|
|
1070
|
+
{ entradas: number; saidas: number; realizado: number }
|
|
1071
|
+
>();
|
|
1072
|
+
|
|
1073
|
+
for (const entry of entradasPrevistas) {
|
|
1074
|
+
const dateKey = entry.vencimento.slice(0, 10);
|
|
1075
|
+
const current = expectedByDate.get(dateKey) || {
|
|
1076
|
+
entradas: 0,
|
|
1077
|
+
saidas: 0,
|
|
1078
|
+
realizado: 0,
|
|
1079
|
+
};
|
|
1080
|
+
current.entradas = Number((current.entradas + Number(entry.valor || 0)).toFixed(2));
|
|
1081
|
+
expectedByDate.set(dateKey, current);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
for (const outflow of saidasPrevistas) {
|
|
1085
|
+
const dateKey = outflow.vencimento.slice(0, 10);
|
|
1086
|
+
const current = expectedByDate.get(dateKey) || {
|
|
1087
|
+
entradas: 0,
|
|
1088
|
+
saidas: 0,
|
|
1089
|
+
realizado: 0,
|
|
1090
|
+
};
|
|
1091
|
+
current.saidas = Number((current.saidas + Number(outflow.valor || 0)).toFixed(2));
|
|
1092
|
+
expectedByDate.set(dateKey, current);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
for (const settlement of confirmedSettlements) {
|
|
1096
|
+
const dateKey = settlement.settled_at.toISOString().slice(0, 10);
|
|
1097
|
+
const current = expectedByDate.get(dateKey) || {
|
|
1098
|
+
entradas: 0,
|
|
1099
|
+
saidas: 0,
|
|
1100
|
+
realizado: 0,
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
const amount = this.fromCents(settlement.amount_cents);
|
|
1104
|
+
if (settlement.settlement_type === 'payable') {
|
|
1105
|
+
current.realizado = Number((current.realizado - amount).toFixed(2));
|
|
1106
|
+
} else if (settlement.settlement_type === 'receivable') {
|
|
1107
|
+
current.realizado = Number((current.realizado + amount).toFixed(2));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
expectedByDate.set(dateKey, current);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const fluxoCaixaPrevisto: Array<{
|
|
1114
|
+
data: string;
|
|
1115
|
+
saldoPrevisto: number;
|
|
1116
|
+
saldoRealizado: number;
|
|
1117
|
+
}> = [];
|
|
1118
|
+
|
|
1119
|
+
let runningProjectedBalance = Number(initialBalance || 0);
|
|
1120
|
+
let runningActualBalance = Number(initialBalance || 0);
|
|
1121
|
+
|
|
1122
|
+
for (
|
|
1123
|
+
let cursor = this.startOfDay(fromDate);
|
|
1124
|
+
cursor <= toDate;
|
|
1125
|
+
cursor = this.addDays(cursor, 1)
|
|
1126
|
+
) {
|
|
1127
|
+
const key = cursor.toISOString().slice(0, 10);
|
|
1128
|
+
const dayData = expectedByDate.get(key) || {
|
|
1129
|
+
entradas: 0,
|
|
1130
|
+
saidas: 0,
|
|
1131
|
+
realizado: 0,
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
runningProjectedBalance = Number(
|
|
1135
|
+
(runningProjectedBalance + dayData.entradas - dayData.saidas).toFixed(2),
|
|
1136
|
+
);
|
|
1137
|
+
runningActualBalance = Number(
|
|
1138
|
+
(runningActualBalance + dayData.realizado).toFixed(2),
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
fluxoCaixaPrevisto.push({
|
|
1142
|
+
data: cursor.toISOString(),
|
|
1143
|
+
saldoPrevisto: runningProjectedBalance,
|
|
1144
|
+
saldoRealizado: runningActualBalance,
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
return {
|
|
1149
|
+
fluxoCaixaPrevisto,
|
|
1150
|
+
entradasPrevistas,
|
|
1151
|
+
saidasPrevistas,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
private resolveScenarioMultiplier(scenarioConfig: FinancialScenarioConfig) {
|
|
1156
|
+
const growthRate = scenarioConfig.crescimentoReceita / 100;
|
|
1157
|
+
const defaultRate = scenarioConfig.taxaInadimplencia / 100;
|
|
1158
|
+
const delayImpact = Math.max(0.5, 1 - scenarioConfig.atrasoMedio / 100);
|
|
1159
|
+
|
|
1160
|
+
const inflow = this.clampNumber(
|
|
1161
|
+
(1 + growthRate) * (1 - defaultRate) * delayImpact,
|
|
1162
|
+
0.1,
|
|
1163
|
+
3,
|
|
1164
|
+
);
|
|
1165
|
+
|
|
1166
|
+
const outflow = this.clampNumber(
|
|
1167
|
+
1 + Math.max(0, -growthRate) * 0.4 + defaultRate * 0.2,
|
|
1168
|
+
0.1,
|
|
1169
|
+
3,
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
return {
|
|
1173
|
+
inflow,
|
|
1174
|
+
outflow,
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
private clampNumber(value: number, min: number, max: number) {
|
|
1179
|
+
if (!Number.isFinite(value)) {
|
|
1180
|
+
return min;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return Math.min(max, Math.max(min, value));
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
private async loadFinancialScenarioSettings(): Promise<FinancialScenarioSettings> {
|
|
1187
|
+
const defaultSettings = this.getDefaultFinancialScenarioSettings();
|
|
1188
|
+
const values = await this.settingService.getSettingValues([
|
|
1189
|
+
'finance-default-scenario',
|
|
1190
|
+
'finance-default-horizon-days',
|
|
1191
|
+
'finance-scenario-base-average-delay-days',
|
|
1192
|
+
'finance-scenario-base-default-rate-percent',
|
|
1193
|
+
'finance-scenario-base-revenue-growth-percent',
|
|
1194
|
+
'finance-scenario-pessimistic-average-delay-days',
|
|
1195
|
+
'finance-scenario-pessimistic-default-rate-percent',
|
|
1196
|
+
'finance-scenario-pessimistic-revenue-growth-percent',
|
|
1197
|
+
'finance-scenario-optimistic-average-delay-days',
|
|
1198
|
+
'finance-scenario-optimistic-default-rate-percent',
|
|
1199
|
+
'finance-scenario-optimistic-revenue-growth-percent',
|
|
1200
|
+
]);
|
|
1201
|
+
|
|
1202
|
+
const base: FinancialScenarioConfig = {
|
|
1203
|
+
...defaultSettings.map.base,
|
|
1204
|
+
atrasoMedio: this.numberSetting(
|
|
1205
|
+
values['finance-scenario-base-average-delay-days'],
|
|
1206
|
+
defaultSettings.map.base.atrasoMedio,
|
|
1207
|
+
),
|
|
1208
|
+
taxaInadimplencia: this.numberSetting(
|
|
1209
|
+
values['finance-scenario-base-default-rate-percent'],
|
|
1210
|
+
defaultSettings.map.base.taxaInadimplencia,
|
|
1211
|
+
),
|
|
1212
|
+
crescimentoReceita: this.numberSetting(
|
|
1213
|
+
values['finance-scenario-base-revenue-growth-percent'],
|
|
1214
|
+
defaultSettings.map.base.crescimentoReceita,
|
|
1215
|
+
),
|
|
1216
|
+
padrao: false,
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
const pessimista: FinancialScenarioConfig = {
|
|
1220
|
+
...defaultSettings.map.pessimista,
|
|
1221
|
+
atrasoMedio: this.numberSetting(
|
|
1222
|
+
values['finance-scenario-pessimistic-average-delay-days'],
|
|
1223
|
+
defaultSettings.map.pessimista.atrasoMedio,
|
|
1224
|
+
),
|
|
1225
|
+
taxaInadimplencia: this.numberSetting(
|
|
1226
|
+
values['finance-scenario-pessimistic-default-rate-percent'],
|
|
1227
|
+
defaultSettings.map.pessimista.taxaInadimplencia,
|
|
1228
|
+
),
|
|
1229
|
+
crescimentoReceita: this.numberSetting(
|
|
1230
|
+
values['finance-scenario-pessimistic-revenue-growth-percent'],
|
|
1231
|
+
defaultSettings.map.pessimista.crescimentoReceita,
|
|
1232
|
+
),
|
|
1233
|
+
padrao: false,
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
const otimista: FinancialScenarioConfig = {
|
|
1237
|
+
...defaultSettings.map.otimista,
|
|
1238
|
+
atrasoMedio: this.numberSetting(
|
|
1239
|
+
values['finance-scenario-optimistic-average-delay-days'],
|
|
1240
|
+
defaultSettings.map.otimista.atrasoMedio,
|
|
1241
|
+
),
|
|
1242
|
+
taxaInadimplencia: this.numberSetting(
|
|
1243
|
+
values['finance-scenario-optimistic-default-rate-percent'],
|
|
1244
|
+
defaultSettings.map.otimista.taxaInadimplencia,
|
|
1245
|
+
),
|
|
1246
|
+
crescimentoReceita: this.numberSetting(
|
|
1247
|
+
values['finance-scenario-optimistic-revenue-growth-percent'],
|
|
1248
|
+
defaultSettings.map.otimista.crescimentoReceita,
|
|
1249
|
+
),
|
|
1250
|
+
padrao: false,
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
const defaultScenario = this.resolveForecastScenario(
|
|
1254
|
+
String(values['finance-default-scenario'] || ''),
|
|
1255
|
+
defaultSettings.defaultScenario,
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
const cenarios = [base, pessimista, otimista].map((item) => ({
|
|
1259
|
+
...item,
|
|
1260
|
+
padrao: item.id === defaultScenario,
|
|
1261
|
+
}));
|
|
1262
|
+
|
|
1263
|
+
const map = {
|
|
1264
|
+
base: cenarios.find((item) => item.id === 'base') || defaultSettings.map.base,
|
|
1265
|
+
pessimista:
|
|
1266
|
+
cenarios.find((item) => item.id === 'pessimista') ||
|
|
1267
|
+
defaultSettings.map.pessimista,
|
|
1268
|
+
otimista:
|
|
1269
|
+
cenarios.find((item) => item.id === 'otimista') ||
|
|
1270
|
+
defaultSettings.map.otimista,
|
|
1271
|
+
} as Record<ForecastScenario, FinancialScenarioConfig>;
|
|
1272
|
+
|
|
1273
|
+
return {
|
|
1274
|
+
defaultScenario,
|
|
1275
|
+
defaultHorizonDays: this.resolveHorizonDays(
|
|
1276
|
+
values['finance-default-horizon-days'],
|
|
1277
|
+
defaultSettings.defaultHorizonDays,
|
|
1278
|
+
),
|
|
1279
|
+
cenarios,
|
|
1280
|
+
map,
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
private getDefaultFinancialScenarioSettings(): FinancialScenarioSettings {
|
|
1285
|
+
const base: FinancialScenarioConfig = {
|
|
1286
|
+
id: 'base',
|
|
1287
|
+
nome: 'Base',
|
|
1288
|
+
descricao: 'Cenário de referência para projeções',
|
|
1289
|
+
atrasoMedio: 5,
|
|
1290
|
+
taxaInadimplencia: 3,
|
|
1291
|
+
crescimentoReceita: 5,
|
|
1292
|
+
padrao: true,
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
const pessimista: FinancialScenarioConfig = {
|
|
1296
|
+
id: 'pessimista',
|
|
1297
|
+
nome: 'Pessimista',
|
|
1298
|
+
descricao: 'Premissas conservadoras com maior risco',
|
|
1299
|
+
atrasoMedio: 12,
|
|
1300
|
+
taxaInadimplencia: 6,
|
|
1301
|
+
crescimentoReceita: -5,
|
|
1302
|
+
padrao: false,
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
const otimista: FinancialScenarioConfig = {
|
|
1306
|
+
id: 'otimista',
|
|
1307
|
+
nome: 'Otimista',
|
|
1308
|
+
descricao: 'Premissas favoráveis de crescimento',
|
|
1309
|
+
atrasoMedio: 2,
|
|
1310
|
+
taxaInadimplencia: 1.5,
|
|
1311
|
+
crescimentoReceita: 10,
|
|
1312
|
+
padrao: false,
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
const cenarios = [base, pessimista, otimista];
|
|
1316
|
+
|
|
1317
|
+
return {
|
|
1318
|
+
defaultScenario: 'base',
|
|
1319
|
+
defaultHorizonDays: 90,
|
|
1320
|
+
cenarios,
|
|
1321
|
+
map: {
|
|
1322
|
+
base,
|
|
1323
|
+
pessimista,
|
|
1324
|
+
otimista,
|
|
1325
|
+
},
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
private numberSetting(value: any, fallback: number) {
|
|
1330
|
+
const parsed = Number(value);
|
|
1331
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
private buildExpectedCashFlowEntries(
|
|
1335
|
+
titles: any[],
|
|
1336
|
+
fromDate: Date,
|
|
1337
|
+
toDate: Date,
|
|
1338
|
+
direction: 'inflow' | 'outflow',
|
|
1339
|
+
) {
|
|
1340
|
+
return (titles || [])
|
|
1341
|
+
.flatMap((title) =>
|
|
1342
|
+
(title?.parcelas || [])
|
|
1343
|
+
.filter((installment) => {
|
|
1344
|
+
const dueDate = this.startOfDay(new Date(installment.vencimento));
|
|
1345
|
+
if (Number.isNaN(dueDate.getTime())) return false;
|
|
1346
|
+
|
|
1347
|
+
const openAmount = Number(installment.valorAberto || 0);
|
|
1348
|
+
const status = String(installment.status || '').toLowerCase();
|
|
1349
|
+
const isOpen =
|
|
1350
|
+
status === 'aberto' || status === 'vencido' || status === 'parcial';
|
|
1351
|
+
|
|
1352
|
+
return (
|
|
1353
|
+
dueDate >= fromDate &&
|
|
1354
|
+
dueDate <= toDate &&
|
|
1355
|
+
isOpen &&
|
|
1356
|
+
openAmount > 0
|
|
1357
|
+
);
|
|
1358
|
+
})
|
|
1359
|
+
.map((installment) => ({
|
|
1360
|
+
categoria:
|
|
1361
|
+
title?.descricao ||
|
|
1362
|
+
(direction === 'inflow' ? title?.cliente : title?.fornecedor) ||
|
|
1363
|
+
'Sem categoria',
|
|
1364
|
+
vencimento: installment.vencimento,
|
|
1365
|
+
valor: Number(installment.valorAberto || installment.valor || 0),
|
|
1366
|
+
})),
|
|
1367
|
+
)
|
|
1368
|
+
.sort(
|
|
1369
|
+
(a, b) =>
|
|
1370
|
+
new Date(a.vencimento).getTime() - new Date(b.vencimento).getTime(),
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
private buildReceivablesCalendarData(receivables: any[]) {
|
|
1375
|
+
const recebiveis = receivables
|
|
1376
|
+
.flatMap((title: any) =>
|
|
1377
|
+
(title.parcelas || []).map((installment: any) => {
|
|
1378
|
+
const bruto = Number(installment.valor || 0);
|
|
1379
|
+
const canal = this.mapReceivableChannelLabel(
|
|
1380
|
+
installment.metodoPagamento || title.canal,
|
|
1381
|
+
);
|
|
1382
|
+
const taxaPercentual =
|
|
1383
|
+
canal === 'Cartão'
|
|
1384
|
+
? 0.03
|
|
1385
|
+
: canal === 'Boleto'
|
|
1386
|
+
? 0.015
|
|
1387
|
+
: canal === 'Pix'
|
|
1388
|
+
? 0.005
|
|
1389
|
+
: 0.01;
|
|
1390
|
+
const taxas = Number((bruto * taxaPercentual).toFixed(2));
|
|
1391
|
+
const liquido = Number((bruto - taxas).toFixed(2));
|
|
1392
|
+
|
|
1393
|
+
return {
|
|
1394
|
+
id: `${title.id}-${installment.id}`,
|
|
1395
|
+
canal,
|
|
1396
|
+
adquirente: title.cliente || '-',
|
|
1397
|
+
dataPrevista: installment.vencimento,
|
|
1398
|
+
bruto,
|
|
1399
|
+
taxas,
|
|
1400
|
+
liquido,
|
|
1401
|
+
status: this.mapReceivableCalendarStatus(installment.status),
|
|
1402
|
+
};
|
|
1403
|
+
}),
|
|
1404
|
+
)
|
|
1405
|
+
.sort(
|
|
1406
|
+
(a, b) =>
|
|
1407
|
+
new Date(a.dataPrevista).getTime() - new Date(b.dataPrevista).getTime(),
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
const adquirenteMap = new Map<
|
|
1411
|
+
string,
|
|
1412
|
+
{ nome: string; transacoes: number; total: number }
|
|
1413
|
+
>();
|
|
1414
|
+
|
|
1415
|
+
for (const recebivel of recebiveis) {
|
|
1416
|
+
const current = adquirenteMap.get(recebivel.adquirente) || {
|
|
1417
|
+
nome: recebivel.adquirente,
|
|
1418
|
+
transacoes: 0,
|
|
1419
|
+
total: 0,
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
current.transacoes += 1;
|
|
1423
|
+
current.total = Number((current.total + recebivel.liquido).toFixed(2));
|
|
1424
|
+
adquirenteMap.set(recebivel.adquirente, current);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const adquirentes = Array.from(adquirenteMap.values()).sort(
|
|
1428
|
+
(a, b) => b.total - a.total,
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
return {
|
|
1432
|
+
recebiveis,
|
|
1433
|
+
adquirentes,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
private mapReceivableCalendarStatus(status?: string | null) {
|
|
1438
|
+
const normalized = (status || '').toLowerCase();
|
|
1439
|
+
|
|
1440
|
+
if (normalized === 'liquidado') {
|
|
1441
|
+
return 'liquidado';
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (['aberto', 'aprovado', 'parcial'].includes(normalized)) {
|
|
1445
|
+
return 'confirmado';
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
return 'pendente';
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
private mapReceivableChannelLabel(channel?: string | null) {
|
|
1452
|
+
const normalized = (channel || '').toLowerCase();
|
|
1453
|
+
const channelMap: Record<string, string> = {
|
|
1454
|
+
cartao: 'Cartão',
|
|
1455
|
+
cartão: 'Cartão',
|
|
1456
|
+
pix: 'Pix',
|
|
1457
|
+
boleto: 'Boleto',
|
|
1458
|
+
transferencia: 'Transferência',
|
|
1459
|
+
transferência: 'Transferência',
|
|
1460
|
+
ted: 'Transferência',
|
|
1461
|
+
doc: 'Transferência',
|
|
1462
|
+
dinheiro: 'Dinheiro',
|
|
1463
|
+
cheque: 'Cheque',
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
return channelMap[normalized] || 'Transferência';
|
|
1467
|
+
}
|
|
1468
|
+
|
|
836
1469
|
async getAccountsReceivableCollectionsDefault() {
|
|
837
1470
|
const today = this.startOfDay(new Date());
|
|
838
1471
|
|
|
@@ -1036,6 +1669,12 @@ export class FinanceService {
|
|
|
1036
1669
|
const remainder = totalAmountCents % data.installments;
|
|
1037
1670
|
|
|
1038
1671
|
const created = await this.prisma.$transaction(async (tx) => {
|
|
1672
|
+
await this.assertDateNotInClosedPeriod(
|
|
1673
|
+
tx,
|
|
1674
|
+
firstDueDate,
|
|
1675
|
+
'register collection agreement',
|
|
1676
|
+
);
|
|
1677
|
+
|
|
1039
1678
|
const title = await tx.financial_title.create({
|
|
1040
1679
|
data: {
|
|
1041
1680
|
person_id: person.id,
|
|
@@ -1058,6 +1697,12 @@ export class FinanceService {
|
|
|
1058
1697
|
const amountCents =
|
|
1059
1698
|
baseInstallmentCents + (index === data.installments - 1 ? remainder : 0);
|
|
1060
1699
|
|
|
1700
|
+
await this.assertDateNotInClosedPeriod(
|
|
1701
|
+
tx,
|
|
1702
|
+
dueDate,
|
|
1703
|
+
'register collection agreement installment',
|
|
1704
|
+
);
|
|
1705
|
+
|
|
1061
1706
|
await tx.financial_installment.create({
|
|
1062
1707
|
data: {
|
|
1063
1708
|
title_id: title.id,
|
|
@@ -1798,6 +2443,8 @@ export class FinanceService {
|
|
|
1798
2443
|
)}`;
|
|
1799
2444
|
|
|
1800
2445
|
await this.prisma.$transaction(async (tx) => {
|
|
2446
|
+
await this.assertDateNotInClosedPeriod(tx, postedDate, 'create transfer');
|
|
2447
|
+
|
|
1801
2448
|
const sourceStatement = await tx.bank_statement.create({
|
|
1802
2449
|
data: {
|
|
1803
2450
|
bank_account_id: sourceAccountId,
|
|
@@ -2257,6 +2904,12 @@ export class FinanceService {
|
|
|
2257
2904
|
const reference = `adjustment:${Date.now()}-${Math.round(Math.random() * 1_000_000)}`;
|
|
2258
2905
|
|
|
2259
2906
|
const created = await this.prisma.$transaction(async (tx) => {
|
|
2907
|
+
await this.assertDateNotInClosedPeriod(
|
|
2908
|
+
tx,
|
|
2909
|
+
postedAt,
|
|
2910
|
+
'create bank statement adjustment',
|
|
2911
|
+
);
|
|
2912
|
+
|
|
2260
2913
|
const statement = await tx.bank_statement.create({
|
|
2261
2914
|
data: {
|
|
2262
2915
|
bank_account_id: bankAccountId,
|
|
@@ -2393,35 +3046,53 @@ export class FinanceService {
|
|
|
2393
3046
|
24,
|
|
2394
3047
|
);
|
|
2395
3048
|
|
|
2396
|
-
const statement = await this.prisma
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
.map((entry) => entry.postedDate)
|
|
2403
|
-
.sort((a, b) => a.getTime() - b.getTime())[0],
|
|
2404
|
-
period_end: normalizedEntries
|
|
2405
|
-
.map((entry) => entry.postedDate)
|
|
2406
|
-
.sort((a, b) => b.getTime() - a.getTime())[0],
|
|
2407
|
-
imported_at: new Date(),
|
|
2408
|
-
imported_by_user_id: userId || null,
|
|
2409
|
-
},
|
|
2410
|
-
select: { id: true },
|
|
2411
|
-
});
|
|
3049
|
+
const statement = await this.prisma.$transaction(async (tx) => {
|
|
3050
|
+
const uniquePostedDates = [
|
|
3051
|
+
...new Set(
|
|
3052
|
+
normalizedEntries.map((entry) => entry.postedDate.toISOString().slice(0, 10)),
|
|
3053
|
+
),
|
|
3054
|
+
];
|
|
2412
3055
|
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
3056
|
+
for (const postedDateText of uniquePostedDates) {
|
|
3057
|
+
await this.assertDateNotInClosedPeriod(
|
|
3058
|
+
tx,
|
|
3059
|
+
new Date(`${postedDateText}T00:00:00.000Z`),
|
|
3060
|
+
'import bank statements',
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
const createdStatement = await tx.bank_statement.create({
|
|
3065
|
+
data: {
|
|
3066
|
+
bank_account_id: bankAccountId,
|
|
3067
|
+
source_type: sourceType,
|
|
3068
|
+
idempotency_key: `import-${uploadedFile.id}-${statementFingerprint}`,
|
|
3069
|
+
period_start: normalizedEntries
|
|
3070
|
+
.map((entry) => entry.postedDate)
|
|
3071
|
+
.sort((a, b) => a.getTime() - b.getTime())[0],
|
|
3072
|
+
period_end: normalizedEntries
|
|
3073
|
+
.map((entry) => entry.postedDate)
|
|
3074
|
+
.sort((a, b) => b.getTime() - a.getTime())[0],
|
|
3075
|
+
imported_at: new Date(),
|
|
3076
|
+
imported_by_user_id: userId || null,
|
|
3077
|
+
},
|
|
3078
|
+
select: { id: true },
|
|
3079
|
+
});
|
|
3080
|
+
|
|
3081
|
+
await tx.bank_statement_line.createMany({
|
|
3082
|
+
data: normalizedEntries.map((entry) => ({
|
|
3083
|
+
bank_statement_id: createdStatement.id,
|
|
3084
|
+
bank_account_id: bankAccountId,
|
|
3085
|
+
external_id: entry.externalId,
|
|
3086
|
+
posted_date: entry.postedDate,
|
|
3087
|
+
amount_cents: entry.amountCents,
|
|
3088
|
+
description: entry.description,
|
|
3089
|
+
status: 'imported',
|
|
3090
|
+
dedupe_key: entry.dedupeKey,
|
|
3091
|
+
})),
|
|
3092
|
+
skipDuplicates: true,
|
|
3093
|
+
});
|
|
3094
|
+
|
|
3095
|
+
return createdStatement;
|
|
2425
3096
|
});
|
|
2426
3097
|
|
|
2427
3098
|
return {
|
|
@@ -2755,40 +3426,51 @@ export class FinanceService {
|
|
|
2755
3426
|
const code = this.generateBankAccountCode(data.bank, data.account);
|
|
2756
3427
|
const name = data.description?.trim() || data.bank;
|
|
2757
3428
|
|
|
2758
|
-
const createdAccount = await this.prisma
|
|
2759
|
-
|
|
2760
|
-
code,
|
|
2761
|
-
name,
|
|
2762
|
-
bank_name: data.bank,
|
|
2763
|
-
agency: data.branch || null,
|
|
2764
|
-
account_number: data.account || null,
|
|
2765
|
-
account_type: accountType,
|
|
2766
|
-
status: 'active',
|
|
2767
|
-
},
|
|
2768
|
-
});
|
|
2769
|
-
|
|
2770
|
-
if (data.initial_balance && data.initial_balance > 0) {
|
|
2771
|
-
const statement = await this.prisma.bank_statement.create({
|
|
3429
|
+
const createdAccount = await this.prisma.$transaction(async (tx) => {
|
|
3430
|
+
const account = await tx.bank_account.create({
|
|
2772
3431
|
data: {
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
3432
|
+
code,
|
|
3433
|
+
name,
|
|
3434
|
+
bank_name: data.bank,
|
|
3435
|
+
agency: data.branch || null,
|
|
3436
|
+
account_number: data.account || null,
|
|
3437
|
+
account_type: accountType,
|
|
3438
|
+
status: 'active',
|
|
2777
3439
|
},
|
|
2778
3440
|
});
|
|
2779
3441
|
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
3442
|
+
if (data.initial_balance && data.initial_balance > 0) {
|
|
3443
|
+
const postedDate = new Date();
|
|
3444
|
+
await this.assertDateNotInClosedPeriod(
|
|
3445
|
+
tx,
|
|
3446
|
+
postedDate,
|
|
3447
|
+
'create bank account initial balance',
|
|
3448
|
+
);
|
|
3449
|
+
|
|
3450
|
+
const statement = await tx.bank_statement.create({
|
|
3451
|
+
data: {
|
|
3452
|
+
bank_account_id: account.id,
|
|
3453
|
+
source_type: 'csv',
|
|
3454
|
+
imported_at: postedDate,
|
|
3455
|
+
imported_by_user_id: userId,
|
|
3456
|
+
},
|
|
3457
|
+
});
|
|
3458
|
+
|
|
3459
|
+
await tx.bank_statement_line.create({
|
|
3460
|
+
data: {
|
|
3461
|
+
bank_statement_id: statement.id,
|
|
3462
|
+
bank_account_id: account.id,
|
|
3463
|
+
posted_date: postedDate,
|
|
3464
|
+
amount_cents: this.toCents(data.initial_balance),
|
|
3465
|
+
description: 'Saldo inicial',
|
|
3466
|
+
status: 'reconciled',
|
|
3467
|
+
dedupe_key: `initial-balance-${account.id}-${Date.now()}`,
|
|
3468
|
+
},
|
|
3469
|
+
});
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
return account;
|
|
3473
|
+
});
|
|
2792
3474
|
|
|
2793
3475
|
const account = await this.prisma.bank_account.findUnique({
|
|
2794
3476
|
where: { id: createdAccount.id },
|
|
@@ -4707,6 +5389,26 @@ export class FinanceService {
|
|
|
4707
5389
|
}));
|
|
4708
5390
|
}
|
|
4709
5391
|
|
|
5392
|
+
private async loadConfirmedSettlements(fromDate: Date, toDate: Date) {
|
|
5393
|
+
return this.prisma.settlement.findMany({
|
|
5394
|
+
where: {
|
|
5395
|
+
status: 'confirmed',
|
|
5396
|
+
settled_at: {
|
|
5397
|
+
gte: fromDate,
|
|
5398
|
+
lte: toDate,
|
|
5399
|
+
},
|
|
5400
|
+
},
|
|
5401
|
+
select: {
|
|
5402
|
+
settled_at: true,
|
|
5403
|
+
amount_cents: true,
|
|
5404
|
+
settlement_type: true,
|
|
5405
|
+
},
|
|
5406
|
+
orderBy: {
|
|
5407
|
+
settled_at: 'asc',
|
|
5408
|
+
},
|
|
5409
|
+
});
|
|
5410
|
+
}
|
|
5411
|
+
|
|
4710
5412
|
private async loadOpenPeriod() {
|
|
4711
5413
|
const openPeriod = await this.prisma.period_close.findFirst({
|
|
4712
5414
|
where: {
|