@fayz-ai/plugin-financial 0.2.0 → 0.8.0

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.
@@ -0,0 +1,544 @@
1
+ // AUTO-GENERATED from 000_plg_rename.sql, 001_financial_base.sql, 002_chart_of_accounts.sql, 003_card_brands.sql, 004_order_to_cash.sql, 005_seed_defaults.sql, 006_rls_policies.sql, 006b_extract_fee_amount.sql, 007_reconciliation.sql, 007b_movement_payment_method_type.sql, 008_split_payment_movements.sql — regenerate with scripts/embed-migrations.mjs
2
+ // SQL files are the source of truth; this inline copy lets the manifest declare
3
+ // migrations as data. Do not edit by hand — run the embed script instead.
4
+
5
+ export const MIGRATION_000_PLG_RENAME = `-- 000_plg_rename.sql — rename legacy financial tables to plg_financial_* for pools
6
+ -- provisioned before the industry-pool rename. Guarded: fires only when the legacy
7
+ -- name exists and the target does not, so fresh pools skip every branch.
8
+ DO $$
9
+ BEGIN
10
+ IF to_regclass('public.financial_movements') IS NOT NULL AND to_regclass('public.plg_financial_movements') IS NULL THEN
11
+ ALTER TABLE public.financial_movements RENAME TO plg_financial_movements;
12
+ END IF;
13
+ IF to_regclass('public.chart_of_accounts') IS NOT NULL AND to_regclass('public.plg_financial_chart_of_accounts') IS NULL THEN
14
+ ALTER TABLE public.chart_of_accounts RENAME TO plg_financial_chart_of_accounts;
15
+ END IF;
16
+ IF to_regclass('public.bank_accounts') IS NOT NULL AND to_regclass('public.plg_financial_bank_accounts') IS NULL THEN
17
+ ALTER TABLE public.bank_accounts RENAME TO plg_financial_bank_accounts;
18
+ END IF;
19
+ IF to_regclass('public.card_brands') IS NOT NULL AND to_regclass('public.plg_financial_card_brands') IS NULL THEN
20
+ ALTER TABLE public.card_brands RENAME TO plg_financial_card_brands;
21
+ END IF;
22
+ IF to_regclass('public.cost_centers') IS NOT NULL AND to_regclass('public.plg_financial_cost_centers') IS NULL THEN
23
+ ALTER TABLE public.cost_centers RENAME TO plg_financial_cost_centers;
24
+ END IF;
25
+ IF to_regclass('public.cash_register_sessions') IS NOT NULL AND to_regclass('public.plg_financial_cash_register_sessions') IS NULL THEN
26
+ ALTER TABLE public.cash_register_sessions RENAME TO plg_financial_cash_register_sessions;
27
+ END IF;
28
+ IF to_regclass('public.payment_methods') IS NOT NULL AND to_regclass('public.plg_financial_payment_methods') IS NULL THEN
29
+ ALTER TABLE public.payment_methods RENAME TO plg_financial_payment_methods;
30
+ END IF;
31
+ IF to_regclass('public.payment_method_types') IS NOT NULL AND to_regclass('public.plg_financial_payment_method_types') IS NULL THEN
32
+ ALTER TABLE public.payment_method_types RENAME TO plg_financial_payment_method_types;
33
+ END IF;
34
+ END $$;
35
+ `
36
+
37
+ export const MIGRATION_001_FINANCIAL_BASE = `-- Financial Plugin: Base Tables
38
+ -- Pattern: uuid PK, tenant_id FK, timestamps, RLS via project_rls.sql auto-detection
39
+
40
+ CREATE TABLE IF NOT EXISTS public.plg_financial_payment_method_types (
41
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
42
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
43
+ name text NOT NULL,
44
+ transaction_type text NOT NULL,
45
+ is_active boolean NOT NULL DEFAULT true,
46
+ allowed_account_types jsonb DEFAULT '[]'::jsonb,
47
+ created_at timestamptz NOT NULL DEFAULT now(),
48
+ updated_at timestamptz NOT NULL DEFAULT now()
49
+ );
50
+ ALTER TABLE public.plg_financial_payment_method_types ENABLE ROW LEVEL SECURITY;
51
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_payment_method_types_tenant ON public.plg_financial_payment_method_types(tenant_id);
52
+
53
+ CREATE TABLE IF NOT EXISTS public.plg_financial_payment_methods (
54
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
55
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
56
+ name text NOT NULL,
57
+ payment_method_type_id uuid REFERENCES public.plg_financial_payment_method_types(id),
58
+ is_active boolean NOT NULL DEFAULT true,
59
+ discount_mode text,
60
+ discount_value numeric(12,2) DEFAULT 0,
61
+ interest_mode text,
62
+ interest_value numeric(12,2) DEFAULT 0,
63
+ min_installments integer DEFAULT 1,
64
+ max_installments integer DEFAULT 1,
65
+ service_filter_mode text DEFAULT 'all',
66
+ service_filter_ids jsonb DEFAULT '[]'::jsonb,
67
+ created_at timestamptz NOT NULL DEFAULT now(),
68
+ updated_at timestamptz NOT NULL DEFAULT now()
69
+ );
70
+ ALTER TABLE public.plg_financial_payment_methods ENABLE ROW LEVEL SECURITY;
71
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_payment_methods_tenant ON public.plg_financial_payment_methods(tenant_id);
72
+
73
+ CREATE TABLE IF NOT EXISTS public.plg_financial_bank_accounts (
74
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
75
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
76
+ name text NOT NULL,
77
+ account_type text NOT NULL DEFAULT 'bank_account',
78
+ bank_name text,
79
+ account_number text,
80
+ agency_number text,
81
+ current_balance numeric(14,2) NOT NULL DEFAULT 0,
82
+ initial_balance numeric(14,2) NOT NULL DEFAULT 0,
83
+ credit_limit numeric(14,2),
84
+ due_day integer,
85
+ closing_day integer,
86
+ is_active boolean NOT NULL DEFAULT true,
87
+ unit_id uuid,
88
+ created_at timestamptz NOT NULL DEFAULT now(),
89
+ updated_at timestamptz NOT NULL DEFAULT now()
90
+ );
91
+ ALTER TABLE public.plg_financial_bank_accounts ENABLE ROW LEVEL SECURITY;
92
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_bank_accounts_tenant ON public.plg_financial_bank_accounts(tenant_id);
93
+
94
+ CREATE TABLE IF NOT EXISTS public.plg_financial_cash_register_sessions (
95
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
96
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
97
+ bank_account_id uuid NOT NULL REFERENCES public.plg_financial_bank_accounts(id),
98
+ status text NOT NULL DEFAULT 'open',
99
+ opened_at timestamptz NOT NULL DEFAULT now(),
100
+ opened_by_user_id uuid,
101
+ opened_by_name text,
102
+ opening_balance numeric(14,2) NOT NULL,
103
+ closed_at timestamptz,
104
+ closed_by_user_id uuid,
105
+ closed_by_name text,
106
+ closing_balance numeric(14,2),
107
+ expected_balance numeric(14,2),
108
+ difference numeric(14,2),
109
+ notes text,
110
+ unit_id uuid,
111
+ created_at timestamptz NOT NULL DEFAULT now(),
112
+ updated_at timestamptz NOT NULL DEFAULT now()
113
+ );
114
+ ALTER TABLE public.plg_financial_cash_register_sessions ENABLE ROW LEVEL SECURITY;
115
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_cash_register_sessions_tenant ON public.plg_financial_cash_register_sessions(tenant_id);
116
+
117
+ -- Financial movements (installments, payments) — plugin-specific, not archetype
118
+ -- Invoices themselves use public.orders (kind='invoice_payable'/'invoice_receivable')
119
+ CREATE TABLE IF NOT EXISTS public.plg_financial_movements (
120
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
121
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
122
+ invoice_id uuid REFERENCES public.orders(id),
123
+ direction text NOT NULL,
124
+ movement_kind text NOT NULL,
125
+ amount numeric(14,2) NOT NULL,
126
+ paid_amount numeric(14,2) DEFAULT 0,
127
+ status text NOT NULL DEFAULT 'pending',
128
+ due_date date NOT NULL,
129
+ payment_date date,
130
+ installment_number integer,
131
+ payment_method_id uuid REFERENCES public.plg_financial_payment_methods(id),
132
+ bank_account_id uuid REFERENCES public.plg_financial_bank_accounts(id),
133
+ cash_session_id uuid REFERENCES public.plg_financial_cash_register_sessions(id),
134
+ card_brand text,
135
+ card_installments integer,
136
+ notes text,
137
+ metadata jsonb DEFAULT '{}'::jsonb,
138
+ created_at timestamptz NOT NULL DEFAULT now(),
139
+ updated_at timestamptz NOT NULL DEFAULT now()
140
+ );
141
+ ALTER TABLE public.plg_financial_movements ENABLE ROW LEVEL SECURITY;
142
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_movements_tenant ON public.plg_financial_movements(tenant_id);
143
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_movements_invoice ON public.plg_financial_movements(invoice_id);
144
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_movements_due ON public.plg_financial_movements(tenant_id, due_date);
145
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_movements_bank ON public.plg_financial_movements(bank_account_id);
146
+ `
147
+
148
+ export const MIGRATION_002_CHART_OF_ACCOUNTS = `-- Financial Plugin: Chart of Accounts + Cost Centers
149
+
150
+ CREATE TABLE IF NOT EXISTS public.plg_financial_chart_of_accounts (
151
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
152
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
153
+ code text NOT NULL,
154
+ name text NOT NULL,
155
+ node_type text NOT NULL DEFAULT 'leaf',
156
+ parent_id uuid REFERENCES public.plg_financial_chart_of_accounts(id),
157
+ is_active boolean NOT NULL DEFAULT true,
158
+ created_at timestamptz NOT NULL DEFAULT now(),
159
+ updated_at timestamptz NOT NULL DEFAULT now()
160
+ );
161
+ ALTER TABLE public.plg_financial_chart_of_accounts ENABLE ROW LEVEL SECURITY;
162
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_chart_of_accounts_tenant ON public.plg_financial_chart_of_accounts(tenant_id);
163
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_chart_of_accounts_parent ON public.plg_financial_chart_of_accounts(parent_id);
164
+
165
+ CREATE TABLE IF NOT EXISTS public.plg_financial_cost_centers (
166
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
167
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
168
+ code text NOT NULL,
169
+ name text NOT NULL,
170
+ is_active boolean NOT NULL DEFAULT true,
171
+ created_at timestamptz NOT NULL DEFAULT now(),
172
+ updated_at timestamptz NOT NULL DEFAULT now()
173
+ );
174
+ ALTER TABLE public.plg_financial_cost_centers ENABLE ROW LEVEL SECURITY;
175
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_cost_centers_tenant ON public.plg_financial_cost_centers(tenant_id);
176
+ `
177
+
178
+ export const MIGRATION_003_CARD_BRANDS = `-- Financial Plugin: Card Brands
179
+
180
+ CREATE TABLE IF NOT EXISTS public.plg_financial_card_brands (
181
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
182
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
183
+ name text NOT NULL,
184
+ is_active boolean NOT NULL DEFAULT true,
185
+ created_at timestamptz NOT NULL DEFAULT now(),
186
+ updated_at timestamptz NOT NULL DEFAULT now()
187
+ );
188
+ ALTER TABLE public.plg_financial_card_brands ENABLE ROW LEVEL SECURITY;
189
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_card_brands_tenant ON public.plg_financial_card_brands(tenant_id);
190
+ `
191
+
192
+ export const MIGRATION_004_ORDER_TO_CASH = `-- Financial plugin — order-to-cash companion SQL.
193
+ -- The atomic "promote an order into an invoice + installment ledger" function
194
+ -- that CRM (approveQuote) and the agenda financial bridge both RPC. It belongs
195
+ -- to the financial plugin (it writes plg_financial_movements + reads orders), and it
196
+ -- ships WITH the plugin so enabling Payments provisions it. Idempotent.
197
+ -- Depends on: public.orders, public.plg_financial_movements (001_financial_base).
198
+
199
+ -- 1. Sequence generator (REC-00001 / PAG-00001 numbering)
200
+ CREATE TABLE IF NOT EXISTS public.sequences (
201
+ tenant_id uuid NOT NULL REFERENCES public.tenants(id),
202
+ kind text NOT NULL,
203
+ current_value bigint NOT NULL DEFAULT 0,
204
+ PRIMARY KEY (tenant_id, kind)
205
+ );
206
+ CREATE OR REPLACE FUNCTION public.next_sequence(p_tenant_id uuid, p_kind text)
207
+ RETURNS bigint LANGUAGE plpgsql AS $$
208
+ DECLARE v_next bigint;
209
+ BEGIN
210
+ INSERT INTO public.sequences (tenant_id, kind, current_value)
211
+ VALUES (p_tenant_id, p_kind, 1)
212
+ ON CONFLICT (tenant_id, kind)
213
+ DO UPDATE SET current_value = public.sequences.current_value + 1
214
+ RETURNING current_value INTO v_next;
215
+ RETURN v_next;
216
+ END;
217
+ $$;
218
+
219
+ -- 2. Order-to-cash columns on the spine orders (plugin extends public.orders)
220
+ ALTER TABLE public.orders
221
+ ADD COLUMN IF NOT EXISTS stage text,
222
+ ADD COLUMN IF NOT EXISTS direction text;
223
+
224
+ -- 3. RLS + grants for plg_financial_movements (table from 001_financial_base.sql)
225
+ ALTER TABLE public.plg_financial_movements ENABLE ROW LEVEL SECURITY;
226
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_financial_movements TO authenticated;
227
+ DO $$
228
+ BEGIN
229
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='plg_financial_movements' AND policyname='plg_financial_movements_select') THEN
230
+ CREATE POLICY plg_financial_movements_select ON public.plg_financial_movements FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
231
+ CREATE POLICY plg_financial_movements_insert ON public.plg_financial_movements FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
232
+ CREATE POLICY plg_financial_movements_update ON public.plg_financial_movements FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
233
+ CREATE POLICY plg_financial_movements_delete ON public.plg_financial_movements FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));
234
+ END IF;
235
+ END $$;
236
+
237
+ -- 4. The function (v2 — backward compatible with the 3-arg CRM call via defaults)
238
+ DROP FUNCTION IF EXISTS public.fn_invoice_from_order(uuid, date, text);
239
+ CREATE OR REPLACE FUNCTION public.fn_invoice_from_order(
240
+ p_order_id uuid,
241
+ p_due_date date DEFAULT NULL,
242
+ p_status text DEFAULT NULL,
243
+ p_installments int DEFAULT 1,
244
+ p_direction text DEFAULT 'credit'
245
+ ) RETURNS uuid
246
+ LANGUAGE plpgsql
247
+ SECURITY DEFINER
248
+ SET search_path = public
249
+ AS $$
250
+ DECLARE
251
+ v_order public.orders%ROWTYPE;
252
+ v_ref text;
253
+ v_seq bigint;
254
+ v_summary text;
255
+ v_contact text;
256
+ v_existing uuid;
257
+ v_kind text;
258
+ v_prefix text;
259
+ v_n int := GREATEST(COALESCE(p_installments, 1), 1);
260
+ v_total numeric;
261
+ v_base numeric;
262
+ v_amt numeric;
263
+ v_alloc numeric := 0;
264
+ v_due date;
265
+ i int;
266
+ BEGIN
267
+ SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;
268
+ IF NOT FOUND THEN RAISE EXCEPTION 'fn_invoice_from_order: order % not found', p_order_id; END IF;
269
+ IF v_order.tenant_id NOT IN (SELECT public.user_tenant_ids()) THEN
270
+ RAISE EXCEPTION 'fn_invoice_from_order: forbidden';
271
+ END IF;
272
+
273
+ SELECT id INTO v_existing FROM public.plg_financial_movements
274
+ WHERE invoice_id = p_order_id AND movement_kind = 'bill' LIMIT 1;
275
+ IF v_existing IS NOT NULL THEN RETURN p_order_id; END IF;
276
+
277
+ IF p_direction = 'debit' THEN v_kind := 'invoice_payable'; v_prefix := 'PAG';
278
+ ELSE v_kind := 'invoice_receivable'; v_prefix := 'REC'; END IF;
279
+
280
+ SELECT public.next_sequence(v_order.tenant_id, v_kind) INTO v_seq;
281
+ v_ref := v_prefix || '-' || lpad(v_seq::text, 5, '0');
282
+
283
+ SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary
284
+ FROM public.order_items WHERE order_id = p_order_id;
285
+ SELECT name INTO v_contact FROM public.people WHERE id = v_order.party_id;
286
+
287
+ UPDATE public.orders SET
288
+ status = COALESCE(p_status, status),
289
+ stage = 'invoiced',
290
+ direction = p_direction,
291
+ reference_number = v_ref,
292
+ metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object(
293
+ 'itemsSummary', COALESCE(v_summary, ''),
294
+ 'contactName', v_contact,
295
+ 'installmentCount', v_n,
296
+ 'quoteNumber', v_order.reference_number,
297
+ 'direction', p_direction
298
+ ),
299
+ updated_at = now()
300
+ WHERE id = p_order_id;
301
+
302
+ v_total := COALESCE(v_order.total, 0);
303
+ v_base := round(v_total / v_n, 2);
304
+ v_due := COALESCE(p_due_date, current_date);
305
+ FOR i IN 1..v_n LOOP
306
+ IF i = v_n THEN v_amt := round(v_total - v_alloc, 2);
307
+ ELSE v_amt := v_base; v_alloc := v_alloc + v_base; END IF;
308
+ INSERT INTO public.plg_financial_movements
309
+ (tenant_id, invoice_id, direction, movement_kind, amount, paid_amount, status, due_date, installment_number)
310
+ VALUES
311
+ (v_order.tenant_id, p_order_id, p_direction, 'bill', v_amt, 0, 'pending',
312
+ (v_due + ((i - 1) || ' months')::interval)::date, i);
313
+ END LOOP;
314
+
315
+ RETURN p_order_id;
316
+ END;
317
+ $$;
318
+ GRANT EXECUTE ON FUNCTION public.fn_invoice_from_order(uuid, date, text, int, text) TO authenticated;
319
+
320
+ -- 5. Derived balances view (Payments page + agenda payment badge read this)
321
+ CREATE OR REPLACE VIEW public.v_invoice_balances AS
322
+ WITH bill AS (
323
+ SELECT
324
+ invoice_id,
325
+ SUM(amount) FILTER (WHERE status <> 'cancelled') AS billed,
326
+ SUM(paid_amount) FILTER (WHERE status <> 'cancelled') AS paid,
327
+ bool_or(status NOT IN ('paid','cancelled') AND due_date < current_date) AS has_overdue
328
+ FROM public.plg_financial_movements
329
+ WHERE movement_kind = 'bill'
330
+ GROUP BY invoice_id
331
+ )
332
+ SELECT
333
+ o.id AS invoice_id, o.tenant_id, o.kind, o.reference_number, o.party_id, o.direction,
334
+ CASE WHEN o.direction = 'debit' OR o.kind = 'invoice_payable' THEN 'payable' ELSE 'receivable' END AS flow,
335
+ COALESCE(b.billed, o.total, 0) AS amount,
336
+ COALESCE(b.paid, 0) AS paid,
337
+ CASE WHEN o.status = 'cancelled' OR o.stage = 'cancelled' THEN 0
338
+ ELSE COALESCE(b.billed, o.total, 0) - COALESCE(b.paid, 0) END AS balance,
339
+ CASE
340
+ WHEN o.status = 'cancelled' OR o.stage = 'cancelled' THEN 'cancelled'
341
+ WHEN COALESCE(b.billed, o.total, 0) = 0 THEN 'open'
342
+ WHEN COALESCE(b.paid, 0) >= COALESCE(b.billed, o.total, 0) THEN 'paid'
343
+ WHEN COALESCE(b.paid, 0) > 0 THEN 'partial'
344
+ WHEN COALESCE(b.has_overdue, false) THEN 'overdue'
345
+ ELSE 'open'
346
+ END AS status,
347
+ o.created_at, o.updated_at
348
+ FROM public.orders o
349
+ LEFT JOIN bill b ON b.invoice_id = o.id
350
+ WHERE o.kind IN ('invoice_receivable','invoice_payable') OR b.invoice_id IS NOT NULL;
351
+ ALTER VIEW public.v_invoice_balances SET (security_invoker = true);
352
+ GRANT SELECT ON public.v_invoice_balances TO authenticated;
353
+
354
+ -- Refresh PostgREST's schema cache so the new RPC/views are visible immediately.
355
+ NOTIFY pgrst, 'reload schema';
356
+ `
357
+
358
+ export const MIGRATION_005_SEED_DEFAULTS = `-- Financial plugin — seed: payment method types + card brands.
359
+ -- These are the read-only "system" registries the plugin's UI assumes exist
360
+ -- (PaymentModal, card selection). They live as registry seedData in TS but were
361
+ -- never written to the DB, so a fresh tenant had none. This backfills them for
362
+ -- every tenant that has none. Idempotent. Plugin-owned so every app gets them.
363
+
364
+ DO $$
365
+ DECLARE t_id uuid;
366
+ BEGIN
367
+ FOR t_id IN (
368
+ SELECT id FROM public.tenants
369
+ WHERE id NOT IN (SELECT DISTINCT tenant_id FROM public.plg_financial_payment_method_types)
370
+ )
371
+ LOOP
372
+ INSERT INTO public.plg_financial_payment_method_types (tenant_id, name, transaction_type) VALUES
373
+ (t_id, 'Cash', 'cash'),
374
+ (t_id, 'PIX', 'pix'),
375
+ (t_id, 'Credit Card', 'credit_card'),
376
+ (t_id, 'Debit Card', 'debit_card'),
377
+ (t_id, 'Bank Transfer', 'bank_transfer'),
378
+ (t_id, 'Check', 'check');
379
+ END LOOP;
380
+ END $$;
381
+
382
+ DO $$
383
+ DECLARE t_id uuid;
384
+ BEGIN
385
+ FOR t_id IN (
386
+ SELECT id FROM public.tenants
387
+ WHERE id NOT IN (SELECT DISTINCT tenant_id FROM public.plg_financial_card_brands)
388
+ )
389
+ LOOP
390
+ INSERT INTO public.plg_financial_card_brands (tenant_id, name) VALUES
391
+ (t_id, 'Visa'), (t_id, 'Mastercard'), (t_id, 'American Express'),
392
+ (t_id, 'Elo'), (t_id, 'Hipercard'), (t_id, 'Diners Club'),
393
+ (t_id, 'Discover'), (t_id, 'JCB'), (t_id, 'Aura'), (t_id, 'Hiper');
394
+ END LOOP;
395
+ END $$;
396
+ `
397
+
398
+ export const MIGRATION_006_RLS_POLICIES = `-- Financial Plugin: RLS policies + grants for ALL base tables.
399
+ --
400
+ -- The base migrations (001/002/003) ENABLE row-level security but relied on an
401
+ -- external \`project_rls.sql\` auto-detection step to create the actual policies.
402
+ -- That step does not run in the Drizzle/companion pipeline, so every table
403
+ -- except plg_financial_movements was RLS-enabled with NO policy → all inserts denied
404
+ -- ("new row violates row-level security policy"). This creates the canonical
405
+ -- per-tenant policies idempotently. Re-running is a no-op (IF NOT EXISTS guards;
406
+ -- plg_financial_movements policies from 004 are skipped).
407
+ --
408
+ -- Canonical RLS form: tenant_id IN (SELECT public.user_tenant_ids()).
409
+
410
+ DO $$
411
+ DECLARE t text;
412
+ BEGIN
413
+ FOR t IN SELECT unnest(ARRAY[
414
+ 'plg_financial_payment_method_types','plg_financial_payment_methods','plg_financial_bank_accounts',
415
+ 'plg_financial_cash_register_sessions','plg_financial_movements',
416
+ 'plg_financial_chart_of_accounts','plg_financial_cost_centers','plg_financial_card_brands'
417
+ ])
418
+ LOOP
419
+ EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', t);
420
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON public.%I TO authenticated', t);
421
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_select') THEN
422
+ EXECUTE format('CREATE POLICY %I ON public.%I FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_select', t);
423
+ END IF;
424
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_insert') THEN
425
+ 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);
426
+ END IF;
427
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_update') THEN
428
+ EXECUTE format('CREATE POLICY %I ON public.%I FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_update', t);
429
+ END IF;
430
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename=t AND policyname=t||'_delete') THEN
431
+ EXECUTE format('CREATE POLICY %I ON public.%I FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()))', t||'_delete', t);
432
+ END IF;
433
+ END LOOP;
434
+ END $$;
435
+ `
436
+
437
+ export const MIGRATION_006B_EXTRACT_FEE_AMOUNT = `-- ---------------------------------------------------------------------------
438
+ -- Financial extract (extrato): net card settlement + transfers
439
+ -- ---------------------------------------------------------------------------
440
+ -- Adds the processing/MDR fee column used by the bank/cash account statement so
441
+ -- the running balance reflects the NET cash that actually lands in the account.
442
+ -- gross = paid_amount (settles the receivable/payable)
443
+ -- net cash = paid_amount - fee_amount (credits only; v1 has no fee on debits)
444
+ ALTER TABLE public.plg_financial_movements
445
+ ADD COLUMN IF NOT EXISTS fee_amount numeric(14,2) NOT NULL DEFAULT 0;
446
+
447
+ COMMENT ON COLUMN public.plg_financial_movements.fee_amount IS
448
+ 'Processing/MDR fee deducted at settlement. Net cash impact = paid_amount - fee_amount (credit). v1: same-day net, no D+N settlement-date modeling.';
449
+
450
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_movements_statement
451
+ ON public.plg_financial_movements (bank_account_id, payment_date)
452
+ WHERE status IN ('paid', 'partial');
453
+
454
+ -- Transfers between accounts = a debit row (source) + credit row (destination),
455
+ -- both status='paid', movement_kind='transfer', correlated via metadata jsonb:
456
+ -- metadata->>'transferId' | 'transferRole' ('out'|'in') | 'counterAccountId'
457
+ `
458
+
459
+ export const MIGRATION_007_RECONCILIATION = `-- ---------------------------------------------------------------------------
460
+ -- Reconciliation (conciliação): bank-statement lines ↔ internal movements
461
+ -- ---------------------------------------------------------------------------
462
+ -- An integration (e.g. open banking) imports bank-statement lines as
463
+ -- plg_financial_movements tagged with (external_source, external_id). The matching
464
+ -- UI then links each imported line to the internal movement it settles, or
465
+ -- accepts it standalone. These columns make a movement carry its bank-line
466
+ -- identity and its reconciliation state.
467
+ -- external_source / external_id : the bank line's provider + unique id
468
+ -- reconciled_at : when the line was matched/accepted (NULL = pending)
469
+ -- matched_movement_id : the internal movement this line reconciles (NULL = standalone)
470
+ ALTER TABLE public.plg_financial_movements
471
+ ADD COLUMN IF NOT EXISTS external_id text,
472
+ ADD COLUMN IF NOT EXISTS external_source text,
473
+ ADD COLUMN IF NOT EXISTS reconciled_at timestamptz,
474
+ ADD COLUMN IF NOT EXISTS matched_movement_id uuid
475
+ REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL;
476
+
477
+ COMMENT ON COLUMN public.plg_financial_movements.external_source IS
478
+ 'Provider of an imported bank-statement line (e.g. plugbank, inter). NULL for app-native movements.';
479
+ COMMENT ON COLUMN public.plg_financial_movements.external_id IS
480
+ 'Provider-unique id of the imported bank line. Forms the import idempotency key with external_source.';
481
+
482
+ -- Idempotency: a given bank line imports at most once per tenant.
483
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_plg_financial_movements_external
484
+ ON public.plg_financial_movements (tenant_id, external_source, external_id)
485
+ WHERE external_id IS NOT NULL AND external_source IS NOT NULL;
486
+
487
+ -- Fast lookup of imported-but-unreconciled lines for the matching screen.
488
+ CREATE INDEX IF NOT EXISTS idx_plg_financial_movements_unreconciled
489
+ ON public.plg_financial_movements (tenant_id, payment_date)
490
+ WHERE external_source IS NOT NULL AND reconciled_at IS NULL;
491
+ `
492
+
493
+ export const MIGRATION_007B_MOVEMENT_PAYMENT_METHOD_TYPE = `-- payment_method_type_id existed on pre-pool installs (added out-of-band on
494
+ -- the salon/beauty DB) but was never captured in the file series; 008 requires
495
+ -- it on plg_financial_movements. No-op where the column already exists.
496
+
497
+ ALTER TABLE public.plg_financial_movements
498
+ ADD COLUMN IF NOT EXISTS payment_method_type_id uuid REFERENCES public.plg_financial_payment_method_types(id);
499
+ `
500
+
501
+ export const MIGRATION_008_SPLIT_PAYMENT_MOVEMENTS = `-- Split bill (obligation) vs payment (cash event).
502
+ -- Each payment is now its own movement_kind='payment' row (one per cash event), so
503
+ -- two payments on one installment show as two extract rows instead of one summed row.
504
+ -- Bills stay pure obligations; v_invoice_balances already filters movement_kind='bill',
505
+ -- so the new payment rows don't affect invoice balances. Backfill is idempotent.
506
+ -- Requires fee_amount (006b_extract_fee_amount.sql).
507
+
508
+ INSERT INTO public.plg_financial_movements
509
+ (tenant_id, invoice_id, direction, movement_kind, amount, paid_amount, fee_amount, status,
510
+ due_date, payment_date, installment_number, payment_method_id, payment_method_type_id,
511
+ bank_account_id, cash_session_id, card_brand, card_installments, notes, metadata)
512
+ SELECT
513
+ b.tenant_id, b.invoice_id, b.direction, 'payment', b.paid_amount, b.paid_amount,
514
+ COALESCE(b.fee_amount, 0), 'paid',
515
+ b.due_date, COALESCE(b.payment_date, b.updated_at::date, CURRENT_DATE), b.installment_number,
516
+ b.payment_method_id, b.payment_method_type_id, b.bank_account_id, b.cash_session_id,
517
+ b.card_brand, b.card_installments, b.notes,
518
+ jsonb_build_object('backfilledFromBill', b.id)
519
+ FROM public.plg_financial_movements b
520
+ WHERE b.movement_kind = 'bill' AND b.paid_amount > 0
521
+ AND NOT EXISTS (
522
+ SELECT 1 FROM public.plg_financial_movements p
523
+ WHERE p.movement_kind = 'payment' AND p.metadata->>'backfilledFromBill' = b.id::text
524
+ );
525
+
526
+ UPDATE public.plg_financial_movements
527
+ SET payment_date = NULL, bank_account_id = NULL, payment_method_id = NULL,
528
+ payment_method_type_id = NULL, cash_session_id = NULL, card_brand = NULL, card_installments = NULL
529
+ WHERE movement_kind = 'bill';
530
+ `
531
+
532
+ export const MIGRATIONS: Array<{ id: string; sql: string }> = [
533
+ { id: "000_plg_rename", sql: MIGRATION_000_PLG_RENAME },
534
+ { id: "001_financial_base", sql: MIGRATION_001_FINANCIAL_BASE },
535
+ { id: "002_chart_of_accounts", sql: MIGRATION_002_CHART_OF_ACCOUNTS },
536
+ { id: "003_card_brands", sql: MIGRATION_003_CARD_BRANDS },
537
+ { id: "004_order_to_cash", sql: MIGRATION_004_ORDER_TO_CASH },
538
+ { id: "005_seed_defaults", sql: MIGRATION_005_SEED_DEFAULTS },
539
+ { id: "006_rls_policies", sql: MIGRATION_006_RLS_POLICIES },
540
+ { id: "006b_extract_fee_amount", sql: MIGRATION_006B_EXTRACT_FEE_AMOUNT },
541
+ { id: "007_reconciliation", sql: MIGRATION_007_RECONCILIATION },
542
+ { id: "007b_movement_payment_method_type", sql: MIGRATION_007B_MOVEMENT_PAYMENT_METHOD_TYPE },
543
+ { id: "008_split_payment_movements", sql: MIGRATION_008_SPLIT_PAYMENT_MOVEMENTS },
544
+ ]
package/src/registries.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { PluginRegistryDef } from '@fayz-ai/core'
2
2
  import type { EntityDef } from '@fayz-ai/core'
