@fayz-ai/plugin-banking-br 0.10.6 → 0.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ function toSyncRun(run) {
49
49
  periodFrom: stats.period_from ?? void 0,
50
50
  periodTo: stats.period_to ?? void 0,
51
51
  duplicates: Number(stats.duplicates ?? 0),
52
- // plg_sync_runs has no created_at: a run is identified by when it STARTED,
52
+ // sync_runs has no created_at: a run is identified by when it STARTED,
53
53
  // which is what the old created_at recorded anyway (the row was inserted at
54
54
  // the end, so the two disagreed by the length of the import).
55
55
  createdAt: run.startedAt ?? ""
@@ -90,7 +90,7 @@ function createOpenBankingProvider() {
90
90
  const { data, error } = await supabase.functions.invoke(FUNCTION, {
91
91
  body: {
92
92
  action: "import_transactions",
93
- // Still `integrationId` on the wire: it now carries a plg_connections
93
+ // Still `integrationId` on the wire: it now carries a connections
94
94
  // id, and keeping the key means the deployed function needs no
95
95
  // coordinated release. The RPC takes the tenant AND the bank account
96
96
  // from that row, so neither can be steered from here.
@@ -329,7 +329,7 @@ var openBankingConnector = {
329
329
  await provider.saveIntegration();
330
330
  },
331
331
  disconnect: () => provider.disconnect(),
332
- // Every import files a `plg_sync_runs` row, and the shared surface renders it.
332
+ // Every import files a `sync_runs` row, and the shared surface renders it.
333
333
  // What is connector-specific is not the VIEW, it is the meaning of `stats`:
334
334
  // "de 01/08 a 12/08 · 3 duplicadas" is three keys only this connector can
335
335
  // read, turned here into the one sentence every run carries. Scoped to the
@@ -393,121 +393,122 @@ function buildBankingDomainEvents() {
393
393
  }
394
394
 
395
395
  // src/migrations/index.ts
396
- var MIGRATION_002_CREDENTIAL_AND_FACTS = `-- ============================================================================
397
- -- plugin-banking-br 002: the credential leaves the tenant database, the import
398
- -- gets a door it cannot be routed around, and the import becomes a FACT.
396
+ var MIGRATION_000_BASELINE = `-- ============================================================================
397
+ -- plugins/plugin-banking-br/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.
399
398
  --
400
- -- \xA71 credential drop api_token + cnpj from public.bank_integrations
401
- -- \xA72 sync runs bank_integration_sync_log speaks the SyncRun vocabulary
402
- -- \xA73 import plugbank_import_movements \u2014 the tenant-safe door
403
- -- \xA74 facts financial.movement.imported
399
+ -- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:
400
+ -- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs
401
+ -- prove the result still matches.
404
402
  --
405
- -- Additive and idempotent: no table is created, no policy is rewritten, and the
406
- -- tenant RLS from 001 already covers every column added here.
403
+ -- This replaced 2 migration files. They are kept, unapplied, in
404
+ -- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no
405
+ -- longer worth replaying.
406
+ --
407
+ -- Object order is fixed rather than dependency-sorted: schemas, types,
408
+ -- sequences, tables, defaults, functions, constraints, views, indexes, foreign
409
+ -- keys, triggers, row security, policies, grants. Dependencies BETWEEN units
410
+ -- are carried by the order packages/db/chain.json declares.
407
411
  -- ============================================================================
408
412
 
409
- -- \xA71 \u2500\u2500 credential \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
410
- -- THE EXPOSURE THIS CLOSES: 001 stored \`api_token text\` and \`cnpj text\` on
411
- -- public.bank_integrations. That table is read from the browser with the
412
- -- publishable key under tenant RLS, so the bank credential was readable by ANY
413
- -- authenticated member of the tenant; and a pool is one Supabase project serving
414
- -- N tenants (beauty-saas and espaco-renova-rio share one), so anything running
415
- -- there with service_role could read every clinic's bank token at once.
416
- --
417
- -- PlugBank differs from RankLayer, which dropped its key with nothing to replace
418
- -- it: PlugBank is a PULL connector and genuinely needs a credential at fetch
419
- -- time. It just must not be THIS copy. The destination is the platform's
420
- -- encrypted ConnectorConnection store, redeemed per call (FAY-1360 + FAY-1389).
421
- -- NEITHER HAS HAPPENED YET: as of 2026-08-12 the live pool still holds the
422
- -- token in this column, this migration is unapplied there, and the deployed
423
- -- plugbank-sync (v6) still reads it. The FAY-1360 rollout must register the
424
- -- credential at the platform BEFORE this drop runs on that pool \u2014 otherwise
425
- -- what is removed here is the only copy, and the connection dies.
426
- --
427
- -- \`cnpj\` goes with it rather than surviving as a masked remnant. It is not
428
- -- decoration: paired with the token it is what identifies the account at the
429
- -- provider (\`/v1/extrato?cnpj=\u2026\`), which makes it half of the credential and
430
- -- therefore the platform's to hold. And nothing on this side reads it \u2014 the
431
- -- live connector panel never renders it, and the unified connect form starts
432
- -- every field empty rather than pre-filling from the row. A masked last-N would
433
- -- be a column added to serve a screen that does not exist, on data that
434
- -- identifies a real company.
435
- --
436
- -- ROTATE any token a pool already stored. DROP COLUMN is a catalog change: the
437
- -- old values stay in the heap until the next table rewrite.
413
+ -- A LANGUAGE sql function binds its body at CREATE time, and 570 functions
414
+ -- sorted by name are not sorted by who calls whom. This is what lets them load
415
+ -- in any order; every body is still compiled on first call, so a genuinely
416
+ -- broken reference surfaces then rather than never. Session-scoped.
417
+ SET check_function_bodies = false;
418
+
419
+
420
+
421
+ -- \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
422
+ -- Cleared BEFORE anything is created, so every object below gets the ACL the
423
+ -- dump describes rather than the image's defaults on top of it. Restored at
424
+ -- the end of this file.
425
+
426
+ DO $dp$ BEGIN
427
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
428
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
429
+ 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$;
430
+ END $dp$;
431
+
432
+ DO $dp$ BEGIN
433
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;
434
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
435
+ 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$;
436
+ END $dp$;
437
+
438
+ DO $dp$ BEGIN
439
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
440
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
441
+ 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$;
442
+ END $dp$;
443
+
444
+ DO $dp$ BEGIN
445
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;
446
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
447
+ 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$;
448
+ END $dp$;
438
449
 
439
- DO $$
450
+ DO $dp$ BEGIN
451
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
452
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
453
+ 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$;
454
+ END $dp$;
455
+
456
+ DO $dp$ BEGIN
457
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;
458
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
459
+ 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$;
460
+ END $dp$;
461
+
462
+
463
+ -- \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
464
+
465
+ CREATE FUNCTION public.plg_bank_emit_movement_imported() RETURNS trigger
466
+ LANGUAGE plpgsql SECURITY DEFINER
467
+ SET search_path TO 'public'
468
+ AS $$
469
+ DECLARE
470
+ v_payload jsonb;
440
471
  BEGIN
441
- IF to_regclass('public.bank_integrations') IS NOT NULL THEN
442
- ALTER TABLE public.bank_integrations DROP COLUMN IF EXISTS api_token;
443
- ALTER TABLE public.bank_integrations DROP COLUMN IF EXISTS cnpj;
472
+ IF NEW.external_source IS NULL OR NEW.external_id IS NULL THEN
473
+ RETURN NULL;
444
474
  END IF;
445
- END $$;
446
475
 
447
- -- \xA72 \u2500\u2500 sync runs \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
448
- -- bank_integration_sync_log already IS a SyncRun: one row per execution, with
449
- -- status/fetched/written/error. It gains the four facts the shared vocabulary
450
- -- has and it lacked, rather than being replaced by a second table beside it \u2014
451
- -- two audit trails for one import is how "the history is wrong" starts.
452
- --
453
- -- The count columns keep their names. \`transactions_fetched\`/\`transactions_
454
- -- imported\` map to SyncRun's \`fetched\`/\`written\` in the data provider, because
455
- -- renaming them would break the deployed edge function's INSERT the moment this
456
- -- migration lands, and a rename that has to be choreographed across two
457
- -- repositories is not worth a nicer column name.
458
- DO $$
459
- BEGIN
460
- IF to_regclass('public.bank_integration_sync_log') IS NULL THEN RETURN; END IF;
461
-
462
- -- Which provider produced the run. One row per execution says nothing about
463
- -- who ran it once a second bank connector shares the table.
464
- ALTER TABLE public.bank_integration_sync_log
465
- ADD COLUMN IF NOT EXISTS provider text NOT NULL DEFAULT 'plugbank';
466
- -- Inbound: we reach the bank, never the other way round.
467
- ALTER TABLE public.bank_integration_sync_log
468
- ADD COLUMN IF NOT EXISTS direction text NOT NULL DEFAULT 'inbound';
469
- -- \`trigger\` is a reserved word; the column is trigger_kind and the TypeScript
470
- -- side reads it as SyncRun.trigger.
471
- ALTER TABLE public.bank_integration_sync_log
472
- ADD COLUMN IF NOT EXISTS trigger_kind text NOT NULL DEFAULT 'manual';
473
- ALTER TABLE public.bank_integration_sync_log
474
- ADD COLUMN IF NOT EXISTS started_at timestamptz;
475
- ALTER TABLE public.bank_integration_sync_log
476
- ADD COLUMN IF NOT EXISTS finished_at timestamptz;
477
-
478
- COMMENT ON COLUMN public.bank_integration_sync_log.trigger_kind IS
479
- 'SyncRun.trigger \u2014 on-write | scheduled | manual | webhook. Named _kind because trigger is reserved.';
480
- END $$;
481
-
482
- -- \xA73 \u2500\u2500 import \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
483
- -- The one door the statement import passes through.
484
- --
485
- -- Today the edge function inserts into plg_financial_movements directly with
486
- -- service_role, and takes \`integrationId\` from the request body without ever
487
- -- comparing it to the caller \u2014 so any authenticated member of ANY tenant in the
488
- -- pool can name another tenant's integration and write into that tenant's
489
- -- ledger. A guard in the function would be a guard the next caller routes
490
- -- around; the database is the only chokepoint that cannot be.
491
- --
492
- -- The tenant is taken from the INTEGRATION ROW, never from the payload, and a
493
- -- caller presenting a user credential must be a member of it. A caller with no
494
- -- auth.uid() is the server (service_role, a migration, the future tenant-scoped
495
- -- token) and is exempt \u2014 the same exemption plg_emit_event makes, for the same
496
- -- reason: it legitimately writes for any tenant. When FAY-1372 mints the
497
- -- tenant-scoped credential, that exemption stops applying to connector code and
498
- -- this function needs no change.
499
- CREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb)
500
- RETURNS jsonb
501
- LANGUAGE plpgsql
502
- SECURITY DEFINER
503
- SET search_path = public
504
- AS $fn$
476
+ -- The contract every consumer reads. Not the row: a whole-row payload would
477
+ -- put the ledger's internal bookkeeping inside every webhook by default.
478
+ v_payload := jsonb_build_object(
479
+ 'movement_id', NEW.id::text,
480
+ 'provider', NEW.external_source,
481
+ 'external_id', NEW.external_id,
482
+ 'direction', NEW.direction,
483
+ 'amount', NEW.amount,
484
+ 'payment_date', NEW.payment_date,
485
+ 'description', NEW.notes,
486
+ 'bank_account_id', NEW.bank_account_id);
487
+
488
+ BEGIN
489
+ PERFORM public.plg_emit_event(
490
+ 'financial.movement.imported', v_payload, 'financial_movement',
491
+ NEW.id::text, NEW.tenant_id, NULL);
492
+ EXCEPTION WHEN OTHERS THEN
493
+ -- Observability must never be able to reject the write that caused it.
494
+ RAISE WARNING '[plg_bank_emit_movement_imported] % on %: %', TG_OP, TG_TABLE_NAME, SQLERRM;
495
+ END;
496
+
497
+ RETURN NULL;
498
+ END;
499
+ $$;
500
+
501
+ CREATE FUNCTION public.plugbank_import_movements(p_payload jsonb) RETURNS jsonb
502
+ LANGUAGE plpgsql SECURITY DEFINER
503
+ SET search_path TO 'public'
504
+ AS $$
505
505
  DECLARE
506
506
  v_tenants uuid[];
507
507
  v_tenant uuid;
508
- v_integration uuid;
508
+ v_connection uuid;
509
+ v_instance text;
509
510
  v_account uuid;
510
- v_provider text;
511
+ v_connector text;
511
512
  v_trigger text;
512
513
  v_from date;
513
514
  v_to date;
@@ -522,22 +523,28 @@ DECLARE
522
523
  v_inserted uuid;
523
524
  v_run uuid;
524
525
  BEGIN
525
- v_integration := nullif(btrim(p_payload->>'integration_id'), '')::uuid;
526
- IF v_integration IS NULL THEN
526
+ v_connection := nullif(btrim(p_payload->>'integration_id'), '')::uuid;
527
+ IF v_connection IS NULL THEN
527
528
  RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'
528
529
  USING ERRCODE = 'invalid_parameter_value';
529
530
  END IF;
530
531
 
531
- SELECT bi.tenant_id, bi.bank_account_id, bi.provider
532
- INTO v_tenant, v_account, v_provider
533
- FROM public.bank_integrations bi
534
- WHERE bi.id = v_integration AND bi.active;
532
+ SELECT c.tenant_id, c.instance_key, c.connector_id
533
+ INTO v_tenant, v_instance, v_connector
534
+ FROM public.plg_connections c
535
+ WHERE c.id = v_connection AND c.active;
535
536
 
536
537
  IF v_tenant IS NULL THEN
537
- RAISE EXCEPTION 'plugbank_import_movements: no active integration %', v_integration
538
+ RAISE EXCEPTION 'plugbank_import_movements: no active connection %', v_connection
538
539
  USING ERRCODE = 'no_data_found';
539
540
  END IF;
540
541
 
542
+ -- The account this connection is for. \`instance_key\` is text because it also
543
+ -- carries things that are not uuids for other connectors (a Bling company, a
544
+ -- gateway instance); here it is the bank account, and '' is the account-less
545
+ -- default that \`bank_account_id IS NULL\` used to mean.
546
+ v_account := nullif(btrim(v_instance), '')::uuid;
547
+
541
548
  -- SECURITY DEFINER bypasses RLS, so the membership check the policy would
542
549
  -- have made has to happen here.
543
550
  SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;
@@ -550,12 +557,18 @@ BEGIN
550
557
  -- another tenant must be told, not silently redirected into this one.
551
558
  IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL
552
559
  AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN
553
- RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the integration'
560
+ RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the connection'
554
561
  USING ERRCODE = 'insufficient_privilege';
555
562
  END IF;
556
563
 
557
- IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL THEN
558
- v_account := (p_payload->>'bank_account_id')::uuid;
564
+ -- Same rule as the tenant, one level down: the account is the connection's,
565
+ -- and a payload naming a different one is a caller importing into the wrong
566
+ -- ledger account. 002 let the payload override it silently \u2014 with one
567
+ -- connection per account that override is how two accounts get mixed.
568
+ IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL
569
+ AND (p_payload->>'bank_account_id')::uuid IS DISTINCT FROM v_account THEN
570
+ RAISE EXCEPTION 'plugbank_import_movements: payload names another bank account; the account comes from the connection'
571
+ USING ERRCODE = 'insufficient_privilege';
559
572
  END IF;
560
573
 
561
574
  v_from := nullif(btrim(p_payload->>'period_from'), '')::date;
@@ -579,9 +592,6 @@ BEGIN
579
592
  CONTINUE;
580
593
  END IF;
581
594
 
582
- -- Idempotency is the partial unique index from the financial plugin's
583
- -- 007_reconciliation.sql, not a SELECT-then-INSERT: two runs of the same
584
- -- window racing each other would both find nothing and both write.
585
595
  INSERT INTO public.plg_financial_movements (
586
596
  tenant_id, direction, movement_kind, amount, paid_amount, status,
587
597
  due_date, payment_date, bank_account_id, notes, external_id, external_source)
@@ -591,7 +601,7 @@ BEGIN
591
601
  'payment', v_amount, v_amount, 'paid',
592
602
  v_date, v_date, v_account,
593
603
  nullif(btrim(v_line->>'description'), ''),
594
- v_external, coalesce(v_provider, 'plugbank'))
604
+ v_external, coalesce(v_connector, 'plugbank'))
595
605
  ON CONFLICT (tenant_id, external_source, external_id)
596
606
  WHERE external_id IS NOT NULL AND external_source IS NOT NULL
597
607
  DO NOTHING
@@ -605,17 +615,26 @@ BEGIN
605
615
  v_inserted := NULL;
606
616
  END LOOP;
607
617
 
608
- UPDATE public.bank_integrations SET last_sync_at = now(), updated_at = now()
609
- WHERE id = v_integration;
618
+ UPDATE public.plg_connections
619
+ SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()
620
+ WHERE id = v_connection;
610
621
 
611
- INSERT INTO public.bank_integration_sync_log (
612
- tenant_id, bank_integration_id, bank_account_id, period_from, period_to,
613
- transactions_fetched, transactions_imported, duplicates, status,
614
- triggered_by_user_id, provider, direction, trigger_kind, started_at, finished_at)
622
+ -- \`stats\` is where the three facts a manual statement import has and a generic
623
+ -- run does not go: the operator picked a period, a re-import of that period is
624
+ -- mostly duplicates by design, and someone pressed the button. The panel reads
625
+ -- all three back from here.
626
+ INSERT INTO public.plg_sync_runs (
627
+ tenant_id, connection_id, connector_id, direction, trigger_kind, status,
628
+ stream, fetched, written, stats, started_at, finished_at)
615
629
  VALUES (
616
- v_tenant, v_integration, v_account, v_from, v_to,
617
- v_fetched, v_imported, v_duplicates, 'success',
618
- auth.uid(), coalesce(v_provider, 'plugbank'), 'inbound', v_trigger, v_started, now())
630
+ v_tenant, v_connection, coalesce(v_connector, 'plugbank'), 'inbound', v_trigger, 'success',
631
+ 'statement', v_fetched, v_imported,
632
+ jsonb_strip_nulls(jsonb_build_object(
633
+ 'duplicates', v_duplicates,
634
+ 'period_from', v_from,
635
+ 'period_to', v_to,
636
+ 'triggered_by_user_id', auth.uid())),
637
+ v_started, now())
619
638
  RETURNING id INTO v_run;
620
639
 
621
640
  RETURN jsonb_build_object(
@@ -625,154 +644,209 @@ BEGIN
625
644
  'imported', v_imported,
626
645
  'duplicates', v_duplicates);
627
646
  END;
628
- $fn$;
647
+ $$;
629
648
 
630
- REVOKE ALL ON FUNCTION public.plugbank_import_movements(jsonb) FROM public, anon;
631
- GRANT EXECUTE ON FUNCTION public.plugbank_import_movements(jsonb) TO authenticated, service_role;
632
649
 
633
- -- \xA74 \u2500\u2500 facts \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
634
- -- One trigger on the ledger rather than an emit call inside the importer. The
635
- -- RPC above, the edge function still inserting directly, a future scheduled
636
- -- pull and a hand-run backfill all end up in the same row \u2014 so the fact exists
637
- -- for all four without any of them having to remember to say so. That is the
638
- -- entire reason plg_emit_event exists (packages/db/migrations/027_domain_events.sql).
639
- --
640
- -- It fires only when external_source is set, which is exactly what "imported"
641
- -- means: a movement that came from outside. An app-native movement typed into
642
- -- the cash register is not an import and must not wake a bank connector.
643
- --
644
- -- Provider-agnostic on purpose. \`provider\` is in the payload, so a second bank
645
- -- connector importing into the same ledger produces the same fact instead of a
646
- -- second vocabulary for the same event.
647
- CREATE OR REPLACE FUNCTION public.plg_bank_emit_movement_imported()
648
- RETURNS trigger
649
- LANGUAGE plpgsql
650
- SECURITY DEFINER
651
- SET search_path = public
652
- AS $fn$
653
- DECLARE
654
- v_payload jsonb;
655
- BEGIN
656
- IF NEW.external_source IS NULL OR NEW.external_id IS NULL THEN
657
- RETURN NULL;
658
- END IF;
650
+ -- \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
659
651
 
660
- -- The contract every consumer reads. Not the row: a whole-row payload would
661
- -- put the ledger's internal bookkeeping inside every webhook by default.
662
- v_payload := jsonb_build_object(
663
- 'movement_id', NEW.id::text,
664
- 'provider', NEW.external_source,
665
- 'external_id', NEW.external_id,
666
- 'direction', NEW.direction,
667
- 'amount', NEW.amount,
668
- 'payment_date', NEW.payment_date,
669
- 'description', NEW.notes,
670
- 'bank_account_id', NEW.bank_account_id);
652
+ 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();
671
653
 
672
- BEGIN
673
- PERFORM public.plg_emit_event(
674
- 'financial.movement.imported', v_payload, 'financial_movement',
675
- NEW.id::text, NEW.tenant_id, NULL);
676
- EXCEPTION WHEN OTHERS THEN
677
- -- Observability must never be able to reject the write that caused it.
678
- RAISE WARNING '[plg_bank_emit_movement_imported] % on %: %', TG_OP, TG_TABLE_NAME, SQLERRM;
679
- END;
680
654
 
681
- RETURN NULL;
682
- END;
683
- $fn$;
655
+ -- \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
684
656
 
685
- REVOKE ALL ON FUNCTION public.plg_bank_emit_movement_imported() FROM public, anon;
657
+ REVOKE ALL ON FUNCTION public.plg_bank_emit_movement_imported() FROM PUBLIC;
658
+ GRANT ALL ON FUNCTION public.plg_bank_emit_movement_imported() TO authenticated;
659
+ GRANT ALL ON FUNCTION public.plg_bank_emit_movement_imported() TO service_role;
686
660
 
687
- -- Guarded on the COLUMN, not just the table: the financial plugin owns both, and
688
- -- attaching this to a ledger without 007_reconciliation.sql would make every
689
- -- movement insert fail on an unknown field \u2014 an addon breaking its own host.
690
- DO $$
691
- BEGIN
692
- IF EXISTS (
693
- SELECT 1 FROM information_schema.columns
694
- WHERE table_schema = 'public'
695
- AND table_name = 'plg_financial_movements'
696
- AND column_name = 'external_source'
697
- ) THEN
698
- DROP TRIGGER IF EXISTS plg_financial_movements_emit_imported ON public.plg_financial_movements;
699
- CREATE TRIGGER plg_financial_movements_emit_imported
700
- AFTER INSERT ON public.plg_financial_movements
701
- FOR EACH ROW EXECUTE FUNCTION public.plg_bank_emit_movement_imported();
702
- END IF;
703
- END $$;
661
+ REVOKE ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) FROM PUBLIC;
662
+ GRANT ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) TO authenticated;
663
+ GRANT ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) TO service_role;
664
+
665
+
666
+ -- \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
667
+
668
+ 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.';
669
+
670
+
671
+ -- \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
672
+ -- Put back, so the NEXT thing installed inherits what the chain had.
673
+
674
+ DO $dp$ BEGIN
675
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
676
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
677
+ 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$;
678
+ END $dp$;
679
+
680
+ DO $dp$ BEGIN
681
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
682
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
683
+ 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$;
684
+ END $dp$;
685
+
686
+ DO $dp$ BEGIN
687
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
688
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
689
+ 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$;
690
+ END $dp$;
691
+
692
+ DO $dp$ BEGIN
693
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
694
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
695
+ 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$;
696
+ END $dp$;
697
+
698
+ DO $dp$ BEGIN
699
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;
700
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
701
+ 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$;
702
+ END $dp$;
703
+
704
+ DO $dp$ BEGIN
705
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;
706
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
707
+ 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$;
708
+ END $dp$;
709
+
710
+ DO $dp$ BEGIN
711
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;
712
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
713
+ 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$;
714
+ END $dp$;
715
+
716
+ DO $dp$ BEGIN
717
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;
718
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
719
+ 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$;
720
+ END $dp$;
721
+
722
+ DO $dp$ BEGIN
723
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
724
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
725
+ 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$;
726
+ END $dp$;
727
+
728
+ DO $dp$ BEGIN
729
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
730
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
731
+ 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$;
732
+ END $dp$;
733
+
734
+ DO $dp$ BEGIN
735
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
736
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
737
+ 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$;
738
+ END $dp$;
739
+
740
+ DO $dp$ BEGIN
741
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;
742
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
743
+ 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$;
744
+ END $dp$;
745
+
746
+ DO $dp$ BEGIN
747
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;
748
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
749
+ 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$;
750
+ END $dp$;
751
+
752
+ DO $dp$ BEGIN
753
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;
754
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
755
+ 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$;
756
+ END $dp$;
757
+
758
+ DO $dp$ BEGIN
759
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;
760
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
761
+ 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$;
762
+ END $dp$;
763
+
764
+ DO $dp$ BEGIN
765
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
766
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
767
+ 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$;
768
+ END $dp$;
769
+
770
+ DO $dp$ BEGIN
771
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;
772
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
773
+ 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$;
774
+ END $dp$;
775
+
776
+ DO $dp$ BEGIN
777
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
778
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
779
+ 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$;
780
+ END $dp$;
781
+
782
+ DO $dp$ BEGIN
783
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
784
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
785
+ 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$;
786
+ END $dp$;
787
+
788
+ DO $dp$ BEGIN
789
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;
790
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
791
+ 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$;
792
+ END $dp$;
793
+
794
+ DO $dp$ BEGIN
795
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;
796
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
797
+ 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$;
798
+ END $dp$;
799
+
800
+ DO $dp$ BEGIN
801
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;
802
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
803
+ 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$;
804
+ END $dp$;
805
+
806
+ DO $dp$ BEGIN
807
+ EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;
808
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
809
+ 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$;
810
+ END $dp$;
811
+
812
+ DO $dp$ BEGIN
813
+ EXECUTE $stmt$--
814
+ -- PostgreSQL database dump complete
815
+ --$stmt$;
816
+ EXCEPTION WHEN insufficient_privilege OR undefined_object THEN
817
+ RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--
818
+ -- PostgreSQL database dump complete
819
+ --$stmt$;
820
+ END $dp$;
704
821
  `;
705
- var MIGRATION_003_ONTO_CONNECTIONS = `-- ============================================================================
706
- -- plugin-banking-br 003 \u2014 PlugBank stops owning tables.
707
- --
708
- -- Retires public.bank_integrations and public.bank_integration_sync_log. The
709
- -- connection is now a row of public.plg_connections and each import is a row of
710
- -- public.plg_sync_runs (packages/db 029). See FAY-1376.
822
+ var MIGRATION_001_THE_IMPORT_RPC_NAMES_REAL_TABLES = `-- ==========================================================================
823
+ -- 001_the_import_rpc_names_real_tables \u2014 plugbank_import_movements
824
+ -- stops reaching through 141's bridge (#352).
711
825
  --
712
- -- This connector is the one that tests the shape, because it is the only one
713
- -- whose key was never (tenant, provider): 001 keyed on
714
- -- UNIQUE (tenant_id, bank_account_id, provider), and the provider filtered by
715
- -- bank_account_id. Two accounts of one clinic are two connections, and they must
716
- -- stay two. That is exactly what \`instance_key\` is:
826
+ -- 107 renamed the connector spine out of the \`plg_\` prefix \u2014 the prefix means
827
+ -- "a plugin owns this", and the connector framework's tables are the BASE's.
828
+ -- 141 left \`plg_connections\` and \`plg_sync_runs\` answering as views so callers
829
+ -- could move without a synchronised deploy. This function never moved, and it
830
+ -- is not alone: \`plg_connections\` had 13 callers and \`connections\` had none.
831
+ -- The bridge became the road.
717
832
  --
718
- -- bank_integrations.bank_account_id \u2192 plg_connections.instance_key
719
- -- (the account uuid as text; '' is the
720
- -- account-less default the panel uses,
721
- -- which is what \`bank_account_id IS NULL\`
722
- -- meant before)
723
- -- bank_integrations.provider \u2192 plg_connections.connector_id
724
- -- bank_integrations.environment \u2192 plg_connections.settings->>'environment'
725
- -- bank_integrations.active \u2192 plg_connections.active
726
- -- bank_integrations.last_sync_at \u2192 plg_connections.last_sync_at
727
- -- UNIQUE (tenant_id, bank_account_id, provider)
728
- -- \u2192 UNIQUE (tenant_id, connector_id, instance_key)
833
+ -- The body is 000_baseline's, unchanged except for the three names. It is not
834
+ -- an edit of that file \u2014 an applied migration is immutable and the ledger
835
+ -- treats drift as a hard stop \u2014 it is a CREATE OR REPLACE that lands after it.
729
836
  --
730
- -- And the run, column by column \u2014 nothing the panel renders is lost:
731
- -- sync_log.bank_integration_id \u2192 plg_sync_runs.connection_id
732
- -- sync_log.transactions_fetched \u2192 plg_sync_runs.fetched
733
- -- sync_log.transactions_imported \u2192 plg_sync_runs.written
734
- -- sync_log.duplicates \u2192 plg_sync_runs.stats->>'duplicates'
735
- -- sync_log.period_from / _to \u2192 plg_sync_runs.stats->>'period_from' / '_to'
736
- -- sync_log.triggered_by_user_id \u2192 plg_sync_runs.stats->>'triggered_by_user_id'
737
- -- sync_log.error_message \u2192 plg_sync_runs.error
738
- -- sync_log.bank_account_id \u2192 dropped: it is the connection's
739
- -- instance_key, and a second copy is a
740
- -- second thing to disagree
741
- -- sync_log.provider \u2192 plg_sync_runs.connector_id
742
- -- sync_log.created_at \u2192 plg_sync_runs.started_at
743
- -- direction / trigger_kind / status / started_at / finished_at \u2192 same names
837
+ -- Why this plugin has to carry its own: the function is banking-br's, and the
838
+ -- views it stops using are the spine's. The spine runs FIRST, so a spine file
839
+ -- cannot drop a view that a plugin applied later still reads. Each package
840
+ -- moves its own callers; the aliases go once nobody is left.
744
841
  --
745
- -- The count columns finally get the contract's names. 002 \xA72 kept
746
- -- transactions_fetched/_imported because renaming them would have broken the
747
- -- deployed edge function's INSERT \u2014 but the function no longer inserts into a
748
- -- log at all: it calls plugbank_import_movements, which is the only writer, and
749
- -- the run is filed here. The choreography 002 was avoiding does not exist.
750
- --
751
- -- Nothing is in production, so this is a drop and not a data migration.
752
- -- ============================================================================
842
+ -- Replay-safe: CREATE OR REPLACE, same signature, same grants (which 000 set
843
+ -- and this does not touch).
844
+ -- ==========================================================================
753
845
 
754
- -- \xA71 \u2500\u2500 the door, re-pointed \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
755
- -- Same name, same payload, same guarantees. \`integration_id\` keeps its key on
756
- -- the wire so the deployed plugbank-sync function needs no coordinated release;
757
- -- what changed is that the uuid it carries now names a plg_connections row.
758
- --
759
- -- Everything 002 \xA73 established still holds and for the same reasons: the tenant
760
- -- comes from the CONNECTION ROW and never from the payload, a caller presenting
761
- -- a user credential must be a member of it, a caller with no auth.uid() is the
762
- -- server and is exempt, and idempotency is the partial unique index rather than
763
- -- a SELECT-then-INSERT that two racing runs would both pass.
764
- --
765
- -- One thing is new and is the point of the migration: the run is filed against
766
- -- \`connection_id\`, so a tenant with two bank accounts gets two histories instead
767
- -- of one interleaved list. plg_sync_runs carries no INSERT grant for
768
- -- \`authenticated\` \u2014 this SECURITY DEFINER function is the server-side writer the
769
- -- table's grants assume.
770
- CREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb)
771
- RETURNS jsonb
772
- LANGUAGE plpgsql
773
- SECURITY DEFINER
774
- SET search_path = public
775
- AS $fn$
846
+ CREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb) RETURNS jsonb
847
+ LANGUAGE plpgsql SECURITY DEFINER
848
+ SET search_path TO 'public'
849
+ AS $$
776
850
  DECLARE
777
851
  v_tenants uuid[];
778
852
  v_tenant uuid;
@@ -802,7 +876,7 @@ BEGIN
802
876
 
803
877
  SELECT c.tenant_id, c.instance_key, c.connector_id
804
878
  INTO v_tenant, v_instance, v_connector
805
- FROM public.plg_connections c
879
+ FROM public.connections c
806
880
  WHERE c.id = v_connection AND c.active;
807
881
 
808
882
  IF v_tenant IS NULL THEN
@@ -886,7 +960,7 @@ BEGIN
886
960
  v_inserted := NULL;
887
961
  END LOOP;
888
962
 
889
- UPDATE public.plg_connections
963
+ UPDATE public.connections
890
964
  SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()
891
965
  WHERE id = v_connection;
892
966
 
@@ -894,7 +968,7 @@ BEGIN
894
968
  -- run does not go: the operator picked a period, a re-import of that period is
895
969
  -- mostly duplicates by design, and someone pressed the button. The panel reads
896
970
  -- all three back from here.
897
- INSERT INTO public.plg_sync_runs (
971
+ INSERT INTO public.sync_runs (
898
972
  tenant_id, connection_id, connector_id, direction, trigger_kind, status,
899
973
  stream, fetched, written, stats, started_at, finished_at)
900
974
  VALUES (
@@ -915,22 +989,14 @@ BEGIN
915
989
  'imported', v_imported,
916
990
  'duplicates', v_duplicates);
917
991
  END;
918
- $fn$;
919
-
920
- REVOKE ALL ON FUNCTION public.plugbank_import_movements(jsonb) FROM public, anon;
921
- GRANT EXECUTE ON FUNCTION public.plugbank_import_movements(jsonb) TO authenticated, service_role;
922
-
923
- COMMENT ON FUNCTION public.plugbank_import_movements(jsonb) IS
924
- 'Import bank-statement lines into the financial ledger and file the run in '
925
- 'plg_sync_runs. The tenant AND the bank account come from the plg_connections '
926
- 'row named by integration_id, never from the payload.';
927
-
928
- -- \xA72 \u2500\u2500 the bespoke tables go \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
929
- -- After the function above, so a pool is never left with the old door pointing
930
- -- at a table that no longer exists.
931
- DROP TABLE IF EXISTS public.bank_integration_sync_log CASCADE;
932
- DROP TABLE IF EXISTS public.bank_integrations CASCADE;
992
+ $$;
993
+ COMMENT ON FUNCTION public.plugbank_import_movements(p_payload jsonb) IS
994
+ '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.';
933
995
  `;
996
+ var MIGRATIONS = [
997
+ { id: "000_baseline", sql: MIGRATION_000_BASELINE },
998
+ { id: "001_the_import_rpc_names_real_tables", sql: MIGRATION_001_THE_IMPORT_RPC_NAMES_REAL_TABLES }
999
+ ];
934
1000
 
935
1001
  // src/index.ts
936
1002
  function createOpenBankingPlugin() {
@@ -987,27 +1053,22 @@ function createOpenBankingPlugin() {
987
1053
  // This plugin creates no table. 003 retired the two it had; what is left is
988
1054
  // the RPC that is the import's one door, and the trigger that turns an
989
1055
  // imported movement into a fact. The connection and its run history are rows
990
- // of plg_connections / plg_sync_runs, owned by packages/db.
991
- migrations: [
992
- {
993
- id: "openbanking-002_credential_and_facts",
994
- version: "0.9.1",
995
- sql: MIGRATION_002_CREDENTIAL_AND_FACTS,
996
- description: "Drop bank_integrations.api_token/cnpj (destination: the Fayz platform credential store, FAY-1360 \u2014 register there BEFORE applying this), add plugbank_import_movements and the financial.movement.imported trigger"
997
- },
998
- {
999
- id: "openbanking-003_onto_connections",
1000
- version: "1.0.0",
1001
- sql: MIGRATION_003_ONTO_CONNECTIONS,
1002
- description: "Retire bank_integrations + bank_integration_sync_log \u2014 the connection is a plg_connections row keyed by instance_key (the bank account) and each import a plg_sync_runs row; plugbank_import_movements re-pointed at both"
1003
- }
1004
- ],
1056
+ // of connections / sync_runs, owned by packages/db.
1057
+ // One baseline per unit since #338 — read from the generated barrel
1058
+ // rather than listed here, so regenerating the SQL never needs a
1059
+ // manifest edit to match.
1060
+ migrations: MIGRATIONS.map((entry) => ({
1061
+ id: `openbanking-${entry.id}`,
1062
+ version: "1.0.0",
1063
+ sql: entry.sql,
1064
+ description: "The whole schema this plugin provisions, as one file: every table, view, function, policy and grant, plus the reference rows it seeds. Replaced the per-change chain in the 1.0.0 squash (#338); the files it collapsed are kept, unapplied, in src/migrations.archive/."
1065
+ })),
1005
1066
  diagnostics: [
1006
1067
  {
1007
1068
  id: "openbanking-ledger-ready",
1008
1069
  description: "The import writes into the financial ledger and needs its reconciliation columns (plugin-financial 007), the generic connection tables (packages/db 029) and the row-level event emitter.",
1009
1070
  requires: {
1010
- tables: ["plg_connections", "plg_sync_runs", "plg_financial_movements"],
1071
+ tables: ["connections", "sync_runs", "plg_financial_movements"],
1011
1072
  rpcs: ["plugbank_import_movements", "plg_emit_event"]
1012
1073
  },
1013
1074
  level: "error"