@flowselections/floriday-authenticatie-module 1.0.13 → 1.0.14

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 (48) hide show
  1. package/dist-lib/components/floriday/FlorydaySyncHealthBanner.d.ts +20 -0
  2. package/dist-lib/components/floriday/FlorydaySyncHealthBanner.d.ts.map +1 -0
  3. package/dist-lib/components/floriday/FlorydaySyncHealthBanner.js +35 -0
  4. package/dist-lib/components/settings/FlorydaySettingsCard.d.ts.map +1 -1
  5. package/dist-lib/components/settings/FlorydaySettingsCard.js +75 -53
  6. package/dist-lib/integrations/supabase/auth-middleware.d.ts +6121 -2360
  7. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  8. package/dist-lib/integrations/supabase/client.d.ts +6121 -2360
  9. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  10. package/dist-lib/integrations/supabase/client.server.d.ts +6121 -2360
  11. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  12. package/dist-lib/integrations/supabase/types.d.ts +6259 -2382
  13. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  14. package/dist-lib/integrations/supabase/types.js +10 -1
  15. package/dist-lib/lib/floricode-product-features.d.ts +50 -0
  16. package/dist-lib/lib/floricode-product-features.d.ts.map +1 -0
  17. package/dist-lib/lib/floricode-product-features.js +314 -0
  18. package/dist-lib/lib/floricode-regulatory-features.d.ts +41 -0
  19. package/dist-lib/lib/floricode-regulatory-features.d.ts.map +1 -0
  20. package/dist-lib/lib/floricode-regulatory-features.js +226 -0
  21. package/dist-lib/lib/floriday-auction-letters.functions.d.ts +9307 -0
  22. package/dist-lib/lib/floriday-auction-letters.functions.d.ts.map +1 -0
  23. package/dist-lib/lib/floriday-auction-letters.functions.js +129 -0
  24. package/dist-lib/lib/floriday-gateway-hmac.d.ts +29 -0
  25. package/dist-lib/lib/floriday-gateway-hmac.d.ts.map +1 -0
  26. package/dist-lib/lib/floriday-gateway-hmac.js +119 -0
  27. package/dist-lib/lib/floriday-labels.functions.d.ts +9312 -0
  28. package/dist-lib/lib/floriday-labels.functions.d.ts.map +1 -0
  29. package/dist-lib/lib/floriday-labels.functions.js +141 -0
  30. package/dist-lib/lib/floriday-public.functions.d.ts +55883 -0
  31. package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -0
  32. package/dist-lib/lib/floriday-public.functions.js +241 -0
  33. package/dist-lib/lib/floriday-scopes.d.ts +20 -0
  34. package/dist-lib/lib/floriday-scopes.d.ts.map +1 -0
  35. package/dist-lib/lib/floriday-scopes.js +89 -0
  36. package/dist-lib/lib/floriday-stickers.functions.d.ts +9304 -0
  37. package/dist-lib/lib/floriday-stickers.functions.d.ts.map +1 -0
  38. package/dist-lib/lib/floriday-stickers.functions.js +128 -0
  39. package/dist-lib/lib/floriday-sync.functions.d.ts +240368 -1916
  40. package/dist-lib/lib/floriday-sync.functions.d.ts.map +1 -1
  41. package/dist-lib/lib/floriday-sync.functions.js +836 -35
  42. package/dist-lib/lib/floriday-writes.functions.d.ts +55812 -0
  43. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -0
  44. package/dist-lib/lib/floriday-writes.functions.js +160 -0
  45. package/dist-lib/lib/floriday.functions.d.ts +125816 -46888
  46. package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
  47. package/dist-lib/lib/floriday.functions.js +451 -58
  48. package/package.json +2 -2
@@ -6,6 +6,7 @@
6
6
  // ============================================================================
7
7
  import { createServerFn } from "@tanstack/react-start";
8
8
  import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
