@fayz-ai/plugin-banking-br 0.9.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -11
- package/dist/connectors/connectorDef.d.ts +1 -1
- package/dist/connectors/connectorDef.d.ts.map +1 -1
- package/dist/data/supabase.d.ts +14 -8
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +839 -127
- package/dist/index.js.map +1 -1
- package/dist/lib/banking-events.d.ts +13 -0
- package/dist/lib/banking-events.d.ts.map +1 -0
- package/dist/migrations/index.d.ts +2 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/schema/index.d.ts +1 -422
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema.cjs +0 -36
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.js +0 -34
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +14 -14
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -6
- package/dist/settings/BankIntegrationSettings.d.ts +0 -3
- package/dist/settings/BankIntegrationSettings.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,62 +1,81 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
|
-
import { Download, Loader2
|
|
2
|
+
import { Download, Loader2 } from 'lucide-react';
|
|
3
3
|
import { DatePicker, Button, toast } from '@fayz-ai/ui';
|
|
4
|
-
import {
|
|
4
|
+
import { connectionStatus, createConnectionStore, getSupabaseClientOptional } from '@fayz-ai/core';
|
|
5
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
-
import { pgTable, timestamps, timestamp, boolean, text, uuid, tenantId, createdAt, integer, date } from '@fayz-ai/db';
|
|
7
6
|
|
|
8
7
|
// src/connectors/connectorDef.tsx
|
|
9
8
|
var FUNCTION = "plugbank-sync";
|
|
9
|
+
var PLUGBANK_CONNECTOR_ID = "plugbank";
|
|
10
|
+
var SETTINGS_KEYS = ["environment"];
|
|
11
|
+
function storeFor(bankAccountId) {
|
|
12
|
+
return createConnectionStore(PLUGBANK_CONNECTOR_ID, {
|
|
13
|
+
settingsKeys: SETTINGS_KEYS,
|
|
14
|
+
instanceKey: bankAccountId ?? ""
|
|
15
|
+
});
|
|
16
|
+
}
|
|
10
17
|
function sb() {
|
|
11
18
|
const supabase = getSupabaseClientOptional();
|
|
12
19
|
if (!supabase) throw new Error("Supabase not initialized");
|
|
13
20
|
return supabase;
|
|
14
21
|
}
|
|
15
|
-
function
|
|
22
|
+
function toIntegration(connection) {
|
|
23
|
+
return {
|
|
24
|
+
id: connection.id ?? "",
|
|
25
|
+
provider: connection.connectorId,
|
|
26
|
+
// Back to `undefined` for the default account, so callers that pass this
|
|
27
|
+
// value straight back into getIntegration() address the same row.
|
|
28
|
+
bankAccountId: connection.instanceKey || void 0,
|
|
29
|
+
environment: connection.settings?.environment ?? "production",
|
|
30
|
+
active: connection.active,
|
|
31
|
+
lastSyncAt: connection.lastSyncAt ?? void 0
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function toSyncRun(run) {
|
|
35
|
+
const stats = run.stats ?? {};
|
|
16
36
|
return {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
...run,
|
|
38
|
+
id: run.id,
|
|
39
|
+
connectionId: run.connectionId,
|
|
40
|
+
connectorId: run.connectorId,
|
|
41
|
+
direction: run.direction,
|
|
42
|
+
triggerKind: run.triggerKind,
|
|
43
|
+
status: run.status,
|
|
44
|
+
fetched: run.fetched ?? 0,
|
|
45
|
+
written: run.written ?? 0,
|
|
46
|
+
error: run.error ?? void 0,
|
|
47
|
+
startedAt: run.startedAt,
|
|
48
|
+
finishedAt: run.finishedAt ?? void 0,
|
|
49
|
+
periodFrom: stats.period_from ?? void 0,
|
|
50
|
+
periodTo: stats.period_to ?? void 0,
|
|
51
|
+
duplicates: Number(stats.duplicates ?? 0),
|
|
52
|
+
// plg_sync_runs has no created_at: a run is identified by when it STARTED,
|
|
53
|
+
// which is what the old created_at recorded anyway (the row was inserted at
|
|
54
|
+
// the end, so the two disagreed by the length of the import).
|
|
55
|
+
createdAt: run.startedAt ?? ""
|
|
25
56
|
};
|
|
26
57
|
}
|
|
27
58
|
function createOpenBankingProvider() {
|
|
28
59
|
return {
|
|
29
60
|
async getIntegration(bankAccountId) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
qb = bankAccountId ? qb.eq("bank_account_id", bankAccountId) : qb.is("bank_account_id", null);
|
|
33
|
-
const { data } = await qb.limit(1).maybeSingle();
|
|
34
|
-
return data ? rowToIntegration(data) : null;
|
|
61
|
+
const connection = await storeFor(bankAccountId).get();
|
|
62
|
+
return connection ? toIntegration(connection) : null;
|
|
35
63
|
},
|
|
36
64
|
async saveIntegration(input) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
tenant_id: tenantId2,
|
|
41
|
-
provider: "plugbank",
|
|
42
|
-
bank_account_id: input.bankAccountId ?? null,
|
|
43
|
-
api_token: input.apiToken,
|
|
44
|
-
cnpj: input.cnpj,
|
|
45
|
-
environment: input.environment ?? "production",
|
|
46
|
-
active: true,
|
|
47
|
-
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
48
|
-
};
|
|
49
|
-
const { data, error } = await supabase.from("bank_integrations").upsert(input.id ? { id: input.id, ...row } : row, { onConflict: "tenant_id,bank_account_id,provider" }).select().single();
|
|
50
|
-
if (error) throw error;
|
|
51
|
-
return rowToIntegration(data);
|
|
52
|
-
},
|
|
53
|
-
async testConnection(input) {
|
|
54
|
-
const supabase = sb();
|
|
55
|
-
const { data, error } = await supabase.functions.invoke(FUNCTION, {
|
|
56
|
-
body: { action: "test_connection", apiToken: input.apiToken, cnpj: input.cnpj, environment: input.environment ?? "production" }
|
|
65
|
+
const saved = await storeFor(input?.bankAccountId).save({
|
|
66
|
+
settings: { environment: input?.environment ?? "production" },
|
|
67
|
+
active: true
|
|
57
68
|
});
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
return toIntegration(saved);
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Scoped to ONE account, where the old provider deleted every plugbank row
|
|
73
|
+
* of the tenant. That breadth was invisible while the panel only ever drove
|
|
74
|
+
* the default account; with two accounts it means disconnecting Itaú also
|
|
75
|
+
* silently kills Banco do Brasil. The narrow verb is the correct one.
|
|
76
|
+
*/
|
|
77
|
+
async disconnect(bankAccountId) {
|
|
78
|
+
await storeFor(bankAccountId).disconnect();
|
|
60
79
|
},
|
|
61
80
|
async fetchStatement(input) {
|
|
62
81
|
const supabase = sb();
|
|
@@ -71,6 +90,10 @@ function createOpenBankingProvider() {
|
|
|
71
90
|
const { data, error } = await supabase.functions.invoke(FUNCTION, {
|
|
72
91
|
body: {
|
|
73
92
|
action: "import_transactions",
|
|
93
|
+
// Still `integrationId` on the wire: it now carries a plg_connections
|
|
94
|
+
// id, and keeping the key means the deployed function needs no
|
|
95
|
+
// coordinated release. The RPC takes the tenant AND the bank account
|
|
96
|
+
// from that row, so neither can be steered from here.
|
|
74
97
|
integrationId: input.integrationId,
|
|
75
98
|
bankAccountId: input.bankAccountId,
|
|
76
99
|
from: input.from,
|
|
@@ -81,24 +104,24 @@ function createOpenBankingProvider() {
|
|
|
81
104
|
if (error) throw new Error(error.message);
|
|
82
105
|
return data;
|
|
83
106
|
},
|
|
84
|
-
async
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
return (data ?? []).map((r) => ({
|
|
88
|
-
id: r.id,
|
|
89
|
-
periodFrom: r.period_from ?? void 0,
|
|
90
|
-
periodTo: r.period_to ?? void 0,
|
|
91
|
-
transactionsFetched: r.transactions_fetched ?? 0,
|
|
92
|
-
transactionsImported: r.transactions_imported ?? 0,
|
|
93
|
-
duplicates: r.duplicates ?? 0,
|
|
94
|
-
status: r.status,
|
|
95
|
-
errorMessage: r.error_message ?? void 0,
|
|
96
|
-
createdAt: r.created_at
|
|
97
|
-
}));
|
|
107
|
+
async getSyncRuns(bankAccountId) {
|
|
108
|
+
const runs = await storeFor(bankAccountId).listRuns({ limit: 20 });
|
|
109
|
+
return runs.map(toSyncRun);
|
|
98
110
|
}
|
|
99
111
|
};
|
|
100
112
|
}
|
|
101
113
|
var provider = createOpenBankingProvider();
|
|
114
|
+
function day(value) {
|
|
115
|
+
return typeof value === "string" && value.length >= 10 ? value.slice(8, 10) + "/" + value.slice(5, 7) : void 0;
|
|
116
|
+
}
|
|
117
|
+
function statementLine(run) {
|
|
118
|
+
const from = day(run.stats?.period_from);
|
|
119
|
+
const to = day(run.stats?.period_to);
|
|
120
|
+
const duplicates = Number(run.stats?.duplicates ?? 0);
|
|
121
|
+
if (!from || !to) return void 0;
|
|
122
|
+
const imported = `${run.written ?? 0} importadas de ${from} a ${to}`;
|
|
123
|
+
return duplicates > 0 ? `${imported} \xB7 ${duplicates} j\xE1 estavam aqui` : imported;
|
|
124
|
+
}
|
|
102
125
|
function brl(n) {
|
|
103
126
|
try {
|
|
104
127
|
return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(n);
|
|
@@ -106,7 +129,7 @@ function brl(n) {
|
|
|
106
129
|
return `R$ ${n.toFixed(2)}`;
|
|
107
130
|
}
|
|
108
131
|
}
|
|
109
|
-
function
|
|
132
|
+
function OpenBankingImport({ onRan }) {
|
|
110
133
|
const [integration, setIntegration] = useState(null);
|
|
111
134
|
const [loaded, setLoaded] = useState(false);
|
|
112
135
|
const [from, setFrom] = useState(() => {
|
|
@@ -119,17 +142,14 @@ function OpenBankingExtraPanel() {
|
|
|
119
142
|
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
120
143
|
const [fetching, setFetching] = useState(false);
|
|
121
144
|
const [importing, setImporting] = useState(false);
|
|
122
|
-
const [log, setLog] = useState([]);
|
|
123
145
|
async function load() {
|
|
124
|
-
|
|
125
|
-
setIntegration(found);
|
|
126
|
-
if (found) void provider.getSyncLog(found.id).then(setLog);
|
|
146
|
+
setIntegration(await provider.getIntegration().catch(() => null));
|
|
127
147
|
setLoaded(true);
|
|
128
148
|
}
|
|
129
149
|
useEffect(() => {
|
|
130
150
|
void load();
|
|
131
151
|
}, []);
|
|
132
|
-
if (!loaded || !integration) return null;
|
|
152
|
+
if (!loaded || !integration?.active) return null;
|
|
133
153
|
async function handleFetch() {
|
|
134
154
|
if (!integration) return;
|
|
135
155
|
setFetching(true);
|
|
@@ -152,11 +172,17 @@ function OpenBankingExtraPanel() {
|
|
|
152
172
|
}
|
|
153
173
|
setImporting(true);
|
|
154
174
|
try {
|
|
155
|
-
const res = await provider.importTransactions({
|
|
175
|
+
const res = await provider.importTransactions({
|
|
176
|
+
integrationId: integration.id,
|
|
177
|
+
bankAccountId: integration.bankAccountId,
|
|
178
|
+
from,
|
|
179
|
+
to,
|
|
180
|
+
lines: toImport
|
|
181
|
+
});
|
|
156
182
|
toast.success(`${res.imported} importadas${res.duplicates ? ` \xB7 ${res.duplicates} duplicadas` : ""}`);
|
|
157
183
|
setLines([]);
|
|
158
184
|
setSelected(/* @__PURE__ */ new Set());
|
|
159
|
-
|
|
185
|
+
onRan?.();
|
|
160
186
|
} catch (e) {
|
|
161
187
|
toast.error(e?.message ?? "Erro ao importar");
|
|
162
188
|
} finally {
|
|
@@ -170,7 +196,7 @@ function OpenBankingExtraPanel() {
|
|
|
170
196
|
return next;
|
|
171
197
|
});
|
|
172
198
|
}
|
|
173
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-4
|
|
199
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
174
200
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
175
201
|
/* @__PURE__ */ jsx(Download, { className: "h-4 w-4 text-muted-foreground" }),
|
|
176
202
|
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Importar extrato" })
|
|
@@ -212,81 +238,699 @@ function OpenBankingExtraPanel() {
|
|
|
212
238
|
] })
|
|
213
239
|
] })
|
|
214
240
|
] }),
|
|
215
|
-
|
|
216
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
217
|
-
/* @__PURE__ */ jsx(History, { className: "h-4 w-4 text-muted-foreground" }),
|
|
218
|
-
/* @__PURE__ */ jsx("h4", { className: "text-sm font-semibold", children: "Hist\xF3rico" })
|
|
219
|
-
] }),
|
|
220
|
-
/* @__PURE__ */ jsx("div", { className: "rounded-md border divide-y text-sm", children: log.map((r) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-3 py-2", children: [
|
|
221
|
-
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: new Date(r.createdAt).toLocaleString("pt-BR") }),
|
|
222
|
-
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
223
|
-
r.periodFrom,
|
|
224
|
-
" \u2192 ",
|
|
225
|
-
r.periodTo
|
|
226
|
-
] }),
|
|
227
|
-
/* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs", children: [
|
|
228
|
-
r.transactionsImported,
|
|
229
|
-
"/",
|
|
230
|
-
r.transactionsFetched,
|
|
231
|
-
" importadas",
|
|
232
|
-
r.duplicates ? ` \xB7 ${r.duplicates} dup` : ""
|
|
233
|
-
] }),
|
|
234
|
-
r.status === "success" ? /* @__PURE__ */ jsx(CheckCircle2, { className: "h-3.5 w-3.5 text-success" }) : /* @__PURE__ */ jsx(AlertCircle, { className: "h-3.5 w-3.5 text-warning" })
|
|
235
|
-
] }, r.id)) })
|
|
236
|
-
] })
|
|
241
|
+
/* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "O token do PlugBank e o CNPJ da conta ficam guardados na plataforma Fayz, n\xE3o neste aplicativo. Quem importa o extrato \xE9 o servidor, que pede as credenciais na hora da consulta." })
|
|
237
242
|
] });
|
|
238
243
|
}
|
|
239
244
|
var openBankingConnector = {
|
|
240
245
|
id: "plugbank",
|
|
246
|
+
// The product is PlugBank, the company in the directory is TecnoSpeed. Same
|
|
247
|
+
// row, two names — and until this said so, the connector took neither the
|
|
248
|
+
// catalogue's copy nor its mark.
|
|
249
|
+
catalogSlug: "tecnospeed",
|
|
241
250
|
hostPluginId: "financial",
|
|
242
251
|
name: "Open Banking \xB7 Tecnospeed PlugBank",
|
|
243
252
|
description: "Importe o extrato banc\xE1rio e concilie em Financeiro \u2192 Concilia\xE7\xE3o.",
|
|
244
253
|
icon: "Landmark",
|
|
245
254
|
authKind: "api-key",
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
255
|
+
// `banking`, not `payments`: reading a statement is not taking a payment, and
|
|
256
|
+
// the platform catalogue files the provider (slug `tecnospeed`) under banking.
|
|
257
|
+
category: "banking",
|
|
258
|
+
// Inbound because WE reach the bank: PlugBank has never heard of Fayz, which
|
|
259
|
+
// is what makes this the canonical pull connector and why it ships an edge
|
|
260
|
+
// function at all.
|
|
261
|
+
direction: "inbound",
|
|
262
|
+
// Sync only. The statement has to exist INSIDE Fayz to be listed, filtered and
|
|
263
|
+
// matched against internal movements — reconciliation is not a question the
|
|
264
|
+
// assistant can answer by asking the bank at the moment it is asked.
|
|
265
|
+
planes: ["sync"],
|
|
266
|
+
docsUrl: "https://tecnospeed.com.br/plugbank/",
|
|
267
|
+
// Write and NOT read, which is unusual enough to be worth saying out loud:
|
|
268
|
+
// `plugbank_import_movements` (migrations/003) only ever INSERTs, with
|
|
269
|
+
// `ON CONFLICT (tenant_id, external_source, external_id) DO NOTHING`. It never
|
|
270
|
+
// SELECTs a movement, so declaring `read` here would be an access the code does
|
|
271
|
+
// not take — and the whole value of this list is that it is not padded.
|
|
272
|
+
//
|
|
273
|
+
// The `emit` is the second half, and it is the one a tenant would not guess:
|
|
274
|
+
// every imported line fires `financial.movement.imported` through the trigger
|
|
275
|
+
// in migrations/002, and that payload carries amount, date, description and
|
|
276
|
+
// bank account to whatever automation is listening. Importing a statement is
|
|
277
|
+
// therefore also publishing it internally.
|
|
278
|
+
permissions: [
|
|
279
|
+
{
|
|
280
|
+
kind: "data",
|
|
281
|
+
resource: "movement",
|
|
282
|
+
actions: ["write"],
|
|
283
|
+
reason: "Gravar no seu financeiro as linhas do extrato banc\xE1rio \u2014 valor, data, descri\xE7\xE3o e conta. Lan\xE7amentos j\xE1 importados n\xE3o entram de novo.",
|
|
284
|
+
sensitive: true
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
kind: "event",
|
|
288
|
+
resource: "financial.movement.imported",
|
|
289
|
+
actions: ["emit"],
|
|
290
|
+
reason: "Avisar o app a cada lan\xE7amento importado, para que suas automa\xE7\xF5es de concilia\xE7\xE3o possam reagir. O aviso leva valor, data e conta.",
|
|
291
|
+
sensitive: true
|
|
292
|
+
}
|
|
249
293
|
],
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
294
|
+
invokedAt: [
|
|
295
|
+
{
|
|
296
|
+
key: "financial.statement.import",
|
|
297
|
+
label: "Voc\xEA escolhe um per\xEDodo aqui e o extrato do banco \xE9 importado",
|
|
298
|
+
plane: "sync",
|
|
299
|
+
trigger: "manual",
|
|
300
|
+
touches: ["movement", "financial.movement.imported"]
|
|
301
|
+
}
|
|
302
|
+
],
|
|
303
|
+
// No credential field. The token and the CNPJ that identify the account are
|
|
304
|
+
// registered in the Fayz platform; anything typed here would be stored in a
|
|
305
|
+
// table every member of the tenant can read.
|
|
306
|
+
//
|
|
307
|
+
// `subscribesTo` is deliberately absent, and so is `managedAutomations`.
|
|
308
|
+
// Nothing that happens inside Fayz should wake this connector: it pulls a
|
|
309
|
+
// statement, and what makes it pull is the operator picking a period below or
|
|
310
|
+
// the Fayz SCHEDULE worker (FAY-1347) — never a domain fact. A subscription
|
|
311
|
+
// written to fill the field would put this connector on the ingress's
|
|
312
|
+
// delivery list for events it has no reaction to.
|
|
313
|
+
//
|
|
314
|
+
// The reconciliation automations ("avise quando entrar um valor acima de X")
|
|
315
|
+
// are real, but they are the TENANT's: they react to
|
|
316
|
+
// `financial.movement.imported` and the tenant may delete them without the
|
|
317
|
+
// connection becoming half-wired. `managedAutomations` is for the wiring the
|
|
318
|
+
// integration IS, and this integration is the import itself.
|
|
319
|
+
// Read from the row the list already batched — one request for N connectors.
|
|
320
|
+
// The account-less default instance (`''`) is the one the panel addresses.
|
|
321
|
+
...connectionStatus(PLUGBANK_CONNECTOR_ID, (connection) => {
|
|
322
|
+
const found = connection ? toIntegration(connection) : null;
|
|
323
|
+
return { connected: !!found?.active, detail: found?.environment, plane: "sync" };
|
|
324
|
+
}),
|
|
325
|
+
// `testConnection` is deleted rather than stubbed: the credential is not in
|
|
326
|
+
// this app, so there is nothing this side can validate, and ConnectorDetail
|
|
327
|
+
// renders the button only when the verb exists.
|
|
328
|
+
saveConnection: async () => {
|
|
329
|
+
await provider.saveIntegration();
|
|
253
330
|
},
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
331
|
+
disconnect: () => provider.disconnect(),
|
|
332
|
+
// Every import files a `plg_sync_runs` row, and the shared surface renders it.
|
|
333
|
+
// What is connector-specific is not the VIEW, it is the meaning of `stats`:
|
|
334
|
+
// "de 01/08 a 12/08 · 3 duplicadas" is three keys only this connector can
|
|
335
|
+
// read, turned here into the one sentence every run carries. Scoped to the
|
|
336
|
+
// default instance — the account this panel addresses.
|
|
337
|
+
async listRuns(options) {
|
|
338
|
+
try {
|
|
339
|
+
const runs = await createConnectionStore(PLUGBANK_CONNECTOR_ID).listRuns(options);
|
|
340
|
+
return runs.map((run) => ({ ...run, message: run.message ?? statementLine(run) }));
|
|
341
|
+
} catch {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
257
344
|
},
|
|
258
|
-
|
|
345
|
+
RunActions: OpenBankingImport
|
|
259
346
|
};
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
347
|
+
|
|
348
|
+
// src/lib/banking-events.ts
|
|
349
|
+
var MOVEMENT_IMPORTED_FIELDS = [
|
|
350
|
+
{ name: "movement_id", label: "ID do lan\xE7amento", type: "id", example: "8f14e45f-\u2026" },
|
|
351
|
+
{ name: "provider", label: "Origem", type: "string", example: "plugbank" },
|
|
352
|
+
{ name: "external_id", label: "Identificador no banco", type: "string", example: "LINE-1" },
|
|
353
|
+
{ name: "direction", label: "Tipo", type: "string", example: "credit" },
|
|
354
|
+
{ name: "amount", label: "Valor", type: "money", example: "R$ 150,00" },
|
|
355
|
+
{ name: "payment_date", label: "Data", type: "date", example: "03/08/2026" },
|
|
356
|
+
{ name: "description", label: "Descri\xE7\xE3o", type: "string", example: "Recebimento PIX" },
|
|
357
|
+
{ name: "bank_account_id", label: "Conta banc\xE1ria", type: "id", example: "3f2a91c4-\u2026" }
|
|
358
|
+
];
|
|
359
|
+
var PAYLOAD_SCHEMA = {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
movement_id: { type: "string", format: "uuid", description: "Movement id in the tenant ledger" },
|
|
363
|
+
provider: {
|
|
364
|
+
type: "string",
|
|
365
|
+
description: "Which integration imported the line \u2014 'plugbank' today. Reads from plg_financial_movements.external_source, so a second bank connector produces the same fact."
|
|
366
|
+
},
|
|
367
|
+
external_id: {
|
|
368
|
+
type: "string",
|
|
369
|
+
description: "The bank's own id for the statement line. With `provider` it is the import idempotency key."
|
|
370
|
+
},
|
|
371
|
+
direction: { type: "string", enum: ["credit", "debit"], description: "Money in or money out" },
|
|
372
|
+
amount: { type: "number", description: "Always positive; `direction` carries the sign" },
|
|
373
|
+
payment_date: { type: "string", format: "date", description: "Date the bank settled the line (YYYY-MM-DD)" },
|
|
374
|
+
description: { type: ["string", "null"], description: "Statement narrative, as the bank wrote it" },
|
|
375
|
+
bank_account_id: { type: ["string", "null"], format: "uuid", description: "Account the line landed in, when known" }
|
|
376
|
+
},
|
|
377
|
+
required: ["movement_id", "provider", "external_id", "direction", "amount", "payment_date"],
|
|
378
|
+
additionalProperties: false
|
|
379
|
+
};
|
|
380
|
+
function buildBankingDomainEvents() {
|
|
381
|
+
return [
|
|
382
|
+
{
|
|
383
|
+
name: "financial.movement.imported",
|
|
384
|
+
title: "Lan\xE7amento banc\xE1rio importado",
|
|
385
|
+
description: "Uma linha do extrato entrou no financeiro",
|
|
386
|
+
trigger: "Quando o extrato \xE9 importado \u2014 pelo painel, pelo conector ou por uma carga manual",
|
|
387
|
+
icon: "Landmark",
|
|
388
|
+
audit: true,
|
|
389
|
+
fields: MOVEMENT_IMPORTED_FIELDS,
|
|
390
|
+
payloadSchema: PAYLOAD_SCHEMA
|
|
391
|
+
}
|
|
392
|
+
];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/migrations/index.ts
|
|
396
|
+
var MIGRATION_002_CREDENTIAL_AND_FACTS = `-- ============================================================================
|
|
397
|
+
-- plugin-banking-br 002: the credential leaves the tenant database, the import
|
|
398
|
+
-- gets a door it cannot be routed around, and the import becomes a FACT.
|
|
399
|
+
--
|
|
400
|
+
-- \xA71 credential drop api_token + cnpj from public.bank_integrations
|
|
401
|
+
-- \xA72 sync runs bank_integration_sync_log speaks the SyncRun vocabulary
|
|
402
|
+
-- \xA73 import plugbank_import_movements \u2014 the tenant-safe door
|
|
403
|
+
-- \xA74 facts financial.movement.imported
|
|
404
|
+
--
|
|
405
|
+
-- Additive and idempotent: no table is created, no policy is rewritten, and the
|
|
406
|
+
-- tenant RLS from 001 already covers every column added here.
|
|
407
|
+
-- ============================================================================
|
|
408
|
+
|
|
409
|
+
-- \xA71 \u2500\u2500 credential \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
410
|
+
-- THE EXPOSURE THIS CLOSES: 001 stored \`api_token text\` and \`cnpj text\` on
|
|
411
|
+
-- public.bank_integrations. That table is read from the browser with the
|
|
412
|
+
-- publishable key under tenant RLS, so the bank credential was readable by ANY
|
|
413
|
+
-- authenticated member of the tenant; and a pool is one Supabase project serving
|
|
414
|
+
-- N tenants (beauty-saas and espaco-renova-rio share one), so anything running
|
|
415
|
+
-- there with service_role could read every clinic's bank token at once.
|
|
416
|
+
--
|
|
417
|
+
-- PlugBank differs from RankLayer, which dropped its key with nothing to replace
|
|
418
|
+
-- it: PlugBank is a PULL connector and genuinely needs a credential at fetch
|
|
419
|
+
-- time. It just must not be THIS copy. The destination is the platform's
|
|
420
|
+
-- encrypted ConnectorConnection store, redeemed per call (FAY-1360 + FAY-1389).
|
|
421
|
+
-- NEITHER HAS HAPPENED YET: as of 2026-08-12 the live pool still holds the
|
|
422
|
+
-- token in this column, this migration is unapplied there, and the deployed
|
|
423
|
+
-- plugbank-sync (v6) still reads it. The FAY-1360 rollout must register the
|
|
424
|
+
-- credential at the platform BEFORE this drop runs on that pool \u2014 otherwise
|
|
425
|
+
-- what is removed here is the only copy, and the connection dies.
|
|
426
|
+
--
|
|
427
|
+
-- \`cnpj\` goes with it rather than surviving as a masked remnant. It is not
|
|
428
|
+
-- decoration: paired with the token it is what identifies the account at the
|
|
429
|
+
-- provider (\`/v1/extrato?cnpj=\u2026\`), which makes it half of the credential and
|
|
430
|
+
-- therefore the platform's to hold. And nothing on this side reads it \u2014 the
|
|
431
|
+
-- live connector panel never renders it, and the unified connect form starts
|
|
432
|
+
-- every field empty rather than pre-filling from the row. A masked last-N would
|
|
433
|
+
-- be a column added to serve a screen that does not exist, on data that
|
|
434
|
+
-- identifies a real company.
|
|
435
|
+
--
|
|
436
|
+
-- ROTATE any token a pool already stored. DROP COLUMN is a catalog change: the
|
|
437
|
+
-- old values stay in the heap until the next table rewrite.
|
|
438
|
+
|
|
439
|
+
DO $$
|
|
440
|
+
BEGIN
|
|
441
|
+
IF to_regclass('public.bank_integrations') IS NOT NULL THEN
|
|
442
|
+
ALTER TABLE public.bank_integrations DROP COLUMN IF EXISTS api_token;
|
|
443
|
+
ALTER TABLE public.bank_integrations DROP COLUMN IF EXISTS cnpj;
|
|
444
|
+
END IF;
|
|
445
|
+
END $$;
|
|
446
|
+
|
|
447
|
+
-- \xA72 \u2500\u2500 sync runs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
448
|
+
-- bank_integration_sync_log already IS a SyncRun: one row per execution, with
|
|
449
|
+
-- status/fetched/written/error. It gains the four facts the shared vocabulary
|
|
450
|
+
-- has and it lacked, rather than being replaced by a second table beside it \u2014
|
|
451
|
+
-- two audit trails for one import is how "the history is wrong" starts.
|
|
452
|
+
--
|
|
453
|
+
-- The count columns keep their names. \`transactions_fetched\`/\`transactions_
|
|
454
|
+
-- imported\` map to SyncRun's \`fetched\`/\`written\` in the data provider, because
|
|
455
|
+
-- renaming them would break the deployed edge function's INSERT the moment this
|
|
456
|
+
-- migration lands, and a rename that has to be choreographed across two
|
|
457
|
+
-- repositories is not worth a nicer column name.
|
|
458
|
+
DO $$
|
|
459
|
+
BEGIN
|
|
460
|
+
IF to_regclass('public.bank_integration_sync_log') IS NULL THEN RETURN; END IF;
|
|
461
|
+
|
|
462
|
+
-- Which provider produced the run. One row per execution says nothing about
|
|
463
|
+
-- who ran it once a second bank connector shares the table.
|
|
464
|
+
ALTER TABLE public.bank_integration_sync_log
|
|
465
|
+
ADD COLUMN IF NOT EXISTS provider text NOT NULL DEFAULT 'plugbank';
|
|
466
|
+
-- Inbound: we reach the bank, never the other way round.
|
|
467
|
+
ALTER TABLE public.bank_integration_sync_log
|
|
468
|
+
ADD COLUMN IF NOT EXISTS direction text NOT NULL DEFAULT 'inbound';
|
|
469
|
+
-- \`trigger\` is a reserved word; the column is trigger_kind and the TypeScript
|
|
470
|
+
-- side reads it as SyncRun.trigger.
|
|
471
|
+
ALTER TABLE public.bank_integration_sync_log
|
|
472
|
+
ADD COLUMN IF NOT EXISTS trigger_kind text NOT NULL DEFAULT 'manual';
|
|
473
|
+
ALTER TABLE public.bank_integration_sync_log
|
|
474
|
+
ADD COLUMN IF NOT EXISTS started_at timestamptz;
|
|
475
|
+
ALTER TABLE public.bank_integration_sync_log
|
|
476
|
+
ADD COLUMN IF NOT EXISTS finished_at timestamptz;
|
|
477
|
+
|
|
478
|
+
COMMENT ON COLUMN public.bank_integration_sync_log.trigger_kind IS
|
|
479
|
+
'SyncRun.trigger \u2014 on-write | scheduled | manual | webhook. Named _kind because trigger is reserved.';
|
|
480
|
+
END $$;
|
|
481
|
+
|
|
482
|
+
-- \xA73 \u2500\u2500 import \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
483
|
+
-- The one door the statement import passes through.
|
|
484
|
+
--
|
|
485
|
+
-- Today the edge function inserts into plg_financial_movements directly with
|
|
486
|
+
-- service_role, and takes \`integrationId\` from the request body without ever
|
|
487
|
+
-- comparing it to the caller \u2014 so any authenticated member of ANY tenant in the
|
|
488
|
+
-- pool can name another tenant's integration and write into that tenant's
|
|
489
|
+
-- ledger. A guard in the function would be a guard the next caller routes
|
|
490
|
+
-- around; the database is the only chokepoint that cannot be.
|
|
491
|
+
--
|
|
492
|
+
-- The tenant is taken from the INTEGRATION ROW, never from the payload, and a
|
|
493
|
+
-- caller presenting a user credential must be a member of it. A caller with no
|
|
494
|
+
-- auth.uid() is the server (service_role, a migration, the future tenant-scoped
|
|
495
|
+
-- token) and is exempt \u2014 the same exemption plg_emit_event makes, for the same
|
|
496
|
+
-- reason: it legitimately writes for any tenant. When FAY-1372 mints the
|
|
497
|
+
-- tenant-scoped credential, that exemption stops applying to connector code and
|
|
498
|
+
-- this function needs no change.
|
|
499
|
+
CREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb)
|
|
500
|
+
RETURNS jsonb
|
|
501
|
+
LANGUAGE plpgsql
|
|
502
|
+
SECURITY DEFINER
|
|
503
|
+
SET search_path = public
|
|
504
|
+
AS $fn$
|
|
505
|
+
DECLARE
|
|
506
|
+
v_tenants uuid[];
|
|
507
|
+
v_tenant uuid;
|
|
508
|
+
v_integration uuid;
|
|
509
|
+
v_account uuid;
|
|
510
|
+
v_provider text;
|
|
511
|
+
v_trigger text;
|
|
512
|
+
v_from date;
|
|
513
|
+
v_to date;
|
|
514
|
+
v_started timestamptz := now();
|
|
515
|
+
v_line jsonb;
|
|
516
|
+
v_external text;
|
|
517
|
+
v_amount numeric(14,2);
|
|
518
|
+
v_date date;
|
|
519
|
+
v_fetched int := 0;
|
|
520
|
+
v_imported int := 0;
|
|
521
|
+
v_duplicates int := 0;
|
|
522
|
+
v_inserted uuid;
|
|
523
|
+
v_run uuid;
|
|
524
|
+
BEGIN
|
|
525
|
+
v_integration := nullif(btrim(p_payload->>'integration_id'), '')::uuid;
|
|
526
|
+
IF v_integration IS NULL THEN
|
|
527
|
+
RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'
|
|
528
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
529
|
+
END IF;
|
|
530
|
+
|
|
531
|
+
SELECT bi.tenant_id, bi.bank_account_id, bi.provider
|
|
532
|
+
INTO v_tenant, v_account, v_provider
|
|
533
|
+
FROM public.bank_integrations bi
|
|
534
|
+
WHERE bi.id = v_integration AND bi.active;
|
|
535
|
+
|
|
536
|
+
IF v_tenant IS NULL THEN
|
|
537
|
+
RAISE EXCEPTION 'plugbank_import_movements: no active integration %', v_integration
|
|
538
|
+
USING ERRCODE = 'no_data_found';
|
|
539
|
+
END IF;
|
|
540
|
+
|
|
541
|
+
-- SECURITY DEFINER bypasses RLS, so the membership check the policy would
|
|
542
|
+
-- have made has to happen here.
|
|
543
|
+
SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;
|
|
544
|
+
IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN
|
|
545
|
+
RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant
|
|
546
|
+
USING ERRCODE = 'insufficient_privilege';
|
|
547
|
+
END IF;
|
|
548
|
+
|
|
549
|
+
-- Refusing beats ignoring: a caller that believes it is importing into
|
|
550
|
+
-- another tenant must be told, not silently redirected into this one.
|
|
551
|
+
IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL
|
|
552
|
+
AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN
|
|
553
|
+
RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the integration'
|
|
554
|
+
USING ERRCODE = 'insufficient_privilege';
|
|
555
|
+
END IF;
|
|
556
|
+
|
|
557
|
+
IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL THEN
|
|
558
|
+
v_account := (p_payload->>'bank_account_id')::uuid;
|
|
559
|
+
END IF;
|
|
560
|
+
|
|
561
|
+
v_from := nullif(btrim(p_payload->>'period_from'), '')::date;
|
|
562
|
+
v_to := nullif(btrim(p_payload->>'period_to'), '')::date;
|
|
563
|
+
v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));
|
|
564
|
+
IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN
|
|
565
|
+
RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger
|
|
566
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
567
|
+
END IF;
|
|
568
|
+
|
|
569
|
+
FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))
|
|
570
|
+
LOOP
|
|
571
|
+
v_fetched := v_fetched + 1;
|
|
572
|
+
v_external := nullif(btrim(v_line->>'external_id'), '');
|
|
573
|
+
v_date := nullif(btrim(v_line->>'date'), '')::date;
|
|
574
|
+
v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));
|
|
575
|
+
|
|
576
|
+
-- A line with no provider id cannot be recognised on re-delivery, so it
|
|
577
|
+
-- would import again on every run. Skipping it is the lesser wrong.
|
|
578
|
+
IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN
|
|
579
|
+
CONTINUE;
|
|
580
|
+
END IF;
|
|
581
|
+
|
|
582
|
+
-- Idempotency is the partial unique index from the financial plugin's
|
|
583
|
+
-- 007_reconciliation.sql, not a SELECT-then-INSERT: two runs of the same
|
|
584
|
+
-- window racing each other would both find nothing and both write.
|
|
585
|
+
INSERT INTO public.plg_financial_movements (
|
|
586
|
+
tenant_id, direction, movement_kind, amount, paid_amount, status,
|
|
587
|
+
due_date, payment_date, bank_account_id, notes, external_id, external_source)
|
|
588
|
+
VALUES (
|
|
589
|
+
v_tenant,
|
|
590
|
+
CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,
|
|
591
|
+
'payment', v_amount, v_amount, 'paid',
|
|
592
|
+
v_date, v_date, v_account,
|
|
593
|
+
nullif(btrim(v_line->>'description'), ''),
|
|
594
|
+
v_external, coalesce(v_provider, 'plugbank'))
|
|
595
|
+
ON CONFLICT (tenant_id, external_source, external_id)
|
|
596
|
+
WHERE external_id IS NOT NULL AND external_source IS NOT NULL
|
|
597
|
+
DO NOTHING
|
|
598
|
+
RETURNING id INTO v_inserted;
|
|
599
|
+
|
|
600
|
+
IF v_inserted IS NULL THEN
|
|
601
|
+
v_duplicates := v_duplicates + 1;
|
|
602
|
+
ELSE
|
|
603
|
+
v_imported := v_imported + 1;
|
|
604
|
+
END IF;
|
|
605
|
+
v_inserted := NULL;
|
|
606
|
+
END LOOP;
|
|
607
|
+
|
|
608
|
+
UPDATE public.bank_integrations SET last_sync_at = now(), updated_at = now()
|
|
609
|
+
WHERE id = v_integration;
|
|
610
|
+
|
|
611
|
+
INSERT INTO public.bank_integration_sync_log (
|
|
612
|
+
tenant_id, bank_integration_id, bank_account_id, period_from, period_to,
|
|
613
|
+
transactions_fetched, transactions_imported, duplicates, status,
|
|
614
|
+
triggered_by_user_id, provider, direction, trigger_kind, started_at, finished_at)
|
|
615
|
+
VALUES (
|
|
616
|
+
v_tenant, v_integration, v_account, v_from, v_to,
|
|
617
|
+
v_fetched, v_imported, v_duplicates, 'success',
|
|
618
|
+
auth.uid(), coalesce(v_provider, 'plugbank'), 'inbound', v_trigger, v_started, now())
|
|
619
|
+
RETURNING id INTO v_run;
|
|
620
|
+
|
|
621
|
+
RETURN jsonb_build_object(
|
|
622
|
+
'ok', true,
|
|
623
|
+
'sync_run_id', v_run,
|
|
624
|
+
'fetched', v_fetched,
|
|
625
|
+
'imported', v_imported,
|
|
626
|
+
'duplicates', v_duplicates);
|
|
627
|
+
END;
|
|
628
|
+
$fn$;
|
|
629
|
+
|
|
630
|
+
REVOKE ALL ON FUNCTION public.plugbank_import_movements(jsonb) FROM public, anon;
|
|
631
|
+
GRANT EXECUTE ON FUNCTION public.plugbank_import_movements(jsonb) TO authenticated, service_role;
|
|
632
|
+
|
|
633
|
+
-- \xA74 \u2500\u2500 facts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
634
|
+
-- One trigger on the ledger rather than an emit call inside the importer. The
|
|
635
|
+
-- RPC above, the edge function still inserting directly, a future scheduled
|
|
636
|
+
-- pull and a hand-run backfill all end up in the same row \u2014 so the fact exists
|
|
637
|
+
-- for all four without any of them having to remember to say so. That is the
|
|
638
|
+
-- entire reason plg_emit_event exists (packages/db/migrations/027_domain_events.sql).
|
|
639
|
+
--
|
|
640
|
+
-- It fires only when external_source is set, which is exactly what "imported"
|
|
641
|
+
-- means: a movement that came from outside. An app-native movement typed into
|
|
642
|
+
-- the cash register is not an import and must not wake a bank connector.
|
|
643
|
+
--
|
|
644
|
+
-- Provider-agnostic on purpose. \`provider\` is in the payload, so a second bank
|
|
645
|
+
-- connector importing into the same ledger produces the same fact instead of a
|
|
646
|
+
-- second vocabulary for the same event.
|
|
647
|
+
CREATE OR REPLACE FUNCTION public.plg_bank_emit_movement_imported()
|
|
648
|
+
RETURNS trigger
|
|
649
|
+
LANGUAGE plpgsql
|
|
650
|
+
SECURITY DEFINER
|
|
651
|
+
SET search_path = public
|
|
652
|
+
AS $fn$
|
|
653
|
+
DECLARE
|
|
654
|
+
v_payload jsonb;
|
|
655
|
+
BEGIN
|
|
656
|
+
IF NEW.external_source IS NULL OR NEW.external_id IS NULL THEN
|
|
657
|
+
RETURN NULL;
|
|
658
|
+
END IF;
|
|
659
|
+
|
|
660
|
+
-- The contract every consumer reads. Not the row: a whole-row payload would
|
|
661
|
+
-- put the ledger's internal bookkeeping inside every webhook by default.
|
|
662
|
+
v_payload := jsonb_build_object(
|
|
663
|
+
'movement_id', NEW.id::text,
|
|
664
|
+
'provider', NEW.external_source,
|
|
665
|
+
'external_id', NEW.external_id,
|
|
666
|
+
'direction', NEW.direction,
|
|
667
|
+
'amount', NEW.amount,
|
|
668
|
+
'payment_date', NEW.payment_date,
|
|
669
|
+
'description', NEW.notes,
|
|
670
|
+
'bank_account_id', NEW.bank_account_id);
|
|
671
|
+
|
|
672
|
+
BEGIN
|
|
673
|
+
PERFORM public.plg_emit_event(
|
|
674
|
+
'financial.movement.imported', v_payload, 'financial_movement',
|
|
675
|
+
NEW.id::text, NEW.tenant_id, NULL);
|
|
676
|
+
EXCEPTION WHEN OTHERS THEN
|
|
677
|
+
-- Observability must never be able to reject the write that caused it.
|
|
678
|
+
RAISE WARNING '[plg_bank_emit_movement_imported] % on %: %', TG_OP, TG_TABLE_NAME, SQLERRM;
|
|
679
|
+
END;
|
|
680
|
+
|
|
681
|
+
RETURN NULL;
|
|
682
|
+
END;
|
|
683
|
+
$fn$;
|
|
684
|
+
|
|
685
|
+
REVOKE ALL ON FUNCTION public.plg_bank_emit_movement_imported() FROM public, anon;
|
|
686
|
+
|
|
687
|
+
-- Guarded on the COLUMN, not just the table: the financial plugin owns both, and
|
|
688
|
+
-- attaching this to a ledger without 007_reconciliation.sql would make every
|
|
689
|
+
-- movement insert fail on an unknown field \u2014 an addon breaking its own host.
|
|
690
|
+
DO $$
|
|
691
|
+
BEGIN
|
|
692
|
+
IF EXISTS (
|
|
693
|
+
SELECT 1 FROM information_schema.columns
|
|
694
|
+
WHERE table_schema = 'public'
|
|
695
|
+
AND table_name = 'plg_financial_movements'
|
|
696
|
+
AND column_name = 'external_source'
|
|
697
|
+
) THEN
|
|
698
|
+
DROP TRIGGER IF EXISTS plg_financial_movements_emit_imported ON public.plg_financial_movements;
|
|
699
|
+
CREATE TRIGGER plg_financial_movements_emit_imported
|
|
700
|
+
AFTER INSERT ON public.plg_financial_movements
|
|
701
|
+
FOR EACH ROW EXECUTE FUNCTION public.plg_bank_emit_movement_imported();
|
|
702
|
+
END IF;
|
|
703
|
+
END $$;
|
|
704
|
+
`;
|
|
705
|
+
var MIGRATION_003_ONTO_CONNECTIONS = `-- ============================================================================
|
|
706
|
+
-- plugin-banking-br 003 \u2014 PlugBank stops owning tables.
|
|
707
|
+
--
|
|
708
|
+
-- Retires public.bank_integrations and public.bank_integration_sync_log. The
|
|
709
|
+
-- connection is now a row of public.plg_connections and each import is a row of
|
|
710
|
+
-- public.plg_sync_runs (packages/db 029). See FAY-1376.
|
|
711
|
+
--
|
|
712
|
+
-- This connector is the one that tests the shape, because it is the only one
|
|
713
|
+
-- whose key was never (tenant, provider): 001 keyed on
|
|
714
|
+
-- UNIQUE (tenant_id, bank_account_id, provider), and the provider filtered by
|
|
715
|
+
-- bank_account_id. Two accounts of one clinic are two connections, and they must
|
|
716
|
+
-- stay two. That is exactly what \`instance_key\` is:
|
|
717
|
+
--
|
|
718
|
+
-- bank_integrations.bank_account_id \u2192 plg_connections.instance_key
|
|
719
|
+
-- (the account uuid as text; '' is the
|
|
720
|
+
-- account-less default the panel uses,
|
|
721
|
+
-- which is what \`bank_account_id IS NULL\`
|
|
722
|
+
-- meant before)
|
|
723
|
+
-- bank_integrations.provider \u2192 plg_connections.connector_id
|
|
724
|
+
-- bank_integrations.environment \u2192 plg_connections.settings->>'environment'
|
|
725
|
+
-- bank_integrations.active \u2192 plg_connections.active
|
|
726
|
+
-- bank_integrations.last_sync_at \u2192 plg_connections.last_sync_at
|
|
727
|
+
-- UNIQUE (tenant_id, bank_account_id, provider)
|
|
728
|
+
-- \u2192 UNIQUE (tenant_id, connector_id, instance_key)
|
|
729
|
+
--
|
|
730
|
+
-- And the run, column by column \u2014 nothing the panel renders is lost:
|
|
731
|
+
-- sync_log.bank_integration_id \u2192 plg_sync_runs.connection_id
|
|
732
|
+
-- sync_log.transactions_fetched \u2192 plg_sync_runs.fetched
|
|
733
|
+
-- sync_log.transactions_imported \u2192 plg_sync_runs.written
|
|
734
|
+
-- sync_log.duplicates \u2192 plg_sync_runs.stats->>'duplicates'
|
|
735
|
+
-- sync_log.period_from / _to \u2192 plg_sync_runs.stats->>'period_from' / '_to'
|
|
736
|
+
-- sync_log.triggered_by_user_id \u2192 plg_sync_runs.stats->>'triggered_by_user_id'
|
|
737
|
+
-- sync_log.error_message \u2192 plg_sync_runs.error
|
|
738
|
+
-- sync_log.bank_account_id \u2192 dropped: it is the connection's
|
|
739
|
+
-- instance_key, and a second copy is a
|
|
740
|
+
-- second thing to disagree
|
|
741
|
+
-- sync_log.provider \u2192 plg_sync_runs.connector_id
|
|
742
|
+
-- sync_log.created_at \u2192 plg_sync_runs.started_at
|
|
743
|
+
-- direction / trigger_kind / status / started_at / finished_at \u2192 same names
|
|
744
|
+
--
|
|
745
|
+
-- The count columns finally get the contract's names. 002 \xA72 kept
|
|
746
|
+
-- transactions_fetched/_imported because renaming them would have broken the
|
|
747
|
+
-- deployed edge function's INSERT \u2014 but the function no longer inserts into a
|
|
748
|
+
-- log at all: it calls plugbank_import_movements, which is the only writer, and
|
|
749
|
+
-- the run is filed here. The choreography 002 was avoiding does not exist.
|
|
750
|
+
--
|
|
751
|
+
-- Nothing is in production, so this is a drop and not a data migration.
|
|
752
|
+
-- ============================================================================
|
|
753
|
+
|
|
754
|
+
-- \xA71 \u2500\u2500 the door, re-pointed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
755
|
+
-- Same name, same payload, same guarantees. \`integration_id\` keeps its key on
|
|
756
|
+
-- the wire so the deployed plugbank-sync function needs no coordinated release;
|
|
757
|
+
-- what changed is that the uuid it carries now names a plg_connections row.
|
|
758
|
+
--
|
|
759
|
+
-- Everything 002 \xA73 established still holds and for the same reasons: the tenant
|
|
760
|
+
-- comes from the CONNECTION ROW and never from the payload, a caller presenting
|
|
761
|
+
-- a user credential must be a member of it, a caller with no auth.uid() is the
|
|
762
|
+
-- server and is exempt, and idempotency is the partial unique index rather than
|
|
763
|
+
-- a SELECT-then-INSERT that two racing runs would both pass.
|
|
764
|
+
--
|
|
765
|
+
-- One thing is new and is the point of the migration: the run is filed against
|
|
766
|
+
-- \`connection_id\`, so a tenant with two bank accounts gets two histories instead
|
|
767
|
+
-- of one interleaved list. plg_sync_runs carries no INSERT grant for
|
|
768
|
+
-- \`authenticated\` \u2014 this SECURITY DEFINER function is the server-side writer the
|
|
769
|
+
-- table's grants assume.
|
|
770
|
+
CREATE OR REPLACE FUNCTION public.plugbank_import_movements(p_payload jsonb)
|
|
771
|
+
RETURNS jsonb
|
|
772
|
+
LANGUAGE plpgsql
|
|
773
|
+
SECURITY DEFINER
|
|
774
|
+
SET search_path = public
|
|
775
|
+
AS $fn$
|
|
776
|
+
DECLARE
|
|
777
|
+
v_tenants uuid[];
|
|
778
|
+
v_tenant uuid;
|
|
779
|
+
v_connection uuid;
|
|
780
|
+
v_instance text;
|
|
781
|
+
v_account uuid;
|
|
782
|
+
v_connector text;
|
|
783
|
+
v_trigger text;
|
|
784
|
+
v_from date;
|
|
785
|
+
v_to date;
|
|
786
|
+
v_started timestamptz := now();
|
|
787
|
+
v_line jsonb;
|
|
788
|
+
v_external text;
|
|
789
|
+
v_amount numeric(14,2);
|
|
790
|
+
v_date date;
|
|
791
|
+
v_fetched int := 0;
|
|
792
|
+
v_imported int := 0;
|
|
793
|
+
v_duplicates int := 0;
|
|
794
|
+
v_inserted uuid;
|
|
795
|
+
v_run uuid;
|
|
796
|
+
BEGIN
|
|
797
|
+
v_connection := nullif(btrim(p_payload->>'integration_id'), '')::uuid;
|
|
798
|
+
IF v_connection IS NULL THEN
|
|
799
|
+
RAISE EXCEPTION 'plugbank_import_movements: integration_id is required'
|
|
800
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
801
|
+
END IF;
|
|
802
|
+
|
|
803
|
+
SELECT c.tenant_id, c.instance_key, c.connector_id
|
|
804
|
+
INTO v_tenant, v_instance, v_connector
|
|
805
|
+
FROM public.plg_connections c
|
|
806
|
+
WHERE c.id = v_connection AND c.active;
|
|
807
|
+
|
|
808
|
+
IF v_tenant IS NULL THEN
|
|
809
|
+
RAISE EXCEPTION 'plugbank_import_movements: no active connection %', v_connection
|
|
810
|
+
USING ERRCODE = 'no_data_found';
|
|
811
|
+
END IF;
|
|
812
|
+
|
|
813
|
+
-- The account this connection is for. \`instance_key\` is text because it also
|
|
814
|
+
-- carries things that are not uuids for other connectors (a Bling company, a
|
|
815
|
+
-- gateway instance); here it is the bank account, and '' is the account-less
|
|
816
|
+
-- default that \`bank_account_id IS NULL\` used to mean.
|
|
817
|
+
v_account := nullif(btrim(v_instance), '')::uuid;
|
|
818
|
+
|
|
819
|
+
-- SECURITY DEFINER bypasses RLS, so the membership check the policy would
|
|
820
|
+
-- have made has to happen here.
|
|
821
|
+
SELECT array_agg(t) INTO v_tenants FROM public.user_tenant_ids() AS t;
|
|
822
|
+
IF auth.uid() IS NOT NULL AND NOT (v_tenant = ANY (COALESCE(v_tenants, '{}'::uuid[]))) THEN
|
|
823
|
+
RAISE EXCEPTION 'plugbank_import_movements: caller is not a member of tenant %', v_tenant
|
|
824
|
+
USING ERRCODE = 'insufficient_privilege';
|
|
825
|
+
END IF;
|
|
826
|
+
|
|
827
|
+
-- Refusing beats ignoring: a caller that believes it is importing into
|
|
828
|
+
-- another tenant must be told, not silently redirected into this one.
|
|
829
|
+
IF nullif(btrim(p_payload->>'tenant_id'), '') IS NOT NULL
|
|
830
|
+
AND (p_payload->>'tenant_id')::uuid IS DISTINCT FROM v_tenant THEN
|
|
831
|
+
RAISE EXCEPTION 'plugbank_import_movements: payload names another tenant; the tenant comes from the connection'
|
|
832
|
+
USING ERRCODE = 'insufficient_privilege';
|
|
833
|
+
END IF;
|
|
834
|
+
|
|
835
|
+
-- Same rule as the tenant, one level down: the account is the connection's,
|
|
836
|
+
-- and a payload naming a different one is a caller importing into the wrong
|
|
837
|
+
-- ledger account. 002 let the payload override it silently \u2014 with one
|
|
838
|
+
-- connection per account that override is how two accounts get mixed.
|
|
839
|
+
IF nullif(btrim(p_payload->>'bank_account_id'), '') IS NOT NULL
|
|
840
|
+
AND (p_payload->>'bank_account_id')::uuid IS DISTINCT FROM v_account THEN
|
|
841
|
+
RAISE EXCEPTION 'plugbank_import_movements: payload names another bank account; the account comes from the connection'
|
|
842
|
+
USING ERRCODE = 'insufficient_privilege';
|
|
843
|
+
END IF;
|
|
844
|
+
|
|
845
|
+
v_from := nullif(btrim(p_payload->>'period_from'), '')::date;
|
|
846
|
+
v_to := nullif(btrim(p_payload->>'period_to'), '')::date;
|
|
847
|
+
v_trigger := lower(coalesce(nullif(btrim(p_payload->>'trigger'), ''), 'manual'));
|
|
848
|
+
IF v_trigger NOT IN ('on-write', 'scheduled', 'manual', 'webhook') THEN
|
|
849
|
+
RAISE EXCEPTION 'plugbank_import_movements: unknown trigger %', v_trigger
|
|
850
|
+
USING ERRCODE = 'invalid_parameter_value';
|
|
851
|
+
END IF;
|
|
852
|
+
|
|
853
|
+
FOR v_line IN SELECT * FROM jsonb_array_elements(coalesce(p_payload->'lines', '[]'::jsonb))
|
|
854
|
+
LOOP
|
|
855
|
+
v_fetched := v_fetched + 1;
|
|
856
|
+
v_external := nullif(btrim(v_line->>'external_id'), '');
|
|
857
|
+
v_date := nullif(btrim(v_line->>'date'), '')::date;
|
|
858
|
+
v_amount := abs(coalesce((v_line->>'amount')::numeric, 0));
|
|
859
|
+
|
|
860
|
+
-- A line with no provider id cannot be recognised on re-delivery, so it
|
|
861
|
+
-- would import again on every run. Skipping it is the lesser wrong.
|
|
862
|
+
IF v_external IS NULL OR v_date IS NULL OR v_amount = 0 THEN
|
|
863
|
+
CONTINUE;
|
|
864
|
+
END IF;
|
|
865
|
+
|
|
866
|
+
INSERT INTO public.plg_financial_movements (
|
|
867
|
+
tenant_id, direction, movement_kind, amount, paid_amount, status,
|
|
868
|
+
due_date, payment_date, bank_account_id, notes, external_id, external_source)
|
|
869
|
+
VALUES (
|
|
870
|
+
v_tenant,
|
|
871
|
+
CASE WHEN upper(coalesce(v_line->>'type', 'C')) LIKE 'C%' THEN 'credit' ELSE 'debit' END,
|
|
872
|
+
'payment', v_amount, v_amount, 'paid',
|
|
873
|
+
v_date, v_date, v_account,
|
|
874
|
+
nullif(btrim(v_line->>'description'), ''),
|
|
875
|
+
v_external, coalesce(v_connector, 'plugbank'))
|
|
876
|
+
ON CONFLICT (tenant_id, external_source, external_id)
|
|
877
|
+
WHERE external_id IS NOT NULL AND external_source IS NOT NULL
|
|
878
|
+
DO NOTHING
|
|
879
|
+
RETURNING id INTO v_inserted;
|
|
880
|
+
|
|
881
|
+
IF v_inserted IS NULL THEN
|
|
882
|
+
v_duplicates := v_duplicates + 1;
|
|
883
|
+
ELSE
|
|
884
|
+
v_imported := v_imported + 1;
|
|
885
|
+
END IF;
|
|
886
|
+
v_inserted := NULL;
|
|
887
|
+
END LOOP;
|
|
888
|
+
|
|
889
|
+
UPDATE public.plg_connections
|
|
890
|
+
SET last_sync_at = now(), status = 'connected', status_detail = NULL, updated_at = now()
|
|
891
|
+
WHERE id = v_connection;
|
|
892
|
+
|
|
893
|
+
-- \`stats\` is where the three facts a manual statement import has and a generic
|
|
894
|
+
-- run does not go: the operator picked a period, a re-import of that period is
|
|
895
|
+
-- mostly duplicates by design, and someone pressed the button. The panel reads
|
|
896
|
+
-- all three back from here.
|
|
897
|
+
INSERT INTO public.plg_sync_runs (
|
|
898
|
+
tenant_id, connection_id, connector_id, direction, trigger_kind, status,
|
|
899
|
+
stream, fetched, written, stats, started_at, finished_at)
|
|
900
|
+
VALUES (
|
|
901
|
+
v_tenant, v_connection, coalesce(v_connector, 'plugbank'), 'inbound', v_trigger, 'success',
|
|
902
|
+
'statement', v_fetched, v_imported,
|
|
903
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
904
|
+
'duplicates', v_duplicates,
|
|
905
|
+
'period_from', v_from,
|
|
906
|
+
'period_to', v_to,
|
|
907
|
+
'triggered_by_user_id', auth.uid())),
|
|
908
|
+
v_started, now())
|
|
909
|
+
RETURNING id INTO v_run;
|
|
910
|
+
|
|
911
|
+
RETURN jsonb_build_object(
|
|
912
|
+
'ok', true,
|
|
913
|
+
'sync_run_id', v_run,
|
|
914
|
+
'fetched', v_fetched,
|
|
915
|
+
'imported', v_imported,
|
|
916
|
+
'duplicates', v_duplicates);
|
|
917
|
+
END;
|
|
918
|
+
$fn$;
|
|
919
|
+
|
|
920
|
+
REVOKE ALL ON FUNCTION public.plugbank_import_movements(jsonb) FROM public, anon;
|
|
921
|
+
GRANT EXECUTE ON FUNCTION public.plugbank_import_movements(jsonb) TO authenticated, service_role;
|
|
922
|
+
|
|
923
|
+
COMMENT ON FUNCTION public.plugbank_import_movements(jsonb) IS
|
|
924
|
+
'Import bank-statement lines into the financial ledger and file the run in '
|
|
925
|
+
'plg_sync_runs. The tenant AND the bank account come from the plg_connections '
|
|
926
|
+
'row named by integration_id, never from the payload.';
|
|
927
|
+
|
|
928
|
+
-- \xA72 \u2500\u2500 the bespoke tables go \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
929
|
+
-- After the function above, so a pool is never left with the old door pointing
|
|
930
|
+
-- at a table that no longer exists.
|
|
931
|
+
DROP TABLE IF EXISTS public.bank_integration_sync_log CASCADE;
|
|
932
|
+
DROP TABLE IF EXISTS public.bank_integrations CASCADE;
|
|
933
|
+
`;
|
|
290
934
|
|
|
291
935
|
// src/index.ts
|
|
292
936
|
function createOpenBankingPlugin() {
|
|
@@ -294,16 +938,84 @@ function createOpenBankingPlugin() {
|
|
|
294
938
|
id: "openbanking",
|
|
295
939
|
name: "Open Banking",
|
|
296
940
|
icon: "Landmark",
|
|
297
|
-
version: "0.1
|
|
941
|
+
version: "0.9.1",
|
|
942
|
+
apiVersion: 1,
|
|
298
943
|
scope: "addon",
|
|
299
944
|
defaultEnabled: true,
|
|
300
945
|
dependencies: ["financial"],
|
|
301
946
|
navigation: [],
|
|
302
947
|
routes: [],
|
|
303
|
-
connectors: [openBankingConnector]
|
|
948
|
+
connectors: [openBankingConnector],
|
|
949
|
+
// The fact the import produces. Declared HERE and not on plugin-financial
|
|
950
|
+
// because the trigger that emits it ships in this plugin's 002: a host
|
|
951
|
+
// advertising an event that only exists when an addon is installed is the
|
|
952
|
+
// same broken promise as an automation the ingress never delivers.
|
|
953
|
+
events: buildBankingDomainEvents(),
|
|
954
|
+
// `emitsFrom` is deliberately absent. It generates
|
|
955
|
+
// `registry.<entity>.<action>` keys for every row change, and "imported" is
|
|
956
|
+
// not a row action — it is an INSERT carrying an external_source, which only
|
|
957
|
+
// a purpose-built trigger can tell apart from a sale typed into the till.
|
|
958
|
+
functions: [
|
|
959
|
+
{
|
|
960
|
+
slug: "plugbank-sync",
|
|
961
|
+
// Invoked from the browser by an authenticated operator picking a
|
|
962
|
+
// period, so Supabase must check the caller's JWT. It is not reached
|
|
963
|
+
// through the Fayz ingress — nothing in Fayz wakes a pull connector —
|
|
964
|
+
// which is what lets verifyJwt stay true.
|
|
965
|
+
ingress: "none",
|
|
966
|
+
verifyJwt: true,
|
|
967
|
+
// NAMES only. PLUGBANK_BASE_URL selects production vs sandbox; the
|
|
968
|
+
// per-tenant token and account CNPJ are NOT here, because a Supabase
|
|
969
|
+
// function secret is per-POOL and a pool serves N tenants with N
|
|
970
|
+
// different bank accounts. Those come from the Fayz platform per call.
|
|
971
|
+
secrets: ["PLUGBANK_BASE_URL", "SUPABASE_URL", "SUPABASE_SERVICE_ROLE_KEY"],
|
|
972
|
+
// What this connector's credential may touch. Direct table access is
|
|
973
|
+
// not on the list on purpose (2026-08-11 ADR): the import goes through
|
|
974
|
+
// the RPC, which takes the tenant from the integration row instead of
|
|
975
|
+
// from the request body.
|
|
976
|
+
grants: ["plugbank_import_movements"]
|
|
977
|
+
}
|
|
978
|
+
],
|
|
979
|
+
declaredRpcs: [
|
|
980
|
+
{
|
|
981
|
+
name: "plugbank_import_movements",
|
|
982
|
+
kind: "write",
|
|
983
|
+
description: "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.",
|
|
984
|
+
audits: false
|
|
985
|
+
}
|
|
986
|
+
],
|
|
987
|
+
// This plugin creates no table. 003 retired the two it had; what is left is
|
|
988
|
+
// the RPC that is the import's one door, and the trigger that turns an
|
|
989
|
+
// imported movement into a fact. The connection and its run history are rows
|
|
990
|
+
// of plg_connections / plg_sync_runs, owned by packages/db.
|
|
991
|
+
migrations: [
|
|
992
|
+
{
|
|
993
|
+
id: "openbanking-002_credential_and_facts",
|
|
994
|
+
version: "0.9.1",
|
|
995
|
+
sql: MIGRATION_002_CREDENTIAL_AND_FACTS,
|
|
996
|
+
description: "Drop bank_integrations.api_token/cnpj (destination: the Fayz platform credential store, FAY-1360 \u2014 register there BEFORE applying this), add plugbank_import_movements and the financial.movement.imported trigger"
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
id: "openbanking-003_onto_connections",
|
|
1000
|
+
version: "1.0.0",
|
|
1001
|
+
sql: MIGRATION_003_ONTO_CONNECTIONS,
|
|
1002
|
+
description: "Retire bank_integrations + bank_integration_sync_log \u2014 the connection is a plg_connections row keyed by instance_key (the bank account) and each import a plg_sync_runs row; plugbank_import_movements re-pointed at both"
|
|
1003
|
+
}
|
|
1004
|
+
],
|
|
1005
|
+
diagnostics: [
|
|
1006
|
+
{
|
|
1007
|
+
id: "openbanking-ledger-ready",
|
|
1008
|
+
description: "The import writes into the financial ledger and needs its reconciliation columns (plugin-financial 007), the generic connection tables (packages/db 029) and the row-level event emitter.",
|
|
1009
|
+
requires: {
|
|
1010
|
+
tables: ["plg_connections", "plg_sync_runs", "plg_financial_movements"],
|
|
1011
|
+
rpcs: ["plugbank_import_movements", "plg_emit_event"]
|
|
1012
|
+
},
|
|
1013
|
+
level: "error"
|
|
1014
|
+
}
|
|
1015
|
+
]
|
|
304
1016
|
};
|
|
305
1017
|
}
|
|
306
1018
|
|
|
307
|
-
export {
|
|
1019
|
+
export { MOVEMENT_IMPORTED_FIELDS, buildBankingDomainEvents, createOpenBankingPlugin, createOpenBankingProvider, openBankingConnector };
|
|
308
1020
|
//# sourceMappingURL=index.js.map
|
|
309
1021
|
//# sourceMappingURL=index.js.map
|