@flowselections/floriday-authenticatie-module 1.0.12 → 1.0.13

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.
Files changed (29) hide show
  1. package/dist-lib/assets/floriday-logo.png.asset.json +11 -0
  2. package/dist-lib/components/floriday/FlorydaySyncJobsCard.d.ts +2 -0
  3. package/dist-lib/components/floriday/FlorydaySyncJobsCard.d.ts.map +1 -0
  4. package/dist-lib/components/floriday/FlorydaySyncJobsCard.js +99 -0
  5. package/dist-lib/components/floriday/TokenHealthCard.d.ts +2 -0
  6. package/dist-lib/components/floriday/TokenHealthCard.d.ts.map +1 -0
  7. package/dist-lib/components/floriday/TokenHealthCard.js +93 -0
  8. package/dist-lib/components/settings/FlorydaySettingsCard.d.ts.map +1 -1
  9. package/dist-lib/components/settings/FlorydaySettingsCard.js +116 -3
  10. package/dist-lib/index.d.ts +1 -0
  11. package/dist-lib/index.d.ts.map +1 -1
  12. package/dist-lib/index.js +2 -1
  13. package/dist-lib/integrations/supabase/auth-middleware.d.ts +4685 -595
  14. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  15. package/dist-lib/integrations/supabase/client.d.ts +4685 -595
  16. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  17. package/dist-lib/integrations/supabase/client.server.d.ts +4685 -595
  18. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  19. package/dist-lib/integrations/supabase/client.server.js +8 -2
  20. package/dist-lib/integrations/supabase/types.d.ts +4788 -586
  21. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  22. package/dist-lib/integrations/supabase/types.js +11 -0
  23. package/dist-lib/lib/floriday-sync.functions.d.ts +49839 -0
  24. package/dist-lib/lib/floriday-sync.functions.d.ts.map +1 -0
  25. package/dist-lib/lib/floriday-sync.functions.js +306 -0
  26. package/dist-lib/lib/floriday.functions.d.ts +82115 -6526
  27. package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
  28. package/dist-lib/lib/floriday.functions.js +475 -37
  29. package/package.json +5 -4
@@ -37,9 +37,9 @@ const FLORYDAY_ENDPOINTS = {
37
37
  apiBaseUrl: "https://api.floriday.io",
38
38
  },
39
39
  };