9
+ import { FLORYDAY_CLOCK_SUPPLY_PERMISSION_MESSAGE, FLORYDAY_CONTRACT_PERMISSION_MESSAGE, FLORYDAY_STANDARD_SCOPE, getFlorydayScopeForPath, isClockSupplyScope, isContractScope, } from "./floriday-scopes";
9
10
  /**
10
11
  * Valideert een GS1 GLN: exact 13 cijfers met geldige check digit.
11
12
  * Retourneert null als geldig, anders een nl-foutmelding.
@@ -37,7 +38,10 @@ const FLORYDAY_ENDPOINTS = {
37
38
  apiBaseUrl: "https://api.floriday.io",
38
39
  },
39
40
  };
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
+ // LET OP: houd scopes centraal in `floriday-scopes.ts`. De standaard token-
42
+ // refresh gebruikt bewust alleen basisrechten; module-specifieke rechten zoals
43
+ // Klokaanvoer worden pas aangevraagd voor endpoints die ze nodig hebben.
44
+ const DEFAULT_SCOPE = FLORYDAY_STANDARD_SCOPE;
41
45
  export const SUPPLIERS_API_VERSION = "suppliers-api-2026v1";
42
46
  const TOKEN_REFRESH_LEEWAY_S = 300;
43
47
  function mask(value, keep = 4) {
@@ -103,14 +107,16 @@ export async function getFlorydayAccessToken(supabase, env, options = {}) {
103
107
  const row = await getActiveFlorydayCredentials(supabase, env);
104
108
  if (!row)
105
109
  throw new Error(`Geen actieve Floriday-koppeling voor ${env}`);
110
+ const requestedScope = options.scope ?? DEFAULT_SCOPE;
111
+ const canReuseStoredToken = requestedScope === DEFAULT_SCOPE;
106
112
  const now = Date.now();
107
113
  const expiresAt = row.token_expires_at ? Date.parse(row.token_expires_at) : 0;
108
- if (!options.forceRefresh && row.access_token && expiresAt - TOKEN_REFRESH_LEEWAY_S * 1000 > now) {
114
+ if (canReuseStoredToken && !options.forceRefresh && row.access_token && expiresAt - TOKEN_REFRESH_LEEWAY_S * 1000 > now) {
109
115
  return row.access_token;
110
116
  }
111
117
  // Concurrency-guard: vlak voor de tokenrequest opnieuw lezen — een parallelle worker
112
118
  // kan zojuist een fresh token hebben geschreven en dan hoeven we niet nóg eens te refreshen.
113
- if (!options.forceRefresh) {
119
+ if (canReuseStoredToken && !options.forceRefresh) {
114
120
  const { data: fresh } = await supabase
115
121
  .from("floriday_connections")
116
122
  .select("access_token, token_expires_at")
@@ -134,19 +140,21 @@ export async function getFlorydayAccessToken(supabase, env, options = {}) {
134
140
  grant_type: "client_credentials",
135
141
  client_id: deepSanitizeCredential(row.client_id),
136
142
  client_secret: deepSanitizeCredential(row.client_secret),
137
- scope: DEFAULT_SCOPE,
143
+ scope: requestedScope,
138
144
  }).toString(),
139
145
  });
140
146
  const text = await tokenRes.text();
141
147
  if (!tokenRes.ok) {
142
- throw new Error(`Floriday token-aanvraag mislukt [${tokenRes.status}]: ${text.slice(0, 300)}`);
148
+ throw createFlorydayTokenError(tokenRes.status, text, requestedScope);
143
149
  }
144
150
  json = JSON.parse(text);
145
151
  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);
152
+ if (canReuseStoredToken) {
153
+ await supabase
154
+ .from("floriday_connections")
155
+ .update({ access_token: json.access_token, token_expires_at: newExpiresAt })
156
+ .eq("id", row.id);
157
+ }
150
158
  // Best-effort logging (faalt nooit hard).
151
159
  await supabase.from("floriday_token_refresh_log").insert({
152
160
  connection_id: row.id,
@@ -166,6 +174,19 @@ export async function getFlorydayAccessToken(supabase, env, options = {}) {
166
174
  }
167
175
  return json.access_token;
168
176
  }
177
+ function createFlorydayTokenError(status, body, scope) {
178
+ const normalized = body.toLowerCase();
179
+ const policyDenied = normalized.includes("access_denied") || normalized.includes("policy evaluation failed");
180
+ if (status === 401 && policyDenied) {
181
+ if (isClockSupplyScope(scope)) {
182
+ return new Error(FLORYDAY_CLOCK_SUPPLY_PERMISSION_MESSAGE);
183
+ }
184
+ if (isContractScope(scope)) {
185
+ return new Error(FLORYDAY_CONTRACT_PERMISSION_MESSAGE);
186
+ }
187
+ }
188
+ return new Error(`Floriday token-aanvraag mislukt [${status}]: ${body.slice(0, 300)}`);
189
+ }
169
190
  async function fetchAndStoreFlorydayOrganizationName(supabase, env, connectionId) {
170
191
  try {
171
192
  // Stap 1: bepaal de eigen organizationId via trade-settings.
@@ -249,18 +270,33 @@ export async function florydayFetch(supabase, pathOrEnv, pathOrInit, maybeInit =
249
270
  headers.set("Accept", "application/json");
250
271
  return fetch(url, { ...init, headers });
251
272
  };
252
- let token = await getFlorydayAccessToken(supabase, env);
273
+ const tokenScope = getFlorydayScopeForPath(path, init.method ?? "GET");
274
+ let token = await getFlorydayAccessToken(supabase, env, { scope: tokenScope });
253
275
  let res = await doFetch(token);
254
276
  // Auto-retry op 401/403 met een force-refreshed token. Dit is het vangnet
255
277
  // voor het geval de cron faalt of een race-condition ons toch een verlopen
256
278
  // token gaf. Body wordt niet gelezen vóór de retry — als de caller .json()
257
279
  // doet werkt dat normaal op het nieuwe Response object.
258
280
  if (res.status === 401 || res.status === 403) {
259
- token = await getFlorydayAccessToken(supabase, env, { forceRefresh: true });
281
+ token = await getFlorydayAccessToken(supabase, env, { forceRefresh: true, scope: tokenScope });
260
282
  res = await doFetch(token);
261
283
  if (res.status === 401 || res.status === 403) {
262
284
  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)}`);
285
+ // Scope-specifieke meldingen alleen bij 401 (= token/scope-probleem).
286
+ // Bij een persistente 403 ná token-refresh is het bijna altijd een
287
+ // verkeerd pad of een rechten-issue op de resource zelf; dan willen
288
+ // we de Floriday-response één-op-één laten zien, niet een misleidende
289
+ // "controleer rechten"-tekst.
290
+ if (res.status === 401) {
291
+ if (isClockSupplyScope(tokenScope)) {
292
+ throw new Error(FLORYDAY_CLOCK_SUPPLY_PERMISSION_MESSAGE);
293
+ }
294
+ if (isContractScope(tokenScope)) {
295
+ throw new Error(FLORYDAY_CONTRACT_PERMISSION_MESSAGE);
296
+ }
297
+ throw new Error(`Floriday API 401 na vernieuwen van de verbinding. Body: ${body.slice(0, 300)}`);
298
+ }
299
+ throw new Error(`Floriday weigert ${(init.method ?? "GET").toUpperCase()} ${path} (403 Forbidden). Body: ${body.slice(0, 300)}`);
264
300
  }
265
301
  }
266
302
  return res;
@@ -370,6 +406,24 @@ export const upsertFlorydayConnection = createServerFn({ method: "POST" })
370
406
  "sales-orders",
371
407
  "customer-organizations",
372
408
  "carriers",
409
+ "fulfillment-orders",
410
+ "customer-stickers",
411
+ "warehouses",
412
+ "packings",
413
+ "delivery-locations",
414
+ "clock-supplies",
415
+ "delivery-orders",
416
+ "clock-presales",
417
+ "contracts",
418
+ "blanket-orders",
419
+ "additional-services",
420
+ "commercial-service-types",
421
+ "collections",
422
+ "price-groups",
423
+ "delivery-condition-sets",
424
+ "sales-strategies",
425
+ "base-items",
426
+ "contract-trade-item-groups",
373
427
  ];
374
428
  for (const r of resources) {
375
429
  try {
@@ -379,6 +433,14 @@ export const upsertFlorydayConnection = createServerFn({ method: "POST" })
379
433
  console.error(`[floriday] auto-schedule ${r} cron failed:`, e);
380
434
  }
381
435
  }
436
+ // Plan ook de dagelijkse referentiedata-refresh (schema + code-lijsten)
437
+ try {
438
+ const { scheduleFlorydayReferenceDataRefreshInternal } = await import("./floriday-sync.functions");
439
+ await scheduleFlorydayReferenceDataRefreshInternal(context.supabase);
440
+ }
441
+ catch (e) {
442
+ console.error("[floriday] auto-schedule reference-data cron failed:", e);
443
+ }
382
444
  }
383
445
  catch (e) {
384
446
  console.error("[floriday] auto-schedule resource crons failed:", e);
@@ -456,6 +518,27 @@ const MANAGED_FLORYDAY_CRON_JOB_NAMES = new Set([
456
518
  "floriday-salesorders-sync-5min",
457
519
  "floriday-customer-organizations-sync-twice-daily",
458
520
  "floriday-carriers-sync-twice-daily",
521
+ "floriday-fulfillment-orders-sync-5min",
522
+ "floriday-customer-stickers-sync-5min",
523
+ "floriday-warehouses-sync-twice-daily",
524
+ "floriday-packings-sync-daily",
525
+ "floriday-delivery-locations-sync-twice-daily",
526
+ "floriday-clock-supplies-sync-5min",
527
+ "floriday-delivery-orders-sync-5min",
528
+ "floriday-clock-presales-sync-5min",
529
+ "floriday-contracts-sync-hourly",
530
+ "floriday-blanket-orders-sync-15min",
531
+ // Stamdata
532
+ "floriday-additional-services-sync-hourly",
533
+ "floriday-commercial-service-types-sync-hourly",
534
+ "floriday-collections-sync-hourly",
535
+ "floriday-price-groups-sync-hourly",
536
+ "floriday-delivery-condition-sets-sync-hourly",
537
+ "floriday-sales-strategies-sync-hourly",
538
+ "floriday-base-items-sync-hourly",
539
+ "floriday-contract-trade-item-groups-sync-hourly",
540
+ // Referentiedata (schema + code-lijsten)
541
+ "floriday-reference-data-refresh-daily",
459
542
  ]);
460
543
  export function resolveFlorydayCronBaseUrl() {
461
544
  const custom = process.env.FLORIDAY_CRON_BASE_URL?.trim();
@@ -651,8 +734,13 @@ export async function syncFloridayEntityBySequence(supabase, env, connectionId,
651
734
  while (cursor < maxSeq && safety < 1000) {
652
735
  safety++;
653
736
  const batch = await fetchBySequence(supabase, env, opts.bySequenceEndpointBase, cursor);
654
- if (batch.length === 0)
737
+ if (batch.length === 0) {
738
+ // Floriday-richtlijn: bij een lege batch zijn er geen nieuwe wijzigingen
739
+ // tot het huidige max-sequence-punt. Cursor doorzetten zodat we niet
740
+ // elke run dezelfde lege call doen.
741
+ cursor = maxSeq;
655
742
  break;
743
+ }
656
744
  const highest = batch.reduce((m, item) => {
657
745
  const s = Number(item?.sequenceNumber ?? item?.sequence_number ?? 0);
658
746
  return s > m ? s : m;
@@ -662,16 +750,22 @@ export async function syncFloridayEntityBySequence(supabase, env, connectionId,
662
750
  if (highest <= cursor)
663
751
  break;
664
752
  cursor = highest;
665
- if (batch.length < SEQUENCE_BATCH_CAP)
753
+ if (batch.length < SEQUENCE_BATCH_CAP) {
754
+ // Kleine batch ⇒ caught up tot maxSeq op moment van eerste call.
755
+ cursor = Math.max(cursor, maxSeq);
666
756
  break;
757
+ }
667
758
  }
759
+ const nowIso = new Date().toISOString();
668
760
  await supabase
669
761
  .from("floriday_sync_state")
670
762
  .upsert({
671
763
  connection_id: connectionId,
672
764
  entity_type: opts.entityType,
673
765
  last_sequence_number: cursor,
674
- last_synced_at: new Date().toISOString(),
766
+ last_synced_at: nowIso,
767
+ last_success_at: nowIso,
768
+ last_items_processed: totalSynced,
675
769
  last_error: null,
676
770
  });
677
771
  return {
@@ -695,36 +789,32 @@ export async function syncFloridayEntityBySequence(supabase, env, connectionId,
695
789
  }
696
790
  }
697
791
  // ─── 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;
792
+ //
793
+ // Floriday-richtlijn: gebruik altijd sequence-endpoints en vermijd per-id
794
+ // `GET /organizations/{id}` aanroepen. De klantnaam wordt daarom lokaal
795
+ // opgehaald uit `floriday_customers_cache`, die wordt gevuld door de eigen
796
+ // `customer-organizations` sync via `/organizations/sync` (CUSTOMER-filter).
797
+ async function lookupBuyerOrganizationNames(supabase, ids) {
798
+ const map = new Map();
799
+ if (ids.length === 0)
800
+ return map;
801
+ // .in() heeft een PostgREST-limiet; batch in stukken van 200.
802
+ const CHUNK = 200;
803
+ for (let i = 0; i < ids.length; i += CHUNK) {
804
+ const slice = ids.slice(i, i + CHUNK);
805
+ const { data, error } = await supabase
806
+ .from("floriday_customers_cache")
807
+ .select("organization_id, name")
808
+ .in("organization_id", slice);
809
+ if (error) {
810
+ console.error(`[floriday] customers-cache lookup faalde voor ${slice.length} ids: ${error.message}`);
811
+ continue;
714
812
  }
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(",")}`);
813
+ for (const row of (data ?? [])) {
814
+ map.set(row.organization_id, row.name ?? null);
719
815
  }
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
816
  }
817
+ return map;
728
818
  }
729
819
  export async function runFloridayConnectionsSync(supabase, env) {
730
820
  const row = await getActiveFlorydayCredentials(supabase, env);
@@ -754,15 +844,9 @@ export async function runFloridayConnectionsSync(supabase, env) {
754
844
  .filter(Boolean);
755
845
  if (baseRows.length === 0)
756
846
  return;
757
- // Verzamel unieke buyer_organization_ids en haal namen op (parallel, max 5 tegelijk).
847
+ // Naam-enrichment uit lokale customers-cache (geen externe API-call).
758
848
  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
- }
849
+ const nameMap = await lookupBuyerOrganizationNames(supabase, uniqueIds);
766
850
  const rows = baseRows.map((r) => ({
767
851
  ...r,
768
852
  buyer_organization_name: nameMap.get(r.buyer_organization_id) ?? null,
@@ -814,18 +898,38 @@ export const getFlorydaySyncStatus = createServerFn({ method: "POST" })
814
898
  "sales-orders": "floriday_salesorders_cache",
815
899
  "customer-organizations": "floriday_customers_cache",
816
900
  "carriers": "floriday_carriers_cache",
901
+ "fulfillment-orders": "floriday_fulfillment_orders_cache",
902
+ "customer-stickers": "floriday_customer_stickers_cache",
903
+ "warehouses": "floriday_warehouses_cache",
904
+ "packings": "floriday_packings_cache",
905
+ "delivery-locations": "floriday_delivery_locations_cache",
906
+ "clock-supplies": "floriday_clock_supplies_cache",
907
+ "delivery-orders": "floriday_delivery_orders_cache",
908
+ "clock-presales": "floriday_clock_presales_cache",
909
+ "contracts": "floriday_contracts_cache",
910
+ "blanket-orders": "floriday_blanket_orders_cache",
911
+ "additional-services": "floriday_additional_services_cache",
912
+ "commercial-service-types": "floriday_commercial_service_types_cache",
913
+ "collections": "floriday_collections_cache",
914
+ "price-groups": "floriday_price_groups_cache",
915
+ "delivery-condition-sets": "floriday_delivery_condition_sets_cache",
916
+ "sales-strategies": "floriday_sales_strategies_cache",
917
+ "base-items": "floriday_base_items_cache",
918
+ "contract-trade-item-groups": "floriday_contract_trade_item_groups_cache",
817
919
  };
818
920
  const statuses = [];
819
921
  for (const s of (states ?? [])) {
820
- let cached_count = 0;
821
922
  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
- }
923
+ // Verberg legacy/onbeheerde entity-types (bv. oude agreements, trips,
924
+ // invoices, daytrade-offerings) zodat de UI alleen actuele bronnen toont.
925
+ if (!table)
926
+ continue;
927
+ let cached_count = 0;
928
+ const { count } = await context.supabase
929
+ .from(table)
930
+ .select("connection_id", { count: "exact", head: true })
931
+ .eq("connection_id", row.id);
932
+ cached_count = count ?? 0;
829
933
  statuses.push({
830
934
  entity_type: s.entity_type,
831
935
  last_sequence_number: s.last_sequence_number,
@@ -887,3 +991,292 @@ export const reinstallFlorydayCrons = createServerFn({ method: "POST" })
887
991
  const conn = await scheduleFlorydayConnectionsSyncInternal(context.supabase);
888
992
  return { ok: true, token, connections: conn };
889
993
  });
994
+ async function hashIdempotencyKey(input) {
995
+ // Web-Crypto is beschikbaar in de Worker-runtime; geen Node-crypto nodig.
996
+ const bytes = new TextEncoder().encode(input);
997
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
998
+ const hex = Array.from(new Uint8Array(digest))
999
+ .map((b) => b.toString(16).padStart(2, "0"))
1000
+ .join("");
1001
+ return hex.slice(0, 32);
1002
+ }
1003
+ /**
1004
+ * Eén centrale schrijfactie naar Floriday voor álle modules.
1005
+ *
1006
+ * - Token-refresh, X-Api-Key en 401-retry komen gratis (via florydayFetch).
1007
+ * - Idempotency-Key wordt automatisch berekend uit (module, action, body)
1008
+ * als de caller er geen meegeeft — Floriday accepteert deze header voor
1009
+ * POST/PUT/PATCH/DELETE en geeft op herhaling dezelfde response.
1010
+ * - Iedere poging — geslaagd of niet — landt in `floriday_write_log`.
1011
+ * - Bij duplicate idempotency-key wordt de eerdere log-rij teruggegeven
1012
+ * zonder Floriday opnieuw aan te roepen.
1013
+ */
1014
+ export async function florydayWrite(supabase, env, opts) {
1015
+ const conn = await getActiveFlorydayCredentials(supabase, env);
1016
+ if (!conn) {
1017
+ return {
1018
+ ok: false,
1019
+ status: 0,
1020
+ data: null,
1021
+ attemptId: null,
1022
+ error: `Geen actieve Floriday-koppeling voor ${env}`,
1023
+ };
1024
+ }
1025
+ const idemKey = opts.idempotencyKey ??
1026
+ (await hashIdempotencyKey(`${opts.module}|${opts.action}|${opts.referenceId ?? ""}|${JSON.stringify(opts.body ?? null)}`));
1027
+ // Idempotency-guard: zelfde (connection, module, action, key) al verwerkt?
1028
+ const { data: existing } = await supabase
1029
+ .from("floriday_write_log")
1030
+ .select("id, ok, http_status, response, error")
1031
+ .eq("connection_id", conn.id)
1032
+ .eq("module", opts.module)
1033
+ .eq("action", opts.action)
1034
+ .eq("idempotency_key", idemKey)
1035
+ .maybeSingle();
1036
+ if (existing) {
1037
+ return {
1038
+ ok: Boolean(existing.ok),
1039
+ status: Number(existing.http_status ?? 0),
1040
+ data: existing.response ?? null,
1041
+ attemptId: existing.id ?? null,
1042
+ error: existing.error ?? undefined,
1043
+ };
1044
+ }
1045
+ // Pre-flight enum-validatie tegen lokale code-list-cache. Een ongeldige
1046
+ // waarde wordt direct geblokt + gelogd, zonder Floriday-call.
1047
+ if (opts.validateEnums?.length && opts.body != null) {
1048
+ const violation = await validateBodyEnums(supabase, env, opts.body, opts.validateEnums);
1049
+ if (violation) {
1050
+ const errorMsg = `Veld "${violation.field}" heeft ongeldige waarde "${violation.value}" voor lijst "${violation.listName}". Toegestaan: ${violation.allowed.slice(0, 8).join(", ")}${violation.allowed.length > 8 ? "…" : ""}`;
1051
+ const { data: inserted } = await supabase
1052
+ .from("floriday_write_log")
1053
+ .insert({
1054
+ connection_id: conn.id,
1055
+ module: opts.module,
1056
+ action: opts.action,
1057
+ resource: opts.module,
1058
+ floriday_id: opts.referenceId ?? null,
1059
+ request: opts.body ?? null,
1060
+ response: null,
1061
+ http_status: 0,
1062
+ ok: false,
1063
+ error: errorMsg,
1064
+ idempotency_key: idemKey,
1065
+ caller: opts.caller ?? "self",
1066
+ })
1067
+ .select("id")
1068
+ .maybeSingle();
1069
+ return {
1070
+ ok: false,
1071
+ status: 0,
1072
+ data: null,
1073
+ attemptId: inserted?.id ?? null,
1074
+ error: errorMsg,
1075
+ };
1076
+ }
1077
+ }
1078
+ const headers = {
1079
+ "Content-Type": "application/json",
1080
+ "Idempotency-Key": idemKey,
1081
+ };
1082
+ let status = 0;
1083
+ let parsed = null;
1084
+ let text = "";
1085
+ let ok = false;
1086
+ let errMsg;
1087
+ try {
1088
+ const res = await florydayFetch(supabase, env, opts.path, {
1089
+ method: opts.method,
1090
+ headers,
1091
+ body: opts.body == null ? undefined : JSON.stringify(opts.body),
1092
+ });
1093
+ status = res.status;
1094
+ text = await res.text().catch(() => "");
1095
+ parsed = text ? safeJson(text) : null;
1096
+ ok = res.ok;
1097
+ if (!ok)
1098
+ errMsg = `Floriday ${status}: ${text.slice(0, 300)}`;
1099
+ }
1100
+ catch (e) {
1101
+ errMsg = String(e?.message ?? e);
1102
+ }
1103
+ const { data: inserted } = await supabase
1104
+ .from("floriday_write_log")
1105
+ .insert({
1106
+ connection_id: conn.id,
1107
+ module: opts.module,
1108
+ action: opts.action,
1109
+ resource: opts.module,
1110
+ floriday_id: opts.referenceId ?? null,
1111
+ request: opts.body ?? null,
1112
+ response: parsed,
1113
+ http_status: status,
1114
+ ok,
1115
+ error: errMsg ?? null,
1116
+ idempotency_key: idemKey,
1117
+ caller: opts.caller ?? "self",
1118
+ })
1119
+ .select("id")
1120
+ .maybeSingle();
1121
+ return {
1122
+ ok,
1123
+ status,
1124
+ data: parsed,
1125
+ attemptId: inserted?.id ?? null,
1126
+ error: errMsg,
1127
+ };
1128
+ }
1129
+ function safeJson(text) {
1130
+ try {
1131
+ return JSON.parse(text);
1132
+ }
1133
+ catch {
1134
+ return text;
1135
+ }
1136
+ }
1137
+ /**
1138
+ * Loopt door `validateEnums` heen en zoekt de eerste waarde in `body`
1139
+ * die niet voorkomt in de bijbehorende code-lijst. Velden ondersteunen
1140
+ * dot-pad-notatie met `[]` voor arrays, bv. `lines[].salesUnit`.
1141
+ */
1142
+ async function validateBodyEnums(supabase, env, body, rules) {
1143
+ const cache = new Map();
1144
+ const loadList = async (listName) => {
1145
+ if (cache.has(listName))
1146
+ return cache.get(listName);
1147
+ const { data: rows } = await supabase
1148
+ .from("floriday_code_lists_cache")
1149
+ .select("code")
1150
+ .eq("environment", env)
1151
+ .eq("list_name", listName);
1152
+ const codes = (rows ?? []).map((r) => r.code);
1153
+ cache.set(listName, codes);
1154
+ return codes;
1155
+ };
1156
+ const resolvePath = (root, path) => {
1157
+ const parts = path.split(".");
1158
+ let current = [root];
1159
+ for (const part of parts) {
1160
+ const isArr = part.endsWith("[]");
1161
+ const key = isArr ? part.slice(0, -2) : part;
1162
+ const next = [];
1163
+ for (const node of current) {
1164
+ if (node == null || typeof node !== "object")
1165
+ continue;
1166
+ const value = node[key];
1167
+ if (value === undefined || value === null)
1168
+ continue;
1169
+ if (isArr) {
1170
+ if (Array.isArray(value))
1171
+ next.push(...value);
1172
+ }
1173
+ else {
1174
+ next.push(value);
1175
+ }
1176
+ }
1177
+ current = next;
1178
+ }
1179
+ return current;
1180
+ };
1181
+ for (const rule of rules) {
1182
+ const values = resolvePath(body, rule.field);
1183
+ if (values.length === 0)
1184
+ continue;
1185
+ const allowed = await loadList(rule.listName);
1186
+ if (allowed.length === 0)
1187
+ continue; // cache leeg → laat Floriday valideren
1188
+ for (const v of values) {
1189
+ const str = String(v);
1190
+ if (!allowed.includes(str)) {
1191
+ return { field: rule.field, value: str, listName: rule.listName, allowed };
1192
+ }
1193
+ }
1194
+ }
1195
+ return null;
1196
+ }
1197
+ // ─── Sync-gezondheid (gap-detectie) ─────────────────────────────────────────
1198
+ const SYNC_INTERVAL_MIN = {
1199
+ "connections": 720,
1200
+ "trade-items": 5,
1201
+ "sales-orders": 5,
1202
+ "customer-organizations": 720,
1203
+ "carriers": 720,
1204
+ "fulfillment-orders": 5,
1205
+ "customer-stickers": 5,
1206
+ "warehouses": 720,
1207
+ "packings": 1440,
1208
+ "delivery-locations": 720,
1209
+ "clock-supplies": 5,
1210
+ "delivery-orders": 5,
1211
+ "clock-presales": 5,
1212
+ "contracts": 60,
1213
+ "blanket-orders": 15,
1214
+ // Stamdata wijzigt zelden — uur tot dag is voldoende
1215
+ "additional-services": 60,
1216
+ "commercial-service-types": 60,
1217
+ "collections": 60,
1218
+ "price-groups": 60,
1219
+ "delivery-condition-sets": 60,
1220
+ "sales-strategies": 60,
1221
+ "base-items": 60,
1222
+ "contract-trade-item-groups": 60,
1223
+ };
1224
+ export const getFlorydaySyncHealth = createServerFn({ method: "POST" })
1225
+ .middleware([requireSupabaseAuth])
1226
+ .inputValidator((input) => {
1227
+ if (!["staging", "live"].includes(input.environment)) {
1228
+ throw new Error("Ongeldige environment");
1229
+ }
1230
+ return input;
1231
+ })
1232
+ .handler(async ({ data, context }) => {
1233
+ const row = await getActiveFlorydayCredentials(context.supabase, data.environment);
1234
+ if (!row)
1235
+ return { ok: false, error: `Geen actieve koppeling voor ${data.environment}` };
1236
+ const { data: states } = await context.supabase
1237
+ .from("floriday_sync_state")
1238
+ .select("entity_type, last_success_at, last_synced_at, last_error")
1239
+ .eq("connection_id", row.id);
1240
+ const items = [];
1241
+ const now = Date.now();
1242
+ for (const entity of Object.keys(SYNC_INTERVAL_MIN)) {
1243
+ const interval = SYNC_INTERVAL_MIN[entity];
1244
+ const s = (states ?? []).find((r) => r.entity_type === entity);
1245
+ const last = s?.last_success_at ?? s?.last_synced_at ?? null;
1246
+ const minsBehind = last
1247
+ ? Math.max(0, Math.round((now - Date.parse(last)) / 60000) - interval)
1248
+ : 99999;
1249
+ items.push({
1250
+ entity_type: entity,
1251
+ last_success_at: last,
1252
+ expected_interval_min: interval,
1253
+ minutes_behind: minsBehind,
1254
+ is_stale: minsBehind > interval, // > 2× interval achter
1255
+ last_error: s?.last_error ?? null,
1256
+ });
1257
+ }
1258
+ return { ok: true, items };
1259
+ });
1260
+ // ─── Cursor reset (backfill) ────────────────────────────────────────────────
1261
+ export const resetFlorydaySyncCursor = createServerFn({ method: "POST" })
1262
+ .middleware([requireSupabaseAuth])
1263
+ .inputValidator((input) => {
1264
+ if (!["staging", "live"].includes(input.environment)) {
1265
+ throw new Error("Ongeldige environment");
1266
+ }
1267
+ if (!input.entityType?.trim())
1268
+ throw new Error("entityType is verplicht");
1269
+ return input;
1270
+ })
1271
+ .handler(async ({ data, context }) => {
1272
+ const row = await getActiveFlorydayCredentials(context.supabase, data.environment);
1273
+ if (!row)
1274
+ throw new Error(`Geen actieve koppeling voor ${data.environment}`);
1275
+ const { error } = await context.supabase.rpc("reset_floriday_sync_cursor", {
1276
+ p_connection_id: row.id,
1277
+ p_entity_type: data.entityType,
1278
+ });
1279
+ if (error)
1280
+ throw new Error(error.message);
1281
+ return { ok: true };
1282
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/floriday-authenticatie-module",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
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": "2.5.3",
40
+ "@lovable.dev/vite-tanstack-config": "2.6.4",
41
41
  "@types/node": "^22.16.5",
42
42
  "@types/react": "^19.2.0",
43
43
  "@types/react-dom": "^19.2.0",