@fayz-ai/plugin-banking-br 0.10.41 → 0.10.43

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,609 @@
1
+ // src/migrations/index.ts
2
+ var MIGRATION_000_BASELINE = `-- ============================================================================
3
+ -- plugins/plugin-banking-br/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.
4
+ --
5
+ -- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:
6
+ -- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs
7
+ -- prove the result still matches.
8
+ --
9
+ -- This replaced 2 migration files. They are kept, unapplied, in
10
+ -- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no
11
+ -- longer worth replaying.
12
+ --
13
+ -- Object order is fixed rather than dependency-sorted: schemas, types,
14
+ -- sequences, tables, defaults, functions, constraints, views, indexes, foreign
15
+ -- keys, triggers, row security, policies, grants. Dependencies BETWEEN units
16
+ -- are carried by the order packages/db/chain.json declares.
17
+ -- ============================================================================
18
+
19
+ -- A LANGUAGE sql function binds its body at CREATE time, and 570 functions
20
+ -- sorted by name are not sorted by who calls whom. This is what lets them load
21
+ -- in any order; every body is still compiled on first call, so a genuinely
22
+ -- broken reference surfaces then rather than never. Session-scoped.
23
+ SET check_function_bodies = false;
24
+
25
+
26
+
27
+ -- \u2500\u2500 default privileges: reset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
28
+ -- Cleared BEFORE anything is created, so every object below gets the ACL the
29
+ -- dump describes rather than the image's defaults on top of it. Restored at
30
+ -- the end of this file.
31
+
32
+ DO $dp$ BEGIN
33
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
34
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
35
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
36
+ END $dp$;
37
+
38
+ DO $dp$ BEGIN
39
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
40
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
41
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
42
+ END $dp$;
43
+
44
+ DO $dp$ BEGIN
45
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
46
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
47
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
48
+ END $dp$;
49
+
50
+ DO $dp$ BEGIN
51
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
52
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
53
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
54
+ END $dp$;
55
+
56
+ DO $dp$ BEGIN
57
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
58
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
59
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
60
+ END $dp$;
61
+
62
+ DO $dp$ BEGIN
63
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
64
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
65
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
66
+ END $dp$;
67
+
68
+
69
+ -- \u2500\u2500 function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
70
+
71
+ CREATE FUNCTION public.plg_bank_emit_movement_imported() RETURNS trigger
72
+ LANGUAGE plpgsql SECURITY DEFINER
73
+ SET search_path TO 'public'
74
+ AS $$
75
+ DECLARE
76
+ v_payload jsonb;
77
+ BEGIN
78
+ IF NEW.external_source IS NULL OR NEW.external_id IS NULL THEN
79
+ RETURN NULL;
80
+ END IF;
81
+
82
+ -- The contract every consumer reads. Not the row: a whole-row payload would
83
+ -- put the ledger's internal bookkeeping inside every webhook by default.
84
+ v_payload := jsonb_build_object(
85
+ 'movement_id', NEW.id::text,
86
+ 'provider', NEW.external_source,
87
+ 'external_id', NEW.external_id,
88
+ 'direction', NEW.direction,
89
+ 'amount', NEW.amount,
90
+ 'payment_date', NEW.payment_date,
91
+ 'description', NEW.notes,
92
+ 'bank_account_id', NEW.bank_account_id);
93
+
94
+ BEGIN
95
+ PERFORM public.plg_emit_event(
96
+ 'financial.movement.imported', v_payload, 'financial_movement',
97
+ NEW.id::text, NEW.tenant_id, NULL);
98
+ EXCEPTION WHEN OTHERS THEN
99
+ -- Observability must never be able to reject the write that caused it.
100
+ RAISE WARNING '[plg_bank_emit_movement_imported] % on %: %', TG_OP, TG_TABLE_NAME, SQLERRM;
101
+ END;
102
+
103
+ RETURN NULL;
104
+ END;
105
+ $$;
106
+
107
+ CREATE FUNCTION public.plugbank_import_movements(p_payload jsonb) RETURNS jsonb
108
+ LANGUAGE plpgsql SECURITY DEFINER
109
+ SET search_path TO 'public'
110
+ AS $$
111
+ DECLARE
112
+ v_tenants uuid[];
113
+ v_tenant uuid;
114
+ v_connection uuid;
115
+ v_instance text;
116
+ v_account uuid;
117
+ v_connector text;
118
+ v_trigger text;
119
+ v_from date;
120
+ v_to date;
121
+ v_started timestamptz := now();
122
+ v_line jsonb;
123
+ v_external text;
124
+ v_amount numeric(14,2);
125
+ v_date date;
126
+ v_fetched int := 0;
127
+ v_imported int := 0;
128
+ v_duplicates int := 0;
129
+ v_inserted uuid;
130
+ v_run uuid;
131
+ BEGIN
132
+ v_connection := nullif(btrim(p_payload->>'integration_id'), '')::uuid;
133
+ IF v_connection IS NULL THEN
134
+ RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'
135
+ USING ERRCODE = 'invalid_parameter_value';
136
+ END IF;
137
+
138
+ SELECT c.tenant_id, c.instance_key, c.connector_id
139
+ INTO v_tenant, v_instance, v_connector
140
+ FROM public.plg_connections c
141
+ WHERE c.id = v_connection AND c.active;
142
+
143
+ IF v_tenant IS NULL THEN
144
+ RAISE EXCEPTION 'plugbank_import_movements: no active connection %', v_connection
145
+ USING ERRCODE = 'no_data_found';
146
+ END IF;
147
+
148
+ -- The account this connection is for. \`instance_key\` is text because it also
149
+ -- carries things that are not uuids for other connectors (a Bling company, a
150
+ -- gateway instance); here it is the bank account, and '' is the account-less
151
+ -- default that \`bank_account_id IS NULL\` used to mean.
152
+ v_account := nullif(btrim(v_instance), '')::uuid;
153
+
154
+ -- SECURITY DEFINER bypasses RLS, so the membership check the policy would
155
+ -- have made has to happen here.
156
+ SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;
157
+ IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN
158
+ RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant
159
+ USING ERRCODE = 'insufficient_privilege';
160
+ END IF;
161
+
162
+ -- Refusing beats ignoring: a caller that believes it is importing into
163
+ -- another tenant must be told, not silently redirected into this one.
164
+ IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL
165
+ AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN
166
+ RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the connection'
167
+ USING ERRCODE = 'insufficient_privilege';
168
+ END IF;
169
+
170
+ -- Same rule as the tenant, one level down: the account is the connection's,
171
+ -- and a payload naming a different one is a caller importing into the wrong
172
+ -- ledger account. 002 let the payload override it silently \u2014 with one
173
+ -- connection per account that override is how two accounts get mixed.
174
+ IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL
175
+ AND (p_payload->>'bank_account_id')::uuid IS DISTINCT FROM v_account THEN
176
+ RAISE EXCEPTION 'plugbank_import_movements: payload names another bank account; the account comes from the connection'
177
+ USING ERRCODE = 'insufficient_privilege';
178
+ END IF;
179
+
180
+ v_from := nullif(btrim(p_payload->>'period_from'), '')::date;
181
+ v_to := nullif(btrim(p_payload->>'period_to'), '')::date;
182
+ v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));
183
+ IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN
184
+ RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger
185
+ USING ERRCODE = 'invalid_parameter_value';
186
+ END IF;
187
+
188
+ FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))
189
+ LOOP
190
+ v_fetched := v_fetched + 1;
191
+ v_external := nullif(btrim(v_line->>'external_id'), '');
192
+ v_date := nullif(btrim(v_line->>'date'), '')::date;
193
+ v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));
194
+
195
+ -- A line with no provider id cannot be recognised on re-delivery, so it
196
+ -- would import again on every run. Skipping it is the lesser wrong.
197
+ IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN
198
+ CONTINUE;
199
+ END IF;
200
+
201
+ INSERT INTO public.plg_financial_movements (
202
+ tenant_id, direction, movement_kind, amount, paid_amount, status,
203
+ due_date, payment_date, bank_account_id, notes, external_id, external_source)
204
+ VALUES (
205
+ v_tenant,
206
+ CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,
207
+ 'payment', v_amount, v_amount, 'paid',
208
+ v_date, v_date, v_account,
209
+ nullif(btrim(v_line->>'description'), ''),
210
+ v_external, coalesce(v_connector, 'plugbank'))
211
+ ON CONFLICT (tenant_id, external_source, external_id)
212
+ WHERE external_id IS NOT NULL AND external_source IS NOT NULL
213
+ DO NOTHING
214
+ RETURNING id INTO v_inserted;
215
+
216
+ IF v_inserted IS NULL THEN
217
+ v_duplicates := v_duplicates + 1;
218
+ ELSE
219
+ v_imported := v_imported + 1;
220
+ END IF;
221
+ v_inserted := NULL;
222
+ END LOOP;
223
+
224
+ UPDATE public.plg_connections
225
+ SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()
226
+ WHERE id = v_connection;
227
+
228
+ -- \`stats\` is where the three facts a manual statement import has and a generic
229
+ -- run does not go: the operator picked a period, a re-import of that period is
230
+ -- mostly duplicates by design, and someone pressed the button. The panel reads
231
+ -- all three back from here.
232
+ INSERT INTO public.plg_sync_runs (
233
+ tenant_id, connection_id, connector_id, direction, trigger_kind, status,
234
+ stream, fetched, written, stats, started_at, finished_at)
235
+ VALUES (
236
+ v_tenant, v_connection, coalesce(v_connector, 'plugbank'), 'inbound', v_trigger, 'success',
237
+ 'statement', v_fetched, v_imported,
238
+ jsonb_strip_nulls(jsonb_build_object(
239
+ 'duplicates', v_duplicates,
240
+ 'period_from', v_from,
241
+ 'period_to', v_to,
242
+ 'triggered_by_user_id', auth.uid())),
243
+ v_started, now())
244
+ RETURNING id INTO v_run;
245
+
246
+ RETURN jsonb_build_object(
247
+ 'ok', true,
248
+ 'sync_run_id', v_run,
249
+ 'fetched', v_fetched,
250
+ 'imported', v_imported,
251
+ 'duplicates', v_duplicates);
252
+ END;
253
+ $$;
254
+
255
+
256
+ -- \u2500\u2500 trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
257
+
258
+ CREATE TRIGGER plg_financial_movements_emit_imported AFTER INSERT ON public.plg_financial_movements FOR EACH ROW EXECUTE FUNCTION public.plg_bank_emit_movement_imported();
259
+
260
+
261
+ -- \u2500\u2500 acl \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
262
+
263
+ REVOKE ALL ON FUNCTION public.plg_bank_emit_movement_imported() FROM PUBLIC;
264
+ GRANT ALL ON FUNCTION public.plg_bank_emit_movement_imported() TO authenticated;
265
+ GRANT ALL ON FUNCTION public.plg_bank_emit_movement_imported() TO service_role;
266
+
267
+ REVOKE ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) FROM PUBLIC;
268
+ GRANT ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) TO authenticated;
269
+ GRANT ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) TO service_role;
270
+
271
+
272
+ -- \u2500\u2500 comment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
273
+
274
+ COMMENT ON FUNCTION public.plugbank_import_movements(p_payload jsonb) IS 'Import bank-statement lines into the financial ledger and file the run in plg_sync_runs. The tenant AND the bank account come from the plg_connections row named by integration_id, never from the payload.';
275
+
276
+
277
+ -- \u2500\u2500 default privileges: restore \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
278
+ -- Put back, so the NEXT thing installed inherits what the chain had.
279
+
280
+ DO $dp$ BEGIN
281
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
282
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
283
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
284
+ END $dp$;
285
+
286
+ DO $dp$ BEGIN
287
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
288
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
289
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
290
+ END $dp$;
291
+
292
+ DO $dp$ BEGIN
293
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
294
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
295
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
296
+ END $dp$;
297
+
298
+ DO $dp$ BEGIN
299
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
300
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
301
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
302
+ END $dp$;
303
+
304
+ DO $dp$ BEGIN
305
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
306
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
307
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
308
+ END $dp$;
309
+
310
+ DO $dp$ BEGIN
311
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
312
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
313
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
314
+ END $dp$;
315
+
316
+ DO $dp$ BEGIN
317
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
318
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
319
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
320
+ END $dp$;
321
+
322
+ DO $dp$ BEGIN
323
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
324
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
325
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
326
+ END $dp$;
327
+
328
+ DO $dp$ BEGIN
329
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
330
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
331
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
332
+ END $dp$;
333
+
334
+ DO $dp$ BEGIN
335
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
336
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
337
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
338
+ END $dp$;
339
+
340
+ DO $dp$ BEGIN
341
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
342
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
343
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
344
+ END $dp$;
345
+
346
+ DO $dp$ BEGIN
347
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
348
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
349
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
350
+ END $dp$;
351
+
352
+ DO $dp$ BEGIN
353
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
354
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
355
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
356
+ END $dp$;
357
+
358
+ DO $dp$ BEGIN
359
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
360
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
361
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
362
+ END $dp$;
363
+
364
+ DO $dp$ BEGIN
365
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
366
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
367
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
368
+ END $dp$;
369
+
370
+ DO $dp$ BEGIN
371
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
372
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
373
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
374
+ END $dp$;
375
+
376
+ DO $dp$ BEGIN
377
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
378
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
379
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
380
+ END $dp$;
381
+
382
+ DO $dp$ BEGIN
383
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
384
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
385
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
386
+ END $dp$;
387
+
388
+ DO $dp$ BEGIN
389
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
390
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
391
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
392
+ END $dp$;
393
+
394
+ DO $dp$ BEGIN
395
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
396
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
397
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
398
+ END $dp$;
399
+
400
+ DO $dp$ BEGIN
401
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
402
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
403
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
404
+ END $dp$;
405
+
406
+ DO $dp$ BEGIN
407
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
408
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
409
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
410
+ END $dp$;
411
+
412
+ DO $dp$ BEGIN
413
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
414
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
415
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
416
+ END $dp$;
417
+
418
+ DO $dp$ BEGIN
419
+ EXECUTE $stmt$--
420
+ -- PostgreSQL database dump complete
421
+ --$stmt$;
422
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
423
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--
424
+ -- PostgreSQL database dump complete
425
+ --$stmt$;
426
+ END $dp$;
427
+ `;
428
+ var MIGRATION_001_THE_IMPORT_RPC_NAMES_REAL_TABLES = `-- ==========================================================================
429
+ -- 001_the_import_rpc_names_real_tables \u2014 plugbank_import_movements
430
+ -- stops reaching through 141's bridge (#352).
431
+ --
432
+ -- 107 renamed the connector spine out of the \`plg_\` prefix \u2014 the prefix means
433
+ -- "a plugin owns this", and the connector framework's tables are the BASE's.
434
+ -- 141 left \`plg_connections\` and \`plg_sync_runs\` answering as views so callers
435
+ -- could move without a synchronised deploy. This function never moved, and it
436
+ -- is not alone: \`plg_connections\` had 13 callers and \`connections\` had none.
437
+ -- The bridge became the road.
438
+ --
439
+ -- The body is 000_baseline's, unchanged except for the three names. It is not
440
+ -- an edit of that file \u2014 an applied migration is immutable and the ledger
441
+ -- treats drift as a hard stop \u2014 it is a CREATE OR REPLACE that lands after it.
442
+ --
443
+ -- Why this plugin has to carry its own: the function is banking-br's, and the
444
+ -- views it stops using are the spine's. The spine runs FIRST, so a spine file
445
+ -- cannot drop a view that a plugin applied later still reads. Each package
446
+ -- moves its own callers; the aliases go once nobody is left.
447
+ --
448
+ -- Replay-safe: CREATE OR REPLACE, same signature, same grants (which 000 set
449
+ -- and this does not touch).
450
+ -- ==========================================================================
451
+
452
+ CREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb) RETURNS jsonb
453
+ LANGUAGE plpgsql SECURITY DEFINER
454
+ SET search_path TO 'public'
455
+ AS $$
456
+ DECLARE
457
+ v_tenants uuid[];
458
+ v_tenant uuid;
459
+ v_connection uuid;
460
+ v_instance text;
461
+ v_account uuid;
462
+ v_connector text;
463
+ v_trigger text;
464
+ v_from date;
465
+ v_to date;
466
+ v_started timestamptz := now();
467
+ v_line jsonb;
468
+ v_external text;
469
+ v_amount numeric(14,2);
470
+ v_date date;
471
+ v_fetched int := 0;
472
+ v_imported int := 0;
473
+ v_duplicates int := 0;
474
+ v_inserted uuid;
475
+ v_run uuid;
476
+ BEGIN
477
+ v_connection := nullif(btrim(p_payload->>'integration_id'), '')::uuid;
478
+ IF v_connection IS NULL THEN
479
+ RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'
480
+ USING ERRCODE = 'invalid_parameter_value';
481
+ END IF;
482
+
483
+ SELECT c.tenant_id, c.instance_key, c.connector_id
484
+ INTO v_tenant, v_instance, v_connector
485
+ FROM public.connections c
486
+ WHERE c.id = v_connection AND c.active;
487
+
488
+ IF v_tenant IS NULL THEN
489
+ RAISE EXCEPTION 'plugbank_import_movements: no active connection %', v_connection
490
+ USING ERRCODE = 'no_data_found';
491
+ END IF;
492
+
493
+ -- The account this connection is for. \`instance_key\` is text because it also
494
+ -- carries things that are not uuids for other connectors (a Bling company, a
495
+ -- gateway instance); here it is the bank account, and '' is the account-less
496
+ -- default that \`bank_account_id IS NULL\` used to mean.
497
+ v_account := nullif(btrim(v_instance), '')::uuid;
498
+
499
+ -- SECURITY DEFINER bypasses RLS, so the membership check the policy would
500
+ -- have made has to happen here.
501
+ SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;
502
+ IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN
503
+ RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant
504
+ USING ERRCODE = 'insufficient_privilege';
505
+ END IF;
506
+
507
+ -- Refusing beats ignoring: a caller that believes it is importing into
508
+ -- another tenant must be told, not silently redirected into this one.
509
+ IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL
510
+ AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN
511
+ RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the connection'
512
+ USING ERRCODE = 'insufficient_privilege';
513
+ END IF;
514
+
515
+ -- Same rule as the tenant, one level down: the account is the connection's,
516
+ -- and a payload naming a different one is a caller importing into the wrong
517
+ -- ledger account. 002 let the payload override it silently \u2014 with one
518
+ -- connection per account that override is how two accounts get mixed.
519
+ IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL
520
+ AND (p_payload->>'bank_account_id')::uuid IS DISTINCT FROM v_account THEN
521
+ RAISE EXCEPTION 'plugbank_import_movements: payload names another bank account; the account comes from the connection'
522
+ USING ERRCODE = 'insufficient_privilege';
523
+ END IF;
524
+
525
+ v_from := nullif(btrim(p_payload->>'period_from'), '')::date;
526
+ v_to := nullif(btrim(p_payload->>'period_to'), '')::date;
527
+ v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));
528
+ IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN
529
+ RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger
530
+ USING ERRCODE = 'invalid_parameter_value';
531
+ END IF;
532
+
533
+ FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))
534
+ LOOP
535
+ v_fetched := v_fetched + 1;
536
+ v_external := nullif(btrim(v_line->>'external_id'), '');
537
+ v_date := nullif(btrim(v_line->>'date'), '')::date;
538
+ v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));
539
+
540
+ -- A line with no provider id cannot be recognised on re-delivery, so it
541
+ -- would import again on every run. Skipping it is the lesser wrong.
542
+ IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN
543
+ CONTINUE;
544
+ END IF;
545
+
546
+ INSERT INTO public.plg_financial_movements (
547
+ tenant_id, direction, movement_kind, amount, paid_amount, status,
548
+ due_date, payment_date, bank_account_id, notes, external_id, external_source)
549
+ VALUES (
550
+ v_tenant,
551
+ CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,
552
+ 'payment', v_amount, v_amount, 'paid',
553
+ v_date, v_date, v_account,
554
+ nullif(btrim(v_line->>'description'), ''),
555
+ v_external, coalesce(v_connector, 'plugbank'))
556
+ ON CONFLICT (tenant_id, external_source, external_id)
557
+ WHERE external_id IS NOT NULL AND external_source IS NOT NULL
558
+ DO NOTHING
559
+ RETURNING id INTO v_inserted;
560
+
561
+ IF v_inserted IS NULL THEN
562
+ v_duplicates := v_duplicates + 1;
563
+ ELSE
564
+ v_imported := v_imported + 1;
565
+ END IF;
566
+ v_inserted := NULL;
567
+ END LOOP;
568
+
569
+ UPDATE public.connections
570
+ SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()
571
+ WHERE id = v_connection;
572
+
573
+ -- \`stats\` is where the three facts a manual statement import has and a generic
574
+ -- run does not go: the operator picked a period, a re-import of that period is
575
+ -- mostly duplicates by design, and someone pressed the button. The panel reads
576
+ -- all three back from here.
577
+ INSERT INTO public.sync_runs (
578
+ tenant_id, connection_id, connector_id, direction, trigger_kind, status,
579
+ stream, fetched, written, stats, started_at, finished_at)
580
+ VALUES (
581
+ v_tenant, v_connection, coalesce(v_connector, 'plugbank'), 'inbound', v_trigger, 'success',
582
+ 'statement', v_fetched, v_imported,
583
+ jsonb_strip_nulls(jsonb_build_object(
584
+ 'duplicates', v_duplicates,
585
+ 'period_from', v_from,
586
+ 'period_to', v_to,
587
+ 'triggered_by_user_id', auth.uid())),
588
+ v_started, now())
589
+ RETURNING id INTO v_run;
590
+
591
+ RETURN jsonb_build_object(
592
+ 'ok', true,
593
+ 'sync_run_id', v_run,
594
+ 'fetched', v_fetched,
595
+ 'imported', v_imported,
596
+ 'duplicates', v_duplicates);
597
+ END;
598
+ $$;
599
+ COMMENT ON FUNCTION public.plugbank_import_movements(p_payload jsonb) IS
600
+ 'Import bank-statement lines into the financial ledger and file the run in public.sync_runs. The tenant AND the bank account come from the public.connections row named by integration_id, never from the payload.';
601
+ `;
602
+ var MIGRATIONS = [
603
+ { id: "000_baseline", sql: MIGRATION_000_BASELINE },
604
+ { id: "001_the_import_rpc_names_real_tables", sql: MIGRATION_001_THE_IMPORT_RPC_NAMES_REAL_TABLES }
605
+ ];
606
+
607
+ export { MIGRATIONS, MIGRATION_000_BASELINE, MIGRATION_001_THE_IMPORT_RPC_NAMES_REAL_TABLES };
608
+ //# sourceMappingURL=main.js.map
609
+ //# sourceMappingURL=main.js.map