3
+ import { T } from './data/tables'
3
4
 
4
5
  // ---------------------------------------------------------------------------
5
6
  // Payment Method Types (seeded — Cash, PIX, Credit Card, etc.)
@@ -16,7 +17,7 @@ const paymentMethodTypeEntity: EntityDef = {
16
17
  { key: 'transactionType', label: 'Transaction Type', type: 'select', options: ['cash', 'credit_card', 'debit_card', 'pix', 'bank_transfer', 'check', 'voucher'], showInTable: true, required: true },
17
18
  { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
18
19
  ],
19
- data: { table: 'payment_method_types', tenantScoped: true },
20
+ data: { table: T.paymentMethodTypes, tenantScoped: true },
20
21
  }
21
22
 
22
23
  // ---------------------------------------------------------------------------
@@ -33,14 +34,14 @@ const paymentMethodEntity: EntityDef = {
33
34
  { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
34
35
  // FK to the seeded payment_method_types catalog — load real ids at runtime
35
36
  // (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 },
37
+ { key: 'paymentMethodTypeId', label: 'Type', type: 'relation', relation: { table: T.paymentMethodTypes, labelField: 'name' }, showInTable: true, required: true },
37
38
  { key: 'discountValue', label: 'Discount %', type: 'number', showInTable: true },
38
39
  { key: 'interestValue', label: 'Interest %', type: 'number', showInTable: true },
39
40
  { key: 'minInstallments', label: 'Min Installments', type: 'number', defaultValue: 1 },
40
41
  { key: 'maxInstallments', label: 'Max Installments', type: 'number', defaultValue: 1, showInTable: true },
41
42
  { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
42
43
  ],
43
- data: { table: 'payment_methods', tenantScoped: true },
44
+ data: { table: T.paymentMethods, tenantScoped: true },
44
45
  }
45
46
 
46
47
  // ---------------------------------------------------------------------------
@@ -60,7 +61,7 @@ const chartOfAccountsEntity: EntityDef = {
60
61
  { key: 'parentId', label: 'Parent', type: 'text', showInTable: false },
61
62
  { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
62
63
  ],
63
- data: { table: 'chart_of_accounts', tenantScoped: true },
64
+ data: { table: T.chartOfAccounts, tenantScoped: true },
64
65
  }
65
66
 
66
67
  // ---------------------------------------------------------------------------
@@ -78,7 +79,7 @@ const costCenterEntity: EntityDef = {
78
79
  { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
79
80
  { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
80
81
  ],
81
- data: { table: 'cost_centers', tenantScoped: true },
82
+ data: { table: T.costCenters, tenantScoped: true },
82
83
  }
83
84
 
84
85
  // ---------------------------------------------------------------------------
@@ -108,7 +109,7 @@ const bankAccountEntity: EntityDef = {
108
109
  { key: 'dueDay', label: 'Due Day', type: 'number' },
109
110
  { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
110
111
  ],
111
- data: { table: 'bank_accounts', tenantScoped: true },
112
+ data: { table: T.bankAccounts, tenantScoped: true },
112
113
  }
113
114
 
114
115
  // ---------------------------------------------------------------------------
@@ -125,7 +126,7 @@ const cardBrandEntity: EntityDef = {
125
126
  { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
126
127
  { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
127
128
  ],
128
- data: { table: 'card_brands', tenantScoped: true },
129
+ data: { table: T.cardBrands, tenantScoped: true },
129
130
  }
130
131
 
131
132
  // ---------------------------------------------------------------------------