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

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.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/data/supabase.ts","../src/connectors/connectorDef.tsx","../src/lib/banking-events.ts","../src/migrations/index.ts","../src/index.ts"],"names":["createConnectionStore"],"mappings":"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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAsT3C,IAAM,8BAAA,GAAiC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","file":"index.js","sourcesContent":["// Open Banking plugin — Supabase data provider.\n//\n// Thin control-plane client that writes no SQL. The connection is a row of\n// public.plg_connections and each import is a row of public.plg_sync_runs\n// (packages/db 029), reached through createConnectionStore — the bespoke\n// bank_integrations / bank_integration_sync_log pair this file used to address\n// was the generic shape under a private name (FAY-1376, migration 003).\n//\n// The heavy lifting (calling the Tecnospeed PlugBank API, normalizing lines,\n// importing into plg_financial_movements with idempotency) runs in the\n// `plugbank-sync` Edge Function, declared on the manifest's `functions[]`.\n//\n// MULTI-ACCOUNT IS THE POINT HERE. The old key was\n// UNIQUE (tenant_id, bank_account_id, provider) and every read filtered by\n// bank_account_id: a clinic with two bank accounts had two connections. That is\n// preserved exactly, by `instance_key` — one store per account, and the\n// account-less default (the panel's case) is instance_key `''`, which is what\n// `bank_account_id IS NULL` used to mean.\n//\n// It never touches a credential. The PlugBank token and the CNPJ that identifies\n// the account will live in the Fayz platform, redeemed per call (FAY-1360 +\n// FAY-1389; today they still sit on the live pool's legacy bank_integrations).\n// plg_connections has no credential column at all, so \"this file never touches\n// one\" is a property of the table rather than of this file.\nimport { createConnectionStore, getSupabaseClientOptional } from '@fayz-ai/core'\nimport type { ConnectionConfig, ConnectionStore, SyncRun } from '@fayz-ai/core'\nimport type {\n BankIntegration, SaveIntegrationInput, BankSyncRun,\n FetchStatementResult, ImportResult, BankLine,\n} from '../types'\n\nconst FUNCTION = 'plugbank-sync'\n\nexport const PLUGBANK_CONNECTOR_ID = 'plugbank'\n\n/** Non-credential keys this connector stores. `environment` selects prod/sandbox. */\nconst SETTINGS_KEYS = ['environment']\n\n/**\n * The store for ONE bank account. `undefined` is the account-less default, not\n * \"any account\": resolving it to `''` keeps the old `bank_account_id IS NULL`\n * filter, so the panel that never picks an account keeps addressing the same\n * single connection it always did.\n */\nfunction storeFor(bankAccountId?: string): ConnectionStore {\n return createConnectionStore(PLUGBANK_CONNECTOR_ID, {\n settingsKeys: SETTINGS_KEYS,\n instanceKey: bankAccountId ?? '',\n })\n}\n\nfunction sb() {\n const supabase = getSupabaseClientOptional() as any\n if (!supabase) throw new Error('Supabase not initialized')\n return supabase\n}\n\nexport function toIntegration(connection: ConnectionConfig): BankIntegration {\n return {\n id: connection.id ?? '',\n provider: connection.connectorId,\n // Back to `undefined` for the default account, so callers that pass this\n // value straight back into getIntegration() address the same row.\n bankAccountId: connection.instanceKey || undefined,\n environment: (connection.settings?.environment as string | undefined) ?? 'production',\n active: connection.active,\n lastSyncAt: connection.lastSyncAt ?? undefined,\n }\n}\n\n/**\n * The audit row, read as the connector contract's SyncRun. The three facts a\n * MANUAL statement import has and a generic run does not — the period the\n * operator picked, and the duplicates a re-import of that period produces by\n * design — live in `stats`, which is the column that exists for exactly this.\n */\nexport function toSyncRun(run: SyncRun): BankSyncRun {\n const stats = run.stats ?? {}\n return {\n ...run,\n id: run.id,\n connectionId: run.connectionId,\n connectorId: run.connectorId,\n direction: run.direction,\n triggerKind: run.triggerKind,\n status: run.status,\n fetched: run.fetched ?? 0,\n written: run.written ?? 0,\n error: run.error ?? undefined,\n startedAt: run.startedAt,\n finishedAt: run.finishedAt ?? undefined,\n periodFrom: (stats.period_from as string | undefined) ?? undefined,\n periodTo: (stats.period_to as string | undefined) ?? undefined,\n duplicates: Number(stats.duplicates ?? 0),\n // plg_sync_runs has no created_at: a run is identified by when it STARTED,\n // which is what the old created_at recorded anyway (the row was inserted at\n // the end, so the two disagreed by the length of the import).\n createdAt: run.startedAt ?? '',\n }\n}\n\nexport interface OpenBankingProvider {\n getIntegration(bankAccountId?: string): Promise<BankIntegration | null>\n saveIntegration(input?: SaveIntegrationInput): Promise<BankIntegration>\n disconnect(bankAccountId?: string): Promise<void>\n fetchStatement(input: { integrationId: string; from: string; to: string }): Promise<FetchStatementResult>\n importTransactions(input: { integrationId: string; bankAccountId?: string; from: string; to: string; lines: BankLine[] }): Promise<ImportResult>\n getSyncRuns(bankAccountId?: string): Promise<BankSyncRun[]>\n}\n\nexport function createOpenBankingProvider(): OpenBankingProvider {\n return {\n async getIntegration(bankAccountId) {\n const connection = await storeFor(bankAccountId).get()\n return connection ? toIntegration(connection) : null\n },\n\n async saveIntegration(input) {\n const saved = await storeFor(input?.bankAccountId).save({\n settings: { environment: input?.environment ?? 'production' },\n active: true,\n })\n return toIntegration(saved)\n },\n\n /**\n * Scoped to ONE account, where the old provider deleted every plugbank row\n * of the tenant. That breadth was invisible while the panel only ever drove\n * the default account; with two accounts it means disconnecting Itaú also\n * silently kills Banco do Brasil. The narrow verb is the correct one.\n */\n async disconnect(bankAccountId) {\n await storeFor(bankAccountId).disconnect()\n },\n\n async fetchStatement(input) {\n const supabase = sb()\n const { data, error } = await supabase.functions.invoke(FUNCTION, {\n body: { action: 'fetch_statement', integrationId: input.integrationId, from: input.from, to: input.to },\n })\n if (error) throw new Error(error.message)\n return data as FetchStatementResult\n },\n\n async importTransactions(input) {\n const supabase = sb()\n const { data, error } = await supabase.functions.invoke(FUNCTION, {\n body: {\n action: 'import_transactions',\n // Still `integrationId` on the wire: it now carries a plg_connections\n // id, and keeping the key means the deployed function needs no\n // coordinated release. The RPC takes the tenant AND the bank account\n // from that row, so neither can be steered from here.\n integrationId: input.integrationId,\n bankAccountId: input.bankAccountId,\n from: input.from,\n to: input.to,\n lines: input.lines,\n },\n })\n if (error) throw new Error(error.message)\n return data as ImportResult\n },\n\n async getSyncRuns(bankAccountId) {\n const runs = await storeFor(bankAccountId).listRuns({ limit: 20 })\n return runs.map(toSyncRun)\n },\n }\n}\n","// Open Banking (Tecnospeed PlugBank) as a unified ConnectorDefinition.\n//\n// Surfaces inside plugin-financial's settings → Integrations tab (the host),\n// rendered by the shared ConnectorsHub. Importing a statement is an ACT, so it\n// arrives through `RunActions` and lands on the Sincronizações tab, above the\n// history the shared surface renders from `plg_sync_runs`. It used to be an\n// `ExtraPanel` on the settings tab with its own hand-written run list under it.\n//\n// A PULL connector: the bank does not know Fayz, so an edge function reaches out\n// to it and needs a credential at fetch time. That credential is registered in\n// the Fayz platform, not here — the connect form asks for no secret, because a\n// secret typed into this app would land in a table any member of the tenant can\n// read. See migration 002 §1.\nimport React, { useEffect, useState } from 'react'\nimport { Download, Loader2 } from 'lucide-react'\nimport { Button, DatePicker, toast } from '@fayz-ai/ui'\nimport { connectionStatus, createConnectionStore, type ConnectorDefinition } from '@fayz-ai/core'\nimport type { SyncRun } from '@fayz-ai/core'\nimport { createOpenBankingProvider, toIntegration, PLUGBANK_CONNECTOR_ID } from '../data/supabase'\nimport type { BankIntegration, BankLine } from '../types'\n\nconst provider = createOpenBankingProvider()\n\n/** dd/mm from the ISO date the RPC stores in `stats`. */\nfunction day(value: unknown): string | undefined {\n return typeof value === 'string' && value.length >= 10 ? value.slice(8, 10) + '/' + value.slice(5, 7) : undefined\n}\n\n/**\n * What one statement import DID, as the sentence every run carries.\n *\n * The period and the duplicate count live in `stats` — three keys the RPC writes\n * and only this connector can read (migrations §plg_sync_runs). The shared\n * history renders `message`, so the interpretation belongs here rather than in a\n * view that would have to learn one connector's jsonb.\n */\nfunction statementLine(run: SyncRun): string | undefined {\n const from = day(run.stats?.period_from)\n const to = day(run.stats?.period_to)\n const duplicates = Number(run.stats?.duplicates ?? 0)\n if (!from || !to) return undefined\n const imported = `${run.written ?? 0} importadas de ${from} a ${to}`\n // A re-import of the same period is mostly duplicates BY DESIGN, so saying so\n // is what stops \"0 importadas\" reading as a failure.\n return duplicates > 0 ? `${imported} · ${duplicates} já estavam aqui` : imported\n}\n\nfunction brl(n: number): string {\n try { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(n) }\n catch { return `R$ ${n.toFixed(2)}` }\n}\n\nfunction OpenBankingImport({ onRan }: { onRan?: () => void }) {\n const [integration, setIntegration] = useState<BankIntegration | null>(null)\n const [loaded, setLoaded] = useState(false)\n const [from, setFrom] = useState(() => { const d = new Date(); d.setDate(d.getDate() - 30); return d.toISOString().slice(0, 10) })\n const [to, setTo] = useState(() => new Date().toISOString().slice(0, 10))\n const [lines, setLines] = useState<BankLine[]>([])\n const [selected, setSelected] = useState<Set<string>>(new Set())\n const [fetching, setFetching] = useState(false)\n const [importing, setImporting] = useState(false)\n\n async function load() {\n setIntegration(await provider.getIntegration().catch(() => null))\n setLoaded(true)\n }\n useEffect(() => { void load() }, [])\n\n // Nothing to import until the connection is live. `active` and not merely \"a\n // row exists\": disconnect is soft now, so a revoked connection still has one.\n if (!loaded || !integration?.active) return null\n\n async function handleFetch() {\n if (!integration) return\n setFetching(true)\n try {\n const res = await provider.fetchStatement({ integrationId: integration.id, from, to })\n setLines(res.lines)\n setSelected(new Set(res.lines.map((l) => l.externalId)))\n } catch (e: any) { toast.error(e?.message ?? 'Erro ao buscar extrato') }\n finally { setFetching(false) }\n }\n\n async function handleImport() {\n if (!integration) return\n const toImport = lines.filter((l) => selected.has(l.externalId))\n if (toImport.length === 0) { toast.error('Selecione ao menos uma linha'); return }\n setImporting(true)\n try {\n const res = await provider.importTransactions({\n integrationId: integration.id, bankAccountId: integration.bankAccountId, from, to, lines: toImport,\n })\n toast.success(`${res.imported} importadas${res.duplicates ? ` · ${res.duplicates} duplicadas` : ''}`)\n setLines([]); setSelected(new Set())\n onRan?.()\n } catch (e: any) { toast.error(e?.message ?? 'Erro ao importar') }\n finally { setImporting(false) }\n }\n\n function toggle(id: string) {\n setSelected((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next })\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center gap-2\"><Download className=\"h-4 w-4 text-muted-foreground\" /><h4 className=\"text-sm font-semibold\">Importar extrato</h4></div>\n <div className=\"flex flex-wrap items-end gap-3\">\n <div><span className=\"text-xs font-medium text-muted-foreground\">De</span><DatePicker value={from} onChange={setFrom} className=\"mt-1\" /></div>\n <div><span className=\"text-xs font-medium text-muted-foreground\">Até</span><DatePicker value={to} onChange={setTo} className=\"mt-1\" /></div>\n <Button variant=\"outline\" size=\"sm\" onClick={handleFetch} disabled={fetching}>\n {fetching ? <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> : <Download className=\"h-3.5 w-3.5\" />} Buscar extrato\n </Button>\n </div>\n\n {lines.length > 0 && (\n <>\n <div className=\"rounded-md border divide-y max-h-72 overflow-auto\">\n {lines.map((l) => (\n <label key={l.externalId} className=\"flex items-center gap-3 px-3 py-2 text-sm cursor-pointer hover:bg-muted/30\">\n <input type=\"checkbox\" checked={selected.has(l.externalId)} onChange={() => toggle(l.externalId)} />\n <span className=\"text-muted-foreground text-xs w-20 shrink-0\">{l.date}</span>\n <span className=\"flex-1 truncate\">{l.description}</span>\n <span className={`font-semibold shrink-0 ${l.type === 'C' ? 'text-success' : 'text-destructive'}`}>\n {l.type === 'C' ? '+' : '-'}{brl(l.amount)}\n </span>\n </label>\n ))}\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs text-muted-foreground\">{selected.size} de {lines.length} selecionadas</span>\n <Button size=\"sm\" onClick={handleImport} disabled={importing || selected.size === 0}>\n {importing ? <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> : <Download className=\"h-3.5 w-3.5\" />} Importar selecionadas\n </Button>\n </div>\n </>\n )}\n\n <p className=\"text-[11px] text-muted-foreground\">\n O token do PlugBank e o CNPJ da conta ficam guardados na plataforma Fayz, não neste\n aplicativo. Quem importa o extrato é o servidor, que pede as credenciais na hora da consulta.\n </p>\n </div>\n )\n}\n\nexport const openBankingConnector: ConnectorDefinition = {\n id: 'plugbank',\n // The product is PlugBank, the company in the directory is TecnoSpeed. Same\n // row, two names — and until this said so, the connector took neither the\n // catalogue's copy nor its mark.\n catalogSlug: 'tecnospeed',\n hostPluginId: 'financial',\n name: 'Open Banking · Tecnospeed PlugBank',\n description: 'Importe o extrato bancário e concilie em Financeiro → Conciliação.',\n icon: 'Landmark',\n authKind: 'api-key',\n // `banking`, not `payments`: reading a statement is not taking a payment, and\n // the platform catalogue files the provider (slug `tecnospeed`) under banking.\n category: 'banking',\n // Inbound because WE reach the bank: PlugBank has never heard of Fayz, which\n // is what makes this the canonical pull connector and why it ships an edge\n // function at all.\n direction: 'inbound',\n // Sync only. The statement has to exist INSIDE Fayz to be listed, filtered and\n // matched against internal movements — reconciliation is not a question the\n // assistant can answer by asking the bank at the moment it is asked.\n planes: ['sync'],\n docsUrl: 'https://tecnospeed.com.br/plugbank/',\n // Write and NOT read, which is unusual enough to be worth saying out loud:\n // `plugbank_import_movements` (migrations/003) only ever INSERTs, with\n // `ON CONFLICT (tenant_id, external_source, external_id) DO NOTHING`. It never\n // SELECTs a movement, so declaring `read` here would be an access the code does\n // not take — and the whole value of this list is that it is not padded.\n //\n // The `emit` is the second half, and it is the one a tenant would not guess:\n // every imported line fires `financial.movement.imported` through the trigger\n // in migrations/002, and that payload carries amount, date, description and\n // bank account to whatever automation is listening. Importing a statement is\n // therefore also publishing it internally.\n permissions: [\n {\n kind: 'data',\n resource: 'movement',\n actions: ['write'],\n reason:\n 'Gravar no seu financeiro as linhas do extrato bancário — valor, data, descrição e conta. Lançamentos já importados não entram de novo.',\n sensitive: true,\n },\n {\n kind: 'event',\n resource: 'financial.movement.imported',\n actions: ['emit'],\n reason:\n 'Avisar o app a cada lançamento importado, para que suas automações de conciliação possam reagir. O aviso leva valor, data e conta.',\n sensitive: true,\n },\n ],\n invokedAt: [\n {\n key: 'financial.statement.import',\n label: 'Você escolhe um período aqui e o extrato do banco é importado',\n plane: 'sync',\n trigger: 'manual',\n touches: ['movement', 'financial.movement.imported'],\n },\n ],\n // No credential field. The token and the CNPJ that identify the account are\n // registered in the Fayz platform; anything typed here would be stored in a\n // table every member of the tenant can read.\n //\n // `subscribesTo` is deliberately absent, and so is `managedAutomations`.\n // Nothing that happens inside Fayz should wake this connector: it pulls a\n // statement, and what makes it pull is the operator picking a period below or\n // the Fayz SCHEDULE worker (FAY-1347) — never a domain fact. A subscription\n // written to fill the field would put this connector on the ingress's\n // delivery list for events it has no reaction to.\n //\n // The reconciliation automations (\"avise quando entrar um valor acima de X\")\n // are real, but they are the TENANT's: they react to\n // `financial.movement.imported` and the tenant may delete them without the\n // connection becoming half-wired. `managedAutomations` is for the wiring the\n // integration IS, and this integration is the import itself.\n // Read from the row the list already batched — one request for N connectors.\n // The account-less default instance (`''`) is the one the panel addresses.\n ...connectionStatus(PLUGBANK_CONNECTOR_ID, (connection) => {\n const found = connection ? toIntegration(connection) : null\n return { connected: !!found?.active, detail: found?.environment, plane: 'sync' }\n }),\n // `testConnection` is deleted rather than stubbed: the credential is not in\n // this app, so there is nothing this side can validate, and ConnectorDetail\n // renders the button only when the verb exists.\n saveConnection: async () => { await provider.saveIntegration() },\n disconnect: () => provider.disconnect(),\n // Every import files a `plg_sync_runs` row, and the shared surface renders it.\n // What is connector-specific is not the VIEW, it is the meaning of `stats`:\n // \"de 01/08 a 12/08 · 3 duplicadas\" is three keys only this connector can\n // read, turned here into the one sentence every run carries. Scoped to the\n // default instance — the account this panel addresses.\n async listRuns(options) {\n try {\n const runs = await createConnectionStore(PLUGBANK_CONNECTOR_ID).listRuns(options)\n return runs.map((run) => ({ ...run, message: run.message ?? statementLine(run) }))\n } catch {\n return []\n }\n },\n RunActions: OpenBankingImport,\n}\n","import type { EventField, PluginEventDefinition } from '@fayz-ai/core'\n\n// ---------------------------------------------------------------------------\n// The FACT of the bank import.\n//\n// The emitter is not in this file and cannot be: a statement line reaches the\n// ledger from the edge function's insert, from plugbank_import_movements, from\n// a future scheduled pull and occasionally from a hand-run backfill. The one\n// place all four end up is the row, so the emitter is a trigger on\n// plg_financial_movements (migration 002 §4) and this is its contract.\n//\n// This is the fact that turns reconciliation from a screen someone remembers to\n// open into something the tenant can react to — \"avise quando entrar um valor\n// acima de X\" is an automation on this event and nothing else.\n//\n// `provider` is in the payload because the trigger fires for ANY imported\n// movement, not only PlugBank's. A second bank connector writing into the same\n// ledger produces the same fact rather than a second vocabulary for it.\n// ---------------------------------------------------------------------------\n\n/** Payload keys, in the order the trigger builds them. */\nexport const MOVEMENT_IMPORTED_FIELDS: EventField[] = [\n { name: 'movement_id', label: 'ID do lançamento', type: 'id', example: '8f14e45f-…' },\n { name: 'provider', label: 'Origem', type: 'string', example: 'plugbank' },\n { name: 'external_id', label: 'Identificador no banco', type: 'string', example: 'LINE-1' },\n { name: 'direction', label: 'Tipo', type: 'string', example: 'credit' },\n { name: 'amount', label: 'Valor', type: 'money', example: 'R$ 150,00' },\n { name: 'payment_date', label: 'Data', type: 'date', example: '03/08/2026' },\n { name: 'description', label: 'Descrição', type: 'string', example: 'Recebimento PIX' },\n { name: 'bank_account_id', label: 'Conta bancária', type: 'id', example: '3f2a91c4-…' },\n]\n\nconst PAYLOAD_SCHEMA: Record<string, unknown> = {\n type: 'object',\n properties: {\n movement_id: { type: 'string', format: 'uuid', description: 'Movement id in the tenant ledger' },\n provider: {\n type: 'string',\n description:\n \"Which integration imported the line — 'plugbank' today. Reads from plg_financial_movements.external_source, so a second bank connector produces the same fact.\",\n },\n external_id: {\n type: 'string',\n description: \"The bank's own id for the statement line. With `provider` it is the import idempotency key.\",\n },\n direction: { type: 'string', enum: ['credit', 'debit'], description: 'Money in or money out' },\n amount: { type: 'number', description: 'Always positive; `direction` carries the sign' },\n payment_date: { type: 'string', format: 'date', description: 'Date the bank settled the line (YYYY-MM-DD)' },\n description: { type: ['string', 'null'], description: 'Statement narrative, as the bank wrote it' },\n bank_account_id: { type: ['string', 'null'], format: 'uuid', description: 'Account the line landed in, when known' },\n },\n required: ['movement_id', 'provider', 'external_id', 'direction', 'amount', 'payment_date'],\n additionalProperties: false,\n}\n\n/**\n * `audit: true`. Money arriving from outside the app is exactly the subject\n * where \"where did this come from, and when?\" needs an answer that survives a\n * later edit — sharper still when the answer is a connector rather than a\n * person. There is no `updated` counterpart on purpose: a statement line is not\n * revised, it is imported once and then reconciled, and reconciliation is the\n * financial plugin's fact to declare, not this addon's.\n */\nexport function buildBankingDomainEvents(): PluginEventDefinition[] {\n return [\n {\n name: 'financial.movement.imported',\n title: 'Lançamento bancário importado',\n description: 'Uma linha do extrato entrou no financeiro',\n trigger: 'Quando o extrato é importado — pelo painel, pelo conector ou por uma carga manual',\n icon: 'Landmark',\n audit: true,\n fields: MOVEMENT_IMPORTED_FIELDS,\n payloadSchema: PAYLOAD_SCHEMA,\n },\n ]\n}\n","// AUTO-GENERATED from 002_credential_and_facts.sql, 003_onto_connections.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_002_CREDENTIAL_AND_FACTS = `-- ============================================================================\n-- plugin-banking-br 002: the credential leaves the tenant database, the import\n-- gets a door it cannot be routed around, and the import becomes a FACT.\n--\n-- §1 credential drop api_token + cnpj from public.bank_integrations\n-- §2 sync runs bank_integration_sync_log speaks the SyncRun vocabulary\n-- §3 import plugbank_import_movements — the tenant-safe door\n-- §4 facts financial.movement.imported\n--\n-- Additive and idempotent: no table is created, no policy is rewritten, and the\n-- tenant RLS from 001 already covers every column added here.\n-- ============================================================================\n\n-- §1 ── credential ───────────────────────────────────────────────────────────\n-- THE EXPOSURE THIS CLOSES: 001 stored \\`api_token text\\` and \\`cnpj text\\` on\n-- public.bank_integrations. That table is read from the browser with the\n-- publishable key under tenant RLS, so the bank credential was readable by ANY\n-- authenticated member of the tenant; and a pool is one Supabase project serving\n-- N tenants (beauty-saas and espaco-renova-rio share one), so anything running\n-- there with service_role could read every clinic's bank token at once.\n--\n-- PlugBank differs from RankLayer, which dropped its key with nothing to replace\n-- it: PlugBank is a PULL connector and genuinely needs a credential at fetch\n-- time. It just must not be THIS copy. The destination is the platform's\n-- encrypted ConnectorConnection store, redeemed per call (FAY-1360 + FAY-1389).\n-- NEITHER HAS HAPPENED YET: as of 2026-08-12 the live pool still holds the\n-- token in this column, this migration is unapplied there, and the deployed\n-- plugbank-sync (v6) still reads it. The FAY-1360 rollout must register the\n-- credential at the platform BEFORE this drop runs on that pool — otherwise\n-- what is removed here is the only copy, and the connection dies.\n--\n-- \\`cnpj\\` goes with it rather than surviving as a masked remnant. It is not\n-- decoration: paired with the token it is what identifies the account at the\n-- provider (\\`/v1/extrato?cnpj=…\\`), which makes it half of the credential and\n-- therefore the platform's to hold. And nothing on this side reads it — the\n-- live connector panel never renders it, and the unified connect form starts\n-- every field empty rather than pre-filling from the row. A masked last-N would\n-- be a column added to serve a screen that does not exist, on data that\n-- identifies a real company.\n--\n-- ROTATE any token a pool already stored. DROP COLUMN is a catalog change: the\n-- old values stay in the heap until the next table rewrite.\n\nDO $$\nBEGIN\n IF to_regclass('public.bank_integrations') IS NOT NULL THEN\n ALTER TABLE public.bank_integrations DROP COLUMN IF EXISTS api_token;\n ALTER TABLE public.bank_integrations DROP COLUMN IF EXISTS cnpj;\n END IF;\nEND $$;\n\n-- §2 ── sync runs ────────────────────────────────────────────────────────────\n-- bank_integration_sync_log already IS a SyncRun: one row per execution, with\n-- status/fetched/written/error. It gains the four facts the shared vocabulary\n-- has and it lacked, rather than being replaced by a second table beside it —\n-- two audit trails for one import is how \"the history is wrong\" starts.\n--\n-- The count columns keep their names. \\`transactions_fetched\\`/\\`transactions_\n-- imported\\` map to SyncRun's \\`fetched\\`/\\`written\\` in the data provider, because\n-- renaming them would break the deployed edge function's INSERT the moment this\n-- migration lands, and a rename that has to be choreographed across two\n-- repositories is not worth a nicer column name.\nDO $$\nBEGIN\n IF to_regclass('public.bank_integration_sync_log') IS NULL THEN RETURN; END IF;\n\n -- Which provider produced the run. One row per execution says nothing about\n -- who ran it once a second bank connector shares the table.\n ALTER TABLE public.bank_integration_sync_log\n ADD COLUMN IF NOT EXISTS provider text NOT NULL DEFAULT 'plugbank';\n -- Inbound: we reach the bank, never the other way round.\n ALTER TABLE public.bank_integration_sync_log\n ADD COLUMN IF NOT EXISTS direction text NOT NULL DEFAULT 'inbound';\n -- \\`trigger\\` is a reserved word; the column is trigger_kind and the TypeScript\n -- side reads it as SyncRun.trigger.\n ALTER TABLE public.bank_integration_sync_log\n ADD COLUMN IF NOT EXISTS trigger_kind text NOT NULL DEFAULT 'manual';\n ALTER TABLE public.bank_integration_sync_log\n ADD COLUMN IF NOT EXISTS started_at timestamptz;\n ALTER TABLE public.bank_integration_sync_log\n ADD COLUMN IF NOT EXISTS finished_at timestamptz;\n\n COMMENT ON COLUMN public.bank_integration_sync_log.trigger_kind IS\n 'SyncRun.trigger — on-write | scheduled | manual | webhook. Named _kind because trigger is reserved.';\nEND $$;\n\n-- §3 ── import ───────────────────────────────────────────────────────────────\n-- The one door the statement import passes through.\n--\n-- Today the edge function inserts into plg_financial_movements directly with\n-- service_role, and takes \\`integrationId\\` from the request body without ever\n-- comparing it to the caller — so any authenticated member of ANY tenant in the\n-- pool can name another tenant's integration and write into that tenant's\n-- ledger. A guard in the function would be a guard the next caller routes\n-- around; the database is the only chokepoint that cannot be.\n--\n-- The tenant is taken from the INTEGRATION ROW, never from the payload, and a\n-- caller presenting a user credential must be a member of it. A caller with no\n-- auth.uid() is the server (service_role, a migration, the future tenant-scoped\n-- token) and is exempt — the same exemption plg_emit_event makes, for the same\n-- reason: it legitimately writes for any tenant. When FAY-1372 mints the\n-- tenant-scoped credential, that exemption stops applying to connector code and\n-- this function needs no change.\nCREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public\nAS $fn$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_integration uuid;\n v_account uuid;\n v_provider text;\n v_trigger text;\n v_from date;\n v_to date;\n v_started timestamptz := now();\n v_line jsonb;\n v_external text;\n v_amount numeric(14,2);\n v_date date;\n v_fetched int := 0;\n v_imported int := 0;\n v_duplicates int := 0;\n v_inserted uuid;\n v_run uuid;\nBEGIN\n v_integration := nullif(btrim(p_payload->>'integration_id'), '')::uuid;\n IF v_integration IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n SELECT bi.tenant_id, bi.bank_account_id, bi.provider\n INTO v_tenant, v_account, v_provider\n FROM public.bank_integrations bi\n WHERE bi.id = v_integration AND bi.active;\n\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: no active integration %', v_integration\n USING ERRCODE = 'no_data_found';\n END IF;\n\n -- SECURITY DEFINER bypasses RLS, so the membership check the policy would\n -- have made has to happen here.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN\n RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Refusing beats ignoring: a caller that believes it is importing into\n -- another tenant must be told, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL\n AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the integration'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL THEN\n v_account := (p_payload->>'bank_account_id')::uuid;\n END IF;\n\n v_from := nullif(btrim(p_payload->>'period_from'), '')::date;\n v_to := nullif(btrim(p_payload->>'period_to'), '')::date;\n v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));\n IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN\n RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))\n LOOP\n v_fetched := v_fetched + 1;\n v_external := nullif(btrim(v_line->>'external_id'), '');\n v_date := nullif(btrim(v_line->>'date'), '')::date;\n v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));\n\n -- A line with no provider id cannot be recognised on re-delivery, so it\n -- would import again on every run. Skipping it is the lesser wrong.\n IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN\n CONTINUE;\n END IF;\n\n -- Idempotency is the partial unique index from the financial plugin's\n -- 007_reconciliation.sql, not a SELECT-then-INSERT: two runs of the same\n -- window racing each other would both find nothing and both write.\n INSERT INTO public.plg_financial_movements (\n tenant_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, bank_account_id, notes, external_id, external_source)\n VALUES (\n v_tenant,\n CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,\n 'payment', v_amount, v_amount, 'paid',\n v_date, v_date, v_account,\n nullif(btrim(v_line->>'description'), ''),\n v_external, coalesce(v_provider, 'plugbank'))\n ON CONFLICT (tenant_id, external_source, external_id)\n WHERE external_id IS NOT NULL AND external_source IS NOT NULL\n DO NOTHING\n RETURNING id INTO v_inserted;\n\n IF v_inserted IS NULL THEN\n v_duplicates := v_duplicates + 1;\n ELSE\n v_imported := v_imported + 1;\n END IF;\n v_inserted := NULL;\n END LOOP;\n\n UPDATE public.bank_integrations SET last_sync_at = now(), updated_at = now()\n WHERE id = v_integration;\n\n INSERT INTO public.bank_integration_sync_log (\n tenant_id, bank_integration_id, bank_account_id, period_from, period_to,\n transactions_fetched, transactions_imported, duplicates, status,\n triggered_by_user_id, provider, direction, trigger_kind, started_at, finished_at)\n VALUES (\n v_tenant, v_integration, v_account, v_from, v_to,\n v_fetched, v_imported, v_duplicates, 'success',\n auth.uid(), coalesce(v_provider, 'plugbank'), 'inbound', v_trigger, v_started, now())\n RETURNING id INTO v_run;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sync_run_id', v_run,\n 'fetched', v_fetched,\n 'imported', v_imported,\n 'duplicates', v_duplicates);\nEND;\n$fn$;\n\nREVOKE ALL ON FUNCTION public.plugbank_import_movements(jsonb) FROM public, anon;\nGRANT EXECUTE ON FUNCTION public.plugbank_import_movements(jsonb) TO authenticated, service_role;\n\n-- §4 ── facts ────────────────────────────────────────────────────────────────\n-- One trigger on the ledger rather than an emit call inside the importer. The\n-- RPC above, the edge function still inserting directly, a future scheduled\n-- pull and a hand-run backfill all end up in the same row — so the fact exists\n-- for all four without any of them having to remember to say so. That is the\n-- entire reason plg_emit_event exists (packages/db/migrations/027_domain_events.sql).\n--\n-- It fires only when external_source is set, which is exactly what \"imported\"\n-- means: a movement that came from outside. An app-native movement typed into\n-- the cash register is not an import and must not wake a bank connector.\n--\n-- Provider-agnostic on purpose. \\`provider\\` is in the payload, so a second bank\n-- connector importing into the same ledger produces the same fact instead of a\n-- second vocabulary for the same event.\nCREATE OR REPLACE FUNCTION public.plg_bank_emit_movement_imported()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public\nAS $fn$\nDECLARE\n v_payload jsonb;\nBEGIN\n IF NEW.external_source IS NULL OR NEW.external_id IS NULL THEN\n RETURN NULL;\n END IF;\n\n -- The contract every consumer reads. Not the row: a whole-row payload would\n -- put the ledger's internal bookkeeping inside every webhook by default.\n v_payload := jsonb_build_object(\n 'movement_id', NEW.id::text,\n 'provider', NEW.external_source,\n 'external_id', NEW.external_id,\n 'direction', NEW.direction,\n 'amount', NEW.amount,\n 'payment_date', NEW.payment_date,\n 'description', NEW.notes,\n 'bank_account_id', NEW.bank_account_id);\n\n BEGIN\n PERFORM public.plg_emit_event(\n 'financial.movement.imported', v_payload, 'financial_movement',\n NEW.id::text, NEW.tenant_id, NULL);\n EXCEPTION WHEN OTHERS THEN\n -- Observability must never be able to reject the write that caused it.\n RAISE WARNING '[plg_bank_emit_movement_imported] % on %: %', TG_OP, TG_TABLE_NAME, SQLERRM;\n END;\n\n RETURN NULL;\nEND;\n$fn$;\n\nREVOKE ALL ON FUNCTION public.plg_bank_emit_movement_imported() FROM public, anon;\n\n-- Guarded on the COLUMN, not just the table: the financial plugin owns both, and\n-- attaching this to a ledger without 007_reconciliation.sql would make every\n-- movement insert fail on an unknown field — an addon breaking its own host.\nDO $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public'\n AND table_name = 'plg_financial_movements'\n AND column_name = 'external_source'\n ) THEN\n DROP TRIGGER IF EXISTS plg_financial_movements_emit_imported ON public.plg_financial_movements;\n CREATE TRIGGER plg_financial_movements_emit_imported\n AFTER INSERT ON public.plg_financial_movements\n FOR EACH ROW EXECUTE FUNCTION public.plg_bank_emit_movement_imported();\n END IF;\nEND $$;\n`\n\nexport const MIGRATION_003_ONTO_CONNECTIONS = `-- ============================================================================\n-- plugin-banking-br 003 — PlugBank stops owning tables.\n--\n-- Retires public.bank_integrations and public.bank_integration_sync_log. The\n-- connection is now a row of public.plg_connections and each import is a row of\n-- public.plg_sync_runs (packages/db 029). See FAY-1376.\n--\n-- This connector is the one that tests the shape, because it is the only one\n-- whose key was never (tenant, provider): 001 keyed on\n-- UNIQUE (tenant_id, bank_account_id, provider), and the provider filtered by\n-- bank_account_id. Two accounts of one clinic are two connections, and they must\n-- stay two. That is exactly what \\`instance_key\\` is:\n--\n-- bank_integrations.bank_account_id → plg_connections.instance_key\n-- (the account uuid as text; '' is the\n-- account-less default the panel uses,\n-- which is what \\`bank_account_id IS NULL\\`\n-- meant before)\n-- bank_integrations.provider → plg_connections.connector_id\n-- bank_integrations.environment → plg_connections.settings->>'environment'\n-- bank_integrations.active → plg_connections.active\n-- bank_integrations.last_sync_at → plg_connections.last_sync_at\n-- UNIQUE (tenant_id, bank_account_id, provider)\n-- → UNIQUE (tenant_id, connector_id, instance_key)\n--\n-- And the run, column by column — nothing the panel renders is lost:\n-- sync_log.bank_integration_id → plg_sync_runs.connection_id\n-- sync_log.transactions_fetched → plg_sync_runs.fetched\n-- sync_log.transactions_imported → plg_sync_runs.written\n-- sync_log.duplicates → plg_sync_runs.stats->>'duplicates'\n-- sync_log.period_from / _to → plg_sync_runs.stats->>'period_from' / '_to'\n-- sync_log.triggered_by_user_id → plg_sync_runs.stats->>'triggered_by_user_id'\n-- sync_log.error_message → plg_sync_runs.error\n-- sync_log.bank_account_id → dropped: it is the connection's\n-- instance_key, and a second copy is a\n-- second thing to disagree\n-- sync_log.provider → plg_sync_runs.connector_id\n-- sync_log.created_at → plg_sync_runs.started_at\n-- direction / trigger_kind / status / started_at / finished_at → same names\n--\n-- The count columns finally get the contract's names. 002 §2 kept\n-- transactions_fetched/_imported because renaming them would have broken the\n-- deployed edge function's INSERT — but the function no longer inserts into a\n-- log at all: it calls plugbank_import_movements, which is the only writer, and\n-- the run is filed here. The choreography 002 was avoiding does not exist.\n--\n-- Nothing is in production, so this is a drop and not a data migration.\n-- ============================================================================\n\n-- §1 ── the door, re-pointed ─────────────────────────────────────────────────\n-- Same name, same payload, same guarantees. \\`integration_id\\` keeps its key on\n-- the wire so the deployed plugbank-sync function needs no coordinated release;\n-- what changed is that the uuid it carries now names a plg_connections row.\n--\n-- Everything 002 §3 established still holds and for the same reasons: the tenant\n-- comes from the CONNECTION ROW and never from the payload, a caller presenting\n-- a user credential must be a member of it, a caller with no auth.uid() is the\n-- server and is exempt, and idempotency is the partial unique index rather than\n-- a SELECT-then-INSERT that two racing runs would both pass.\n--\n-- One thing is new and is the point of the migration: the run is filed against\n-- \\`connection_id\\`, so a tenant with two bank accounts gets two histories instead\n-- of one interleaved list. plg_sync_runs carries no INSERT grant for\n-- \\`authenticated\\` — this SECURITY DEFINER function is the server-side writer the\n-- table's grants assume.\nCREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public\nAS $fn$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_connection uuid;\n v_instance text;\n v_account uuid;\n v_connector text;\n v_trigger text;\n v_from date;\n v_to date;\n v_started timestamptz := now();\n v_line jsonb;\n v_external text;\n v_amount numeric(14,2);\n v_date date;\n v_fetched int := 0;\n v_imported int := 0;\n v_duplicates int := 0;\n v_inserted uuid;\n v_run uuid;\nBEGIN\n v_connection := nullif(btrim(p_payload->>'integration_id'), '')::uuid;\n IF v_connection IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n SELECT c.tenant_id, c.instance_key, c.connector_id\n INTO v_tenant, v_instance, v_connector\n FROM public.plg_connections c\n WHERE c.id = v_connection AND c.active;\n\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: no active connection %', v_connection\n USING ERRCODE = 'no_data_found';\n END IF;\n\n -- The account this connection is for. \\`instance_key\\` is text because it also\n -- carries things that are not uuids for other connectors (a Bling company, a\n -- gateway instance); here it is the bank account, and '' is the account-less\n -- default that \\`bank_account_id IS NULL\\` used to mean.\n v_account := nullif(btrim(v_instance), '')::uuid;\n\n -- SECURITY DEFINER bypasses RLS, so the membership check the policy would\n -- have made has to happen here.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN\n RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Refusing beats ignoring: a caller that believes it is importing into\n -- another tenant must be told, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL\n AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Same rule as the tenant, one level down: the account is the connection's,\n -- and a payload naming a different one is a caller importing into the wrong\n -- ledger account. 002 let the payload override it silently — with one\n -- connection per account that override is how two accounts get mixed.\n IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL\n AND (p_payload->>'bank_account_id')::uuid IS DISTINCT FROM v_account THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another bank account; the account comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n v_from := nullif(btrim(p_payload->>'period_from'), '')::date;\n v_to := nullif(btrim(p_payload->>'period_to'), '')::date;\n v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));\n IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN\n RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))\n LOOP\n v_fetched := v_fetched + 1;\n v_external := nullif(btrim(v_line->>'external_id'), '');\n v_date := nullif(btrim(v_line->>'date'), '')::date;\n v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));\n\n -- A line with no provider id cannot be recognised on re-delivery, so it\n -- would import again on every run. Skipping it is the lesser wrong.\n IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN\n CONTINUE;\n END IF;\n\n INSERT INTO public.plg_financial_movements (\n tenant_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, bank_account_id, notes, external_id, external_source)\n VALUES (\n v_tenant,\n CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,\n 'payment', v_amount, v_amount, 'paid',\n v_date, v_date, v_account,\n nullif(btrim(v_line->>'description'), ''),\n v_external, coalesce(v_connector, 'plugbank'))\n ON CONFLICT (tenant_id, external_source, external_id)\n WHERE external_id IS NOT NULL AND external_source IS NOT NULL\n DO NOTHING\n RETURNING id INTO v_inserted;\n\n IF v_inserted IS NULL THEN\n v_duplicates := v_duplicates + 1;\n ELSE\n v_imported := v_imported + 1;\n END IF;\n v_inserted := NULL;\n END LOOP;\n\n UPDATE public.plg_connections\n SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()\n WHERE id = v_connection;\n\n -- \\`stats\\` is where the three facts a manual statement import has and a generic\n -- run does not go: the operator picked a period, a re-import of that period is\n -- mostly duplicates by design, and someone pressed the button. The panel reads\n -- all three back from here.\n INSERT INTO public.plg_sync_runs (\n tenant_id, connection_id, connector_id, direction, trigger_kind, status,\n stream, fetched, written, stats, started_at, finished_at)\n VALUES (\n v_tenant, v_connection, coalesce(v_connector, 'plugbank'), 'inbound', v_trigger, 'success',\n 'statement', v_fetched, v_imported,\n jsonb_strip_nulls(jsonb_build_object(\n 'duplicates', v_duplicates,\n 'period_from', v_from,\n 'period_to', v_to,\n 'triggered_by_user_id', auth.uid())),\n v_started, now())\n RETURNING id INTO v_run;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sync_run_id', v_run,\n 'fetched', v_fetched,\n 'imported', v_imported,\n 'duplicates', v_duplicates);\nEND;\n$fn$;\n\nREVOKE ALL ON FUNCTION public.plugbank_import_movements(jsonb) FROM public, anon;\nGRANT EXECUTE ON FUNCTION public.plugbank_import_movements(jsonb) TO authenticated, service_role;\n\nCOMMENT ON FUNCTION public.plugbank_import_movements(jsonb) IS\n 'Import bank-statement lines into the financial ledger and file the run in '\n 'plg_sync_runs. The tenant AND the bank account come from the plg_connections '\n 'row named by integration_id, never from the payload.';\n\n-- §2 ── the bespoke tables go ────────────────────────────────────────────────\n-- After the function above, so a pool is never left with the old door pointing\n-- at a table that no longer exists.\nDROP TABLE IF EXISTS public.bank_integration_sync_log CASCADE;\nDROP TABLE IF EXISTS public.bank_integrations CASCADE;\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"002_credential_and_facts\", sql: MIGRATION_002_CREDENTIAL_AND_FACTS },\n { id: \"003_onto_connections\", sql: MIGRATION_003_ONTO_CONNECTIONS },\n]\n","// Open Banking plugin (Tecnospeed PlugBank) — public surface + manifest.\n//\n// The canonical PULL connector: the provider has never heard of Fayz, so an edge\n// function reaches out to it. Settings-only (no left-nav page) — it contributes\n// its connector into plugin-financial's settings → Integrations, and imported\n// lines are matched in Financeiro → Conciliação (enable the financial\n// `reconciliation` module).\nimport type { PluginManifest } from '@fayz-ai/core'\nimport { openBankingConnector } from './connectors/connectorDef'\nimport { buildBankingDomainEvents } from './lib/banking-events'\nimport { MIGRATION_002_CREDENTIAL_AND_FACTS, MIGRATION_003_ONTO_CONNECTIONS } from './migrations'\n\nexport { createOpenBankingProvider } from './data/supabase'\nexport { openBankingConnector } from './connectors/connectorDef'\nexport { buildBankingDomainEvents, MOVEMENT_IMPORTED_FIELDS } from './lib/banking-events'\nexport * from './schema'\nexport * from './types'\n\n// Addon plugin: contributes the PlugBank connector into its host\n// (plugin-financial). It appears in Financial settings → Integrations — no\n// top-level settings tab of its own.\nexport function createOpenBankingPlugin(): PluginManifest {\n return {\n id: 'openbanking',\n name: 'Open Banking',\n icon: 'Landmark',\n version: '0.9.1',\n apiVersion: 1,\n scope: 'addon',\n defaultEnabled: true,\n dependencies: ['financial'],\n navigation: [],\n routes: [],\n connectors: [openBankingConnector],\n // The fact the import produces. Declared HERE and not on plugin-financial\n // because the trigger that emits it ships in this plugin's 002: a host\n // advertising an event that only exists when an addon is installed is the\n // same broken promise as an automation the ingress never delivers.\n events: buildBankingDomainEvents(),\n // `emitsFrom` is deliberately absent. It generates\n // `registry.<entity>.<action>` keys for every row change, and \"imported\" is\n // not a row action — it is an INSERT carrying an external_source, which only\n // a purpose-built trigger can tell apart from a sale typed into the till.\n functions: [\n {\n slug: 'plugbank-sync',\n // Invoked from the browser by an authenticated operator picking a\n // period, so Supabase must check the caller's JWT. It is not reached\n // through the Fayz ingress — nothing in Fayz wakes a pull connector —\n // which is what lets verifyJwt stay true.\n ingress: 'none',\n verifyJwt: true,\n // NAMES only. PLUGBANK_BASE_URL selects production vs sandbox; the\n // per-tenant token and account CNPJ are NOT here, because a Supabase\n // function secret is per-POOL and a pool serves N tenants with N\n // different bank accounts. Those come from the Fayz platform per call.\n secrets: ['PLUGBANK_BASE_URL', 'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],\n // What this connector's credential may touch. Direct table access is\n // not on the list on purpose (2026-08-11 ADR): the import goes through\n // the RPC, which takes the tenant from the integration row instead of\n // from the request body.\n grants: ['plugbank_import_movements'],\n },\n ],\n declaredRpcs: [\n {\n name: 'plugbank_import_movements',\n kind: 'write',\n description:\n 'Import bank-statement lines into the financial ledger. Idempotent on (tenant, provider, external id); the tenant comes from the integration row, never from the payload.',\n audits: false,\n },\n ],\n // This plugin creates no table. 003 retired the two it had; what is left is\n // the RPC that is the import's one door, and the trigger that turns an\n // imported movement into a fact. The connection and its run history are rows\n // of plg_connections / plg_sync_runs, owned by packages/db.\n migrations: [\n {\n id: 'openbanking-002_credential_and_facts',\n version: '0.9.1',\n sql: MIGRATION_002_CREDENTIAL_AND_FACTS,\n description:\n 'Drop bank_integrations.api_token/cnpj (destination: the Fayz platform credential store, FAY-1360 — register there BEFORE applying this), add plugbank_import_movements and the financial.movement.imported trigger',\n },\n {\n id: 'openbanking-003_onto_connections',\n version: '1.0.0',\n sql: MIGRATION_003_ONTO_CONNECTIONS,\n description:\n 'Retire bank_integrations + bank_integration_sync_log — 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',\n },\n ],\n diagnostics: [\n {\n id: 'openbanking-ledger-ready',\n description:\n '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.',\n requires: {\n tables: ['plg_connections', 'plg_sync_runs', 'plg_financial_movements'],\n rpcs: ['plugbank_import_movements', 'plg_emit_event'],\n },\n level: 'error',\n },\n ],\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/data/supabase.ts","../src/connectors/connectorDef.tsx","../src/lib/banking-events.ts","../src/migrations/index.ts","../src/index.ts"],"names":["createConnectionStore"],"mappings":"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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;;AAAA;;AAAA;;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA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","file":"index.js","sourcesContent":["// Open Banking plugin — Supabase data provider.\n//\n// Thin control-plane client that writes no SQL. The connection is a row of\n// public.connections and each import is a row of public.sync_runs\n// (packages/db 029), reached through createConnectionStore — the bespoke\n// bank_integrations / bank_integration_sync_log pair this file used to address\n// was the generic shape under a private name (FAY-1376, migration 003).\n//\n// The heavy lifting (calling the Tecnospeed PlugBank API, normalizing lines,\n// importing into plg_financial_movements with idempotency) runs in the\n// `plugbank-sync` Edge Function, declared on the manifest's `functions[]`.\n//\n// MULTI-ACCOUNT IS THE POINT HERE. The old key was\n// UNIQUE (tenant_id, bank_account_id, provider) and every read filtered by\n// bank_account_id: a clinic with two bank accounts had two connections. That is\n// preserved exactly, by `instance_key` — one store per account, and the\n// account-less default (the panel's case) is instance_key `''`, which is what\n// `bank_account_id IS NULL` used to mean.\n//\n// It never touches a credential. The PlugBank token and the CNPJ that identifies\n// the account will live in the Fayz platform, redeemed per call (FAY-1360 +\n// FAY-1389; today they still sit on the live pool's legacy bank_integrations).\n// connections has no credential column at all, so \"this file never touches\n// one\" is a property of the table rather than of this file.\nimport { createConnectionStore, getSupabaseClientOptional } from '@fayz-ai/core'\nimport type { ConnectionConfig, ConnectionStore, SyncRun } from '@fayz-ai/core'\nimport type {\n BankIntegration, SaveIntegrationInput, BankSyncRun,\n FetchStatementResult, ImportResult, BankLine,\n} from '../types'\n\nconst FUNCTION = 'plugbank-sync'\n\nexport const PLUGBANK_CONNECTOR_ID = 'plugbank'\n\n/** Non-credential keys this connector stores. `environment` selects prod/sandbox. */\nconst SETTINGS_KEYS = ['environment']\n\n/**\n * The store for ONE bank account. `undefined` is the account-less default, not\n * \"any account\": resolving it to `''` keeps the old `bank_account_id IS NULL`\n * filter, so the panel that never picks an account keeps addressing the same\n * single connection it always did.\n */\nfunction storeFor(bankAccountId?: string): ConnectionStore {\n return createConnectionStore(PLUGBANK_CONNECTOR_ID, {\n settingsKeys: SETTINGS_KEYS,\n instanceKey: bankAccountId ?? '',\n })\n}\n\nfunction sb() {\n const supabase = getSupabaseClientOptional() as any\n if (!supabase) throw new Error('Supabase not initialized')\n return supabase\n}\n\nexport function toIntegration(connection: ConnectionConfig): BankIntegration {\n return {\n id: connection.id ?? '',\n provider: connection.connectorId,\n // Back to `undefined` for the default account, so callers that pass this\n // value straight back into getIntegration() address the same row.\n bankAccountId: connection.instanceKey || undefined,\n environment: (connection.settings?.environment as string | undefined) ?? 'production',\n active: connection.active,\n lastSyncAt: connection.lastSyncAt ?? undefined,\n }\n}\n\n/**\n * The audit row, read as the connector contract's SyncRun. The three facts a\n * MANUAL statement import has and a generic run does not — the period the\n * operator picked, and the duplicates a re-import of that period produces by\n * design — live in `stats`, which is the column that exists for exactly this.\n */\nexport function toSyncRun(run: SyncRun): BankSyncRun {\n const stats = run.stats ?? {}\n return {\n ...run,\n id: run.id,\n connectionId: run.connectionId,\n connectorId: run.connectorId,\n direction: run.direction,\n triggerKind: run.triggerKind,\n status: run.status,\n fetched: run.fetched ?? 0,\n written: run.written ?? 0,\n error: run.error ?? undefined,\n startedAt: run.startedAt,\n finishedAt: run.finishedAt ?? undefined,\n periodFrom: (stats.period_from as string | undefined) ?? undefined,\n periodTo: (stats.period_to as string | undefined) ?? undefined,\n duplicates: Number(stats.duplicates ?? 0),\n // sync_runs has no created_at: a run is identified by when it STARTED,\n // which is what the old created_at recorded anyway (the row was inserted at\n // the end, so the two disagreed by the length of the import).\n createdAt: run.startedAt ?? '',\n }\n}\n\nexport interface OpenBankingProvider {\n getIntegration(bankAccountId?: string): Promise<BankIntegration | null>\n saveIntegration(input?: SaveIntegrationInput): Promise<BankIntegration>\n disconnect(bankAccountId?: string): Promise<void>\n fetchStatement(input: { integrationId: string; from: string; to: string }): Promise<FetchStatementResult>\n importTransactions(input: { integrationId: string; bankAccountId?: string; from: string; to: string; lines: BankLine[] }): Promise<ImportResult>\n getSyncRuns(bankAccountId?: string): Promise<BankSyncRun[]>\n}\n\nexport function createOpenBankingProvider(): OpenBankingProvider {\n return {\n async getIntegration(bankAccountId) {\n const connection = await storeFor(bankAccountId).get()\n return connection ? toIntegration(connection) : null\n },\n\n async saveIntegration(input) {\n const saved = await storeFor(input?.bankAccountId).save({\n settings: { environment: input?.environment ?? 'production' },\n active: true,\n })\n return toIntegration(saved)\n },\n\n /**\n * Scoped to ONE account, where the old provider deleted every plugbank row\n * of the tenant. That breadth was invisible while the panel only ever drove\n * the default account; with two accounts it means disconnecting Itaú also\n * silently kills Banco do Brasil. The narrow verb is the correct one.\n */\n async disconnect(bankAccountId) {\n await storeFor(bankAccountId).disconnect()\n },\n\n async fetchStatement(input) {\n const supabase = sb()\n const { data, error } = await supabase.functions.invoke(FUNCTION, {\n body: { action: 'fetch_statement', integrationId: input.integrationId, from: input.from, to: input.to },\n })\n if (error) throw new Error(error.message)\n return data as FetchStatementResult\n },\n\n async importTransactions(input) {\n const supabase = sb()\n const { data, error } = await supabase.functions.invoke(FUNCTION, {\n body: {\n action: 'import_transactions',\n // Still `integrationId` on the wire: it now carries a connections\n // id, and keeping the key means the deployed function needs no\n // coordinated release. The RPC takes the tenant AND the bank account\n // from that row, so neither can be steered from here.\n integrationId: input.integrationId,\n bankAccountId: input.bankAccountId,\n from: input.from,\n to: input.to,\n lines: input.lines,\n },\n })\n if (error) throw new Error(error.message)\n return data as ImportResult\n },\n\n async getSyncRuns(bankAccountId) {\n const runs = await storeFor(bankAccountId).listRuns({ limit: 20 })\n return runs.map(toSyncRun)\n },\n }\n}\n","// Open Banking (Tecnospeed PlugBank) as a unified ConnectorDefinition.\n//\n// Surfaces inside plugin-financial's settings → Integrations tab (the host),\n// rendered by the shared ConnectorsHub. Importing a statement is an ACT, so it\n// arrives through `RunActions` and lands on the Sincronizações tab, above the\n// history the shared surface renders from `sync_runs`. It used to be an\n// `ExtraPanel` on the settings tab with its own hand-written run list under it.\n//\n// A PULL connector: the bank does not know Fayz, so an edge function reaches out\n// to it and needs a credential at fetch time. That credential is registered in\n// the Fayz platform, not here — the connect form asks for no secret, because a\n// secret typed into this app would land in a table any member of the tenant can\n// read. See migration 002 §1.\nimport React, { useEffect, useState } from 'react'\nimport { Download, Loader2 } from 'lucide-react'\nimport { Button, DatePicker, toast } from '@fayz-ai/ui'\nimport { connectionStatus, createConnectionStore, type ConnectorDefinition } from '@fayz-ai/core'\nimport type { SyncRun } from '@fayz-ai/core'\nimport { createOpenBankingProvider, toIntegration, PLUGBANK_CONNECTOR_ID } from '../data/supabase'\nimport type { BankIntegration, BankLine } from '../types'\n\nconst provider = createOpenBankingProvider()\n\n/** dd/mm from the ISO date the RPC stores in `stats`. */\nfunction day(value: unknown): string | undefined {\n return typeof value === 'string' && value.length >= 10 ? value.slice(8, 10) + '/' + value.slice(5, 7) : undefined\n}\n\n/**\n * What one statement import DID, as the sentence every run carries.\n *\n * The period and the duplicate count live in `stats` — three keys the RPC writes\n * and only this connector can read (migrations §sync_runs). The shared\n * history renders `message`, so the interpretation belongs here rather than in a\n * view that would have to learn one connector's jsonb.\n */\nfunction statementLine(run: SyncRun): string | undefined {\n const from = day(run.stats?.period_from)\n const to = day(run.stats?.period_to)\n const duplicates = Number(run.stats?.duplicates ?? 0)\n if (!from || !to) return undefined\n const imported = `${run.written ?? 0} importadas de ${from} a ${to}`\n // A re-import of the same period is mostly duplicates BY DESIGN, so saying so\n // is what stops \"0 importadas\" reading as a failure.\n return duplicates > 0 ? `${imported} · ${duplicates} já estavam aqui` : imported\n}\n\nfunction brl(n: number): string {\n try { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(n) }\n catch { return `R$ ${n.toFixed(2)}` }\n}\n\nfunction OpenBankingImport({ onRan }: { onRan?: () => void }) {\n const [integration, setIntegration] = useState<BankIntegration | null>(null)\n const [loaded, setLoaded] = useState(false)\n const [from, setFrom] = useState(() => { const d = new Date(); d.setDate(d.getDate() - 30); return d.toISOString().slice(0, 10) })\n const [to, setTo] = useState(() => new Date().toISOString().slice(0, 10))\n const [lines, setLines] = useState<BankLine[]>([])\n const [selected, setSelected] = useState<Set<string>>(new Set())\n const [fetching, setFetching] = useState(false)\n const [importing, setImporting] = useState(false)\n\n async function load() {\n setIntegration(await provider.getIntegration().catch(() => null))\n setLoaded(true)\n }\n useEffect(() => { void load() }, [])\n\n // Nothing to import until the connection is live. `active` and not merely \"a\n // row exists\": disconnect is soft now, so a revoked connection still has one.\n if (!loaded || !integration?.active) return null\n\n async function handleFetch() {\n if (!integration) return\n setFetching(true)\n try {\n const res = await provider.fetchStatement({ integrationId: integration.id, from, to })\n setLines(res.lines)\n setSelected(new Set(res.lines.map((l) => l.externalId)))\n } catch (e: any) { toast.error(e?.message ?? 'Erro ao buscar extrato') }\n finally { setFetching(false) }\n }\n\n async function handleImport() {\n if (!integration) return\n const toImport = lines.filter((l) => selected.has(l.externalId))\n if (toImport.length === 0) { toast.error('Selecione ao menos uma linha'); return }\n setImporting(true)\n try {\n const res = await provider.importTransactions({\n integrationId: integration.id, bankAccountId: integration.bankAccountId, from, to, lines: toImport,\n })\n toast.success(`${res.imported} importadas${res.duplicates ? ` · ${res.duplicates} duplicadas` : ''}`)\n setLines([]); setSelected(new Set())\n onRan?.()\n } catch (e: any) { toast.error(e?.message ?? 'Erro ao importar') }\n finally { setImporting(false) }\n }\n\n function toggle(id: string) {\n setSelected((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next })\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center gap-2\"><Download className=\"h-4 w-4 text-muted-foreground\" /><h4 className=\"text-sm font-semibold\">Importar extrato</h4></div>\n <div className=\"flex flex-wrap items-end gap-3\">\n <div><span className=\"text-xs font-medium text-muted-foreground\">De</span><DatePicker value={from} onChange={setFrom} className=\"mt-1\" /></div>\n <div><span className=\"text-xs font-medium text-muted-foreground\">Até</span><DatePicker value={to} onChange={setTo} className=\"mt-1\" /></div>\n <Button variant=\"outline\" size=\"sm\" onClick={handleFetch} disabled={fetching}>\n {fetching ? <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> : <Download className=\"h-3.5 w-3.5\" />} Buscar extrato\n </Button>\n </div>\n\n {lines.length > 0 && (\n <>\n <div className=\"rounded-md border divide-y max-h-72 overflow-auto\">\n {lines.map((l) => (\n <label key={l.externalId} className=\"flex items-center gap-3 px-3 py-2 text-sm cursor-pointer hover:bg-muted/30\">\n <input type=\"checkbox\" checked={selected.has(l.externalId)} onChange={() => toggle(l.externalId)} />\n <span className=\"text-muted-foreground text-xs w-20 shrink-0\">{l.date}</span>\n <span className=\"flex-1 truncate\">{l.description}</span>\n <span className={`font-semibold shrink-0 ${l.type === 'C' ? 'text-success' : 'text-destructive'}`}>\n {l.type === 'C' ? '+' : '-'}{brl(l.amount)}\n </span>\n </label>\n ))}\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-xs text-muted-foreground\">{selected.size} de {lines.length} selecionadas</span>\n <Button size=\"sm\" onClick={handleImport} disabled={importing || selected.size === 0}>\n {importing ? <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> : <Download className=\"h-3.5 w-3.5\" />} Importar selecionadas\n </Button>\n </div>\n </>\n )}\n\n <p className=\"text-[11px] text-muted-foreground\">\n O token do PlugBank e o CNPJ da conta ficam guardados na plataforma Fayz, não neste\n aplicativo. Quem importa o extrato é o servidor, que pede as credenciais na hora da consulta.\n </p>\n </div>\n )\n}\n\nexport const openBankingConnector: ConnectorDefinition = {\n id: 'plugbank',\n // The product is PlugBank, the company in the directory is TecnoSpeed. Same\n // row, two names — and until this said so, the connector took neither the\n // catalogue's copy nor its mark.\n catalogSlug: 'tecnospeed',\n hostPluginId: 'financial',\n name: 'Open Banking · Tecnospeed PlugBank',\n description: 'Importe o extrato bancário e concilie em Financeiro → Conciliação.',\n icon: 'Landmark',\n authKind: 'api-key',\n // `banking`, not `payments`: reading a statement is not taking a payment, and\n // the platform catalogue files the provider (slug `tecnospeed`) under banking.\n category: 'banking',\n // Inbound because WE reach the bank: PlugBank has never heard of Fayz, which\n // is what makes this the canonical pull connector and why it ships an edge\n // function at all.\n direction: 'inbound',\n // Sync only. The statement has to exist INSIDE Fayz to be listed, filtered and\n // matched against internal movements — reconciliation is not a question the\n // assistant can answer by asking the bank at the moment it is asked.\n planes: ['sync'],\n docsUrl: 'https://tecnospeed.com.br/plugbank/',\n // Write and NOT read, which is unusual enough to be worth saying out loud:\n // `plugbank_import_movements` (migrations/003) only ever INSERTs, with\n // `ON CONFLICT (tenant_id, external_source, external_id) DO NOTHING`. It never\n // SELECTs a movement, so declaring `read` here would be an access the code does\n // not take — and the whole value of this list is that it is not padded.\n //\n // The `emit` is the second half, and it is the one a tenant would not guess:\n // every imported line fires `financial.movement.imported` through the trigger\n // in migrations/002, and that payload carries amount, date, description and\n // bank account to whatever automation is listening. Importing a statement is\n // therefore also publishing it internally.\n permissions: [\n {\n kind: 'data',\n resource: 'movement',\n actions: ['write'],\n reason:\n 'Gravar no seu financeiro as linhas do extrato bancário — valor, data, descrição e conta. Lançamentos já importados não entram de novo.',\n sensitive: true,\n },\n {\n kind: 'event',\n resource: 'financial.movement.imported',\n actions: ['emit'],\n reason:\n 'Avisar o app a cada lançamento importado, para que suas automações de conciliação possam reagir. O aviso leva valor, data e conta.',\n sensitive: true,\n },\n ],\n invokedAt: [\n {\n key: 'financial.statement.import',\n label: 'Você escolhe um período aqui e o extrato do banco é importado',\n plane: 'sync',\n trigger: 'manual',\n touches: ['movement', 'financial.movement.imported'],\n },\n ],\n // No credential field. The token and the CNPJ that identify the account are\n // registered in the Fayz platform; anything typed here would be stored in a\n // table every member of the tenant can read.\n //\n // `subscribesTo` is deliberately absent, and so is `managedAutomations`.\n // Nothing that happens inside Fayz should wake this connector: it pulls a\n // statement, and what makes it pull is the operator picking a period below or\n // the Fayz SCHEDULE worker (FAY-1347) — never a domain fact. A subscription\n // written to fill the field would put this connector on the ingress's\n // delivery list for events it has no reaction to.\n //\n // The reconciliation automations (\"avise quando entrar um valor acima de X\")\n // are real, but they are the TENANT's: they react to\n // `financial.movement.imported` and the tenant may delete them without the\n // connection becoming half-wired. `managedAutomations` is for the wiring the\n // integration IS, and this integration is the import itself.\n // Read from the row the list already batched — one request for N connectors.\n // The account-less default instance (`''`) is the one the panel addresses.\n ...connectionStatus(PLUGBANK_CONNECTOR_ID, (connection) => {\n const found = connection ? toIntegration(connection) : null\n return { connected: !!found?.active, detail: found?.environment, plane: 'sync' }\n }),\n // `testConnection` is deleted rather than stubbed: the credential is not in\n // this app, so there is nothing this side can validate, and ConnectorDetail\n // renders the button only when the verb exists.\n saveConnection: async () => { await provider.saveIntegration() },\n disconnect: () => provider.disconnect(),\n // Every import files a `sync_runs` row, and the shared surface renders it.\n // What is connector-specific is not the VIEW, it is the meaning of `stats`:\n // \"de 01/08 a 12/08 · 3 duplicadas\" is three keys only this connector can\n // read, turned here into the one sentence every run carries. Scoped to the\n // default instance — the account this panel addresses.\n async listRuns(options) {\n try {\n const runs = await createConnectionStore(PLUGBANK_CONNECTOR_ID).listRuns(options)\n return runs.map((run) => ({ ...run, message: run.message ?? statementLine(run) }))\n } catch {\n return []\n }\n },\n RunActions: OpenBankingImport,\n}\n","import type { EventField, PluginEventDefinition } from '@fayz-ai/core'\n\n// ---------------------------------------------------------------------------\n// The FACT of the bank import.\n//\n// The emitter is not in this file and cannot be: a statement line reaches the\n// ledger from the edge function's insert, from plugbank_import_movements, from\n// a future scheduled pull and occasionally from a hand-run backfill. The one\n// place all four end up is the row, so the emitter is a trigger on\n// plg_financial_movements (migration 002 §4) and this is its contract.\n//\n// This is the fact that turns reconciliation from a screen someone remembers to\n// open into something the tenant can react to — \"avise quando entrar um valor\n// acima de X\" is an automation on this event and nothing else.\n//\n// `provider` is in the payload because the trigger fires for ANY imported\n// movement, not only PlugBank's. A second bank connector writing into the same\n// ledger produces the same fact rather than a second vocabulary for it.\n// ---------------------------------------------------------------------------\n\n/** Payload keys, in the order the trigger builds them. */\nexport const MOVEMENT_IMPORTED_FIELDS: EventField[] = [\n { name: 'movement_id', label: 'ID do lançamento', type: 'id', example: '8f14e45f-…' },\n { name: 'provider', label: 'Origem', type: 'string', example: 'plugbank' },\n { name: 'external_id', label: 'Identificador no banco', type: 'string', example: 'LINE-1' },\n { name: 'direction', label: 'Tipo', type: 'string', example: 'credit' },\n { name: 'amount', label: 'Valor', type: 'money', example: 'R$ 150,00' },\n { name: 'payment_date', label: 'Data', type: 'date', example: '03/08/2026' },\n { name: 'description', label: 'Descrição', type: 'string', example: 'Recebimento PIX' },\n { name: 'bank_account_id', label: 'Conta bancária', type: 'id', example: '3f2a91c4-…' },\n]\n\nconst PAYLOAD_SCHEMA: Record<string, unknown> = {\n type: 'object',\n properties: {\n movement_id: { type: 'string', format: 'uuid', description: 'Movement id in the tenant ledger' },\n provider: {\n type: 'string',\n description:\n \"Which integration imported the line — 'plugbank' today. Reads from plg_financial_movements.external_source, so a second bank connector produces the same fact.\",\n },\n external_id: {\n type: 'string',\n description: \"The bank's own id for the statement line. With `provider` it is the import idempotency key.\",\n },\n direction: { type: 'string', enum: ['credit', 'debit'], description: 'Money in or money out' },\n amount: { type: 'number', description: 'Always positive; `direction` carries the sign' },\n payment_date: { type: 'string', format: 'date', description: 'Date the bank settled the line (YYYY-MM-DD)' },\n description: { type: ['string', 'null'], description: 'Statement narrative, as the bank wrote it' },\n bank_account_id: { type: ['string', 'null'], format: 'uuid', description: 'Account the line landed in, when known' },\n },\n required: ['movement_id', 'provider', 'external_id', 'direction', 'amount', 'payment_date'],\n additionalProperties: false,\n}\n\n/**\n * `audit: true`. Money arriving from outside the app is exactly the subject\n * where \"where did this come from, and when?\" needs an answer that survives a\n * later edit — sharper still when the answer is a connector rather than a\n * person. There is no `updated` counterpart on purpose: a statement line is not\n * revised, it is imported once and then reconciled, and reconciliation is the\n * financial plugin's fact to declare, not this addon's.\n */\nexport function buildBankingDomainEvents(): PluginEventDefinition[] {\n return [\n {\n name: 'financial.movement.imported',\n title: 'Lançamento bancário importado',\n description: 'Uma linha do extrato entrou no financeiro',\n trigger: 'Quando o extrato é importado — pelo painel, pelo conector ou por uma carga manual',\n icon: 'Landmark',\n audit: true,\n fields: MOVEMENT_IMPORTED_FIELDS,\n payloadSchema: PAYLOAD_SCHEMA,\n },\n ]\n}\n","// AUTO-GENERATED from 000_baseline.sql, 001_the_import_rpc_names_real_tables.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_000_BASELINE = `-- ============================================================================\n-- plugins/plugin-banking-br/src/migrations/000_baseline.sql — what installing this provisions, as one file.\n--\n-- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:\n-- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs\n-- prove the result still matches.\n--\n-- This replaced 2 migration files. They are kept, unapplied, in\n-- migrations.archive/ — a year of decisions is worth reading even when it is no\n-- longer worth replaying.\n--\n-- Object order is fixed rather than dependency-sorted: schemas, types,\n-- sequences, tables, defaults, functions, constraints, views, indexes, foreign\n-- keys, triggers, row security, policies, grants. Dependencies BETWEEN units\n-- are carried by the order packages/db/chain.json declares.\n-- ============================================================================\n\n-- A LANGUAGE sql function binds its body at CREATE time, and 570 functions\n-- sorted by name are not sorted by who calls whom. This is what lets them load\n-- in any order; every body is still compiled on first call, so a genuinely\n-- broken reference surfaces then rather than never. Session-scoped.\nSET check_function_bodies = false;\n\n\n\n-- ── default privileges: reset ───────────────────────────────────────────\n-- Cleared BEFORE anything is created, so every object below gets the ACL the\n-- dump describes rather than the image's defaults on top of it. Restored at\n-- the end of this file.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEND $dp$;\n\n\n-- ── function ────────────────────────────────────────────────────────────\n\nCREATE FUNCTION public.plg_bank_emit_movement_imported() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_payload jsonb;\nBEGIN\n IF NEW.external_source IS NULL OR NEW.external_id IS NULL THEN\n RETURN NULL;\n END IF;\n\n -- The contract every consumer reads. Not the row: a whole-row payload would\n -- put the ledger's internal bookkeeping inside every webhook by default.\n v_payload := jsonb_build_object(\n 'movement_id', NEW.id::text,\n 'provider', NEW.external_source,\n 'external_id', NEW.external_id,\n 'direction', NEW.direction,\n 'amount', NEW.amount,\n 'payment_date', NEW.payment_date,\n 'description', NEW.notes,\n 'bank_account_id', NEW.bank_account_id);\n\n BEGIN\n PERFORM public.plg_emit_event(\n 'financial.movement.imported', v_payload, 'financial_movement',\n NEW.id::text, NEW.tenant_id, NULL);\n EXCEPTION WHEN OTHERS THEN\n -- Observability must never be able to reject the write that caused it.\n RAISE WARNING '[plg_bank_emit_movement_imported] % on %: %', TG_OP, TG_TABLE_NAME, SQLERRM;\n END;\n\n RETURN NULL;\nEND;\n$$;\n\nCREATE FUNCTION public.plugbank_import_movements(p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_connection uuid;\n v_instance text;\n v_account uuid;\n v_connector text;\n v_trigger text;\n v_from date;\n v_to date;\n v_started timestamptz := now();\n v_line jsonb;\n v_external text;\n v_amount numeric(14,2);\n v_date date;\n v_fetched int := 0;\n v_imported int := 0;\n v_duplicates int := 0;\n v_inserted uuid;\n v_run uuid;\nBEGIN\n v_connection := nullif(btrim(p_payload->>'integration_id'), '')::uuid;\n IF v_connection IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n SELECT c.tenant_id, c.instance_key, c.connector_id\n INTO v_tenant, v_instance, v_connector\n FROM public.plg_connections c\n WHERE c.id = v_connection AND c.active;\n\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: no active connection %', v_connection\n USING ERRCODE = 'no_data_found';\n END IF;\n\n -- The account this connection is for. \\`instance_key\\` is text because it also\n -- carries things that are not uuids for other connectors (a Bling company, a\n -- gateway instance); here it is the bank account, and '' is the account-less\n -- default that \\`bank_account_id IS NULL\\` used to mean.\n v_account := nullif(btrim(v_instance), '')::uuid;\n\n -- SECURITY DEFINER bypasses RLS, so the membership check the policy would\n -- have made has to happen here.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN\n RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Refusing beats ignoring: a caller that believes it is importing into\n -- another tenant must be told, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL\n AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Same rule as the tenant, one level down: the account is the connection's,\n -- and a payload naming a different one is a caller importing into the wrong\n -- ledger account. 002 let the payload override it silently — with one\n -- connection per account that override is how two accounts get mixed.\n IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL\n AND (p_payload->>'bank_account_id')::uuid IS DISTINCT FROM v_account THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another bank account; the account comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n v_from := nullif(btrim(p_payload->>'period_from'), '')::date;\n v_to := nullif(btrim(p_payload->>'period_to'), '')::date;\n v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));\n IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN\n RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))\n LOOP\n v_fetched := v_fetched + 1;\n v_external := nullif(btrim(v_line->>'external_id'), '');\n v_date := nullif(btrim(v_line->>'date'), '')::date;\n v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));\n\n -- A line with no provider id cannot be recognised on re-delivery, so it\n -- would import again on every run. Skipping it is the lesser wrong.\n IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN\n CONTINUE;\n END IF;\n\n INSERT INTO public.plg_financial_movements (\n tenant_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, bank_account_id, notes, external_id, external_source)\n VALUES (\n v_tenant,\n CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,\n 'payment', v_amount, v_amount, 'paid',\n v_date, v_date, v_account,\n nullif(btrim(v_line->>'description'), ''),\n v_external, coalesce(v_connector, 'plugbank'))\n ON CONFLICT (tenant_id, external_source, external_id)\n WHERE external_id IS NOT NULL AND external_source IS NOT NULL\n DO NOTHING\n RETURNING id INTO v_inserted;\n\n IF v_inserted IS NULL THEN\n v_duplicates := v_duplicates + 1;\n ELSE\n v_imported := v_imported + 1;\n END IF;\n v_inserted := NULL;\n END LOOP;\n\n UPDATE public.plg_connections\n SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()\n WHERE id = v_connection;\n\n -- \\`stats\\` is where the three facts a manual statement import has and a generic\n -- run does not go: the operator picked a period, a re-import of that period is\n -- mostly duplicates by design, and someone pressed the button. The panel reads\n -- all three back from here.\n INSERT INTO public.plg_sync_runs (\n tenant_id, connection_id, connector_id, direction, trigger_kind, status,\n stream, fetched, written, stats, started_at, finished_at)\n VALUES (\n v_tenant, v_connection, coalesce(v_connector, 'plugbank'), 'inbound', v_trigger, 'success',\n 'statement', v_fetched, v_imported,\n jsonb_strip_nulls(jsonb_build_object(\n 'duplicates', v_duplicates,\n 'period_from', v_from,\n 'period_to', v_to,\n 'triggered_by_user_id', auth.uid())),\n v_started, now())\n RETURNING id INTO v_run;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sync_run_id', v_run,\n 'fetched', v_fetched,\n 'imported', v_imported,\n 'duplicates', v_duplicates);\nEND;\n$$;\n\n\n-- ── trigger ─────────────────────────────────────────────────────────────\n\nCREATE TRIGGER plg_financial_movements_emit_imported AFTER INSERT ON public.plg_financial_movements FOR EACH ROW EXECUTE FUNCTION public.plg_bank_emit_movement_imported();\n\n\n-- ── acl ─────────────────────────────────────────────────────────────────\n\nREVOKE ALL ON FUNCTION public.plg_bank_emit_movement_imported() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_bank_emit_movement_imported() TO authenticated;\nGRANT ALL ON FUNCTION public.plg_bank_emit_movement_imported() TO service_role;\n\nREVOKE ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) TO service_role;\n\n\n-- ── comment ─────────────────────────────────────────────────────────────\n\nCOMMENT 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.';\n\n\n-- ── default privileges: restore ─────────────────────────────────────────\n-- Put back, so the NEXT thing installed inherits what the chain had.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set — %', $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEND $dp$;\n`\n\nexport const MIGRATION_001_THE_IMPORT_RPC_NAMES_REAL_TABLES = `-- ==========================================================================\n-- 001_the_import_rpc_names_real_tables — plugbank_import_movements\n-- stops reaching through 141's bridge (#352).\n--\n-- 107 renamed the connector spine out of the \\`plg_\\` prefix — the prefix means\n-- \"a plugin owns this\", and the connector framework's tables are the BASE's.\n-- 141 left \\`plg_connections\\` and \\`plg_sync_runs\\` answering as views so callers\n-- could move without a synchronised deploy. This function never moved, and it\n-- is not alone: \\`plg_connections\\` had 13 callers and \\`connections\\` had none.\n-- The bridge became the road.\n--\n-- The body is 000_baseline's, unchanged except for the three names. It is not\n-- an edit of that file — an applied migration is immutable and the ledger\n-- treats drift as a hard stop — it is a CREATE OR REPLACE that lands after it.\n--\n-- Why this plugin has to carry its own: the function is banking-br's, and the\n-- views it stops using are the spine's. The spine runs FIRST, so a spine file\n-- cannot drop a view that a plugin applied later still reads. Each package\n-- moves its own callers; the aliases go once nobody is left.\n--\n-- Replay-safe: CREATE OR REPLACE, same signature, same grants (which 000 set\n-- and this does not touch).\n-- ==========================================================================\n\nCREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_connection uuid;\n v_instance text;\n v_account uuid;\n v_connector text;\n v_trigger text;\n v_from date;\n v_to date;\n v_started timestamptz := now();\n v_line jsonb;\n v_external text;\n v_amount numeric(14,2);\n v_date date;\n v_fetched int := 0;\n v_imported int := 0;\n v_duplicates int := 0;\n v_inserted uuid;\n v_run uuid;\nBEGIN\n v_connection := nullif(btrim(p_payload->>'integration_id'), '')::uuid;\n IF v_connection IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n SELECT c.tenant_id, c.instance_key, c.connector_id\n INTO v_tenant, v_instance, v_connector\n FROM public.connections c\n WHERE c.id = v_connection AND c.active;\n\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: no active connection %', v_connection\n USING ERRCODE = 'no_data_found';\n END IF;\n\n -- The account this connection is for. \\`instance_key\\` is text because it also\n -- carries things that are not uuids for other connectors (a Bling company, a\n -- gateway instance); here it is the bank account, and '' is the account-less\n -- default that \\`bank_account_id IS NULL\\` used to mean.\n v_account := nullif(btrim(v_instance), '')::uuid;\n\n -- SECURITY DEFINER bypasses RLS, so the membership check the policy would\n -- have made has to happen here.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN\n RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Refusing beats ignoring: a caller that believes it is importing into\n -- another tenant must be told, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL\n AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Same rule as the tenant, one level down: the account is the connection's,\n -- and a payload naming a different one is a caller importing into the wrong\n -- ledger account. 002 let the payload override it silently — with one\n -- connection per account that override is how two accounts get mixed.\n IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL\n AND (p_payload->>'bank_account_id')::uuid IS DISTINCT FROM v_account THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another bank account; the account comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n v_from := nullif(btrim(p_payload->>'period_from'), '')::date;\n v_to := nullif(btrim(p_payload->>'period_to'), '')::date;\n v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));\n IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN\n RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))\n LOOP\n v_fetched := v_fetched + 1;\n v_external := nullif(btrim(v_line->>'external_id'), '');\n v_date := nullif(btrim(v_line->>'date'), '')::date;\n v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));\n\n -- A line with no provider id cannot be recognised on re-delivery, so it\n -- would import again on every run. Skipping it is the lesser wrong.\n IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN\n CONTINUE;\n END IF;\n\n INSERT INTO public.plg_financial_movements (\n tenant_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, bank_account_id, notes, external_id, external_source)\n VALUES (\n v_tenant,\n CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,\n 'payment', v_amount, v_amount, 'paid',\n v_date, v_date, v_account,\n nullif(btrim(v_line->>'description'), ''),\n v_external, coalesce(v_connector, 'plugbank'))\n ON CONFLICT (tenant_id, external_source, external_id)\n WHERE external_id IS NOT NULL AND external_source IS NOT NULL\n DO NOTHING\n RETURNING id INTO v_inserted;\n\n IF v_inserted IS NULL THEN\n v_duplicates := v_duplicates + 1;\n ELSE\n v_imported := v_imported + 1;\n END IF;\n v_inserted := NULL;\n END LOOP;\n\n UPDATE public.connections\n SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()\n WHERE id = v_connection;\n\n -- \\`stats\\` is where the three facts a manual statement import has and a generic\n -- run does not go: the operator picked a period, a re-import of that period is\n -- mostly duplicates by design, and someone pressed the button. The panel reads\n -- all three back from here.\n INSERT INTO public.sync_runs (\n tenant_id, connection_id, connector_id, direction, trigger_kind, status,\n stream, fetched, written, stats, started_at, finished_at)\n VALUES (\n v_tenant, v_connection, coalesce(v_connector, 'plugbank'), 'inbound', v_trigger, 'success',\n 'statement', v_fetched, v_imported,\n jsonb_strip_nulls(jsonb_build_object(\n 'duplicates', v_duplicates,\n 'period_from', v_from,\n 'period_to', v_to,\n 'triggered_by_user_id', auth.uid())),\n v_started, now())\n RETURNING id INTO v_run;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sync_run_id', v_run,\n 'fetched', v_fetched,\n 'imported', v_imported,\n 'duplicates', v_duplicates);\nEND;\n$$;\nCOMMENT ON FUNCTION public.plugbank_import_movements(p_payload jsonb) IS\n '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.';\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"000_baseline\", sql: MIGRATION_000_BASELINE },\n { id: \"001_the_import_rpc_names_real_tables\", sql: MIGRATION_001_THE_IMPORT_RPC_NAMES_REAL_TABLES },\n]\n","// Open Banking plugin (Tecnospeed PlugBank) — public surface + manifest.\n//\n// The canonical PULL connector: the provider has never heard of Fayz, so an edge\n// function reaches out to it. Settings-only (no left-nav page) — it contributes\n// its connector into plugin-financial's settings → Integrations, and imported\n// lines are matched in Financeiro → Conciliação (enable the financial\n// `reconciliation` module).\nimport type { PluginManifest } from '@fayz-ai/core'\nimport { openBankingConnector } from './connectors/connectorDef'\nimport { buildBankingDomainEvents } from './lib/banking-events'\nimport { MIGRATIONS } from './migrations'\n\nexport { createOpenBankingProvider } from './data/supabase'\nexport { openBankingConnector } from './connectors/connectorDef'\nexport { buildBankingDomainEvents, MOVEMENT_IMPORTED_FIELDS } from './lib/banking-events'\nexport * from './schema'\nexport * from './types'\n\n// Addon plugin: contributes the PlugBank connector into its host\n// (plugin-financial). It appears in Financial settings → Integrations — no\n// top-level settings tab of its own.\nexport function createOpenBankingPlugin(): PluginManifest {\n return {\n id: 'openbanking',\n name: 'Open Banking',\n icon: 'Landmark',\n version: '0.9.1',\n apiVersion: 1,\n scope: 'addon',\n defaultEnabled: true,\n dependencies: ['financial'],\n navigation: [],\n routes: [],\n connectors: [openBankingConnector],\n // The fact the import produces. Declared HERE and not on plugin-financial\n // because the trigger that emits it ships in this plugin's 002: a host\n // advertising an event that only exists when an addon is installed is the\n // same broken promise as an automation the ingress never delivers.\n events: buildBankingDomainEvents(),\n // `emitsFrom` is deliberately absent. It generates\n // `registry.<entity>.<action>` keys for every row change, and \"imported\" is\n // not a row action — it is an INSERT carrying an external_source, which only\n // a purpose-built trigger can tell apart from a sale typed into the till.\n functions: [\n {\n slug: 'plugbank-sync',\n // Invoked from the browser by an authenticated operator picking a\n // period, so Supabase must check the caller's JWT. It is not reached\n // through the Fayz ingress — nothing in Fayz wakes a pull connector —\n // which is what lets verifyJwt stay true.\n ingress: 'none',\n verifyJwt: true,\n // NAMES only. PLUGBANK_BASE_URL selects production vs sandbox; the\n // per-tenant token and account CNPJ are NOT here, because a Supabase\n // function secret is per-POOL and a pool serves N tenants with N\n // different bank accounts. Those come from the Fayz platform per call.\n secrets: ['PLUGBANK_BASE_URL', 'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],\n // What this connector's credential may touch. Direct table access is\n // not on the list on purpose (2026-08-11 ADR): the import goes through\n // the RPC, which takes the tenant from the integration row instead of\n // from the request body.\n grants: ['plugbank_import_movements'],\n },\n ],\n declaredRpcs: [\n {\n name: 'plugbank_import_movements',\n kind: 'write',\n description:\n 'Import bank-statement lines into the financial ledger. Idempotent on (tenant, provider, external id); the tenant comes from the integration row, never from the payload.',\n audits: false,\n },\n ],\n // This plugin creates no table. 003 retired the two it had; what is left is\n // the RPC that is the import's one door, and the trigger that turns an\n // imported movement into a fact. The connection and its run history are rows\n // of connections / sync_runs, owned by packages/db.\n // One baseline per unit since #338 — read from the generated barrel\n // rather than listed here, so regenerating the SQL never needs a\n // manifest edit to match.\n migrations: MIGRATIONS.map((entry) => ({\n id: `openbanking-${entry.id}`,\n version: '1.0.0',\n sql: entry.sql,\n description:\n '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/.',\n })),\n diagnostics: [\n {\n id: 'openbanking-ledger-ready',\n description:\n '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.',\n requires: {\n tables: ['connections', 'sync_runs', 'plg_financial_movements'],\n rpcs: ['plugbank_import_movements', 'plg_emit_event'],\n },\n level: 'error',\n },\n ],\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
- export declare const MIGRATION_002_CREDENTIAL_AND_FACTS = "-- ============================================================================\n-- plugin-banking-br 002: the credential leaves the tenant database, the import\n-- gets a door it cannot be routed around, and the import becomes a FACT.\n--\n-- \u00A71 credential drop api_token + cnpj from public.bank_integrations\n-- \u00A72 sync runs bank_integration_sync_log speaks the SyncRun vocabulary\n-- \u00A73 import plugbank_import_movements \u2014 the tenant-safe door\n-- \u00A74 facts financial.movement.imported\n--\n-- Additive and idempotent: no table is created, no policy is rewritten, and the\n-- tenant RLS from 001 already covers every column added here.\n-- ============================================================================\n\n-- \u00A71 \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\n-- THE EXPOSURE THIS CLOSES: 001 stored `api_token text` and `cnpj text` on\n-- public.bank_integrations. That table is read from the browser with the\n-- publishable key under tenant RLS, so the bank credential was readable by ANY\n-- authenticated member of the tenant; and a pool is one Supabase project serving\n-- N tenants (beauty-saas and espaco-renova-rio share one), so anything running\n-- there with service_role could read every clinic's bank token at once.\n--\n-- PlugBank differs from RankLayer, which dropped its key with nothing to replace\n-- it: PlugBank is a PULL connector and genuinely needs a credential at fetch\n-- time. It just must not be THIS copy. The destination is the platform's\n-- encrypted ConnectorConnection store, redeemed per call (FAY-1360 + FAY-1389).\n-- NEITHER HAS HAPPENED YET: as of 2026-08-12 the live pool still holds the\n-- token in this column, this migration is unapplied there, and the deployed\n-- plugbank-sync (v6) still reads it. The FAY-1360 rollout must register the\n-- credential at the platform BEFORE this drop runs on that pool \u2014 otherwise\n-- what is removed here is the only copy, and the connection dies.\n--\n-- `cnpj` goes with it rather than surviving as a masked remnant. It is not\n-- decoration: paired with the token it is what identifies the account at the\n-- provider (`/v1/extrato?cnpj=\u2026`), which makes it half of the credential and\n-- therefore the platform's to hold. And nothing on this side reads it \u2014 the\n-- live connector panel never renders it, and the unified connect form starts\n-- every field empty rather than pre-filling from the row. A masked last-N would\n-- be a column added to serve a screen that does not exist, on data that\n-- identifies a real company.\n--\n-- ROTATE any token a pool already stored. DROP COLUMN is a catalog change: the\n-- old values stay in the heap until the next table rewrite.\n\nDO $$\nBEGIN\n IF to_regclass('public.bank_integrations') IS NOT NULL THEN\n ALTER TABLE public.bank_integrations DROP COLUMN IF EXISTS api_token;\n ALTER TABLE public.bank_integrations DROP COLUMN IF EXISTS cnpj;\n END IF;\nEND $$;\n\n-- \u00A72 \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\n-- bank_integration_sync_log already IS a SyncRun: one row per execution, with\n-- status/fetched/written/error. It gains the four facts the shared vocabulary\n-- has and it lacked, rather than being replaced by a second table beside it \u2014\n-- two audit trails for one import is how \"the history is wrong\" starts.\n--\n-- The count columns keep their names. `transactions_fetched`/`transactions_\n-- imported` map to SyncRun's `fetched`/`written` in the data provider, because\n-- renaming them would break the deployed edge function's INSERT the moment this\n-- migration lands, and a rename that has to be choreographed across two\n-- repositories is not worth a nicer column name.\nDO $$\nBEGIN\n IF to_regclass('public.bank_integration_sync_log') IS NULL THEN RETURN; END IF;\n\n -- Which provider produced the run. One row per execution says nothing about\n -- who ran it once a second bank connector shares the table.\n ALTER TABLE public.bank_integration_sync_log\n ADD COLUMN IF NOT EXISTS provider text NOT NULL DEFAULT 'plugbank';\n -- Inbound: we reach the bank, never the other way round.\n ALTER TABLE public.bank_integration_sync_log\n ADD COLUMN IF NOT EXISTS direction text NOT NULL DEFAULT 'inbound';\n -- `trigger` is a reserved word; the column is trigger_kind and the TypeScript\n -- side reads it as SyncRun.trigger.\n ALTER TABLE public.bank_integration_sync_log\n ADD COLUMN IF NOT EXISTS trigger_kind text NOT NULL DEFAULT 'manual';\n ALTER TABLE public.bank_integration_sync_log\n ADD COLUMN IF NOT EXISTS started_at timestamptz;\n ALTER TABLE public.bank_integration_sync_log\n ADD COLUMN IF NOT EXISTS finished_at timestamptz;\n\n COMMENT ON COLUMN public.bank_integration_sync_log.trigger_kind IS\n 'SyncRun.trigger \u2014 on-write | scheduled | manual | webhook. Named _kind because trigger is reserved.';\nEND $$;\n\n-- \u00A73 \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\n-- The one door the statement import passes through.\n--\n-- Today the edge function inserts into plg_financial_movements directly with\n-- service_role, and takes `integrationId` from the request body without ever\n-- comparing it to the caller \u2014 so any authenticated member of ANY tenant in the\n-- pool can name another tenant's integration and write into that tenant's\n-- ledger. A guard in the function would be a guard the next caller routes\n-- around; the database is the only chokepoint that cannot be.\n--\n-- The tenant is taken from the INTEGRATION ROW, never from the payload, and a\n-- caller presenting a user credential must be a member of it. A caller with no\n-- auth.uid() is the server (service_role, a migration, the future tenant-scoped\n-- token) and is exempt \u2014 the same exemption plg_emit_event makes, for the same\n-- reason: it legitimately writes for any tenant. When FAY-1372 mints the\n-- tenant-scoped credential, that exemption stops applying to connector code and\n-- this function needs no change.\nCREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public\nAS $fn$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_integration uuid;\n v_account uuid;\n v_provider text;\n v_trigger text;\n v_from date;\n v_to date;\n v_started timestamptz := now();\n v_line jsonb;\n v_external text;\n v_amount numeric(14,2);\n v_date date;\n v_fetched int := 0;\n v_imported int := 0;\n v_duplicates int := 0;\n v_inserted uuid;\n v_run uuid;\nBEGIN\n v_integration := nullif(btrim(p_payload->>'integration_id'), '')::uuid;\n IF v_integration IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n SELECT bi.tenant_id, bi.bank_account_id, bi.provider\n INTO v_tenant, v_account, v_provider\n FROM public.bank_integrations bi\n WHERE bi.id = v_integration AND bi.active;\n\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: no active integration %', v_integration\n USING ERRCODE = 'no_data_found';\n END IF;\n\n -- SECURITY DEFINER bypasses RLS, so the membership check the policy would\n -- have made has to happen here.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN\n RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Refusing beats ignoring: a caller that believes it is importing into\n -- another tenant must be told, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL\n AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the integration'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL THEN\n v_account := (p_payload->>'bank_account_id')::uuid;\n END IF;\n\n v_from := nullif(btrim(p_payload->>'period_from'), '')::date;\n v_to := nullif(btrim(p_payload->>'period_to'), '')::date;\n v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));\n IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN\n RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))\n LOOP\n v_fetched := v_fetched + 1;\n v_external := nullif(btrim(v_line->>'external_id'), '');\n v_date := nullif(btrim(v_line->>'date'), '')::date;\n v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));\n\n -- A line with no provider id cannot be recognised on re-delivery, so it\n -- would import again on every run. Skipping it is the lesser wrong.\n IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN\n CONTINUE;\n END IF;\n\n -- Idempotency is the partial unique index from the financial plugin's\n -- 007_reconciliation.sql, not a SELECT-then-INSERT: two runs of the same\n -- window racing each other would both find nothing and both write.\n INSERT INTO public.plg_financial_movements (\n tenant_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, bank_account_id, notes, external_id, external_source)\n VALUES (\n v_tenant,\n CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,\n 'payment', v_amount, v_amount, 'paid',\n v_date, v_date, v_account,\n nullif(btrim(v_line->>'description'), ''),\n v_external, coalesce(v_provider, 'plugbank'))\n ON CONFLICT (tenant_id, external_source, external_id)\n WHERE external_id IS NOT NULL AND external_source IS NOT NULL\n DO NOTHING\n RETURNING id INTO v_inserted;\n\n IF v_inserted IS NULL THEN\n v_duplicates := v_duplicates + 1;\n ELSE\n v_imported := v_imported + 1;\n END IF;\n v_inserted := NULL;\n END LOOP;\n\n UPDATE public.bank_integrations SET last_sync_at = now(), updated_at = now()\n WHERE id = v_integration;\n\n INSERT INTO public.bank_integration_sync_log (\n tenant_id, bank_integration_id, bank_account_id, period_from, period_to,\n transactions_fetched, transactions_imported, duplicates, status,\n triggered_by_user_id, provider, direction, trigger_kind, started_at, finished_at)\n VALUES (\n v_tenant, v_integration, v_account, v_from, v_to,\n v_fetched, v_imported, v_duplicates, 'success',\n auth.uid(), coalesce(v_provider, 'plugbank'), 'inbound', v_trigger, v_started, now())\n RETURNING id INTO v_run;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sync_run_id', v_run,\n 'fetched', v_fetched,\n 'imported', v_imported,\n 'duplicates', v_duplicates);\nEND;\n$fn$;\n\nREVOKE ALL ON FUNCTION public.plugbank_import_movements(jsonb) FROM public, anon;\nGRANT EXECUTE ON FUNCTION public.plugbank_import_movements(jsonb) TO authenticated, service_role;\n\n-- \u00A74 \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\n-- One trigger on the ledger rather than an emit call inside the importer. The\n-- RPC above, the edge function still inserting directly, a future scheduled\n-- pull and a hand-run backfill all end up in the same row \u2014 so the fact exists\n-- for all four without any of them having to remember to say so. That is the\n-- entire reason plg_emit_event exists (packages/db/migrations/027_domain_events.sql).\n--\n-- It fires only when external_source is set, which is exactly what \"imported\"\n-- means: a movement that came from outside. An app-native movement typed into\n-- the cash register is not an import and must not wake a bank connector.\n--\n-- Provider-agnostic on purpose. `provider` is in the payload, so a second bank\n-- connector importing into the same ledger produces the same fact instead of a\n-- second vocabulary for the same event.\nCREATE OR REPLACE FUNCTION public.plg_bank_emit_movement_imported()\nRETURNS trigger\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public\nAS $fn$\nDECLARE\n v_payload jsonb;\nBEGIN\n IF NEW.external_source IS NULL OR NEW.external_id IS NULL THEN\n RETURN NULL;\n END IF;\n\n -- The contract every consumer reads. Not the row: a whole-row payload would\n -- put the ledger's internal bookkeeping inside every webhook by default.\n v_payload := jsonb_build_object(\n 'movement_id', NEW.id::text,\n 'provider', NEW.external_source,\n 'external_id', NEW.external_id,\n 'direction', NEW.direction,\n 'amount', NEW.amount,\n 'payment_date', NEW.payment_date,\n 'description', NEW.notes,\n 'bank_account_id', NEW.bank_account_id);\n\n BEGIN\n PERFORM public.plg_emit_event(\n 'financial.movement.imported', v_payload, 'financial_movement',\n NEW.id::text, NEW.tenant_id, NULL);\n EXCEPTION WHEN OTHERS THEN\n -- Observability must never be able to reject the write that caused it.\n RAISE WARNING '[plg_bank_emit_movement_imported] % on %: %', TG_OP, TG_TABLE_NAME, SQLERRM;\n END;\n\n RETURN NULL;\nEND;\n$fn$;\n\nREVOKE ALL ON FUNCTION public.plg_bank_emit_movement_imported() FROM public, anon;\n\n-- Guarded on the COLUMN, not just the table: the financial plugin owns both, and\n-- attaching this to a ledger without 007_reconciliation.sql would make every\n-- movement insert fail on an unknown field \u2014 an addon breaking its own host.\nDO $$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM information_schema.columns\n WHERE table_schema = 'public'\n AND table_name = 'plg_financial_movements'\n AND column_name = 'external_source'\n ) THEN\n DROP TRIGGER IF EXISTS plg_financial_movements_emit_imported ON public.plg_financial_movements;\n CREATE TRIGGER plg_financial_movements_emit_imported\n AFTER INSERT ON public.plg_financial_movements\n FOR EACH ROW EXECUTE FUNCTION public.plg_bank_emit_movement_imported();\n END IF;\nEND $$;\n";
2
- export declare const MIGRATION_003_ONTO_CONNECTIONS = "-- ============================================================================\n-- plugin-banking-br 003 \u2014 PlugBank stops owning tables.\n--\n-- Retires public.bank_integrations and public.bank_integration_sync_log. The\n-- connection is now a row of public.plg_connections and each import is a row of\n-- public.plg_sync_runs (packages/db 029). See FAY-1376.\n--\n-- This connector is the one that tests the shape, because it is the only one\n-- whose key was never (tenant, provider): 001 keyed on\n-- UNIQUE (tenant_id, bank_account_id, provider), and the provider filtered by\n-- bank_account_id. Two accounts of one clinic are two connections, and they must\n-- stay two. That is exactly what `instance_key` is:\n--\n-- bank_integrations.bank_account_id \u2192 plg_connections.instance_key\n-- (the account uuid as text; '' is the\n-- account-less default the panel uses,\n-- which is what `bank_account_id IS NULL`\n-- meant before)\n-- bank_integrations.provider \u2192 plg_connections.connector_id\n-- bank_integrations.environment \u2192 plg_connections.settings->>'environment'\n-- bank_integrations.active \u2192 plg_connections.active\n-- bank_integrations.last_sync_at \u2192 plg_connections.last_sync_at\n-- UNIQUE (tenant_id, bank_account_id, provider)\n-- \u2192 UNIQUE (tenant_id, connector_id, instance_key)\n--\n-- And the run, column by column \u2014 nothing the panel renders is lost:\n-- sync_log.bank_integration_id \u2192 plg_sync_runs.connection_id\n-- sync_log.transactions_fetched \u2192 plg_sync_runs.fetched\n-- sync_log.transactions_imported \u2192 plg_sync_runs.written\n-- sync_log.duplicates \u2192 plg_sync_runs.stats->>'duplicates'\n-- sync_log.period_from / _to \u2192 plg_sync_runs.stats->>'period_from' / '_to'\n-- sync_log.triggered_by_user_id \u2192 plg_sync_runs.stats->>'triggered_by_user_id'\n-- sync_log.error_message \u2192 plg_sync_runs.error\n-- sync_log.bank_account_id \u2192 dropped: it is the connection's\n-- instance_key, and a second copy is a\n-- second thing to disagree\n-- sync_log.provider \u2192 plg_sync_runs.connector_id\n-- sync_log.created_at \u2192 plg_sync_runs.started_at\n-- direction / trigger_kind / status / started_at / finished_at \u2192 same names\n--\n-- The count columns finally get the contract's names. 002 \u00A72 kept\n-- transactions_fetched/_imported because renaming them would have broken the\n-- deployed edge function's INSERT \u2014 but the function no longer inserts into a\n-- log at all: it calls plugbank_import_movements, which is the only writer, and\n-- the run is filed here. The choreography 002 was avoiding does not exist.\n--\n-- Nothing is in production, so this is a drop and not a data migration.\n-- ============================================================================\n\n-- \u00A71 \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\n-- Same name, same payload, same guarantees. `integration_id` keeps its key on\n-- the wire so the deployed plugbank-sync function needs no coordinated release;\n-- what changed is that the uuid it carries now names a plg_connections row.\n--\n-- Everything 002 \u00A73 established still holds and for the same reasons: the tenant\n-- comes from the CONNECTION ROW and never from the payload, a caller presenting\n-- a user credential must be a member of it, a caller with no auth.uid() is the\n-- server and is exempt, and idempotency is the partial unique index rather than\n-- a SELECT-then-INSERT that two racing runs would both pass.\n--\n-- One thing is new and is the point of the migration: the run is filed against\n-- `connection_id`, so a tenant with two bank accounts gets two histories instead\n-- of one interleaved list. plg_sync_runs carries no INSERT grant for\n-- `authenticated` \u2014 this SECURITY DEFINER function is the server-side writer the\n-- table's grants assume.\nCREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb)\nRETURNS jsonb\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = public\nAS $fn$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_connection uuid;\n v_instance text;\n v_account uuid;\n v_connector text;\n v_trigger text;\n v_from date;\n v_to date;\n v_started timestamptz := now();\n v_line jsonb;\n v_external text;\n v_amount numeric(14,2);\n v_date date;\n v_fetched int := 0;\n v_imported int := 0;\n v_duplicates int := 0;\n v_inserted uuid;\n v_run uuid;\nBEGIN\n v_connection := nullif(btrim(p_payload->>'integration_id'), '')::uuid;\n IF v_connection IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n SELECT c.tenant_id, c.instance_key, c.connector_id\n INTO v_tenant, v_instance, v_connector\n FROM public.plg_connections c\n WHERE c.id = v_connection AND c.active;\n\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: no active connection %', v_connection\n USING ERRCODE = 'no_data_found';\n END IF;\n\n -- The account this connection is for. `instance_key` is text because it also\n -- carries things that are not uuids for other connectors (a Bling company, a\n -- gateway instance); here it is the bank account, and '' is the account-less\n -- default that `bank_account_id IS NULL` used to mean.\n v_account := nullif(btrim(v_instance), '')::uuid;\n\n -- SECURITY DEFINER bypasses RLS, so the membership check the policy would\n -- have made has to happen here.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN\n RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Refusing beats ignoring: a caller that believes it is importing into\n -- another tenant must be told, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL\n AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Same rule as the tenant, one level down: the account is the connection's,\n -- and a payload naming a different one is a caller importing into the wrong\n -- ledger account. 002 let the payload override it silently \u2014 with one\n -- connection per account that override is how two accounts get mixed.\n IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL\n AND (p_payload->>'bank_account_id')::uuid IS DISTINCT FROM v_account THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another bank account; the account comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n v_from := nullif(btrim(p_payload->>'period_from'), '')::date;\n v_to := nullif(btrim(p_payload->>'period_to'), '')::date;\n v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));\n IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN\n RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))\n LOOP\n v_fetched := v_fetched + 1;\n v_external := nullif(btrim(v_line->>'external_id'), '');\n v_date := nullif(btrim(v_line->>'date'), '')::date;\n v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));\n\n -- A line with no provider id cannot be recognised on re-delivery, so it\n -- would import again on every run. Skipping it is the lesser wrong.\n IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN\n CONTINUE;\n END IF;\n\n INSERT INTO public.plg_financial_movements (\n tenant_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, bank_account_id, notes, external_id, external_source)\n VALUES (\n v_tenant,\n CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,\n 'payment', v_amount, v_amount, 'paid',\n v_date, v_date, v_account,\n nullif(btrim(v_line->>'description'), ''),\n v_external, coalesce(v_connector, 'plugbank'))\n ON CONFLICT (tenant_id, external_source, external_id)\n WHERE external_id IS NOT NULL AND external_source IS NOT NULL\n DO NOTHING\n RETURNING id INTO v_inserted;\n\n IF v_inserted IS NULL THEN\n v_duplicates := v_duplicates + 1;\n ELSE\n v_imported := v_imported + 1;\n END IF;\n v_inserted := NULL;\n END LOOP;\n\n UPDATE public.plg_connections\n SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()\n WHERE id = v_connection;\n\n -- `stats` is where the three facts a manual statement import has and a generic\n -- run does not go: the operator picked a period, a re-import of that period is\n -- mostly duplicates by design, and someone pressed the button. The panel reads\n -- all three back from here.\n INSERT INTO public.plg_sync_runs (\n tenant_id, connection_id, connector_id, direction, trigger_kind, status,\n stream, fetched, written, stats, started_at, finished_at)\n VALUES (\n v_tenant, v_connection, coalesce(v_connector, 'plugbank'), 'inbound', v_trigger, 'success',\n 'statement', v_fetched, v_imported,\n jsonb_strip_nulls(jsonb_build_object(\n 'duplicates', v_duplicates,\n 'period_from', v_from,\n 'period_to', v_to,\n 'triggered_by_user_id', auth.uid())),\n v_started, now())\n RETURNING id INTO v_run;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sync_run_id', v_run,\n 'fetched', v_fetched,\n 'imported', v_imported,\n 'duplicates', v_duplicates);\nEND;\n$fn$;\n\nREVOKE ALL ON FUNCTION public.plugbank_import_movements(jsonb) FROM public, anon;\nGRANT EXECUTE ON FUNCTION public.plugbank_import_movements(jsonb) TO authenticated, service_role;\n\nCOMMENT ON FUNCTION public.plugbank_import_movements(jsonb) IS\n 'Import bank-statement lines into the financial ledger and file the run in '\n 'plg_sync_runs. The tenant AND the bank account come from the plg_connections '\n 'row named by integration_id, never from the payload.';\n\n-- \u00A72 \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\n-- After the function above, so a pool is never left with the old door pointing\n-- at a table that no longer exists.\nDROP TABLE IF EXISTS public.bank_integration_sync_log CASCADE;\nDROP TABLE IF EXISTS public.bank_integrations CASCADE;\n";
1
+ export declare const MIGRATION_000_BASELINE = "-- ============================================================================\n-- plugins/plugin-banking-br/src/migrations/000_baseline.sql \u2014 what installing this provisions, as one file.\n--\n-- GENERATED by scripts/emit-unit-baselines.mjs (#338). Do not edit by hand:\n-- change the schema, regenerate, and let scripts/check-chain-equivalence.mjs\n-- prove the result still matches.\n--\n-- This replaced 2 migration files. They are kept, unapplied, in\n-- migrations.archive/ \u2014 a year of decisions is worth reading even when it is no\n-- longer worth replaying.\n--\n-- Object order is fixed rather than dependency-sorted: schemas, types,\n-- sequences, tables, defaults, functions, constraints, views, indexes, foreign\n-- keys, triggers, row security, policies, grants. Dependencies BETWEEN units\n-- are carried by the order packages/db/chain.json declares.\n-- ============================================================================\n\n-- A LANGUAGE sql function binds its body at CREATE time, and 570 functions\n-- sorted by name are not sorted by who calls whom. This is what lets them load\n-- in any order; every body is still compiled on first call, so a genuinely\n-- broken reference surfaces then rather than never. Session-scoped.\nSET check_function_bodies = false;\n\n\n\n-- \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\n-- Cleared BEFORE anything is created, so every object below gets the ACL the\n-- dump describes rather than the image's defaults on top of it. Restored at\n-- the end of this file.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON SEQUENCES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON FUNCTIONS FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public REVOKE ALL ON TABLES FROM anon, authenticated, service_role, fayz_connector$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\n\n-- \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\n\nCREATE FUNCTION public.plg_bank_emit_movement_imported() RETURNS trigger\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_payload jsonb;\nBEGIN\n IF NEW.external_source IS NULL OR NEW.external_id IS NULL THEN\n RETURN NULL;\n END IF;\n\n -- The contract every consumer reads. Not the row: a whole-row payload would\n -- put the ledger's internal bookkeeping inside every webhook by default.\n v_payload := jsonb_build_object(\n 'movement_id', NEW.id::text,\n 'provider', NEW.external_source,\n 'external_id', NEW.external_id,\n 'direction', NEW.direction,\n 'amount', NEW.amount,\n 'payment_date', NEW.payment_date,\n 'description', NEW.notes,\n 'bank_account_id', NEW.bank_account_id);\n\n BEGIN\n PERFORM public.plg_emit_event(\n 'financial.movement.imported', v_payload, 'financial_movement',\n NEW.id::text, NEW.tenant_id, NULL);\n EXCEPTION WHEN OTHERS THEN\n -- Observability must never be able to reject the write that caused it.\n RAISE WARNING '[plg_bank_emit_movement_imported] % on %: %', TG_OP, TG_TABLE_NAME, SQLERRM;\n END;\n\n RETURN NULL;\nEND;\n$$;\n\nCREATE FUNCTION public.plugbank_import_movements(p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_connection uuid;\n v_instance text;\n v_account uuid;\n v_connector text;\n v_trigger text;\n v_from date;\n v_to date;\n v_started timestamptz := now();\n v_line jsonb;\n v_external text;\n v_amount numeric(14,2);\n v_date date;\n v_fetched int := 0;\n v_imported int := 0;\n v_duplicates int := 0;\n v_inserted uuid;\n v_run uuid;\nBEGIN\n v_connection := nullif(btrim(p_payload->>'integration_id'), '')::uuid;\n IF v_connection IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n SELECT c.tenant_id, c.instance_key, c.connector_id\n INTO v_tenant, v_instance, v_connector\n FROM public.plg_connections c\n WHERE c.id = v_connection AND c.active;\n\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: no active connection %', v_connection\n USING ERRCODE = 'no_data_found';\n END IF;\n\n -- The account this connection is for. `instance_key` is text because it also\n -- carries things that are not uuids for other connectors (a Bling company, a\n -- gateway instance); here it is the bank account, and '' is the account-less\n -- default that `bank_account_id IS NULL` used to mean.\n v_account := nullif(btrim(v_instance), '')::uuid;\n\n -- SECURITY DEFINER bypasses RLS, so the membership check the policy would\n -- have made has to happen here.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN\n RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Refusing beats ignoring: a caller that believes it is importing into\n -- another tenant must be told, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL\n AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Same rule as the tenant, one level down: the account is the connection's,\n -- and a payload naming a different one is a caller importing into the wrong\n -- ledger account. 002 let the payload override it silently \u2014 with one\n -- connection per account that override is how two accounts get mixed.\n IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL\n AND (p_payload->>'bank_account_id')::uuid IS DISTINCT FROM v_account THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another bank account; the account comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n v_from := nullif(btrim(p_payload->>'period_from'), '')::date;\n v_to := nullif(btrim(p_payload->>'period_to'), '')::date;\n v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));\n IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN\n RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))\n LOOP\n v_fetched := v_fetched + 1;\n v_external := nullif(btrim(v_line->>'external_id'), '');\n v_date := nullif(btrim(v_line->>'date'), '')::date;\n v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));\n\n -- A line with no provider id cannot be recognised on re-delivery, so it\n -- would import again on every run. Skipping it is the lesser wrong.\n IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN\n CONTINUE;\n END IF;\n\n INSERT INTO public.plg_financial_movements (\n tenant_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, bank_account_id, notes, external_id, external_source)\n VALUES (\n v_tenant,\n CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,\n 'payment', v_amount, v_amount, 'paid',\n v_date, v_date, v_account,\n nullif(btrim(v_line->>'description'), ''),\n v_external, coalesce(v_connector, 'plugbank'))\n ON CONFLICT (tenant_id, external_source, external_id)\n WHERE external_id IS NOT NULL AND external_source IS NOT NULL\n DO NOTHING\n RETURNING id INTO v_inserted;\n\n IF v_inserted IS NULL THEN\n v_duplicates := v_duplicates + 1;\n ELSE\n v_imported := v_imported + 1;\n END IF;\n v_inserted := NULL;\n END LOOP;\n\n UPDATE public.plg_connections\n SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()\n WHERE id = v_connection;\n\n -- `stats` is where the three facts a manual statement import has and a generic\n -- run does not go: the operator picked a period, a re-import of that period is\n -- mostly duplicates by design, and someone pressed the button. The panel reads\n -- all three back from here.\n INSERT INTO public.plg_sync_runs (\n tenant_id, connection_id, connector_id, direction, trigger_kind, status,\n stream, fetched, written, stats, started_at, finished_at)\n VALUES (\n v_tenant, v_connection, coalesce(v_connector, 'plugbank'), 'inbound', v_trigger, 'success',\n 'statement', v_fetched, v_imported,\n jsonb_strip_nulls(jsonb_build_object(\n 'duplicates', v_duplicates,\n 'period_from', v_from,\n 'period_to', v_to,\n 'triggered_by_user_id', auth.uid())),\n v_started, now())\n RETURNING id INTO v_run;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sync_run_id', v_run,\n 'fetched', v_fetched,\n 'imported', v_imported,\n 'duplicates', v_duplicates);\nEND;\n$$;\n\n\n-- \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\n\nCREATE TRIGGER plg_financial_movements_emit_imported AFTER INSERT ON public.plg_financial_movements FOR EACH ROW EXECUTE FUNCTION public.plg_bank_emit_movement_imported();\n\n\n-- \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\n\nREVOKE ALL ON FUNCTION public.plg_bank_emit_movement_imported() FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plg_bank_emit_movement_imported() TO authenticated;\nGRANT ALL ON FUNCTION public.plg_bank_emit_movement_imported() TO service_role;\n\nREVOKE ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) FROM PUBLIC;\nGRANT ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) TO authenticated;\nGRANT ALL ON FUNCTION public.plugbank_import_movements(p_payload jsonb) TO service_role;\n\n\n-- \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\n\nCOMMENT 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.';\n\n\n-- \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\n-- Put back, so the NEXT thing installed inherits what the chain had.\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON SEQUENCES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON FUNCTIONS TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT MAINTAIN ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO postgres$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO anon$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO authenticated$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin IN SCHEMA public GRANT ALL ON TABLES TO service_role$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n 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$;\nEND $dp$;\n\nDO $dp$ BEGIN\n EXECUTE $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEXCEPTION WHEN insufficient_privilege OR undefined_object THEN\n RAISE NOTICE 'baseline: not ours to set \u2014 %', $stmt$--\n-- PostgreSQL database dump complete\n--$stmt$;\nEND $dp$;\n";
2
+ export declare const MIGRATION_001_THE_IMPORT_RPC_NAMES_REAL_TABLES = "-- ==========================================================================\n-- 001_the_import_rpc_names_real_tables \u2014 plugbank_import_movements\n-- stops reaching through 141's bridge (#352).\n--\n-- 107 renamed the connector spine out of the `plg_` prefix \u2014 the prefix means\n-- \"a plugin owns this\", and the connector framework's tables are the BASE's.\n-- 141 left `plg_connections` and `plg_sync_runs` answering as views so callers\n-- could move without a synchronised deploy. This function never moved, and it\n-- is not alone: `plg_connections` had 13 callers and `connections` had none.\n-- The bridge became the road.\n--\n-- The body is 000_baseline's, unchanged except for the three names. It is not\n-- an edit of that file \u2014 an applied migration is immutable and the ledger\n-- treats drift as a hard stop \u2014 it is a CREATE OR REPLACE that lands after it.\n--\n-- Why this plugin has to carry its own: the function is banking-br's, and the\n-- views it stops using are the spine's. The spine runs FIRST, so a spine file\n-- cannot drop a view that a plugin applied later still reads. Each package\n-- moves its own callers; the aliases go once nobody is left.\n--\n-- Replay-safe: CREATE OR REPLACE, same signature, same grants (which 000 set\n-- and this does not touch).\n-- ==========================================================================\n\nCREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb) RETURNS jsonb\n LANGUAGE plpgsql SECURITY DEFINER\n SET search_path TO 'public'\n AS $$\nDECLARE\n v_tenants uuid[];\n v_tenant uuid;\n v_connection uuid;\n v_instance text;\n v_account uuid;\n v_connector text;\n v_trigger text;\n v_from date;\n v_to date;\n v_started timestamptz := now();\n v_line jsonb;\n v_external text;\n v_amount numeric(14,2);\n v_date date;\n v_fetched int := 0;\n v_imported int := 0;\n v_duplicates int := 0;\n v_inserted uuid;\n v_run uuid;\nBEGIN\n v_connection := nullif(btrim(p_payload->>'integration_id'), '')::uuid;\n IF v_connection IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n SELECT c.tenant_id, c.instance_key, c.connector_id\n INTO v_tenant, v_instance, v_connector\n FROM public.connections c\n WHERE c.id = v_connection AND c.active;\n\n IF v_tenant IS NULL THEN\n RAISE EXCEPTION 'plugbank_import_movements: no active connection %', v_connection\n USING ERRCODE = 'no_data_found';\n END IF;\n\n -- The account this connection is for. `instance_key` is text because it also\n -- carries things that are not uuids for other connectors (a Bling company, a\n -- gateway instance); here it is the bank account, and '' is the account-less\n -- default that `bank_account_id IS NULL` used to mean.\n v_account := nullif(btrim(v_instance), '')::uuid;\n\n -- SECURITY DEFINER bypasses RLS, so the membership check the policy would\n -- have made has to happen here.\n SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;\n IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN\n RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Refusing beats ignoring: a caller that believes it is importing into\n -- another tenant must be told, not silently redirected into this one.\n IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL\n AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n -- Same rule as the tenant, one level down: the account is the connection's,\n -- and a payload naming a different one is a caller importing into the wrong\n -- ledger account. 002 let the payload override it silently \u2014 with one\n -- connection per account that override is how two accounts get mixed.\n IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL\n AND (p_payload->>'bank_account_id')::uuid IS DISTINCT FROM v_account THEN\n RAISE EXCEPTION 'plugbank_import_movements: payload names another bank account; the account comes from the connection'\n USING ERRCODE = 'insufficient_privilege';\n END IF;\n\n v_from := nullif(btrim(p_payload->>'period_from'), '')::date;\n v_to := nullif(btrim(p_payload->>'period_to'), '')::date;\n v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));\n IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN\n RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger\n USING ERRCODE = 'invalid_parameter_value';\n END IF;\n\n FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))\n LOOP\n v_fetched := v_fetched + 1;\n v_external := nullif(btrim(v_line->>'external_id'), '');\n v_date := nullif(btrim(v_line->>'date'), '')::date;\n v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));\n\n -- A line with no provider id cannot be recognised on re-delivery, so it\n -- would import again on every run. Skipping it is the lesser wrong.\n IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN\n CONTINUE;\n END IF;\n\n INSERT INTO public.plg_financial_movements (\n tenant_id, direction, movement_kind, amount, paid_amount, status,\n due_date, payment_date, bank_account_id, notes, external_id, external_source)\n VALUES (\n v_tenant,\n CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,\n 'payment', v_amount, v_amount, 'paid',\n v_date, v_date, v_account,\n nullif(btrim(v_line->>'description'), ''),\n v_external, coalesce(v_connector, 'plugbank'))\n ON CONFLICT (tenant_id, external_source, external_id)\n WHERE external_id IS NOT NULL AND external_source IS NOT NULL\n DO NOTHING\n RETURNING id INTO v_inserted;\n\n IF v_inserted IS NULL THEN\n v_duplicates := v_duplicates + 1;\n ELSE\n v_imported := v_imported + 1;\n END IF;\n v_inserted := NULL;\n END LOOP;\n\n UPDATE public.connections\n SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()\n WHERE id = v_connection;\n\n -- `stats` is where the three facts a manual statement import has and a generic\n -- run does not go: the operator picked a period, a re-import of that period is\n -- mostly duplicates by design, and someone pressed the button. The panel reads\n -- all three back from here.\n INSERT INTO public.sync_runs (\n tenant_id, connection_id, connector_id, direction, trigger_kind, status,\n stream, fetched, written, stats, started_at, finished_at)\n VALUES (\n v_tenant, v_connection, coalesce(v_connector, 'plugbank'), 'inbound', v_trigger, 'success',\n 'statement', v_fetched, v_imported,\n jsonb_strip_nulls(jsonb_build_object(\n 'duplicates', v_duplicates,\n 'period_from', v_from,\n 'period_to', v_to,\n 'triggered_by_user_id', auth.uid())),\n v_started, now())\n RETURNING id INTO v_run;\n\n RETURN jsonb_build_object(\n 'ok', true,\n 'sync_run_id', v_run,\n 'fetched', v_fetched,\n 'imported', v_imported,\n 'duplicates', v_duplicates);\nEND;\n$$;\nCOMMENT ON FUNCTION public.plugbank_import_movements(p_payload jsonb) IS\n '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.';\n";
3
3
  export declare const MIGRATIONS: Array<{
4
4
  id: string;
5
5
  sql: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,kCAAkC,47eAoT9C,CAAA;AAED,eAAO,MAAM,8BAA8B,olXAoO1C,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAGzD,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,u8uBAyalC,CAAA;AAED,eAAO,MAAM,8CAA8C,4vPA6K1D,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAGzD,CAAA"}
package/dist/types.d.ts CHANGED
@@ -19,7 +19,7 @@ export interface SaveIntegrationInput {
19
19
  * and no generic run has: the operator picked a period, and a re-import of that
20
20
  * period is mostly duplicates by design.
21
21
  *
22
- * They are not columns. They are read out of `plg_sync_runs.stats`, which is the
22
+ * They are not columns. They are read out of `sync_runs.stats`, which is the
23
23
  * jsonb the generic run carries so a connector's own counters do not each become
24
24
  * a column on a shared table. The mapping lives in ../data/supabase.ts.
25
25
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fayz-ai/plugin-banking-br",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "description": "[experimental] Fayz SDK — Brazilian open-banking connector (Tecnospeed PlugBank) feeding the financial ledger",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,9 +25,9 @@
25
25
  "react-dom": "^18.0.0 || ^19.0.0"
26
26
  },
27
27
  "dependencies": {
28
- "@fayz-ai/core": "^0.18.0",
29
- "@fayz-ai/db": "^0.13.0",
30
- "@fayz-ai/ui": "^0.18.0"
28
+ "@fayz-ai/core": "^0.21.0",
29
+ "@fayz-ai/db": "^0.14.0",
30
+ "@fayz-ai/ui": "^0.21.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/react": "^18.3.0",