40
- const DEFAULT_SCOPE = "role:app catalog:read sales-order:write organization:read supply:read supply:write sales-order:read delivery-conditions:read fulfillment:write fulfillment:read";
41
- const SUPPLIERS_API_VERSION = "suppliers-api-2026v1";
42
- const TOKEN_REFRESH_LEEWAY_S = 60;
40
+ const DEFAULT_SCOPE = "role:app catalog:read catalog:write sales-order:write organization:read supply:read supply:write sales-order:read delivery-conditions:read fulfillment:write fulfillment:read network:read network:write";
41
+ export const SUPPLIERS_API_VERSION = "suppliers-api-2026v1";
42
+ const TOKEN_REFRESH_LEEWAY_S = 300;
43
43
  function mask(value, keep = 4) {
44
44
  if (!value)
45
45
  return "";
@@ -108,29 +108,62 @@ export async function getFlorydayAccessToken(supabase, env, options = {}) {
108
108
  if (!options.forceRefresh && row.access_token && expiresAt - TOKEN_REFRESH_LEEWAY_S * 1000 > now) {
109
109
  return row.access_token;
110
110
  }
111
- const tokenRes = await fetch(FLORYDAY_ENDPOINTS[env].tokenUrl, {
112
- method: "POST",
113
- headers: {
114
- "Content-Type": "application/x-www-form-urlencoded",
115
- Accept: "application/json",
116
- },
117
- body: new URLSearchParams({
118
- grant_type: "client_credentials",
119
- client_id: deepSanitizeCredential(row.client_id),
120
- client_secret: deepSanitizeCredential(row.client_secret),
121
- scope: DEFAULT_SCOPE,
122
- }).toString(),
123
- });
124
- const text = await tokenRes.text();
125
- if (!tokenRes.ok) {
126
- throw new Error(`Floriday token-aanvraag mislukt [${tokenRes.status}]: ${text}`);
111
+ // Concurrency-guard: vlak voor de tokenrequest opnieuw lezen — een parallelle worker
112
+ // kan zojuist een fresh token hebben geschreven en dan hoeven we niet nóg eens te refreshen.
113
+ if (!options.forceRefresh) {
114
+ const { data: fresh } = await supabase
115
+ .from("floriday_connections")
116
+ .select("access_token, token_expires_at")
117
+ .eq("id", row.id)
118
+ .maybeSingle();
119
+ const freshExp = fresh?.token_expires_at ? Date.parse(fresh.token_expires_at) : 0;
120
+ if (fresh?.access_token && freshExp - TOKEN_REFRESH_LEEWAY_S * 1000 > now) {
121
+ return fresh.access_token;
122
+ }
123
+ }
124
+ let json;
125
+ let newExpiresAt;
126
+ try {
127
+ const tokenRes = await fetch(FLORYDAY_ENDPOINTS[env].tokenUrl, {
128
+ method: "POST",
129
+ headers: {
130
+ "Content-Type": "application/x-www-form-urlencoded",
131
+ Accept: "application/json",
132
+ },
133
+ body: new URLSearchParams({
134
+ grant_type: "client_credentials",
135
+ client_id: deepSanitizeCredential(row.client_id),
136
+ client_secret: deepSanitizeCredential(row.client_secret),
137
+ scope: DEFAULT_SCOPE,
138
+ }).toString(),
139
+ });
140
+ const text = await tokenRes.text();
141
+ if (!tokenRes.ok) {
142
+ throw new Error(`Floriday token-aanvraag mislukt [${tokenRes.status}]: ${text.slice(0, 300)}`);
143
+ }
144
+ json = JSON.parse(text);
145
+ newExpiresAt = new Date(now + json.expires_in * 1000).toISOString();
146
+ await supabase
147
+ .from("floriday_connections")
148
+ .update({ access_token: json.access_token, token_expires_at: newExpiresAt })
149
+ .eq("id", row.id);
150
+ // Best-effort logging (faalt nooit hard).
151
+ await supabase.from("floriday_token_refresh_log").insert({
152
+ connection_id: row.id,
153
+ environment: env,
154
+ ok: true,
155
+ expires_at: newExpiresAt,
156
+ }).then(() => undefined, () => undefined);
157
+ }
158
+ catch (e) {
159
+ await supabase.from("floriday_token_refresh_log").insert({
160
+ connection_id: row.id,
161
+ environment: env,
162
+ ok: false,
163
+ error: String(e?.message ?? e).slice(0, 500),
164
+ }).then(() => undefined, () => undefined);
165
+ throw e;
127
166
  }
128
- const json = JSON.parse(text);
129
- const newExpiresAt = new Date(now + json.expires_in * 1000).toISOString();
130
- await supabase
131
- .from("floriday_connections")
132
- .update({ access_token: json.access_token, token_expires_at: newExpiresAt })
133
- .eq("id", row.id);
134
167
  return json.access_token;
135
168
  }
136
169
  async function fetchAndStoreFlorydayOrganizationName(supabase, env, connectionId) {
@@ -205,16 +238,32 @@ export async function florydayFetch(supabase, pathOrEnv, pathOrInit, maybeInit =
205
238
  const row = await getActiveFlorydayCredentials(supabase, env);
206
239
  if (!row)
207
240
  throw new Error(`Geen actieve Floriday-koppeling voor ${env}`);
208
- const token = await getFlorydayAccessToken(supabase, env);
209
241
  const url = path.startsWith("http")
210
242
  ? path
211
243
  : `${FLORYDAY_ENDPOINTS[env].apiBaseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
212
- const headers = new Headers(init.headers);
213
- headers.set("Authorization", `Bearer ${token}`);
214
- headers.set("X-Api-Key", deepSanitizeCredential(row.api_key));
215
- if (!headers.has("Accept"))
216
- headers.set("Accept", "application/json");
217
- return fetch(url, { ...init, headers });
244
+ const doFetch = async (token) => {
245
+ const headers = new Headers(init.headers);
246
+ headers.set("Authorization", `Bearer ${token}`);
247
+ headers.set("X-Api-Key", deepSanitizeCredential(row.api_key));
248
+ if (!headers.has("Accept"))
249
+ headers.set("Accept", "application/json");
250
+ return fetch(url, { ...init, headers });
251
+ };
252
+ let token = await getFlorydayAccessToken(supabase, env);
253
+ let res = await doFetch(token);
254
+ // Auto-retry op 401/403 met een force-refreshed token. Dit is het vangnet
255
+ // voor het geval de cron faalt of een race-condition ons toch een verlopen
256
+ // token gaf. Body wordt niet gelezen vóór de retry — als de caller .json()
257
+ // doet werkt dat normaal op het nieuwe Response object.
258
+ if (res.status === 401 || res.status === 403) {
259
+ token = await getFlorydayAccessToken(supabase, env, { forceRefresh: true });
260
+ res = await doFetch(token);
261
+ if (res.status === 401 || res.status === 403) {
262
+ const body = await res.clone().text().catch(() => "");
263
+ throw new Error(`Floriday API ${res.status} na force-refresh — controleer client_id/client_secret op /floriday-connection. Body: ${body.slice(0, 200)}`);
264
+ }
265
+ }
266
+ return res;
218
267
  }
219
268
  // ─── Server functions exposed to the UI ─────────────────────────────────────
220
269
  export const listFlorydayConnections = createServerFn({ method: "GET" })
@@ -228,6 +277,41 @@ export const listFlorydayConnections = createServerFn({ method: "GET" })
228
277
  throw new Error(error.message);
229
278
  return (data ?? []).map(toPublic);
230
279
  });
280
+ export const getFlorydayTokenHealth = createServerFn({ method: "GET" })
281
+ .middleware([requireSupabaseAuth])
282
+ .handler(async ({ context }) => {
283
+ const { data: conns } = await context.supabase
284
+ .from("floriday_connections")
285
+ .select("id, environment, token_expires_at, is_active")
286
+ .eq("is_active", true);
287
+ const { data: log } = await context.supabase
288
+ .from("floriday_token_refresh_log")
289
+ .select("connection_id, attempted_at, ok, error, expires_at")
290
+ .order("attempted_at", { ascending: false })
291
+ .limit(20);
292
+ return {
293
+ connections: conns ?? [],
294
+ recent: log ?? [],
295
+ };
296
+ });
297
+ export const forceRefreshFlorydayToken = createServerFn({ method: "POST" })
298
+ .middleware([requireSupabaseAuth])
299
+ .inputValidator((input) => {
300
+ if (!["staging", "live"].includes(input.environment)) {
301
+ throw new Error("Ongeldige environment");
302
+ }
303
+ return input;
304
+ })
305
+ .handler(async ({ data, context }) => {
306
+ await getFlorydayAccessToken(context.supabase, data.environment, { forceRefresh: true });
307
+ const { data: row } = await context.supabase
308
+ .from("floriday_connections")
309
+ .select("token_expires_at")
310
+ .eq("environment", data.environment)
311
+ .eq("is_active", true)
312
+ .maybeSingle();
313
+ return { ok: true, expires_at: row?.token_expires_at ?? null };
314
+ });
231
315
  export const upsertFlorydayConnection = createServerFn({ method: "POST" })
232
316
  .middleware([requireSupabaseAuth])
233
317
  .inputValidator((input) => {
@@ -268,7 +352,36 @@ export const upsertFlorydayConnection = createServerFn({ method: "POST" })
268
352
  await scheduleFlorydayTokenRefreshInternal(context.supabase);
269
353
  }
270
354
  catch (e) {
271
- console.error("[floriday] auto-schedule cron failed:", e);
355
+ console.error("[floriday] auto-schedule token cron failed:", e);
356
+ }
357
+ try {
358
+ await scheduleFlorydayConnectionsSyncInternal(context.supabase);
359
+ }
360
+ catch (e) {
361
+ console.error("[floriday] auto-schedule connections-sync cron failed:", e);
362
+ }
363
+ // Plan ook alle resource-syncs automatisch in (trade-items, sales-orders,
364
+ // customer-organizations, carriers). Best-effort: een mislukte cron breekt
365
+ // het opslaan van credentials niet.
366
+ try {
367
+ const { scheduleFlorydayResourceSyncInternal } = await import("./floriday-sync.functions");
368
+ const resources = [
369
+ "trade-items",
370
+ "sales-orders",
371
+ "customer-organizations",
372
+ "carriers",
373
+ ];
374
+ for (const r of resources) {
375
+ try {
376
+ await scheduleFlorydayResourceSyncInternal(context.supabase, r);
377
+ }
378
+ catch (e) {
379
+ console.error(`[floriday] auto-schedule ${r} cron failed:`, e);
380
+ }
381
+ }
382
+ }
383
+ catch (e) {
384
+ console.error("[floriday] auto-schedule resource crons failed:", e);
272
385
  }
273
386
  // Organisatienaam ophalen gebeurt non-blocking via een aparte call
274
387
  // (testFlorydayConnection / handmatige refresh). Houdt opslag snel.
@@ -322,14 +435,43 @@ export const testFlorydayConnection = createServerFn({ method: "POST" })
322
435
  return { ok: false, error: e?.message ?? "Onbekende fout" };
323
436
  }
324
437
  });
438
+ /**
439
+ * Bepaalt de base-URL waar pg_cron de Lovable-app moet aanroepen.
440
+ *
441
+ * Volgorde (eerste hit wint):
442
+ * 1. `FLORIDAY_CRON_BASE_URL` — custom domain of override
443
+ * 2. `LOVABLE_PROJECT_ID` → `https://project--{id}.lovable.app`
444
+ * 3. Hardcoded fallback voor dít project
445
+ *
446
+ * Bewust NIET terugvallen op `VITE_SUPABASE_PROJECT_ID` /
447
+ * `SUPABASE_PROJECT_ID`: die bevatten de Supabase project-ref, niet de
448
+ * Lovable project-ID — gebruik daarvan was de oorzaak van een stille
449
+ * cron-mismatch waardoor tokens nooit werden ververst.
450
+ */
451
+ const HARDCODED_LOVABLE_PROJECT_ID = "27b6733b-2e2f-4518-b204-681958559055";
452
+ const MANAGED_FLORYDAY_CRON_JOB_NAMES = new Set([
453
+ "refresh-floriday-tokens-hourly",
454
+ "floriday-connections-sync-twice-daily",
455
+ "floriday-trade-items-sync-5min",
456
+ "floriday-salesorders-sync-5min",
457
+ "floriday-customer-organizations-sync-twice-daily",
458
+ "floriday-carriers-sync-twice-daily",
459
+ ]);
460
+ export function resolveFlorydayCronBaseUrl() {
461
+ const custom = process.env.FLORIDAY_CRON_BASE_URL?.trim();
462
+ if (custom)
463
+ return custom.replace(/\/+$/, "");
464
+ const lovableId = process.env.LOVABLE_PROJECT_ID?.trim() || HARDCODED_LOVABLE_PROJECT_ID;
465
+ if (!lovableId) {
466
+ throw new Error("Kan cron-URL niet bepalen: zet FLORIDAY_CRON_BASE_URL of LOVABLE_PROJECT_ID");
467
+ }
468
+ return `https://project--${lovableId}.lovable.app`;
469
+ }
325
470
  async function scheduleFlorydayTokenRefreshInternal(supabase) {
326
- const projectId = process.env.VITE_SUPABASE_PROJECT_ID ?? process.env.SUPABASE_PROJECT_ID;
327
471
  const apikey = process.env.SUPABASE_PUBLISHABLE_KEY ?? process.env.VITE_SUPABASE_PUBLISHABLE_KEY;
328
- if (!projectId)
329
- throw new Error("Project-id ontbreekt in server-env");
330
472
  if (!apikey)
331
473
  throw new Error("Supabase anon key ontbreekt in server-env");
332
- const url = `https://project--${projectId}.lovable.app/api/public/cron/refresh-floriday-tokens`;
474
+ const url = `${resolveFlorydayCronBaseUrl()}/api/public/cron/refresh-floriday-tokens`;
333
475
  const { data, error } = await supabase.rpc("schedule_floriday_token_refresh", { p_url: url, p_apikey: apikey });
334
476
  if (error)
335
477
  throw new Error(error.message);
@@ -449,3 +591,299 @@ export const updateFlorydayPublishingSettings = createServerFn({ method: "POST"
449
591
  throw new Error(error.message);
450
592
  return { ok: true };
451
593
  });
594
+ // ─── Generieke sequence-sync laag ───────────────────────────────────────────
595
+ // Floriday vereist sequence-endpoints i.p.v. GetById: eerst
596
+ // /<entity>/max-sequence, daarna /<entity>/sync/{seq}. Deze helper houdt
597
+ // de cursor in `floriday_sync_state` per (connection, entity).
598
+ const SEQUENCE_BATCH_CAP = 1000;
599
+ async function fetchMaxSequence(supabase, env, maxSequenceEndpoint) {
600
+ const res = await florydayFetch(supabase, env, maxSequenceEndpoint, { cache: "no-store" });
601
+ if (!res.ok) {
602
+ const body = await res.text().catch(() => "");
603
+ throw new Error(`max-sequence [${res.status}]: ${body.slice(0, 200)}`);
604
+ }
605
+ const json = await res.json().catch(() => null);
606
+ if (typeof json === "number")
607
+ return json;
608
+ const n = json?.maxSequenceNumber ?? json?.maxSequence ?? json?.value;
609
+ if (typeof n !== "number") {
610
+ throw new Error(`Onbekende max-sequence response: ${JSON.stringify(json).slice(0, 200)}`);
611
+ }
612
+ return n;
613
+ }
614
+ async function fetchBySequence(supabase, env, bySequenceEndpointBase, fromSequence) {
615
+ const url = `${bySequenceEndpointBase}/${fromSequence}`;
616
+ const res = await florydayFetch(supabase, env, url, { cache: "no-store" });
617
+ if (!res.ok) {
618
+ const body = await res.text().catch(() => "");
619
+ throw new Error(`by-sequence [${res.status}]: ${body.slice(0, 200)}`);
620
+ }
621
+ const json = await res.json().catch(() => null);
622
+ return Array.isArray(json) ? json : Array.isArray(json?.results) ? json.results : [];
623
+ }
624
+ export async function syncFloridayEntityBySequence(supabase, env, connectionId, opts) {
625
+ const t0 = Date.now();
626
+ const { data: stateRow } = await supabase
627
+ .from("floriday_sync_state")
628
+ .select("last_sequence_number")
629
+ .eq("connection_id", connectionId)
630
+ .eq("entity_type", opts.entityType)
631
+ .maybeSingle();
632
+ const startCursor = stateRow?.last_sequence_number ?? 0;
633
+ try {
634
+ // Debug-logging: helpt 401's onderscheid maken tussen scope-issue,
635
+ // expired token en wrong env. Token zelf wordt NIET gelogd.
636
+ try {
637
+ const { data: connRow } = await supabase
638
+ .from("floriday_connections")
639
+ .select("token_expires_at")
640
+ .eq("id", connectionId)
641
+ .maybeSingle();
642
+ console.log("[floriday-sync] resource=%s connection_id=%s scopes=%s expires=%s", opts.entityType, connectionId, DEFAULT_SCOPE, connRow?.token_expires_at ?? "n/a");
643
+ }
644
+ catch {
645
+ /* logging mag nooit de sync breken */
646
+ }
647
+ const maxSeq = await fetchMaxSequence(supabase, env, opts.maxSequenceEndpoint);
648
+ let cursor = startCursor;
649
+ let totalSynced = 0;
650
+ let safety = 0;
651
+ while (cursor < maxSeq && safety < 1000) {
652
+ safety++;
653
+ const batch = await fetchBySequence(supabase, env, opts.bySequenceEndpointBase, cursor);
654
+ if (batch.length === 0)
655
+ break;
656
+ const highest = batch.reduce((m, item) => {
657
+ const s = Number(item?.sequenceNumber ?? item?.sequence_number ?? 0);
658
+ return s > m ? s : m;
659
+ }, cursor);
660
+ await opts.onBatch(batch, highest);
661
+ totalSynced += batch.length;
662
+ if (highest <= cursor)
663
+ break;
664
+ cursor = highest;
665
+ if (batch.length < SEQUENCE_BATCH_CAP)
666
+ break;
667
+ }
668
+ await supabase
669
+ .from("floriday_sync_state")
670
+ .upsert({
671
+ connection_id: connectionId,
672
+ entity_type: opts.entityType,
673
+ last_sequence_number: cursor,
674
+ last_synced_at: new Date().toISOString(),
675
+ last_error: null,
676
+ });
677
+ return {
678
+ synced: totalSynced,
679
+ fromSequence: startCursor,
680
+ toSequence: cursor,
681
+ durationMs: Date.now() - t0,
682
+ };
683
+ }
684
+ catch (e) {
685
+ const msg = e?.message ?? String(e);
686
+ await supabase
687
+ .from("floriday_sync_state")
688
+ .upsert({
689
+ connection_id: connectionId,
690
+ entity_type: opts.entityType,
691
+ last_sequence_number: startCursor,
692
+ last_error: msg.slice(0, 500),
693
+ });
694
+ throw e;
695
+ }
696
+ }
697
+ // ─── Connections sync (productie) ───────────────────────────────────────────
698
+ // In-memory cache voor organisatienamen — TTL 24u, per Worker-instance.
699
+ // Voorkomt dat we voor elke connection-sync opnieuw /organizations/{id} aanroepen.
700
+ const ORG_NAME_TTL_MS = 24 * 60 * 60 * 1000;
701
+ const orgNameCache = new Map();
702
+ async function fetchBuyerOrganizationName(supabase, env, orgId) {
703
+ const key = `${env}:${orgId}`;
704
+ const hit = orgNameCache.get(key);
705
+ if (hit && Date.now() - hit.at < ORG_NAME_TTL_MS)
706
+ return hit.name;
707
+ try {
708
+ const res = await florydayFetch(supabase, env, `/${SUPPLIERS_API_VERSION}/organizations/${orgId}`, { cache: "no-store" });
709
+ if (!res.ok) {
710
+ const body = await res.text().catch(() => "");
711
+ console.error(`[floriday] organizations/${orgId} failed [${res.status}]: ${body.slice(0, 200)}`);
712
+ orgNameCache.set(key, { name: null, at: Date.now() });
713
+ return null;
714
+ }
715
+ const json = await res.json().catch(() => null);
716
+ const name = json?.name ?? json?.commercialName ?? json?.organizationName ?? null;
717
+ if (!name) {
718
+ console.error(`[floriday] organizations/${orgId} OK but no name field; keys=${Object.keys(json ?? {}).join(",")}`);
719
+ }
720
+ orgNameCache.set(key, { name, at: Date.now() });
721
+ return name;
722
+ }
723
+ catch (e) {
724
+ console.error(`[floriday] organizations/${orgId} threw: ${e?.message ?? e}`);
725
+ orgNameCache.set(key, { name: null, at: Date.now() });
726
+ return null;
727
+ }
728
+ }
729
+ export async function runFloridayConnectionsSync(supabase, env) {
730
+ const row = await getActiveFlorydayCredentials(supabase, env);
731
+ if (!row)
732
+ throw new Error(`Geen actieve Floriday-koppeling voor ${env}`);
733
+ return syncFloridayEntityBySequence(supabase, env, row.id, {
734
+ entityType: "connections",
735
+ maxSequenceEndpoint: `/${SUPPLIERS_API_VERSION}/connections/current-max-sequence`,
736
+ bySequenceEndpointBase: `/${SUPPLIERS_API_VERSION}/connections/sync`,
737
+ onBatch: async (items, _highest) => {
738
+ const baseRows = items
739
+ .map((item) => {
740
+ const fid = item?.customerOrganizationId ?? null;
741
+ if (!fid)
742
+ return null;
743
+ return {
744
+ connection_id: row.id,
745
+ floriday_connection_id: String(fid),
746
+ sequence_number: Number(item?.sequenceNumber ?? 0),
747
+ buyer_organization_id: String(fid),
748
+ status: null,
749
+ is_deleted: Boolean(item?.isDeleted),
750
+ data: item,
751
+ synced_at: new Date().toISOString(),
752
+ };
753
+ })
754
+ .filter(Boolean);
755
+ if (baseRows.length === 0)
756
+ return;
757
+ // Verzamel unieke buyer_organization_ids en haal namen op (parallel, max 5 tegelijk).
758
+ const uniqueIds = Array.from(new Set(baseRows.map((r) => r.buyer_organization_id)));
759
+ const nameMap = new Map();
760
+ const CONCURRENCY = 5;
761
+ for (let i = 0; i < uniqueIds.length; i += CONCURRENCY) {
762
+ const slice = uniqueIds.slice(i, i + CONCURRENCY);
763
+ const results = await Promise.all(slice.map((id) => fetchBuyerOrganizationName(supabase, env, id)));
764
+ slice.forEach((id, idx) => nameMap.set(id, results[idx] ?? null));
765
+ }
766
+ const rows = baseRows.map((r) => ({
767
+ ...r,
768
+ buyer_organization_name: nameMap.get(r.buyer_organization_id) ?? null,
769
+ }));
770
+ const { error } = await supabase
771
+ .from("floriday_connections_cache")
772
+ .upsert(rows, { onConflict: "connection_id,floriday_connection_id" });
773
+ if (error)
774
+ throw new Error(`cache upsert: ${error.message}`);
775
+ },
776
+ });
777
+ }
778
+ export const syncFlorydayConnections = createServerFn({ method: "POST" })
779
+ .middleware([requireSupabaseAuth])
780
+ .inputValidator((input) => {
781
+ if (!["staging", "live"].includes(input.environment)) {
782
+ throw new Error("Ongeldige environment");
783
+ }
784
+ return input;
785
+ })
786
+ .handler(async ({ data, context }) => {
787
+ try {
788
+ const result = await runFloridayConnectionsSync(context.supabase, data.environment);
789
+ return { ok: true, ...result };
790
+ }
791
+ catch (e) {
792
+ return { ok: false, error: e?.message ?? "Onbekende fout" };
793
+ }
794
+ });
795
+ export const getFlorydaySyncStatus = createServerFn({ method: "POST" })
796
+ .middleware([requireSupabaseAuth])
797
+ .inputValidator((input) => {
798
+ if (!["staging", "live"].includes(input.environment)) {
799
+ throw new Error("Ongeldige environment");
800
+ }
801
+ return input;
802
+ })
803
+ .handler(async ({ data, context }) => {
804
+ const row = await getActiveFlorydayCredentials(context.supabase, data.environment);
805
+ if (!row)
806
+ return { ok: false, error: `Geen actieve koppeling voor ${data.environment}` };
807
+ const { data: states } = await context.supabase
808
+ .from("floriday_sync_state")
809
+ .select("entity_type, last_sequence_number, last_synced_at, last_error")
810
+ .eq("connection_id", row.id);
811
+ const CACHE_TABLE = {
812
+ "connections": "floriday_connections_cache",
813
+ "trade-items": "floriday_trade_items_cache",
814
+ "sales-orders": "floriday_salesorders_cache",
815
+ "customer-organizations": "floriday_customers_cache",
816
+ "carriers": "floriday_carriers_cache",
817
+ };
818
+ const statuses = [];
819
+ for (const s of (states ?? [])) {
820
+ let cached_count = 0;
821
+ const table = CACHE_TABLE[s.entity_type];
822
+ if (table) {
823
+ const { count } = await context.supabase
824
+ .from(table)
825
+ .select("connection_id", { count: "exact", head: true })
826
+ .eq("connection_id", row.id);
827
+ cached_count = count ?? 0;
828
+ }
829
+ statuses.push({
830
+ entity_type: s.entity_type,
831
+ last_sequence_number: s.last_sequence_number,
832
+ last_synced_at: s.last_synced_at,
833
+ last_error: s.last_error,
834
+ cached_count,
835
+ });
836
+ }
837
+ return { ok: true, statuses };
838
+ });
839
+ async function scheduleFlorydayConnectionsSyncInternal(supabase) {
840
+ const apikey = process.env.SUPABASE_PUBLISHABLE_KEY ?? process.env.VITE_SUPABASE_PUBLISHABLE_KEY;
841
+ if (!apikey)
842
+ throw new Error("Supabase anon key ontbreekt in server-env");
843
+ const url = `${resolveFlorydayCronBaseUrl()}/api/public/cron/sync-floriday-connections`;
844
+ const { data, error } = await supabase.rpc("schedule_floriday_connections_sync", { p_url: url, p_apikey: apikey });
845
+ if (error)
846
+ throw new Error(error.message);
847
+ return { jobid: data, url };
848
+ }
849
+ export const setupFlorydayConnectionsSyncCron = createServerFn({ method: "POST" })
850
+ .middleware([requireSupabaseAuth])
851
+ .handler(async ({ context }) => {
852
+ const { jobid, url } = await scheduleFlorydayConnectionsSyncInternal(context.supabase);
853
+ return { ok: true, jobid, url };
854
+ });
855
+ /**
856
+ * Geeft een snapshot van de geplande Floriday-cron jobs zodat de UI kan
857
+ * tonen welke URL is ingepland (zo zien we direct als er een verkeerde
858
+ * project-ID in de cron staat zoals in de oude bug).
859
+ */
860
+ export const getFlorydayCronStatus = createServerFn({ method: "GET" })
861
+ .middleware([requireSupabaseAuth])
862
+ .handler(async ({ context }) => {
863
+ const expectedBase = (() => {
864
+ try {
865
+ return resolveFlorydayCronBaseUrl();
866
+ }
867
+ catch {
868
+ return null;
869
+ }
870
+ })();
871
+ const { data, error } = await context.supabase
872
+ .rpc("get_floriday_cron_jobs");
873
+ if (error) {
874
+ return { ok: false, error: error.message, expectedBase };
875
+ }
876
+ const jobs = (data ?? []).filter((job) => MANAGED_FLORYDAY_CRON_JOB_NAMES.has(job.jobname));
877
+ return { ok: true, jobs, expectedBase };
878
+ });
879
+ /**
880
+ * Herinstalleert beide Floriday-crons in één call met de correct
881
+ * gedetecteerde base-URL. Idempotent: bestaande jobs worden overschreven.
882
+ */
883
+ export const reinstallFlorydayCrons = createServerFn({ method: "POST" })
884
+ .middleware([requireSupabaseAuth])
885
+ .handler(async ({ context }) => {
886
+ const token = await scheduleFlorydayTokenRefreshInternal(context.supabase);
887
+ const conn = await scheduleFlorydayConnectionsSyncInternal(context.supabase);
888
+ return { ok: true, token, connections: conn };
889
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/floriday-authenticatie-module",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "main": "./dist-lib/index.js",
@@ -37,7 +37,7 @@
37
37
  "devDependencies": {
38
38
  "@flowselections/core": "^1.0.11",
39
39
  "@eslint/js": "^9.32.0",
40
- "@lovable.dev/vite-tanstack-config": "^1.2.0",
40
+ "@lovable.dev/vite-tanstack-config": "2.5.3",
41
41
  "@types/node": "^22.16.5",
42
42
  "@types/react": "^19.2.0",
43
43
  "@types/react-dom": "^19.2.0",
@@ -58,7 +58,8 @@
58
58
  "tw-animate-css": "^1.3.4",
59
59
  "vite-tsconfig-paths": "^6.0.2",
60
60
  "react": "^19.2.0",
61
- "react-dom": "^19.2.0"
61
+ "react-dom": "^19.2.0",
62
+ "nitro": "3.0.260603-beta"
62
63
  },
63
64
  "private": false,
64
65
  "flowselections": {
@@ -66,7 +67,7 @@
66
67
  "pages": [
67
68
  {
68
69
  "export": "FlorydayConnectionPage",
69
- "route": "floriday-authenticatie",
70
+ "route": "floriday-connection",
70
71
  "title": "Connectie"
71
72
  }
72
73
  ]