@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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/dist/FinancialContext.d.ts +2 -0
  3. package/dist/FinancialContext.d.ts.map +1 -1
  4. package/dist/FinancialPage.d.ts.map +1 -1
  5. package/dist/components/PaymentModal.d.ts.map +1 -1
  6. package/dist/components/PersonStatementWidget.d.ts +18 -0
  7. package/dist/components/PersonStatementWidget.d.ts.map +1 -0
  8. package/dist/components/TransferModal.d.ts +7 -0
  9. package/dist/components/TransferModal.d.ts.map +1 -0
  10. package/dist/contract.test.d.ts +2 -0
  11. package/dist/contract.test.d.ts.map +1 -0
  12. package/dist/data/capability.test.d.ts +2 -0
  13. package/dist/data/capability.test.d.ts.map +1 -0
  14. package/dist/data/mock.d.ts.map +1 -1
  15. package/dist/data/supabase.d.ts.map +1 -1
  16. package/dist/data/types.d.ts +13 -2
  17. package/dist/data/types.d.ts.map +1 -1
  18. package/dist/index.cjs +1529 -263
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1437 -171
  23. package/dist/index.js.map +1 -1
  24. package/dist/locales/en.d.ts.map +1 -1
  25. package/dist/locales/pt-BR.d.ts.map +1 -1
  26. package/dist/registries.d.ts.map +1 -1
  27. package/dist/store.d.ts +4 -2
  28. package/dist/store.d.ts.map +1 -1
  29. package/dist/types.d.ts +111 -1
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/views/InvoiceDetailView.d.ts +3 -1
  32. package/dist/views/InvoiceDetailView.d.ts.map +1 -1
  33. package/dist/views/InvoiceFormView.d.ts.map +1 -1
  34. package/dist/views/PayablesView.d.ts +5 -1
  35. package/dist/views/PayablesView.d.ts.map +1 -1
  36. package/dist/views/ReceivablesView.d.ts +5 -1
  37. package/dist/views/ReceivablesView.d.ts.map +1 -1
  38. package/dist/views/ReconciliationView.d.ts +3 -0
  39. package/dist/views/ReconciliationView.d.ts.map +1 -0
  40. package/dist/views/StatementsView.d.ts +3 -1
  41. package/dist/views/StatementsView.d.ts.map +1 -1
  42. package/dist/views/navReferrer.d.ts +5 -0
  43. package/dist/views/navReferrer.d.ts.map +1 -0
  44. package/package.json +8 -5
  45. package/src/FinancialContext.tsx +2 -0
  46. package/src/FinancialPage.tsx +32 -17
  47. package/src/components/PaymentModal.tsx +58 -11
  48. package/src/components/PersonStatementWidget.tsx +133 -0
  49. package/src/components/TransferModal.tsx +150 -0
  50. package/src/contract.test.ts +58 -0
  51. package/src/data/capability.test.ts +90 -0
  52. package/src/data/mock.ts +265 -52
  53. package/src/data/supabase.ts +359 -30
  54. package/src/data/types.ts +22 -3
  55. package/src/index.ts +28 -2
  56. package/src/locales/en.ts +76 -0
  57. package/src/locales/pt-BR.ts +76 -0
  58. package/src/migrations/006_extract_fee_amount.sql +20 -0
  59. package/src/migrations/006_rls_policies.sql +37 -0
  60. package/src/migrations/007_reconciliation.sql +32 -0
  61. package/src/migrations/008_split_payment_movements.sql +29 -0
  62. package/src/registries.ts +3 -1
  63. package/src/store.ts +27 -7
  64. package/src/types.ts +126 -1
  65. package/src/views/InvoiceDetailView.tsx +70 -51
  66. package/src/views/InvoiceFormView.tsx +55 -20
  67. package/src/views/PayablesView.tsx +13 -2
  68. package/src/views/ReceivablesView.tsx +13 -2
  69. package/src/views/ReconciliationView.tsx +211 -0
  70. package/src/views/StatementsView.tsx +241 -29
  71. 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
  }
@@ -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
- { key: 'paymentMethodTypeId', label: 'Type', type: 'select', options: ['cash', 'credit_card', 'debit_card', 'pix', 'bank_transfer', 'check'], showInTable: true, required: true },
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, StatementEntry,
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
- statementEntries: StatementEntry[]
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
- statementEntries: [],
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 statementEntries = await provider.getStatement(query)
142
- set({ statementEntries, statementLoading: false })
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
- bankAccountId: string
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
+ }