@hed-hog/finance 0.0.251 → 0.0.253
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/reverse-settlement.dto.d.ts +1 -0
- package/dist/dto/reverse-settlement.dto.d.ts.map +1 -1
- package/dist/dto/reverse-settlement.dto.js +5 -0
- package/dist/dto/reverse-settlement.dto.js.map +1 -1
- package/dist/finance-installments.controller.d.ts +40 -2
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +38 -2
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance.service.d.ts +39 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +476 -219
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +27 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +457 -142
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +100 -5
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +19 -20
- package/hedhog/frontend/app/page.tsx.ejs +11 -4
- package/hedhog/frontend/messages/en.json +44 -0
- package/hedhog/frontend/messages/pt.json +44 -0
- package/hedhog/query/constraints.sql +3 -1
- package/hedhog/query/settlement-auditability.sql +175 -0
- package/hedhog/table/bank_reconciliation.yaml +11 -0
- package/hedhog/table/settlement.yaml +17 -1
- package/hedhog/table/settlement_allocation.yaml +3 -0
- package/package.json +2 -2
- package/src/dto/reverse-settlement.dto.ts +4 -0
- package/src/finance-installments.controller.ts +45 -12
- package/src/finance.service.ts +498 -114
|
@@ -1729,6 +1729,8 @@ export default function TitulosPagarPage() {
|
|
|
1729
1729
|
const [search, setSearch] = useState('');
|
|
1730
1730
|
const [statusFilter, setStatusFilter] = useState<string>('');
|
|
1731
1731
|
const [editingTitleId, setEditingTitleId] = useState<string | null>(null);
|
|
1732
|
+
const [approvingTitleId, setApprovingTitleId] = useState<string | null>(null);
|
|
1733
|
+
const [reversingTitleId, setReversingTitleId] = useState<string | null>(null);
|
|
1732
1734
|
const [cancelingTitleId, setCancelingTitleId] = useState<string | null>(null);
|
|
1733
1735
|
|
|
1734
1736
|
const editingTitle = useMemo(
|
|
@@ -1792,13 +1794,97 @@ export default function TitulosPagarPage() {
|
|
|
1792
1794
|
|
|
1793
1795
|
await refetch();
|
|
1794
1796
|
showToastHandler?.('success', 'Título cancelado com sucesso');
|
|
1795
|
-
} catch {
|
|
1796
|
-
|
|
1797
|
+
} catch (error: any) {
|
|
1798
|
+
const message = error?.response?.data?.message;
|
|
1799
|
+
showToastHandler?.(
|
|
1800
|
+
'error',
|
|
1801
|
+
typeof message === 'string' && message.trim()
|
|
1802
|
+
? message
|
|
1803
|
+
: 'Não foi possível cancelar o título'
|
|
1804
|
+
);
|
|
1797
1805
|
} finally {
|
|
1798
1806
|
setCancelingTitleId(null);
|
|
1799
1807
|
}
|
|
1800
1808
|
};
|
|
1801
1809
|
|
|
1810
|
+
const handleApproveTitle = async (titleId: string) => {
|
|
1811
|
+
if (!titleId || approvingTitleId) {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
setApprovingTitleId(titleId);
|
|
1816
|
+
try {
|
|
1817
|
+
await request({
|
|
1818
|
+
url: `/finance/accounts-payable/installments/${titleId}/approve`,
|
|
1819
|
+
method: 'PATCH',
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1822
|
+
await refetch();
|
|
1823
|
+
showToastHandler?.('success', 'Título aprovado com sucesso');
|
|
1824
|
+
} catch (error: any) {
|
|
1825
|
+
const message = error?.response?.data?.message;
|
|
1826
|
+
showToastHandler?.(
|
|
1827
|
+
'error',
|
|
1828
|
+
typeof message === 'string' && message.trim()
|
|
1829
|
+
? message
|
|
1830
|
+
: 'Não foi possível aprovar o título'
|
|
1831
|
+
);
|
|
1832
|
+
} finally {
|
|
1833
|
+
setApprovingTitleId(null);
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
const handleSettleTitle = (titleId: string) => {
|
|
1838
|
+
router.push(
|
|
1839
|
+
`/finance/accounts-payable/installments/${titleId}?action=settle`
|
|
1840
|
+
);
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1843
|
+
const handleReverseTitle = async (title: any) => {
|
|
1844
|
+
const titleId = title?.id;
|
|
1845
|
+
|
|
1846
|
+
if (!titleId || reversingTitleId) {
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
const settlementToReverse = (title?.parcelas || [])
|
|
1851
|
+
.flatMap((parcela: any) => parcela?.liquidacoes || [])
|
|
1852
|
+
.find((liquidacao: any) => {
|
|
1853
|
+
return (
|
|
1854
|
+
!!liquidacao?.settlementId &&
|
|
1855
|
+
liquidacao?.status !== 'reversed' &&
|
|
1856
|
+
liquidacao?.status !== 'estornado'
|
|
1857
|
+
);
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
if (!settlementToReverse?.settlementId) {
|
|
1861
|
+
showToastHandler?.('error', 'Nenhuma liquidação ativa para estornar');
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
setReversingTitleId(titleId);
|
|
1866
|
+
try {
|
|
1867
|
+
await request({
|
|
1868
|
+
url: `/finance/accounts-payable/installments/${titleId}/settlements/${settlementToReverse.settlementId}/reverse`,
|
|
1869
|
+
method: 'PATCH',
|
|
1870
|
+
data: {},
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
await refetch();
|
|
1874
|
+
showToastHandler?.('success', 'Estorno realizado com sucesso');
|
|
1875
|
+
} catch (error: any) {
|
|
1876
|
+
const message = error?.response?.data?.message;
|
|
1877
|
+
showToastHandler?.(
|
|
1878
|
+
'error',
|
|
1879
|
+
typeof message === 'string' && message.trim()
|
|
1880
|
+
? message
|
|
1881
|
+
: 'Não foi possível estornar a liquidação'
|
|
1882
|
+
);
|
|
1883
|
+
} finally {
|
|
1884
|
+
setReversingTitleId(null);
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1802
1888
|
const filteredTitulos = titulosPagar.filter((titulo) => {
|
|
1803
1889
|
const matchesSearch =
|
|
1804
1890
|
titulo.documento.toLowerCase().includes(search.toLowerCase()) ||
|
|
@@ -1985,23 +2071,32 @@ export default function TitulosPagarPage() {
|
|
|
1985
2071
|
</DropdownMenuItem>
|
|
1986
2072
|
<DropdownMenuSeparator />
|
|
1987
2073
|
<DropdownMenuItem
|
|
1988
|
-
disabled={
|
|
2074
|
+
disabled={
|
|
2075
|
+
titulo.status !== 'rascunho' ||
|
|
2076
|
+
approvingTitleId === titulo.id
|
|
2077
|
+
}
|
|
2078
|
+
onClick={() => void handleApproveTitle(titulo.id)}
|
|
1989
2079
|
>
|
|
1990
2080
|
<CheckCircle className="mr-2 h-4 w-4" />
|
|
1991
2081
|
{t('table.actions.approve')}
|
|
1992
2082
|
</DropdownMenuItem>
|
|
1993
2083
|
<DropdownMenuItem
|
|
1994
2084
|
disabled={
|
|
1995
|
-
!['aberto', 'parcial'].includes(
|
|
2085
|
+
!['aberto', 'parcial', 'vencido'].includes(
|
|
2086
|
+
titulo.status
|
|
2087
|
+
)
|
|
1996
2088
|
}
|
|
2089
|
+
onClick={() => handleSettleTitle(titulo.id)}
|
|
1997
2090
|
>
|
|
1998
2091
|
<Download className="mr-2 h-4 w-4" />
|
|
1999
2092
|
{t('table.actions.settle')}
|
|
2000
2093
|
</DropdownMenuItem>
|
|
2001
2094
|
<DropdownMenuItem
|
|
2002
2095
|
disabled={
|
|
2003
|
-
!['parcial', 'liquidado'].includes(titulo.status)
|
|
2096
|
+
!['parcial', 'liquidado'].includes(titulo.status) ||
|
|
2097
|
+
reversingTitleId === titulo.id
|
|
2004
2098
|
}
|
|
2099
|
+
onClick={() => void handleReverseTitle(titulo)}
|
|
2005
2100
|
>
|
|
2006
2101
|
<Undo className="mr-2 h-4 w-4" />
|
|
2007
2102
|
{t('table.actions.reverse')}
|
|
@@ -22,14 +22,6 @@ import {
|
|
|
22
22
|
CardHeader,
|
|
23
23
|
CardTitle,
|
|
24
24
|
} from '@/components/ui/card';
|
|
25
|
-
import {
|
|
26
|
-
Dialog,
|
|
27
|
-
DialogContent,
|
|
28
|
-
DialogDescription,
|
|
29
|
-
DialogFooter,
|
|
30
|
-
DialogHeader,
|
|
31
|
-
DialogTitle,
|
|
32
|
-
} from '@/components/ui/dialog';
|
|
33
25
|
import {
|
|
34
26
|
DropdownMenu,
|
|
35
27
|
DropdownMenuContent,
|
|
@@ -54,6 +46,13 @@ import {
|
|
|
54
46
|
SelectTrigger,
|
|
55
47
|
SelectValue,
|
|
56
48
|
} from '@/components/ui/select';
|
|
49
|
+
import {
|
|
50
|
+
Sheet,
|
|
51
|
+
SheetContent,
|
|
52
|
+
SheetDescription,
|
|
53
|
+
SheetHeader,
|
|
54
|
+
SheetTitle,
|
|
55
|
+
} from '@/components/ui/sheet';
|
|
57
56
|
import { StatusBadge } from '@/components/ui/status-badge';
|
|
58
57
|
import {
|
|
59
58
|
Table,
|
|
@@ -522,21 +521,21 @@ export default function TituloReceberDetalhePage() {
|
|
|
522
521
|
</DropdownMenuContent>
|
|
523
522
|
</DropdownMenu>
|
|
524
523
|
|
|
525
|
-
<
|
|
524
|
+
<Sheet
|
|
526
525
|
open={isSettleDialogOpen}
|
|
527
526
|
onOpenChange={setIsSettleDialogOpen}
|
|
528
527
|
>
|
|
529
|
-
<
|
|
530
|
-
<
|
|
531
|
-
<
|
|
532
|
-
<
|
|
528
|
+
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
|
529
|
+
<SheetHeader>
|
|
530
|
+
<SheetTitle>Registrar recebimento</SheetTitle>
|
|
531
|
+
<SheetDescription>
|
|
533
532
|
Informe a parcela e o valor para baixa parcial ou total.
|
|
534
|
-
</
|
|
535
|
-
</
|
|
533
|
+
</SheetDescription>
|
|
534
|
+
</SheetHeader>
|
|
536
535
|
|
|
537
536
|
<Form {...settleForm}>
|
|
538
537
|
<form
|
|
539
|
-
className="space-y-4"
|
|
538
|
+
className="space-y-4 px-1"
|
|
540
539
|
onSubmit={settleForm.handleSubmit(handleSettle)}
|
|
541
540
|
>
|
|
542
541
|
<FormField
|
|
@@ -618,7 +617,7 @@ export default function TituloReceberDetalhePage() {
|
|
|
618
617
|
)}
|
|
619
618
|
/>
|
|
620
619
|
|
|
621
|
-
<
|
|
620
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
622
621
|
<Button
|
|
623
622
|
type="button"
|
|
624
623
|
variant="outline"
|
|
@@ -630,11 +629,11 @@ export default function TituloReceberDetalhePage() {
|
|
|
630
629
|
<Button type="submit" disabled={isSettling}>
|
|
631
630
|
Confirmar recebimento
|
|
632
631
|
</Button>
|
|
633
|
-
</
|
|
632
|
+
</div>
|
|
634
633
|
</form>
|
|
635
634
|
</Form>
|
|
636
|
-
</
|
|
637
|
-
</
|
|
635
|
+
</SheetContent>
|
|
636
|
+
</Sheet>
|
|
638
637
|
</div>
|
|
639
638
|
}
|
|
640
639
|
/>
|
|
@@ -221,8 +221,12 @@ function Alertas({
|
|
|
221
221
|
(e) => e.statusConciliacao === 'pendente'
|
|
222
222
|
).length;
|
|
223
223
|
|
|
224
|
-
const periodoBase = periodoAberto?.inicio
|
|
225
|
-
|
|
224
|
+
const periodoBase = periodoAberto?.inicio
|
|
225
|
+
? new Date(periodoAberto.inicio)
|
|
226
|
+
: new Date();
|
|
227
|
+
const mes = new Intl.DateTimeFormat(locale, { month: 'long' }).format(
|
|
228
|
+
periodoBase
|
|
229
|
+
);
|
|
226
230
|
const periodoAtual = `${mes.charAt(0).toUpperCase()}${mes.slice(1)}/${periodoBase.getFullYear()}`;
|
|
227
231
|
|
|
228
232
|
return (
|
|
@@ -281,6 +285,9 @@ export default function DashboardPage() {
|
|
|
281
285
|
} = data;
|
|
282
286
|
|
|
283
287
|
const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
|
|
288
|
+
const titulosPagarAprovados = titulosPagar.filter(
|
|
289
|
+
(titulo) => titulo.status !== 'rascunho' && titulo.status !== 'cancelado'
|
|
290
|
+
);
|
|
284
291
|
|
|
285
292
|
return (
|
|
286
293
|
<Page>
|
|
@@ -335,7 +342,7 @@ export default function DashboardPage() {
|
|
|
335
342
|
</CardContent>
|
|
336
343
|
</Card>
|
|
337
344
|
<Alertas
|
|
338
|
-
titulosPagar={
|
|
345
|
+
titulosPagar={titulosPagarAprovados}
|
|
339
346
|
extratos={extratos}
|
|
340
347
|
periodoAberto={periodoAberto}
|
|
341
348
|
locale={locale}
|
|
@@ -344,7 +351,7 @@ export default function DashboardPage() {
|
|
|
344
351
|
</div>
|
|
345
352
|
|
|
346
353
|
<ProximosVencimentos
|
|
347
|
-
titulosPagar={
|
|
354
|
+
titulosPagar={titulosPagarAprovados}
|
|
348
355
|
titulosReceber={titulosReceber}
|
|
349
356
|
getPessoaById={getPessoaById}
|
|
350
357
|
t={t}
|
|
@@ -198,6 +198,44 @@
|
|
|
198
198
|
"reverse": "Reverse",
|
|
199
199
|
"cancel": "Cancel"
|
|
200
200
|
},
|
|
201
|
+
"validation": {
|
|
202
|
+
"installmentRequired": "Installment is required",
|
|
203
|
+
"amountGreaterThanZero": "Amount must be greater than zero"
|
|
204
|
+
},
|
|
205
|
+
"dialogs": {
|
|
206
|
+
"cancel": {
|
|
207
|
+
"title": "Confirm cancellation",
|
|
208
|
+
"description": "This action changes the title status to canceled and does not remove audit records.",
|
|
209
|
+
"cancel": "Cancel",
|
|
210
|
+
"confirm": "Confirm cancellation"
|
|
211
|
+
},
|
|
212
|
+
"reverse": {
|
|
213
|
+
"title": "Confirm reversal",
|
|
214
|
+
"description": "This action reverses the selected settlement, recalculates installment balances, and updates the title status.",
|
|
215
|
+
"cancel": "Cancel",
|
|
216
|
+
"confirm": "Confirm reversal"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"settleSheet": {
|
|
220
|
+
"title": "Register settlement",
|
|
221
|
+
"description": "Provide the installment and settlement amount. The backend validates status and amount limits automatically.",
|
|
222
|
+
"installmentLabel": "Installment",
|
|
223
|
+
"installmentPlaceholder": "Select",
|
|
224
|
+
"installmentOption": "Installment {number} - open amount: {amount}",
|
|
225
|
+
"amountLabel": "Amount",
|
|
226
|
+
"descriptionLabel": "Description (optional)",
|
|
227
|
+
"confirm": "Confirm settlement"
|
|
228
|
+
},
|
|
229
|
+
"messages": {
|
|
230
|
+
"approveSuccess": "Title approved successfully",
|
|
231
|
+
"approveError": "Could not approve the title",
|
|
232
|
+
"settleSuccess": "Settlement registered successfully",
|
|
233
|
+
"settleError": "Could not register settlement",
|
|
234
|
+
"reverseSuccess": "Reversal completed successfully",
|
|
235
|
+
"reverseError": "Could not reverse the settlement",
|
|
236
|
+
"cancelSuccess": "Title canceled successfully",
|
|
237
|
+
"cancelError": "Could not cancel the title"
|
|
238
|
+
},
|
|
201
239
|
"documentData": {
|
|
202
240
|
"title": "Document Data",
|
|
203
241
|
"supplier": "Supplier",
|
|
@@ -254,6 +292,12 @@
|
|
|
254
292
|
"fine": "Fine",
|
|
255
293
|
"account": "Account",
|
|
256
294
|
"method": "Method",
|
|
295
|
+
"actions": "Actions",
|
|
296
|
+
"reverseButton": "Reverse",
|
|
297
|
+
"reverseDialogTitle": "Confirm reversal",
|
|
298
|
+
"reverseDialogDescription": "This action creates a reversal for the settlement and recalculates balances and status.",
|
|
299
|
+
"reverseDialogCancel": "Cancel",
|
|
300
|
+
"reverseDialogConfirm": "Confirm reversal",
|
|
257
301
|
"none": "No settlement recorded"
|
|
258
302
|
},
|
|
259
303
|
"audit": { "none": "No audit event" }
|
|
@@ -198,6 +198,44 @@
|
|
|
198
198
|
"reverse": "Estornar",
|
|
199
199
|
"cancel": "Cancelar"
|
|
200
200
|
},
|
|
201
|
+
"validation": {
|
|
202
|
+
"installmentRequired": "Parcela obrigatória",
|
|
203
|
+
"amountGreaterThanZero": "Valor deve ser maior que zero"
|
|
204
|
+
},
|
|
205
|
+
"dialogs": {
|
|
206
|
+
"cancel": {
|
|
207
|
+
"title": "Confirmar cancelamento",
|
|
208
|
+
"description": "Essa ação altera o título para cancelado e não remove os registros de auditoria.",
|
|
209
|
+
"cancel": "Cancelar",
|
|
210
|
+
"confirm": "Confirmar cancelamento"
|
|
211
|
+
},
|
|
212
|
+
"reverse": {
|
|
213
|
+
"title": "Confirmar estorno",
|
|
214
|
+
"description": "Esta ação estorna a liquidação selecionada, recalcula os saldos das parcelas e atualiza o status do título.",
|
|
215
|
+
"cancel": "Cancelar",
|
|
216
|
+
"confirm": "Confirmar estorno"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"settleSheet": {
|
|
220
|
+
"title": "Registrar baixa",
|
|
221
|
+
"description": "Informe a parcela e o valor da baixa. O backend valida os estados e limites de valor automaticamente.",
|
|
222
|
+
"installmentLabel": "Parcela",
|
|
223
|
+
"installmentPlaceholder": "Selecione",
|
|
224
|
+
"installmentOption": "Parcela {number} - em aberto: {amount}",
|
|
225
|
+
"amountLabel": "Valor",
|
|
226
|
+
"descriptionLabel": "Descrição (opcional)",
|
|
227
|
+
"confirm": "Confirmar baixa"
|
|
228
|
+
},
|
|
229
|
+
"messages": {
|
|
230
|
+
"approveSuccess": "Título aprovado com sucesso",
|
|
231
|
+
"approveError": "Não foi possível aprovar o título",
|
|
232
|
+
"settleSuccess": "Baixa registrada com sucesso",
|
|
233
|
+
"settleError": "Não foi possível registrar a baixa",
|
|
234
|
+
"reverseSuccess": "Estorno realizado com sucesso",
|
|
235
|
+
"reverseError": "Não foi possível estornar a liquidação",
|
|
236
|
+
"cancelSuccess": "Título cancelado com sucesso",
|
|
237
|
+
"cancelError": "Não foi possível cancelar o título"
|
|
238
|
+
},
|
|
201
239
|
"documentData": {
|
|
202
240
|
"title": "Dados do Documento",
|
|
203
241
|
"supplier": "Fornecedor",
|
|
@@ -254,6 +292,12 @@
|
|
|
254
292
|
"fine": "Multa",
|
|
255
293
|
"account": "Conta",
|
|
256
294
|
"method": "Método",
|
|
295
|
+
"actions": "Ações",
|
|
296
|
+
"reverseButton": "Estornar",
|
|
297
|
+
"reverseDialogTitle": "Confirmar estorno",
|
|
298
|
+
"reverseDialogDescription": "Esta ação cria o estorno da liquidação e recalcula saldos e status.",
|
|
299
|
+
"reverseDialogCancel": "Cancelar",
|
|
300
|
+
"reverseDialogConfirm": "Confirmar estorno",
|
|
257
301
|
"none": "Nenhuma liquidação registrada"
|
|
258
302
|
},
|
|
259
303
|
"audit": { "none": "Nenhum evento de auditoria" }
|
|
@@ -102,7 +102,9 @@ BEGIN
|
|
|
102
102
|
SELECT COALESCE(SUM(sa.allocated_amount_cents), 0)
|
|
103
103
|
INTO allocated_total_cents
|
|
104
104
|
FROM settlement_allocation sa
|
|
105
|
-
|
|
105
|
+
INNER JOIN settlement s ON s.id = sa.settlement_id
|
|
106
|
+
WHERE sa.installment_id = target_installment_id
|
|
107
|
+
AND s.status <> 'reversed';
|
|
106
108
|
|
|
107
109
|
IF allocated_total_cents > installment_amount_cents THEN
|
|
108
110
|
RAISE EXCEPTION
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
-- Settlement auditability, reversal linkage and reconciliation helpers
|
|
2
|
+
-- Idempotent script for PostgreSQL
|
|
3
|
+
|
|
4
|
+
DO $$
|
|
5
|
+
BEGIN
|
|
6
|
+
IF NOT EXISTS (
|
|
7
|
+
SELECT 1
|
|
8
|
+
FROM pg_type
|
|
9
|
+
WHERE typname = 'settlement_entry_type_enum'
|
|
10
|
+
) THEN
|
|
11
|
+
CREATE TYPE settlement_entry_type_enum AS ENUM ('normal', 'reversal');
|
|
12
|
+
END IF;
|
|
13
|
+
END $$;
|
|
14
|
+
|
|
15
|
+
ALTER TABLE settlement
|
|
16
|
+
ADD COLUMN IF NOT EXISTS entry_type settlement_entry_type_enum;
|
|
17
|
+
|
|
18
|
+
UPDATE settlement
|
|
19
|
+
SET entry_type = 'normal'
|
|
20
|
+
WHERE entry_type IS NULL;
|
|
21
|
+
|
|
22
|
+
ALTER TABLE settlement
|
|
23
|
+
ALTER COLUMN entry_type SET DEFAULT 'normal';
|
|
24
|
+
|
|
25
|
+
ALTER TABLE settlement
|
|
26
|
+
ALTER COLUMN entry_type SET NOT NULL;
|
|
27
|
+
|
|
28
|
+
ALTER TABLE settlement
|
|
29
|
+
ADD COLUMN IF NOT EXISTS reverses_settlement_id INTEGER;
|
|
30
|
+
|
|
31
|
+
DO $$
|
|
32
|
+
BEGIN
|
|
33
|
+
IF NOT EXISTS (
|
|
34
|
+
SELECT 1
|
|
35
|
+
FROM pg_constraint
|
|
36
|
+
WHERE conname = 'fk_settlement_reverses_settlement'
|
|
37
|
+
) THEN
|
|
38
|
+
ALTER TABLE settlement
|
|
39
|
+
ADD CONSTRAINT fk_settlement_reverses_settlement
|
|
40
|
+
FOREIGN KEY (reverses_settlement_id)
|
|
41
|
+
REFERENCES settlement(id)
|
|
42
|
+
ON DELETE RESTRICT
|
|
43
|
+
ON UPDATE CASCADE;
|
|
44
|
+
END IF;
|
|
45
|
+
END $$;
|
|
46
|
+
|
|
47
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_settlement_reverses_settlement_id
|
|
48
|
+
ON settlement(reverses_settlement_id)
|
|
49
|
+
WHERE reverses_settlement_id IS NOT NULL;
|
|
50
|
+
|
|
51
|
+
DO $$
|
|
52
|
+
BEGIN
|
|
53
|
+
BEGIN
|
|
54
|
+
ALTER TABLE settlement
|
|
55
|
+
ALTER COLUMN amount_cents TYPE BIGINT USING amount_cents::BIGINT;
|
|
56
|
+
EXCEPTION
|
|
57
|
+
WHEN undefined_column THEN
|
|
58
|
+
NULL;
|
|
59
|
+
END;
|
|
60
|
+
END $$;
|
|
61
|
+
|
|
62
|
+
ALTER TABLE settlement_allocation
|
|
63
|
+
ADD COLUMN IF NOT EXISTS amount_cents BIGINT;
|
|
64
|
+
|
|
65
|
+
UPDATE settlement_allocation
|
|
66
|
+
SET amount_cents = allocated_amount_cents
|
|
67
|
+
WHERE amount_cents IS NULL;
|
|
68
|
+
|
|
69
|
+
ALTER TABLE settlement_allocation
|
|
70
|
+
ALTER COLUMN amount_cents SET NOT NULL;
|
|
71
|
+
|
|
72
|
+
DO $$
|
|
73
|
+
BEGIN
|
|
74
|
+
BEGIN
|
|
75
|
+
ALTER TABLE settlement_allocation
|
|
76
|
+
ALTER COLUMN allocated_amount_cents TYPE BIGINT USING allocated_amount_cents::BIGINT;
|
|
77
|
+
EXCEPTION
|
|
78
|
+
WHEN undefined_column THEN
|
|
79
|
+
NULL;
|
|
80
|
+
END;
|
|
81
|
+
END $$;
|
|
82
|
+
|
|
83
|
+
DO $$
|
|
84
|
+
BEGIN
|
|
85
|
+
IF NOT EXISTS (
|
|
86
|
+
SELECT 1
|
|
87
|
+
FROM pg_constraint
|
|
88
|
+
WHERE conname = 'chk_settlement_allocation_amount_non_zero'
|
|
89
|
+
) THEN
|
|
90
|
+
ALTER TABLE settlement_allocation
|
|
91
|
+
ADD CONSTRAINT chk_settlement_allocation_amount_non_zero
|
|
92
|
+
CHECK (amount_cents <> 0);
|
|
93
|
+
END IF;
|
|
94
|
+
END $$;
|
|
95
|
+
|
|
96
|
+
ALTER TABLE bank_reconciliation
|
|
97
|
+
ADD COLUMN IF NOT EXISTS reconciled_at TIMESTAMPTZ(6);
|
|
98
|
+
|
|
99
|
+
ALTER TABLE bank_reconciliation
|
|
100
|
+
ADD COLUMN IF NOT EXISTS reconciled_by_user_id INTEGER;
|
|
101
|
+
|
|
102
|
+
DO $$
|
|
103
|
+
BEGIN
|
|
104
|
+
IF NOT EXISTS (
|
|
105
|
+
SELECT 1
|
|
106
|
+
FROM pg_constraint
|
|
107
|
+
WHERE conname = 'fk_bank_reconciliation_reconciled_by_user'
|
|
108
|
+
) THEN
|
|
109
|
+
ALTER TABLE bank_reconciliation
|
|
110
|
+
ADD CONSTRAINT fk_bank_reconciliation_reconciled_by_user
|
|
111
|
+
FOREIGN KEY (reconciled_by_user_id)
|
|
112
|
+
REFERENCES "user"(id)
|
|
113
|
+
ON DELETE SET NULL
|
|
114
|
+
ON UPDATE CASCADE;
|
|
115
|
+
END IF;
|
|
116
|
+
END $$;
|
|
117
|
+
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_settlement_settled_at
|
|
119
|
+
ON settlement(settled_at);
|
|
120
|
+
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_settlement_entry_type
|
|
122
|
+
ON settlement(entry_type);
|
|
123
|
+
|
|
124
|
+
CREATE OR REPLACE FUNCTION fn_prevent_settlement_updates()
|
|
125
|
+
RETURNS TRIGGER
|
|
126
|
+
LANGUAGE plpgsql
|
|
127
|
+
AS $$
|
|
128
|
+
BEGIN
|
|
129
|
+
RAISE EXCEPTION 'Settlement is immutable. Create a reversal instead of updating settlement %', NEW.id;
|
|
130
|
+
END;
|
|
131
|
+
$$;
|
|
132
|
+
|
|
133
|
+
DROP TRIGGER IF EXISTS trg_prevent_settlement_updates ON settlement;
|
|
134
|
+
|
|
135
|
+
CREATE TRIGGER trg_prevent_settlement_updates
|
|
136
|
+
BEFORE UPDATE ON settlement
|
|
137
|
+
FOR EACH ROW
|
|
138
|
+
EXECUTE FUNCTION fn_prevent_settlement_updates();
|
|
139
|
+
|
|
140
|
+
CREATE OR REPLACE VIEW v_settlement_history AS
|
|
141
|
+
SELECT
|
|
142
|
+
fi.title_id,
|
|
143
|
+
s.id AS settlement_id,
|
|
144
|
+
s.entry_type,
|
|
145
|
+
s.reverses_settlement_id,
|
|
146
|
+
CASE
|
|
147
|
+
WHEN s.entry_type = 'reversal' THEN s.reverses_settlement_id
|
|
148
|
+
ELSE s.id
|
|
149
|
+
END AS settlement_group_id,
|
|
150
|
+
s.settlement_type,
|
|
151
|
+
s.status,
|
|
152
|
+
s.settled_at,
|
|
153
|
+
s.amount_cents,
|
|
154
|
+
s.payment_method_id,
|
|
155
|
+
s.bank_account_id,
|
|
156
|
+
s.description,
|
|
157
|
+
s.external_reference,
|
|
158
|
+
s.created_by_user_id,
|
|
159
|
+
s.created_at,
|
|
160
|
+
s.updated_at,
|
|
161
|
+
sa.installment_id,
|
|
162
|
+
fi.installment_number,
|
|
163
|
+
sa.amount_cents AS allocation_amount_cents,
|
|
164
|
+
sa.discount_cents,
|
|
165
|
+
sa.interest_cents,
|
|
166
|
+
sa.penalty_cents,
|
|
167
|
+
br.id AS bank_reconciliation_id,
|
|
168
|
+
CASE
|
|
169
|
+
WHEN br.id IS NOT NULL THEN TRUE
|
|
170
|
+
ELSE FALSE
|
|
171
|
+
END AS reconciled
|
|
172
|
+
FROM settlement s
|
|
173
|
+
INNER JOIN settlement_allocation sa ON sa.settlement_id = s.id
|
|
174
|
+
INNER JOIN financial_installment fi ON fi.id = sa.installment_id
|
|
175
|
+
LEFT JOIN bank_reconciliation br ON br.settlement_id = s.id;
|
|
@@ -20,6 +20,9 @@ columns:
|
|
|
20
20
|
- name: matched_at
|
|
21
21
|
type: datetime
|
|
22
22
|
isNullable: true
|
|
23
|
+
- name: reconciled_at
|
|
24
|
+
type: datetime
|
|
25
|
+
isNullable: true
|
|
23
26
|
- name: matched_by_user_id
|
|
24
27
|
type: fk
|
|
25
28
|
isNullable: true
|
|
@@ -28,6 +31,14 @@ columns:
|
|
|
28
31
|
column: id
|
|
29
32
|
onDelete: SET NULL
|
|
30
33
|
onUpdate: CASCADE
|
|
34
|
+
- name: reconciled_by_user_id
|
|
35
|
+
type: fk
|
|
36
|
+
isNullable: true
|
|
37
|
+
references:
|
|
38
|
+
table: user
|
|
39
|
+
column: id
|
|
40
|
+
onDelete: SET NULL
|
|
41
|
+
onUpdate: CASCADE
|
|
31
42
|
- type: created_at
|
|
32
43
|
- type: updated_at
|
|
33
44
|
|
|
@@ -27,6 +27,10 @@ columns:
|
|
|
27
27
|
- name: settlement_type
|
|
28
28
|
type: enum
|
|
29
29
|
values: [payable, receivable, transfer, adjustment]
|
|
30
|
+
- name: entry_type
|
|
31
|
+
type: enum
|
|
32
|
+
values: [normal, reversal]
|
|
33
|
+
default: normal
|
|
30
34
|
- name: status
|
|
31
35
|
type: enum
|
|
32
36
|
values: [pending, confirmed, reversed]
|
|
@@ -41,6 +45,14 @@ columns:
|
|
|
41
45
|
type: varchar
|
|
42
46
|
length: 120
|
|
43
47
|
isNullable: true
|
|
48
|
+
- name: reverses_settlement_id
|
|
49
|
+
type: fk
|
|
50
|
+
isNullable: true
|
|
51
|
+
references:
|
|
52
|
+
table: settlement
|
|
53
|
+
column: id
|
|
54
|
+
onDelete: RESTRICT
|
|
55
|
+
onUpdate: CASCADE
|
|
44
56
|
- name: created_by_user_id
|
|
45
57
|
type: fk
|
|
46
58
|
isNullable: true
|
|
@@ -55,4 +67,8 @@ columns:
|
|
|
55
67
|
indices:
|
|
56
68
|
- columns: [status]
|
|
57
69
|
- columns: [settled_at]
|
|
58
|
-
- columns: [bank_account_id]
|
|
70
|
+
- columns: [bank_account_id]
|
|
71
|
+
- columns: [entry_type]
|
|
72
|
+
- columns: [settlement_type, settled_at]
|
|
73
|
+
- columns: [reverses_settlement_id]
|
|
74
|
+
isUnique: true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/finance",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.253",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
12
|
"@hed-hog/api-pagination": "0.0.5",
|
|
13
13
|
"@hed-hog/contact": "0.0.251",
|
|
14
|
-
"@hed-hog/api-prisma": "0.0.4",
|
|
15
14
|
"@hed-hog/tag": "0.0.251",
|
|
16
15
|
"@hed-hog/api-locale": "0.0.11",
|
|
16
|
+
"@hed-hog/api-prisma": "0.0.4",
|
|
17
17
|
"@hed-hog/api-types": "0.0.1",
|
|
18
18
|
"@hed-hog/api": "0.0.3",
|
|
19
19
|
"@hed-hog/core": "0.0.251"
|