@fayz-ai/plugin-financial 0.1.2 → 0.1.4
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/LICENSE +21 -0
- package/dist/FinancialContext.d.ts +2 -0
- package/dist/FinancialContext.d.ts.map +1 -1
- package/dist/FinancialPage.d.ts.map +1 -1
- package/dist/components/PaymentModal.d.ts.map +1 -1
- package/dist/components/PersonStatementWidget.d.ts +18 -0
- package/dist/components/PersonStatementWidget.d.ts.map +1 -0
- package/dist/components/TransferModal.d.ts +7 -0
- package/dist/components/TransferModal.d.ts.map +1 -0
- package/dist/contract.test.d.ts +2 -0
- package/dist/contract.test.d.ts.map +1 -0
- package/dist/data/capability.test.d.ts +2 -0
- package/dist/data/capability.test.d.ts.map +1 -0
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +13 -2
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.cjs +1529 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1437 -171
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/registries.d.ts.map +1 -1
- package/dist/store.d.ts +4 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +111 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InvoiceDetailView.d.ts +3 -1
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/dist/views/PayablesView.d.ts +5 -1
- package/dist/views/PayablesView.d.ts.map +1 -1
- package/dist/views/ReceivablesView.d.ts +5 -1
- package/dist/views/ReceivablesView.d.ts.map +1 -1
- package/dist/views/ReconciliationView.d.ts +3 -0
- package/dist/views/ReconciliationView.d.ts.map +1 -0
- package/dist/views/StatementsView.d.ts +3 -1
- package/dist/views/StatementsView.d.ts.map +1 -1
- package/dist/views/navReferrer.d.ts +5 -0
- package/dist/views/navReferrer.d.ts.map +1 -0
- package/package.json +8 -5
- package/src/FinancialContext.tsx +2 -0
- package/src/FinancialPage.tsx +32 -17
- package/src/components/PaymentModal.tsx +58 -11
- package/src/components/PersonStatementWidget.tsx +133 -0
- package/src/components/TransferModal.tsx +150 -0
- package/src/contract.test.ts +58 -0
- package/src/data/capability.test.ts +90 -0
- package/src/data/mock.ts +265 -52
- package/src/data/supabase.ts +359 -30
- package/src/data/types.ts +22 -3
- package/src/index.ts +28 -2
- package/src/locales/en.ts +76 -0
- package/src/locales/pt-BR.ts +76 -0
- package/src/migrations/006_extract_fee_amount.sql +20 -0
- package/src/migrations/006_rls_policies.sql +37 -0
- package/src/migrations/007_reconciliation.sql +32 -0
- package/src/migrations/008_split_payment_movements.sql +29 -0
- package/src/registries.ts +3 -1
- package/src/store.ts +27 -7
- package/src/types.ts +126 -1
- package/src/views/InvoiceDetailView.tsx +70 -51
- package/src/views/InvoiceFormView.tsx +55 -20
- package/src/views/PayablesView.tsx +13 -2
- package/src/views/ReceivablesView.tsx +13 -2
- package/src/views/ReconciliationView.tsx +211 -0
- package/src/views/StatementsView.tsx +241 -29
- package/src/views/navReferrer.ts +24 -0
package/src/locales/en.ts
CHANGED
|
@@ -128,6 +128,21 @@ export const en: Record<string, string> = {
|
|
|
128
128
|
"financial.nav.rules": "Rules",
|
|
129
129
|
"financial.nav.statements": "Statements",
|
|
130
130
|
"financial.nav.summary": "Summary",
|
|
131
|
+
"financial.reconciliation.title": "Reconciliation",
|
|
132
|
+
"financial.reconciliation.subtitle": "Match imported bank lines to your movements",
|
|
133
|
+
"financial.reconciliation.pending": "{{count}} to reconcile",
|
|
134
|
+
"financial.reconciliation.allDone": "All caught up",
|
|
135
|
+
"financial.reconciliation.empty": "Nothing to reconcile",
|
|
136
|
+
"financial.reconciliation.emptyHint": "Imported bank transactions will appear here for matching.",
|
|
137
|
+
"financial.reconciliation.bankLines": "Bank lines",
|
|
138
|
+
"financial.reconciliation.candidates": "Suggested matches",
|
|
139
|
+
"financial.reconciliation.selectLine": "Select a bank line to see suggested matches",
|
|
140
|
+
"financial.reconciliation.noCandidates": "No suggested matches for this line",
|
|
141
|
+
"financial.reconciliation.match": "Match",
|
|
142
|
+
"financial.reconciliation.accept": "Accept as new",
|
|
143
|
+
"financial.reconciliation.acceptHint": "No internal counterpart — keep as a standalone entry",
|
|
144
|
+
"financial.reconciliation.matchScore": "{{score}}% match",
|
|
145
|
+
"financial.reconciliation.refresh": "Refresh",
|
|
131
146
|
"financial.onboarding.back": "Back",
|
|
132
147
|
"financial.onboarding.configureHint": "You can configure this in detail from the Settings gear icon inside the Financial module.",
|
|
133
148
|
"financial.onboarding.getStarted": "Get Started",
|
|
@@ -192,6 +207,8 @@ export const en: Record<string, string> = {
|
|
|
192
207
|
"financial.settingsPage.title": "Financial Settings",
|
|
193
208
|
"financial.payment.noMethods": "No payment methods configured.",
|
|
194
209
|
"financial.payment.configureMethods": "Set up payment methods",
|
|
210
|
+
"financial.payment.noAccounts": "No compatible account. Create one in Settings.",
|
|
211
|
+
"financial.payment.configureAccounts": "Set up accounts",
|
|
195
212
|
"financial.statements.account": "Account",
|
|
196
213
|
"financial.statements.from": "From",
|
|
197
214
|
"financial.statements.noTransactions": "No transactions in this period",
|
|
@@ -328,4 +345,63 @@ export const en: Record<string, string> = {
|
|
|
328
345
|
"financial.movements.title": "Movements",
|
|
329
346
|
"financial.bankAccounts.title": "Bank Accounts",
|
|
330
347
|
"financial.settings.title": "Financial Settings",
|
|
348
|
+
// Settings registry tabs
|
|
349
|
+
"registry.payment-methods": "Payment Methods",
|
|
350
|
+
"registry.payment-methods.description": "Configured payment methods with fees and installment rules",
|
|
351
|
+
"registry.bank-accounts": "Accounts",
|
|
352
|
+
"registry.bank-accounts.description": "Bank accounts, cash registers, credit cards, and digital wallets",
|
|
353
|
+
"registry.chart-of-accounts": "Chart of Accounts",
|
|
354
|
+
"registry.chart-of-accounts.description": "Hierarchical chart of accounts for categorizing transactions",
|
|
355
|
+
"registry.cost-centers": "Cost Centers",
|
|
356
|
+
"registry.cost-centers.description": "Cost centers for expense tracking and departmental budgets",
|
|
357
|
+
"registry.commission-rules": "Commission Rules",
|
|
358
|
+
"registry.commission-rules.description": "Commission rules for service and product revenue sharing",
|
|
359
|
+
"registry.price-tables": "Price Tables",
|
|
360
|
+
"registry.price-tables.description": "Price tables for sale and cost pricing",
|
|
361
|
+
"registry.price-variations": "Price Variations",
|
|
362
|
+
"registry.price-variations.description": "Discount and surcharge rules for pricing",
|
|
363
|
+
"registry.payment-method-types": "Payment Types",
|
|
364
|
+
"registry.payment-method-types.description": "System payment types (cash, card, PIX, etc.)",
|
|
365
|
+
"registry.card-brands": "Card Brands",
|
|
366
|
+
"registry.card-brands.description": "Credit and debit card brands",
|
|
367
|
+
// Statements / extract
|
|
368
|
+
"financial.statements.columnDate": "Date",
|
|
369
|
+
"financial.statements.columnDescription": "Description",
|
|
370
|
+
"financial.statements.columnDebit": "Debit",
|
|
371
|
+
"financial.statements.columnCredit": "Credit",
|
|
372
|
+
"financial.statements.columnFee": "Fee",
|
|
373
|
+
"financial.statements.columnBalance": "Balance",
|
|
374
|
+
"financial.statements.columnMethod": "Method",
|
|
375
|
+
"financial.statements.columnDoc": "Document",
|
|
376
|
+
"financial.statements.installmentShort": "Inst. {{n}}",
|
|
377
|
+
"financial.statements.viewInvoice": "View invoice",
|
|
378
|
+
"financial.person.empty": "No financial activity for this contact yet",
|
|
379
|
+
"financial.statements.opening": "Opening balance",
|
|
380
|
+
"financial.statements.closing": "Closing balance",
|
|
381
|
+
"financial.statements.currentBalance": "Current balance",
|
|
382
|
+
"financial.statements.totalCredits": "Credits",
|
|
383
|
+
"financial.statements.totalDebits": "Debits",
|
|
384
|
+
"financial.statements.totalFees": "Fees",
|
|
385
|
+
"financial.statements.net": "Net",
|
|
386
|
+
"financial.statements.transfer": "Transfer",
|
|
387
|
+
"financial.statements.reconciled": "Reconciled with the bank statement",
|
|
388
|
+
"financial.statements.imported": "Imported from the bank · not reconciled",
|
|
389
|
+
"financial.statements.reconciledWith": "Reconciled with {{ref}}",
|
|
390
|
+
"financial.statements.newTransfer": "New Transfer",
|
|
391
|
+
// Transfer dialog
|
|
392
|
+
"financial.transfer.title": "Transfer Between Accounts",
|
|
393
|
+
"financial.transfer.from": "From account",
|
|
394
|
+
"financial.transfer.to": "To account",
|
|
395
|
+
"financial.transfer.amount": "Amount",
|
|
396
|
+
"financial.transfer.date": "Date",
|
|
397
|
+
"financial.transfer.notes": "Notes",
|
|
398
|
+
"financial.transfer.optionalNotes": "Optional notes...",
|
|
399
|
+
"financial.transfer.selectAccount": "Select account...",
|
|
400
|
+
"financial.transfer.submit": "Transfer",
|
|
401
|
+
"financial.transfer.processing": "Transferring...",
|
|
402
|
+
"financial.transfer.cancel": "Cancel",
|
|
403
|
+
"financial.transfer.created": "Transfer recorded",
|
|
404
|
+
"financial.transfer.failed": "Failed to record transfer",
|
|
405
|
+
"financial.transfer.sameAccount": "Source and destination must differ",
|
|
406
|
+
"financial.transfer.invalidAmount": "Amount must be greater than zero",
|
|
331
407
|
}
|
package/src/locales/pt-BR.ts
CHANGED
|
@@ -128,6 +128,21 @@ export const ptBR: Record<string, string> = {
|
|
|
128
128
|
"financial.nav.rules": "Regras",
|
|
129
129
|
"financial.nav.statements": "Extratos",
|
|
130
130
|
"financial.nav.summary": "Resumo",
|
|
131
|
+
"financial.reconciliation.title": "Conciliação",
|
|
132
|
+
"financial.reconciliation.subtitle": "Concilie as linhas importadas do banco com seus lançamentos",
|
|
133
|
+
"financial.reconciliation.pending": "{{count}} para conciliar",
|
|
134
|
+
"financial.reconciliation.allDone": "Tudo conciliado",
|
|
135
|
+
"financial.reconciliation.empty": "Nada para conciliar",
|
|
136
|
+
"financial.reconciliation.emptyHint": "As transações bancárias importadas aparecem aqui para conciliação.",
|
|
137
|
+
"financial.reconciliation.bankLines": "Linhas do banco",
|
|
138
|
+
"financial.reconciliation.candidates": "Correspondências sugeridas",
|
|
139
|
+
"financial.reconciliation.selectLine": "Selecione uma linha do banco para ver as sugestões",
|
|
140
|
+
"financial.reconciliation.noCandidates": "Nenhuma correspondência sugerida para esta linha",
|
|
141
|
+
"financial.reconciliation.match": "Conciliar",
|
|
142
|
+
"financial.reconciliation.accept": "Aceitar como novo",
|
|
143
|
+
"financial.reconciliation.acceptHint": "Sem contrapartida interna — manter como lançamento avulso",
|
|
144
|
+
"financial.reconciliation.matchScore": "{{score}}% compatível",
|
|
145
|
+
"financial.reconciliation.refresh": "Atualizar",
|
|
131
146
|
"financial.onboarding.back": "Voltar",
|
|
132
147
|
"financial.onboarding.configureHint": "Você pode configurar isso em detalhes pelo ícone de Configurações dentro do módulo Financeiro.",
|
|
133
148
|
"financial.onboarding.getStarted": "Começar",
|
|
@@ -192,6 +207,8 @@ export const ptBR: Record<string, string> = {
|
|
|
192
207
|
"financial.settingsPage.title": "Configurações Financeiras",
|
|
193
208
|
"financial.payment.noMethods": "Nenhuma forma de pagamento configurada.",
|
|
194
209
|
"financial.payment.configureMethods": "Configurar formas de pagamento",
|
|
210
|
+
"financial.payment.noAccounts": "Nenhuma conta compatível. Crie uma nas Configurações.",
|
|
211
|
+
"financial.payment.configureAccounts": "Configurar contas",
|
|
195
212
|
"financial.statements.account": "Conta",
|
|
196
213
|
"financial.statements.from": "De",
|
|
197
214
|
"financial.statements.noTransactions": "Sem transações neste período",
|
|
@@ -328,4 +345,63 @@ export const ptBR: Record<string, string> = {
|
|
|
328
345
|
"financial.movements.title": "Movimentações",
|
|
329
346
|
"financial.bankAccounts.title": "Contas Bancárias",
|
|
330
347
|
"financial.settings.title": "Configurações Financeiras",
|
|
348
|
+
// Abas de registros nas configurações
|
|
349
|
+
"registry.payment-methods": "Formas de Pagamento",
|
|
350
|
+
"registry.payment-methods.description": "Formas de pagamento configuradas com taxas e regras de parcelamento",
|
|
351
|
+
"registry.bank-accounts": "Contas",
|
|
352
|
+
"registry.bank-accounts.description": "Contas bancárias, caixas, cartões de crédito e carteiras digitais",
|
|
353
|
+
"registry.chart-of-accounts": "Plano de Contas",
|
|
354
|
+
"registry.chart-of-accounts.description": "Plano de contas hierárquico para categorizar transações",
|
|
355
|
+
"registry.cost-centers": "Centros de Custo",
|
|
356
|
+
"registry.cost-centers.description": "Centros de custo para controle de despesas e orçamentos por departamento",
|
|
357
|
+
"registry.commission-rules": "Regras de Comissão",
|
|
358
|
+
"registry.commission-rules.description": "Regras de comissão para serviços e produtos",
|
|
359
|
+
"registry.price-tables": "Tabelas de Preço",
|
|
360
|
+
"registry.price-tables.description": "Tabelas de preço de venda e custo",
|
|
361
|
+
"registry.price-variations": "Variações de Preço",
|
|
362
|
+
"registry.price-variations.description": "Regras de desconto e acréscimo de preços",
|
|
363
|
+
"registry.payment-method-types": "Tipos de Pagamento",
|
|
364
|
+
"registry.payment-method-types.description": "Tipos de pagamento do sistema (dinheiro, cartão, PIX, etc.)",
|
|
365
|
+
"registry.card-brands": "Bandeiras de Cartão",
|
|
366
|
+
"registry.card-brands.description": "Bandeiras de cartão de crédito e débito",
|
|
367
|
+
// Extrato
|
|
368
|
+
"financial.statements.columnDate": "Data",
|
|
369
|
+
"financial.statements.columnDescription": "Descrição",
|
|
370
|
+
"financial.statements.columnDebit": "Débito",
|
|
371
|
+
"financial.statements.columnCredit": "Crédito",
|
|
372
|
+
"financial.statements.columnFee": "Taxa",
|
|
373
|
+
"financial.statements.columnBalance": "Saldo",
|
|
374
|
+
"financial.statements.columnMethod": "Forma",
|
|
375
|
+
"financial.statements.columnDoc": "Documento",
|
|
376
|
+
"financial.statements.installmentShort": "Parc. {{n}}",
|
|
377
|
+
"financial.statements.viewInvoice": "Ver fatura",
|
|
378
|
+
"financial.person.empty": "Nenhuma atividade financeira para este contato ainda",
|
|
379
|
+
"financial.statements.opening": "Saldo inicial",
|
|
380
|
+
"financial.statements.closing": "Saldo final",
|
|
381
|
+
"financial.statements.currentBalance": "Saldo atual",
|
|
382
|
+
"financial.statements.totalCredits": "Créditos",
|
|
383
|
+
"financial.statements.totalDebits": "Débitos",
|
|
384
|
+
"financial.statements.totalFees": "Taxas",
|
|
385
|
+
"financial.statements.net": "Líquido",
|
|
386
|
+
"financial.statements.transfer": "Transferência",
|
|
387
|
+
"financial.statements.reconciled": "Conciliado com o extrato bancário",
|
|
388
|
+
"financial.statements.imported": "Importado do banco · não conciliado",
|
|
389
|
+
"financial.statements.reconciledWith": "Conciliado com {{ref}}",
|
|
390
|
+
"financial.statements.newTransfer": "Nova Transferência",
|
|
391
|
+
// Diálogo de transferência
|
|
392
|
+
"financial.transfer.title": "Transferência entre Contas",
|
|
393
|
+
"financial.transfer.from": "Conta de origem",
|
|
394
|
+
"financial.transfer.to": "Conta de destino",
|
|
395
|
+
"financial.transfer.amount": "Valor",
|
|
396
|
+
"financial.transfer.date": "Data",
|
|
397
|
+
"financial.transfer.notes": "Observações",
|
|
398
|
+
"financial.transfer.optionalNotes": "Observações opcionais...",
|
|
399
|
+
"financial.transfer.selectAccount": "Selecione a conta...",
|
|
400
|
+
"financial.transfer.submit": "Transferir",
|
|
401
|
+
"financial.transfer.processing": "Transferindo...",
|
|
402
|
+
"financial.transfer.cancel": "Cancelar",
|
|
403
|
+
"financial.transfer.created": "Transferência registrada",
|
|
404
|
+
"financial.transfer.failed": "Falha ao registrar transferência",
|
|
405
|
+
"financial.transfer.sameAccount": "Origem e destino devem ser diferentes",
|
|
406
|
+
"financial.transfer.invalidAmount": "O valor deve ser maior que zero",
|
|
331
407
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- Financial extract (extrato): net card settlement + transfers
|
|
3
|
+
-- ---------------------------------------------------------------------------
|
|
4
|
+
-- Adds the processing/MDR fee column used by the bank/cash account statement so
|
|
5
|
+
-- the running balance reflects the NET cash that actually lands in the account.
|
|
6
|
+
-- gross = paid_amount (settles the receivable/payable)
|
|
7
|
+
-- net cash = paid_amount - fee_amount (credits only; v1 has no fee on debits)
|
|
8
|
+
ALTER TABLE public.financial_movements
|
|
9
|
+
ADD COLUMN IF NOT EXISTS fee_amount numeric(14,2) NOT NULL DEFAULT 0;
|
|
10
|
+
|
|
11
|
+
COMMENT ON COLUMN public.financial_movements.fee_amount IS
|
|
12
|
+
'Processing/MDR fee deducted at settlement. Net cash impact = paid_amount - fee_amount (credit). v1: same-day net, no D+N settlement-date modeling.';
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_financial_movements_statement
|
|
15
|
+
ON public.financial_movements (bank_account_id, payment_date)
|
|
16
|
+
WHERE status IN ('paid', 'partial');
|
|
17
|
+
|
|
18
|
+
-- Transfers between accounts = a debit row (source) + credit row (destination),
|
|
19
|
+
-- both status='paid', movement_kind='transfer', correlated via metadata jsonb:
|
|
20
|
+
-- metadata->>'transferId' | 'transferRole' ('out'|'in') | 'counterAccountId'
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- Financial Plugin: RLS policies + grants for ALL base tables.
|
|
2
|
+
--
|
|
3
|
+
-- The base migrations (001/002/003) ENABLE row-level security but relied on an
|
|
4
|
+
-- external `project_rls.sql` auto-detection step to create the actual policies.
|
|
5
|
+
-- That step does not run in the Drizzle/companion pipeline, so every table
|
|
6
|
+
-- except financial_movements was RLS-enabled with NO policy → all inserts denied
|
|
7
|
+
-- ("new row violates row-level security policy"). This creates the canonical
|
|
8
|
+
-- per-tenant policies idempotently. Re-running is a no-op (IF NOT EXISTS guards;
|
|
9
|
+
-- financial_movements policies from 004 are skipped).
|
|
10
|
+
--
|
|
11
|
+
-- Canonical RLS form: tenant_id IN (SELECT public.user_tenant_ids()).
|
|
12
|
+
|
|
13
|
+
DO $$
|
|
14
|
+
DECLARE t text;
|
|
15
|
+
BEGIN
|
|
16
|
+
FOR t IN SELECT unnest(ARRAY[
|
|
17
|
+
'payment_method_types','payment_methods','bank_accounts',
|
|
18
|
+
'cash_register_sessions','financial_movements',
|
|
19
|
+
'chart_of_accounts','cost_centers','card_brands'
|
|
20
|
+
])
|
|
21
|
+
LOOP
|
|
22
|
+
EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
|
|
23
|
+
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);
|
|
24
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_select') THEN
|
|
25
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_select', t);
|
|
26
|
+
END IF;
|
|
27
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_insert') THEN
|
|
28
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_insert', t);
|
|
29
|
+
END IF;
|
|
30
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_update') THEN
|
|
31
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_update', t);
|
|
32
|
+
END IF;
|
|
33
|
+
IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_delete') THEN
|
|
34
|
+
EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_delete', t);
|
|
35
|
+
END IF;
|
|
36
|
+
END LOOP;
|
|
37
|
+
END $$;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- ---------------------------------------------------------------------------
|
|
2
|
+
-- Reconciliation (conciliação): bank-statement lines ↔ internal movements
|
|
3
|
+
-- ---------------------------------------------------------------------------
|
|
4
|
+
-- An integration (e.g. open banking) imports bank-statement lines as
|
|
5
|
+
-- financial_movements tagged with (external_source, external_id). The matching
|
|
6
|
+
-- UI then links each imported line to the internal movement it settles, or
|
|
7
|
+
-- accepts it standalone. These columns make a movement carry its bank-line
|
|
8
|
+
-- identity and its reconciliation state.
|
|
9
|
+
-- external_source / external_id : the bank line's provider + unique id
|
|
10
|
+
-- reconciled_at : when the line was matched/accepted (NULL = pending)
|
|
11
|
+
-- matched_movement_id : the internal movement this line reconciles (NULL = standalone)
|
|
12
|
+
ALTER TABLE public.financial_movements
|
|
13
|
+
ADD COLUMN IF NOT EXISTS external_id text,
|
|
14
|
+
ADD COLUMN IF NOT EXISTS external_source text,
|
|
15
|
+
ADD COLUMN IF NOT EXISTS reconciled_at timestamptz,
|
|
16
|
+
ADD COLUMN IF NOT EXISTS matched_movement_id uuid
|
|
17
|
+
REFERENCES public.financial_movements(id) ON DELETE SET NULL;
|
|
18
|
+
|
|
19
|
+
COMMENT ON COLUMN public.financial_movements.external_source IS
|
|
20
|
+
'Provider of an imported bank-statement line (e.g. plugbank, inter). NULL for app-native movements.';
|
|
21
|
+
COMMENT ON COLUMN public.financial_movements.external_id IS
|
|
22
|
+
'Provider-unique id of the imported bank line. Forms the import idempotency key with external_source.';
|
|
23
|
+
|
|
24
|
+
-- Idempotency: a given bank line imports at most once per tenant.
|
|
25
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_financial_movements_external
|
|
26
|
+
ON public.financial_movements (tenant_id, external_source, external_id)
|
|
27
|
+
WHERE external_id IS NOT NULL AND external_source IS NOT NULL;
|
|
28
|
+
|
|
29
|
+
-- Fast lookup of imported-but-unreconciled lines for the matching screen.
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_financial_movements_unreconciled
|
|
31
|
+
ON public.financial_movements (tenant_id, payment_date)
|
|
32
|
+
WHERE external_source IS NOT NULL AND reconciled_at IS NULL;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- Split bill (obligation) vs payment (cash event).
|
|
2
|
+
-- Each payment is now its own movement_kind='payment' row (one per cash event), so
|
|
3
|
+
-- two payments on one installment show as two extract rows instead of one summed row.
|
|
4
|
+
-- Bills stay pure obligations; v_invoice_balances already filters movement_kind='bill',
|
|
5
|
+
-- so the new payment rows don't affect invoice balances. Backfill is idempotent.
|
|
6
|
+
-- Requires fee_amount (006_extract_fee_amount.sql).
|
|
7
|
+
|
|
8
|
+
INSERT INTO public.financial_movements
|
|
9
|
+
(tenant_id, invoice_id, direction, movement_kind, amount, paid_amount, fee_amount, status,
|
|
10
|
+
due_date, payment_date, installment_number, payment_method_id, payment_method_type_id,
|
|
11
|
+
bank_account_id, cash_session_id, card_brand, card_installments, notes, metadata)
|
|
12
|
+
SELECT
|
|
13
|
+
b.tenant_id, b.invoice_id, b.direction, 'payment', b.paid_amount, b.paid_amount,
|
|
14
|
+
COALESCE(b.fee_amount, 0), 'paid',
|
|
15
|
+
b.due_date, COALESCE(b.payment_date, b.updated_at::date, CURRENT_DATE), b.installment_number,
|
|
16
|
+
b.payment_method_id, b.payment_method_type_id, b.bank_account_id, b.cash_session_id,
|
|
17
|
+
b.card_brand, b.card_installments, b.notes,
|
|
18
|
+
jsonb_build_object('backfilledFromBill', b.id)
|
|
19
|
+
FROM public.financial_movements b
|
|
20
|
+
WHERE b.movement_kind = 'bill' AND b.paid_amount > 0
|
|
21
|
+
AND NOT EXISTS (
|
|
22
|
+
SELECT 1 FROM public.financial_movements p
|
|
23
|
+
WHERE p.movement_kind = 'payment' AND p.metadata->>'backfilledFromBill' = b.id::text
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
UPDATE public.financial_movements
|
|
27
|
+
SET payment_date = NULL, bank_account_id = NULL, payment_method_id = NULL,
|
|
28
|
+
payment_method_type_id = NULL, cash_session_id = NULL, card_brand = NULL, card_installments = NULL
|
|
29
|
+
WHERE movement_kind = 'bill';
|
package/src/registries.ts
CHANGED
|
@@ -31,7 +31,9 @@ const paymentMethodEntity: EntityDef = {
|
|
|
31
31
|
defaultSort: 'name',
|
|
32
32
|
fields: [
|
|
33
33
|
{ key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
|
|
34
|
-
|
|
34
|
+
// FK to the seeded payment_method_types catalog — load real ids at runtime
|
|
35
|
+
// (a static enum select would write the string into the uuid column).
|
|
36
|
+
{ key: 'paymentMethodTypeId', label: 'Type', type: 'relation', relation: { table: 'payment_method_types', labelField: 'name' }, showInTable: true, required: true },
|
|
35
37
|
{ key: 'discountValue', label: 'Discount %', type: 'number', showInTable: true },
|
|
36
38
|
{ key: 'interestValue', label: 'Interest %', type: 'number', showInTable: true },
|
|
37
39
|
{ key: 'minInstallments', label: 'Min Installments', type: 'number', defaultValue: 1 },
|
package/src/store.ts
CHANGED
|
@@ -5,9 +5,9 @@ import type { FinancialDataProvider } from './data/types'
|
|
|
5
5
|
import type {
|
|
6
6
|
Invoice, FinancialMovement, BankAccount, CashSession,
|
|
7
7
|
PaymentMethod, PaymentMethodType, CardTransaction,
|
|
8
|
-
FinancialSummary,
|
|
8
|
+
FinancialSummary, StatementResult,
|
|
9
9
|
InvoiceQuery, MovementQuery, StatementQuery,
|
|
10
|
-
CreateInvoiceInput, PayMovementInput,
|
|
10
|
+
CreateInvoiceInput, PayMovementInput, CreateTransferInput,
|
|
11
11
|
OpenCashSessionInput, CloseCashSessionInput,
|
|
12
12
|
DateRange,
|
|
13
13
|
} from './types'
|
|
@@ -39,8 +39,9 @@ export interface FinancialUIState {
|
|
|
39
39
|
summary: FinancialSummary | null
|
|
40
40
|
summaryLoading: boolean
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
statement: StatementResult | null
|
|
43
43
|
statementLoading: boolean
|
|
44
|
+
statementQuery: StatementQuery | null
|
|
44
45
|
|
|
45
46
|
cardTransactions: CardTransaction[]
|
|
46
47
|
cardsLoading: boolean
|
|
@@ -55,6 +56,7 @@ export interface FinancialUIState {
|
|
|
55
56
|
fetchCardTransactions(dateRange?: DateRange): Promise<void>
|
|
56
57
|
createInvoice(input: CreateInvoiceInput): Promise<Invoice>
|
|
57
58
|
payMovement(input: PayMovementInput): Promise<void>
|
|
59
|
+
createTransfer(input: CreateTransferInput): Promise<void>
|
|
58
60
|
cancelInvoice(invoiceId: string): Promise<void>
|
|
59
61
|
openCashSession(input: OpenCashSessionInput): Promise<CashSession>
|
|
60
62
|
closeCashSession(input: CloseCashSessionInput): Promise<CashSession>
|
|
@@ -87,8 +89,9 @@ export function createFinancialStore(provider: FinancialDataProvider): StoreApi<
|
|
|
87
89
|
summary: null,
|
|
88
90
|
summaryLoading: false,
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
statement: null,
|
|
91
93
|
statementLoading: false,
|
|
94
|
+
statementQuery: null,
|
|
92
95
|
|
|
93
96
|
cardTransactions: [],
|
|
94
97
|
cardsLoading: false,
|
|
@@ -137,9 +140,9 @@ export function createFinancialStore(provider: FinancialDataProvider): StoreApi<
|
|
|
137
140
|
|
|
138
141
|
async fetchStatement(query) {
|
|
139
142
|
return dedup('fin:statement:' + JSON.stringify(query), async () => {
|
|
140
|
-
set({ statementLoading: true })
|
|
141
|
-
const
|
|
142
|
-
set({
|
|
143
|
+
set({ statementLoading: true, statementQuery: query })
|
|
144
|
+
const statement = await provider.getStatement(query)
|
|
145
|
+
set({ statement, statementLoading: false })
|
|
143
146
|
})
|
|
144
147
|
},
|
|
145
148
|
|
|
@@ -179,6 +182,23 @@ export function createFinancialStore(provider: FinancialDataProvider): StoreApi<
|
|
|
179
182
|
}
|
|
180
183
|
},
|
|
181
184
|
|
|
185
|
+
async createTransfer(input) {
|
|
186
|
+
try {
|
|
187
|
+
await provider.createTransfer(input)
|
|
188
|
+
const query = get().statementQuery
|
|
189
|
+
const [bankAccounts, summary, statement] = await Promise.all([
|
|
190
|
+
provider.getBankAccounts(),
|
|
191
|
+
provider.getSummary(),
|
|
192
|
+
query ? provider.getStatement(query) : Promise.resolve(get().statement),
|
|
193
|
+
])
|
|
194
|
+
set({ bankAccounts, summary, statement })
|
|
195
|
+
toast.success('Transfer recorded')
|
|
196
|
+
} catch (err: any) {
|
|
197
|
+
toast.error('Failed to record transfer', { description: err?.message })
|
|
198
|
+
throw err
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
182
202
|
async cancelInvoice(invoiceId) {
|
|
183
203
|
try {
|
|
184
204
|
await provider.cancelInvoice(invoiceId)
|
package/src/types.ts
CHANGED
|
@@ -100,10 +100,21 @@ export interface FinancialMovement {
|
|
|
100
100
|
cashSessionId?: string
|
|
101
101
|
cardBrand?: string
|
|
102
102
|
cardInstallments?: number
|
|
103
|
+
/** Processing/MDR fee deducted at settlement (net cash = paidAmount - feeAmount for credits) */
|
|
104
|
+
feeAmount?: number
|
|
103
105
|
debitAccountId?: string
|
|
104
106
|
creditAccountId?: string
|
|
105
107
|
notes?: string
|
|
106
108
|
metadata?: Record<string, unknown>
|
|
109
|
+
// --- Reconciliation (conciliação) ---
|
|
110
|
+
/** Provider of an imported bank-statement line (set on imported movements only) */
|
|
111
|
+
externalSource?: string
|
|
112
|
+
/** Provider-unique id of the imported bank line (idempotency key with externalSource) */
|
|
113
|
+
externalId?: string
|
|
114
|
+
/** When this movement was reconciled — matched or accepted (undefined = pending) */
|
|
115
|
+
reconciledAt?: string
|
|
116
|
+
/** For an imported bank line: the internal movement it was matched to */
|
|
117
|
+
matchedMovementId?: string
|
|
107
118
|
tenantId: string
|
|
108
119
|
createdAt: string
|
|
109
120
|
updatedAt: string
|
|
@@ -258,6 +269,8 @@ export interface PayMovementInput {
|
|
|
258
269
|
cashSessionId?: string
|
|
259
270
|
cardBrand?: string
|
|
260
271
|
cardInstallments?: number
|
|
272
|
+
/** Free-text payment description / memo shown in the extract */
|
|
273
|
+
notes?: string
|
|
261
274
|
}
|
|
262
275
|
|
|
263
276
|
export interface OpenCashSessionInput {
|
|
@@ -320,10 +333,27 @@ export interface MovementQuery {
|
|
|
320
333
|
}
|
|
321
334
|
|
|
322
335
|
export interface StatementQuery {
|
|
323
|
-
|
|
336
|
+
/** Omit (or pass undefined) for a consolidated "All accounts" statement */
|
|
337
|
+
bankAccountId?: string
|
|
324
338
|
dateRange: DateRange
|
|
325
339
|
}
|
|
326
340
|
|
|
341
|
+
export interface CreateTransferInput {
|
|
342
|
+
fromAccountId: string
|
|
343
|
+
toAccountId: string
|
|
344
|
+
amount: number
|
|
345
|
+
/** YYYY-MM-DD — becomes payment_date on both ledger legs */
|
|
346
|
+
date: string
|
|
347
|
+
notes?: string
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface TransferResult {
|
|
351
|
+
/** Shared correlation id stored in both legs' metadata */
|
|
352
|
+
transferId: string
|
|
353
|
+
debitMovementId: string
|
|
354
|
+
creditMovementId: string
|
|
355
|
+
}
|
|
356
|
+
|
|
327
357
|
export interface PaginatedResult<T> {
|
|
328
358
|
data: T[]
|
|
329
359
|
total: number
|
|
@@ -345,10 +375,53 @@ export interface FinancialSummary {
|
|
|
345
375
|
overduePayableAmount: number
|
|
346
376
|
}
|
|
347
377
|
|
|
378
|
+
/** What a statement row represents — a normal movement or one leg of a transfer */
|
|
379
|
+
export type StatementEntryKind = 'movement' | 'transfer-in' | 'transfer-out'
|
|
380
|
+
|
|
348
381
|
export interface StatementEntry {
|
|
349
382
|
movement: FinancialMovement
|
|
350
383
|
invoice?: Invoice
|
|
384
|
+
/** NET cash running balance after this row */
|
|
351
385
|
runningBalance: number
|
|
386
|
+
/** Amount that settled the receivable/payable (= paidAmount) */
|
|
387
|
+
gross: number
|
|
388
|
+
/** Processing/MDR fee deducted (>= 0) */
|
|
389
|
+
fee: number
|
|
390
|
+
/** Cash impact: gross - fee for credit; gross for debit */
|
|
391
|
+
net: number
|
|
392
|
+
entryKind: StatementEntryKind
|
|
393
|
+
/** For transfers: the other side's account */
|
|
394
|
+
counterAccountId?: string
|
|
395
|
+
counterAccountName?: string
|
|
396
|
+
/** When this row is a reconciled bank line, the internal movement it was matched to. */
|
|
397
|
+
reconciledWith?: {
|
|
398
|
+
movementId: string
|
|
399
|
+
description?: string
|
|
400
|
+
invoiceId?: string
|
|
401
|
+
fiscalNumber?: string
|
|
402
|
+
contactName?: string
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Full statement result with header context (opening/closing balance + period totals) */
|
|
407
|
+
export interface StatementResult {
|
|
408
|
+
entries: StatementEntry[]
|
|
409
|
+
/** Balance as of start-of-day dateFrom (excludes in-range rows) */
|
|
410
|
+
openingBalance: number
|
|
411
|
+
/** openingBalance + net of all in-range rows */
|
|
412
|
+
closingBalance: number
|
|
413
|
+
/** Sum of NET credits in range */
|
|
414
|
+
totalCredits: number
|
|
415
|
+
/** Sum of NET debits (cash out) in range */
|
|
416
|
+
totalDebits: number
|
|
417
|
+
/** Sum of fees in range */
|
|
418
|
+
totalFees: number
|
|
419
|
+
/** totalCredits - totalDebits */
|
|
420
|
+
net: number
|
|
421
|
+
/** Echo of the queried account; undefined for consolidated */
|
|
422
|
+
accountId?: string
|
|
423
|
+
/** Ledger-computed current balance of the selected account (as of today) */
|
|
424
|
+
accountCurrentBalance?: number
|
|
352
425
|
}
|
|
353
426
|
|
|
354
427
|
export interface CashSessionSummary {
|
|
@@ -357,3 +430,55 @@ export interface CashSessionSummary {
|
|
|
357
430
|
totalInflow: number
|
|
358
431
|
totalOutflow: number
|
|
359
432
|
}
|
|
433
|
+
|
|
434
|
+
// ============================================================
|
|
435
|
+
// RECONCILIATION (conciliação)
|
|
436
|
+
// ============================================================
|
|
437
|
+
|
|
438
|
+
/** A normalized bank-statement line, ready to import as a financial movement. */
|
|
439
|
+
export interface BankStatementLine {
|
|
440
|
+
/** Provider-unique transaction id (idempotency key with externalSource) */
|
|
441
|
+
externalId: string
|
|
442
|
+
/** Provider that produced the line (e.g. 'plugbank', 'inter') */
|
|
443
|
+
externalSource: string
|
|
444
|
+
/** credit = money in, debit = money out */
|
|
445
|
+
direction: TransactionDirection
|
|
446
|
+
amount: number
|
|
447
|
+
/** YYYY-MM-DD settlement date */
|
|
448
|
+
date: string
|
|
449
|
+
description: string
|
|
450
|
+
/** Account the line belongs to; falls back to the import's bankAccountId */
|
|
451
|
+
bankAccountId?: string
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export interface ImportBankTransactionsInput {
|
|
455
|
+
/** Default account for lines that don't carry their own */
|
|
456
|
+
bankAccountId?: string
|
|
457
|
+
lines: BankStatementLine[]
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export interface ImportBankTransactionsResult {
|
|
461
|
+
imported: number
|
|
462
|
+
/** Lines skipped because they were already imported (idempotency) */
|
|
463
|
+
duplicates: number
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export interface UnreconciledQuery {
|
|
467
|
+
bankAccountId?: string
|
|
468
|
+
dateRange?: DateRange
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** A candidate internal movement to reconcile an imported bank line against. */
|
|
472
|
+
export interface ReconciliationCandidate {
|
|
473
|
+
movement: FinancialMovement
|
|
474
|
+
invoice?: Invoice
|
|
475
|
+
/** 0..1 confidence from amount + date proximity */
|
|
476
|
+
score: number
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export interface ReconcileInput {
|
|
480
|
+
/** The imported bank-line movement (externalSource set) */
|
|
481
|
+
bankMovementId: string
|
|
482
|
+
/** Internal movement to link; omit to accept the line standalone */
|
|
483
|
+
matchedMovementId?: string
|
|
484
|
+
}
|