@hed-hog/finance 0.0.3 → 0.0.224
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/hedhog/data/menu.yaml +18 -23
- package/hedhog/frontend/app/_lib/formatters.ts.ejs +20 -0
- package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +87 -0
- package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +265 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +388 -0
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +364 -0
- package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +388 -0
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +385 -0
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +364 -0
- package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +274 -0
- package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +401 -0
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +266 -0
- package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +244 -0
- package/hedhog/frontend/app/page.tsx.ejs +313 -15
- package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +285 -0
- package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +195 -0
- package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +321 -0
- package/hedhog/query/constraints.sql +169 -0
- package/package.json +4 -4
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from '@/components/ui/card';
|
|
12
|
+
import { Label } from '@/components/ui/label';
|
|
13
|
+
import { Money } from '@/components/ui/money';
|
|
14
|
+
import { PageHeader } from '@/components/ui/page-header';
|
|
15
|
+
import { Slider } from '@/components/ui/slider';
|
|
16
|
+
import {
|
|
17
|
+
Check,
|
|
18
|
+
Lightbulb,
|
|
19
|
+
Target,
|
|
20
|
+
TrendingDown,
|
|
21
|
+
TrendingUp,
|
|
22
|
+
} from 'lucide-react';
|
|
23
|
+
import { useState } from 'react';
|
|
24
|
+
import { formatarMoeda } from '../../_lib/formatters';
|
|
25
|
+
import { useFinanceData } from '../../_lib/use-finance-data';
|
|
26
|
+
|
|
27
|
+
export default function CenariosPage() {
|
|
28
|
+
const { data } = useFinanceData();
|
|
29
|
+
const { cenarios, kpis } = data;
|
|
30
|
+
|
|
31
|
+
const [cenarioAtivo, setCenarioAtivo] = useState('1');
|
|
32
|
+
const [premissas, setPremissas] = useState({
|
|
33
|
+
atrasoMedio: 5,
|
|
34
|
+
taxaInadimplencia: 3,
|
|
35
|
+
crescimentoReceita: 5,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const cenarioSelecionado = cenarios.find((c) => c.id === cenarioAtivo);
|
|
39
|
+
|
|
40
|
+
// Simulação de impacto baseada nas premissas
|
|
41
|
+
const impactoSaldo =
|
|
42
|
+
kpis.saldoCaixa * (1 + premissas.crescimentoReceita / 100);
|
|
43
|
+
const impactoInadimplencia =
|
|
44
|
+
kpis.inadimplencia * (1 + premissas.taxaInadimplencia / 10);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="space-y-6">
|
|
48
|
+
<PageHeader
|
|
49
|
+
title="Cenários"
|
|
50
|
+
description="Simule diferentes cenários financeiros"
|
|
51
|
+
breadcrumbs={[
|
|
52
|
+
{ label: 'Planejamento', href: '/finance/planning/cash-flow-forecast' },
|
|
53
|
+
{ label: 'Cenários' },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
|
|
57
|
+
<div className="grid gap-4 md:grid-cols-3">
|
|
58
|
+
{cenarios.map((cenario) => {
|
|
59
|
+
const isAtivo = cenario.id === cenarioAtivo;
|
|
60
|
+
const Icon =
|
|
61
|
+
cenario.nome === 'Otimista'
|
|
62
|
+
? TrendingUp
|
|
63
|
+
: cenario.nome === 'Pessimista'
|
|
64
|
+
? TrendingDown
|
|
65
|
+
: Target;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Card
|
|
69
|
+
key={cenario.id}
|
|
70
|
+
className={`cursor-pointer transition-all ${
|
|
71
|
+
isAtivo
|
|
72
|
+
? 'border-primary ring-2 ring-primary/20'
|
|
73
|
+
: 'hover:border-muted-foreground/50'
|
|
74
|
+
}`}
|
|
75
|
+
onClick={() => {
|
|
76
|
+
setCenarioAtivo(cenario.id);
|
|
77
|
+
setPremissas({
|
|
78
|
+
atrasoMedio: cenario.atrasoMedio,
|
|
79
|
+
taxaInadimplencia: cenario.taxaInadimplencia,
|
|
80
|
+
crescimentoReceita: cenario.crescimentoReceita,
|
|
81
|
+
});
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<CardHeader>
|
|
85
|
+
<div className="flex items-center justify-between">
|
|
86
|
+
<div className="flex items-center gap-3">
|
|
87
|
+
<div
|
|
88
|
+
className={`flex h-10 w-10 items-center justify-center rounded-lg ${
|
|
89
|
+
cenario.nome === 'Otimista'
|
|
90
|
+
? 'bg-green-100'
|
|
91
|
+
: cenario.nome === 'Pessimista'
|
|
92
|
+
? 'bg-red-100'
|
|
93
|
+
: 'bg-blue-100'
|
|
94
|
+
}`}
|
|
95
|
+
>
|
|
96
|
+
<Icon
|
|
97
|
+
className={`h-5 w-5 ${
|
|
98
|
+
cenario.nome === 'Otimista'
|
|
99
|
+
? 'text-green-600'
|
|
100
|
+
: cenario.nome === 'Pessimista'
|
|
101
|
+
? 'text-red-600'
|
|
102
|
+
: 'text-blue-600'
|
|
103
|
+
}`}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
<div>
|
|
107
|
+
<CardTitle className="text-base">
|
|
108
|
+
{cenario.nome}
|
|
109
|
+
</CardTitle>
|
|
110
|
+
<CardDescription>{cenario.descricao}</CardDescription>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
{isAtivo && (
|
|
114
|
+
<Badge className="bg-primary text-primary-foreground">
|
|
115
|
+
<Check className="mr-1 h-3 w-3" />
|
|
116
|
+
Ativo
|
|
117
|
+
</Badge>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</CardHeader>
|
|
121
|
+
<CardContent>
|
|
122
|
+
<div className="grid grid-cols-3 gap-4 text-center">
|
|
123
|
+
<div>
|
|
124
|
+
<p className="text-xs text-muted-foreground">Atraso</p>
|
|
125
|
+
<p className="font-semibold">{cenario.atrasoMedio} dias</p>
|
|
126
|
+
</div>
|
|
127
|
+
<div>
|
|
128
|
+
<p className="text-xs text-muted-foreground">
|
|
129
|
+
Inadimplência
|
|
130
|
+
</p>
|
|
131
|
+
<p className="font-semibold">
|
|
132
|
+
{cenario.taxaInadimplencia}%
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<p className="text-xs text-muted-foreground">Crescimento</p>
|
|
137
|
+
<p
|
|
138
|
+
className={`font-semibold ${
|
|
139
|
+
cenario.crescimentoReceita >= 0
|
|
140
|
+
? 'text-green-600'
|
|
141
|
+
: 'text-red-600'
|
|
142
|
+
}`}
|
|
143
|
+
>
|
|
144
|
+
{cenario.crescimentoReceita > 0 && '+'}
|
|
145
|
+
{cenario.crescimentoReceita}%
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</CardContent>
|
|
150
|
+
</Card>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
156
|
+
<Card>
|
|
157
|
+
<CardHeader>
|
|
158
|
+
<CardTitle className="flex items-center gap-2">
|
|
159
|
+
<Lightbulb className="h-4 w-4" />
|
|
160
|
+
Editor de Premissas
|
|
161
|
+
</CardTitle>
|
|
162
|
+
<CardDescription>
|
|
163
|
+
Ajuste as premissas do cenário {cenarioSelecionado?.nome}
|
|
164
|
+
</CardDescription>
|
|
165
|
+
</CardHeader>
|
|
166
|
+
<CardContent className="space-y-6">
|
|
167
|
+
<div className="space-y-4">
|
|
168
|
+
<div className="space-y-2">
|
|
169
|
+
<div className="flex items-center justify-between">
|
|
170
|
+
<Label>Atraso Médio de Recebimento</Label>
|
|
171
|
+
<span className="text-sm font-medium">
|
|
172
|
+
{premissas.atrasoMedio} dias
|
|
173
|
+
</span>
|
|
174
|
+
</div>
|
|
175
|
+
<Slider
|
|
176
|
+
value={[premissas.atrasoMedio]}
|
|
177
|
+
onValueChange={([value]) =>
|
|
178
|
+
setPremissas({
|
|
179
|
+
...premissas,
|
|
180
|
+
atrasoMedio: value ?? premissas.atrasoMedio,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
max={30}
|
|
184
|
+
step={1}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div className="space-y-2">
|
|
189
|
+
<div className="flex items-center justify-between">
|
|
190
|
+
<Label>Taxa de Inadimplência</Label>
|
|
191
|
+
<span className="text-sm font-medium">
|
|
192
|
+
{premissas.taxaInadimplencia}%
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
195
|
+
<Slider
|
|
196
|
+
value={[premissas.taxaInadimplencia]}
|
|
197
|
+
onValueChange={([value]) =>
|
|
198
|
+
setPremissas({
|
|
199
|
+
...premissas,
|
|
200
|
+
taxaInadimplencia: value ?? premissas.taxaInadimplencia,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
max={15}
|
|
204
|
+
step={0.5}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="space-y-2">
|
|
209
|
+
<div className="flex items-center justify-between">
|
|
210
|
+
<Label>Crescimento de Receita</Label>
|
|
211
|
+
<span
|
|
212
|
+
className={`text-sm font-medium ${
|
|
213
|
+
premissas.crescimentoReceita >= 0
|
|
214
|
+
? 'text-green-600'
|
|
215
|
+
: 'text-red-600'
|
|
216
|
+
}`}
|
|
217
|
+
>
|
|
218
|
+
{premissas.crescimentoReceita > 0 && '+'}
|
|
219
|
+
{premissas.crescimentoReceita}%
|
|
220
|
+
</span>
|
|
221
|
+
</div>
|
|
222
|
+
<Slider
|
|
223
|
+
value={[premissas.crescimentoReceita + 10]} // Offset para permitir valores negativos
|
|
224
|
+
onValueChange={([value]) =>
|
|
225
|
+
setPremissas({
|
|
226
|
+
...premissas,
|
|
227
|
+
crescimentoReceita:
|
|
228
|
+
(value ?? premissas.crescimentoReceita + 10) - 10,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
max={25}
|
|
232
|
+
step={1}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div className="flex justify-end gap-2 pt-4">
|
|
238
|
+
<Button
|
|
239
|
+
variant="outline"
|
|
240
|
+
onClick={() => {
|
|
241
|
+
if (cenarioSelecionado) {
|
|
242
|
+
setPremissas({
|
|
243
|
+
atrasoMedio: cenarioSelecionado.atrasoMedio,
|
|
244
|
+
taxaInadimplencia: cenarioSelecionado.taxaInadimplencia,
|
|
245
|
+
crescimentoReceita: cenarioSelecionado.crescimentoReceita,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
Restaurar
|
|
251
|
+
</Button>
|
|
252
|
+
<Button>Aplicar Cenário</Button>
|
|
253
|
+
</div>
|
|
254
|
+
</CardContent>
|
|
255
|
+
</Card>
|
|
256
|
+
|
|
257
|
+
<Card>
|
|
258
|
+
<CardHeader>
|
|
259
|
+
<CardTitle>Impacto Projetado</CardTitle>
|
|
260
|
+
<CardDescription>Resultado das premissas aplicadas</CardDescription>
|
|
261
|
+
</CardHeader>
|
|
262
|
+
<CardContent>
|
|
263
|
+
<div className="space-y-6">
|
|
264
|
+
<div className="rounded-lg border p-4">
|
|
265
|
+
<p className="text-sm text-muted-foreground">
|
|
266
|
+
Saldo Projetado (30 dias)
|
|
267
|
+
</p>
|
|
268
|
+
<p className="text-3xl font-bold">
|
|
269
|
+
<Money value={impactoSaldo} />
|
|
270
|
+
</p>
|
|
271
|
+
<p
|
|
272
|
+
className={`text-sm ${
|
|
273
|
+
impactoSaldo >= kpis.saldoCaixa
|
|
274
|
+
? 'text-green-600'
|
|
275
|
+
: 'text-red-600'
|
|
276
|
+
}`}
|
|
277
|
+
>
|
|
278
|
+
{impactoSaldo >= kpis.saldoCaixa ? '+' : ''}
|
|
279
|
+
{formatarMoeda(impactoSaldo - kpis.saldoCaixa)} vs atual
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div className="rounded-lg border p-4">
|
|
284
|
+
<p className="text-sm text-muted-foreground">
|
|
285
|
+
Inadimplência Projetada
|
|
286
|
+
</p>
|
|
287
|
+
<p className="text-3xl font-bold text-red-600">
|
|
288
|
+
<Money value={impactoInadimplencia} />
|
|
289
|
+
</p>
|
|
290
|
+
<p className="text-sm text-muted-foreground">
|
|
291
|
+
{formatarMoeda(impactoInadimplencia - kpis.inadimplencia)} vs
|
|
292
|
+
atual
|
|
293
|
+
</p>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div className="rounded-lg border p-4">
|
|
297
|
+
<p className="text-sm text-muted-foreground">
|
|
298
|
+
Fluxo de Caixa Previsto
|
|
299
|
+
</p>
|
|
300
|
+
<p
|
|
301
|
+
className={`text-3xl font-bold ${
|
|
302
|
+
premissas.crescimentoReceita >= 0
|
|
303
|
+
? 'text-green-600'
|
|
304
|
+
: 'text-red-600'
|
|
305
|
+
}`}
|
|
306
|
+
>
|
|
307
|
+
{premissas.crescimentoReceita >= 0 ? 'Positivo' : 'Negativo'}
|
|
308
|
+
</p>
|
|
309
|
+
<p className="text-sm text-muted-foreground">
|
|
310
|
+
Com crescimento de {premissas.crescimentoReceita}%
|
|
311
|
+
</p>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</CardContent>
|
|
315
|
+
</Card>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
-- Regras de integridade do módulo financeiro (PostgreSQL)
|
|
2
|
+
-- Script idempotente: pode ser executado mais de uma vez.
|
|
3
|
+
|
|
4
|
+
-- 1) CHECKs simples de domínio
|
|
5
|
+
DO $$
|
|
6
|
+
BEGIN
|
|
7
|
+
IF NOT EXISTS (
|
|
8
|
+
SELECT 1
|
|
9
|
+
FROM pg_constraint
|
|
10
|
+
WHERE conname = 'chk_financial_installment_open_amount_non_negative'
|
|
11
|
+
) THEN
|
|
12
|
+
ALTER TABLE financial_installment
|
|
13
|
+
ADD CONSTRAINT chk_financial_installment_open_amount_non_negative
|
|
14
|
+
CHECK (open_amount_cents >= 0);
|
|
15
|
+
END IF;
|
|
16
|
+
|
|
17
|
+
IF NOT EXISTS (
|
|
18
|
+
SELECT 1
|
|
19
|
+
FROM pg_constraint
|
|
20
|
+
WHERE conname = 'chk_financial_installment_open_amount_lte_amount'
|
|
21
|
+
) THEN
|
|
22
|
+
ALTER TABLE financial_installment
|
|
23
|
+
ADD CONSTRAINT chk_financial_installment_open_amount_lte_amount
|
|
24
|
+
CHECK (open_amount_cents <= amount_cents);
|
|
25
|
+
END IF;
|
|
26
|
+
|
|
27
|
+
IF NOT EXISTS (
|
|
28
|
+
SELECT 1
|
|
29
|
+
FROM pg_constraint
|
|
30
|
+
WHERE conname = 'chk_period_close_period_end_gte_start'
|
|
31
|
+
) THEN
|
|
32
|
+
ALTER TABLE period_close
|
|
33
|
+
ADD CONSTRAINT chk_period_close_period_end_gte_start
|
|
34
|
+
CHECK (period_end >= period_start);
|
|
35
|
+
END IF;
|
|
36
|
+
END $$;
|
|
37
|
+
|
|
38
|
+
-- 2) Unicidade em tabelas de vínculo N:N
|
|
39
|
+
DO $$
|
|
40
|
+
BEGIN
|
|
41
|
+
IF NOT EXISTS (
|
|
42
|
+
SELECT 1
|
|
43
|
+
FROM pg_constraint
|
|
44
|
+
WHERE conname = 'uq_financial_installment_tag_installment_tag'
|
|
45
|
+
) THEN
|
|
46
|
+
ALTER TABLE financial_installment_tag
|
|
47
|
+
ADD CONSTRAINT uq_financial_installment_tag_installment_tag
|
|
48
|
+
UNIQUE (installment_id, tag_id);
|
|
49
|
+
END IF;
|
|
50
|
+
|
|
51
|
+
IF NOT EXISTS (
|
|
52
|
+
SELECT 1
|
|
53
|
+
FROM pg_constraint
|
|
54
|
+
WHERE conname = 'uq_installment_allocation_installment_cost_center'
|
|
55
|
+
) THEN
|
|
56
|
+
ALTER TABLE installment_allocation
|
|
57
|
+
ADD CONSTRAINT uq_installment_allocation_installment_cost_center
|
|
58
|
+
UNIQUE (installment_id, cost_center_id);
|
|
59
|
+
END IF;
|
|
60
|
+
|
|
61
|
+
IF NOT EXISTS (
|
|
62
|
+
SELECT 1
|
|
63
|
+
FROM pg_constraint
|
|
64
|
+
WHERE conname = 'uq_settlement_allocation_settlement_installment'
|
|
65
|
+
) THEN
|
|
66
|
+
ALTER TABLE settlement_allocation
|
|
67
|
+
ADD CONSTRAINT uq_settlement_allocation_settlement_installment
|
|
68
|
+
UNIQUE (settlement_id, installment_id);
|
|
69
|
+
END IF;
|
|
70
|
+
END $$;
|
|
71
|
+
|
|
72
|
+
-- 3) Política de alocação por parcela (somatórios não podem exceder amount_cents)
|
|
73
|
+
-- settlement_allocation: soma(allocated_amount_cents) por installment_id <= financial_installment.amount_cents
|
|
74
|
+
|
|
75
|
+
CREATE OR REPLACE FUNCTION fn_check_settlement_allocation_total_per_installment()
|
|
76
|
+
RETURNS TRIGGER
|
|
77
|
+
LANGUAGE plpgsql
|
|
78
|
+
AS $$
|
|
79
|
+
DECLARE
|
|
80
|
+
target_installment_id financial_installment.id%TYPE;
|
|
81
|
+
installment_amount_cents integer;
|
|
82
|
+
allocated_total_cents bigint;
|
|
83
|
+
BEGIN
|
|
84
|
+
target_installment_id := COALESCE(NEW.installment_id, OLD.installment_id);
|
|
85
|
+
|
|
86
|
+
SELECT fi.amount_cents
|
|
87
|
+
INTO installment_amount_cents
|
|
88
|
+
FROM financial_installment fi
|
|
89
|
+
WHERE fi.id = target_installment_id;
|
|
90
|
+
|
|
91
|
+
IF installment_amount_cents IS NULL THEN
|
|
92
|
+
RETURN NULL;
|
|
93
|
+
END IF;
|
|
94
|
+
|
|
95
|
+
SELECT COALESCE(SUM(sa.allocated_amount_cents), 0)
|
|
96
|
+
INTO allocated_total_cents
|
|
97
|
+
FROM settlement_allocation sa
|
|
98
|
+
WHERE sa.installment_id = target_installment_id;
|
|
99
|
+
|
|
100
|
+
IF allocated_total_cents > installment_amount_cents THEN
|
|
101
|
+
RAISE EXCEPTION
|
|
102
|
+
'Soma de settlement_allocation (%) excede amount_cents (%) para installment_id=%',
|
|
103
|
+
allocated_total_cents,
|
|
104
|
+
installment_amount_cents,
|
|
105
|
+
target_installment_id;
|
|
106
|
+
END IF;
|
|
107
|
+
|
|
108
|
+
RETURN NULL;
|
|
109
|
+
END;
|
|
110
|
+
$$;
|
|
111
|
+
|
|
112
|
+
DROP TRIGGER IF EXISTS trg_check_settlement_allocation_total_per_installment
|
|
113
|
+
ON settlement_allocation;
|
|
114
|
+
|
|
115
|
+
CREATE CONSTRAINT TRIGGER trg_check_settlement_allocation_total_per_installment
|
|
116
|
+
AFTER INSERT OR UPDATE OR DELETE
|
|
117
|
+
ON settlement_allocation
|
|
118
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
119
|
+
FOR EACH ROW
|
|
120
|
+
EXECUTE FUNCTION fn_check_settlement_allocation_total_per_installment();
|
|
121
|
+
|
|
122
|
+
-- installment_allocation: soma(allocated_amount_cents) por installment_id <= financial_installment.amount_cents
|
|
123
|
+
|
|
124
|
+
CREATE OR REPLACE FUNCTION fn_check_installment_allocation_total_per_installment()
|
|
125
|
+
RETURNS TRIGGER
|
|
126
|
+
LANGUAGE plpgsql
|
|
127
|
+
AS $$
|
|
128
|
+
DECLARE
|
|
129
|
+
target_installment_id financial_installment.id%TYPE;
|
|
130
|
+
installment_amount_cents integer;
|
|
131
|
+
allocated_total_cents bigint;
|
|
132
|
+
BEGIN
|
|
133
|
+
target_installment_id := COALESCE(NEW.installment_id, OLD.installment_id);
|
|
134
|
+
|
|
135
|
+
SELECT fi.amount_cents
|
|
136
|
+
INTO installment_amount_cents
|
|
137
|
+
FROM financial_installment fi
|
|
138
|
+
WHERE fi.id = target_installment_id;
|
|
139
|
+
|
|
140
|
+
IF installment_amount_cents IS NULL THEN
|
|
141
|
+
RETURN NULL;
|
|
142
|
+
END IF;
|
|
143
|
+
|
|
144
|
+
SELECT COALESCE(SUM(ia.allocated_amount_cents), 0)
|
|
145
|
+
INTO allocated_total_cents
|
|
146
|
+
FROM installment_allocation ia
|
|
147
|
+
WHERE ia.installment_id = target_installment_id;
|
|
148
|
+
|
|
149
|
+
IF allocated_total_cents > installment_amount_cents THEN
|
|
150
|
+
RAISE EXCEPTION
|
|
151
|
+
'Soma de installment_allocation (%) excede amount_cents (%) para installment_id=%',
|
|
152
|
+
allocated_total_cents,
|
|
153
|
+
installment_amount_cents,
|
|
154
|
+
target_installment_id;
|
|
155
|
+
END IF;
|
|
156
|
+
|
|
157
|
+
RETURN NULL;
|
|
158
|
+
END;
|
|
159
|
+
$$;
|
|
160
|
+
|
|
161
|
+
DROP TRIGGER IF EXISTS trg_check_installment_allocation_total_per_installment
|
|
162
|
+
ON installment_allocation;
|
|
163
|
+
|
|
164
|
+
CREATE CONSTRAINT TRIGGER trg_check_installment_allocation_total_per_installment
|
|
165
|
+
AFTER INSERT OR UPDATE OR DELETE
|
|
166
|
+
ON installment_allocation
|
|
167
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
168
|
+
FOR EACH ROW
|
|
169
|
+
EXECUTE FUNCTION fn_check_installment_allocation_total_per_installment();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/finance",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.224",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"@nestjs/core": "^11",
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
|
+
"@hed-hog/api-locale": "0.0.11",
|
|
12
13
|
"@hed-hog/api-prisma": "0.0.4",
|
|
13
14
|
"@hed-hog/api-pagination": "0.0.5",
|
|
14
|
-
"@hed-hog/api-locale": "0.0.11",
|
|
15
15
|
"@hed-hog/api": "0.0.3",
|
|
16
|
-
"@hed-hog/tag": "0.0.
|
|
17
|
-
"@hed-hog/contact": "0.0.
|
|
16
|
+
"@hed-hog/tag": "0.0.223",
|
|
17
|
+
"@hed-hog/contact": "0.0.223",
|
|
18
18
|
"@hed-hog/api-types": "0.0.1"
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|