@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.
- package/dist-lib/components/floriday/FlorydaySyncHealthBanner.d.ts +20 -0
- package/dist-lib/components/floriday/FlorydaySyncHealthBanner.d.ts.map +1 -0
- package/dist-lib/components/floriday/FlorydaySyncHealthBanner.js +35 -0
- package/dist-lib/components/settings/FlorydaySettingsCard.d.ts.map +1 -1
- package/dist-lib/components/settings/FlorydaySettingsCard.js +75 -53
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +6121 -2360
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +6121 -2360
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +6121 -2360
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +6259 -2382
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +10 -1
- package/dist-lib/lib/floricode-product-features.d.ts +50 -0
- package/dist-lib/lib/floricode-product-features.d.ts.map +1 -0
- package/dist-lib/lib/floricode-product-features.js +314 -0
- package/dist-lib/lib/floricode-regulatory-features.d.ts +41 -0
- package/dist-lib/lib/floricode-regulatory-features.d.ts.map +1 -0
- package/dist-lib/lib/floricode-regulatory-features.js +226 -0
- package/dist-lib/lib/floriday-auction-letters.functions.d.ts +9307 -0
- package/dist-lib/lib/floriday-auction-letters.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-auction-letters.functions.js +129 -0
- package/dist-lib/lib/floriday-gateway-hmac.d.ts +29 -0
- package/dist-lib/lib/floriday-gateway-hmac.d.ts.map +1 -0
- package/dist-lib/lib/floriday-gateway-hmac.js +119 -0
- package/dist-lib/lib/floriday-labels.functions.d.ts +9312 -0
- package/dist-lib/lib/floriday-labels.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-labels.functions.js +141 -0
- package/dist-lib/lib/floriday-public.functions.d.ts +55883 -0
- package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-public.functions.js +241 -0
- package/dist-lib/lib/floriday-scopes.d.ts +20 -0
- package/dist-lib/lib/floriday-scopes.d.ts.map +1 -0
- package/dist-lib/lib/floriday-scopes.js +89 -0
- package/dist-lib/lib/floriday-stickers.functions.d.ts +9304 -0
- package/dist-lib/lib/floriday-stickers.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-stickers.functions.js +128 -0
- package/dist-lib/lib/floriday-sync.functions.d.ts +240368 -1916
- package/dist-lib/lib/floriday-sync.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-sync.functions.js +836 -35
- package/dist-lib/lib/floriday-writes.functions.d.ts +55812 -0
- package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-writes.functions.js +160 -0
- package/dist-lib/lib/floriday.functions.d.ts +125816 -46888
- package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday.functions.js +451 -58
- 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
|
-
|
|
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:
|
|
143
|
+
scope: requestedScope,
|
|
138
144
|
}).toString(),
|
|
139
145
|
});
|
|
140
146
|
const text = await tokenRes.text();
|
|
141
147
|
if (!tokenRes.ok) {
|
|
142
|
-
throw
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
699
|
-
//
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
if (
|
|
706
|
-
return
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
|
716
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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.
|
|
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.
|
|
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",
|