@flowselections/floriday-authenticatie-module 1.0.17 → 1.0.18

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 (62) hide show
  1. package/dist-lib/components/floriday/FlorydayConnectionPage.d.ts.map +1 -1
  2. package/dist-lib/components/floriday/FlorydayConnectionPage.js +2 -1
  3. package/dist-lib/components/settings/FloricodeSettingsCard.d.ts +2 -0
  4. package/dist-lib/components/settings/FloricodeSettingsCard.d.ts.map +1 -0
  5. package/dist-lib/components/settings/FloricodeSettingsCard.js +235 -0
  6. package/dist-lib/index.d.ts.map +1 -1
  7. package/dist-lib/index.js +2 -0
  8. package/dist-lib/integrations/supabase/auth-middleware.d.ts +484 -3
  9. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  10. package/dist-lib/integrations/supabase/client.d.ts +484 -3
  11. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  12. package/dist-lib/integrations/supabase/client.server.d.ts +484 -3
  13. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  14. package/dist-lib/integrations/supabase/types.d.ts +492 -3
  15. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  16. package/dist-lib/lib/floricode-credentials.server.d.ts +21 -0
  17. package/dist-lib/lib/floricode-credentials.server.d.ts.map +1 -0
  18. package/dist-lib/lib/floricode-credentials.server.js +158 -0
  19. package/dist-lib/lib/floricode-product-features.d.ts.map +1 -1
  20. package/dist-lib/lib/floricode-product-features.js +19 -33
  21. package/dist-lib/lib/floricode-regulatory-features.d.ts.map +1 -1
  22. package/dist-lib/lib/floricode-regulatory-features.js +51 -41
  23. package/dist-lib/lib/floricode-sync.server.d.ts +35 -0
  24. package/dist-lib/lib/floricode-sync.server.d.ts.map +1 -0
  25. package/dist-lib/lib/floricode-sync.server.js +211 -0
  26. package/dist-lib/lib/floricode.functions.d.ts +78555 -0
  27. package/dist-lib/lib/floricode.functions.d.ts.map +1 -0
  28. package/dist-lib/lib/floricode.functions.js +178 -0
  29. package/dist-lib/lib/floriday-auction-letters.functions.d.ts +484 -3
  30. package/dist-lib/lib/floriday-auction-letters.functions.d.ts.map +1 -1
  31. package/dist-lib/lib/floriday-auto-process.server.d.ts +34 -0
  32. package/dist-lib/lib/floriday-auto-process.server.d.ts.map +1 -0
  33. package/dist-lib/lib/floriday-auto-process.server.js +365 -0
  34. package/dist-lib/lib/floriday-fulfillment-lookup.server.d.ts +19 -0
  35. package/dist-lib/lib/floriday-fulfillment-lookup.server.d.ts.map +1 -0
  36. package/dist-lib/lib/floriday-fulfillment-lookup.server.js +130 -0
  37. package/dist-lib/lib/floriday-gateway-auth.d.ts +24 -0
  38. package/dist-lib/lib/floriday-gateway-auth.d.ts.map +1 -0
  39. package/dist-lib/lib/floriday-gateway-auth.js +127 -0
  40. package/dist-lib/lib/floriday-labels.functions.d.ts +484 -3
  41. package/dist-lib/lib/floriday-labels.functions.d.ts.map +1 -1
  42. package/dist-lib/lib/floriday-labels.functions.js +3 -2
  43. package/dist-lib/lib/floriday-public.functions.d.ts +2904 -18
  44. package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -1
  45. package/dist-lib/lib/floriday-rate-limit.server.d.ts +21 -0
  46. package/dist-lib/lib/floriday-rate-limit.server.d.ts.map +1 -0
  47. package/dist-lib/lib/floriday-rate-limit.server.js +75 -0
  48. package/dist-lib/lib/floriday-stickers.functions.d.ts +484 -3
  49. package/dist-lib/lib/floriday-stickers.functions.d.ts.map +1 -1
  50. package/dist-lib/lib/floriday-sync.functions.d.ts +15972 -99
  51. package/dist-lib/lib/floriday-sync.functions.d.ts.map +1 -1
  52. package/dist-lib/lib/floriday-writes.functions.d.ts +2952 -42
  53. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  54. package/dist-lib/lib/floriday-writes.functions.js +29 -4
  55. package/dist-lib/lib/floriday.functions.d.ts +9054 -198
  56. package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
  57. package/dist-lib/lib/floriday.functions.js +39 -4
  58. package/dist-lib/lib/planning-helpers.d.ts +30 -0
  59. package/dist-lib/lib/planning-helpers.d.ts.map +1 -0
  60. package/dist-lib/lib/planning-helpers.js +62 -0
  61. package/package.json +3 -2
  62. package/public/flowselections-assets/floriday-authenticatie/floricode-logo.png +0 -0
@@ -0,0 +1,365 @@
1
+ // ============================================================================
2
+ // Auto-fulfillment state-machine voor Floriday sales orders (2026v1).
3
+ // ----------------------------------------------------------------------------
4
+ // Belangrijkste verschil met de vorige versie: deze module doet GEEN eigen
5
+ // `acknowledge` of `create-delivery-order` meer. Beide worden door Floriday
6
+ // zelf afgehandeld nadat de klant de order committeert (na
7
+ // `cancellationDeadline`). Deze module is een pure observer die één actie
8
+ // uitvoert: het versturen van de fulfillment-order zodra alles klaarstaat.
9
+ //
10
+ // Statussen:
11
+ // waiting_for_commit — cancellationDeadline nog niet verstreken
12
+ // waiting_for_delivery_order — commit rond, maar Floriday heeft nog geen
13
+ // delivery-order gepushed (wachten op sync)
14
+ // waiting — timing (autoProcessMoment) nog niet bereikt
15
+ // sending_fulfillment — POST /fulfillment-orders bezig
16
+ // done — fulfillment-order verzonden
17
+ // error — onherstelbaar
18
+ // ============================================================================
19
+ import { florydayWrite, getActiveFlorydayCredentials, getActiveFlorydayEnvironment, } from "./floriday.functions";
20
+ import { computeAutoProcessDate, isPickdayToday } from "./planning-helpers";
21
+ // ─── Helpers ────────────────────────────────────────────────────────────────
22
+ async function readSalesOrderFromCache(supabase, connectionId, salesOrderId) {
23
+ const { data } = await supabase
24
+ .from("floriday_salesorders_cache")
25
+ .select("floriday_id, data")
26
+ .eq("connection_id", connectionId)
27
+ .eq("floriday_id", salesOrderId)
28
+ .maybeSingle();
29
+ return data;
30
+ }
31
+ /**
32
+ * Zoek de delivery-order in cache waarvan één van de fulfillmentRequests naar
33
+ * deze sales-order verwijst. Retourneert zowel de delivery-order als de
34
+ * matchende fulfillmentRequest(s).
35
+ */
36
+ async function findDeliveryOrderForSalesOrder(supabase, connectionId, salesOrderId) {
37
+ // Floriday bewaart de link in data.fulfillmentRequests[].salesOrderId.
38
+ // We gebruiken jsonb containment via .contains() zodat Postgres een GIN
39
+ // index kan raken; geen full-table scan.
40
+ const { data, error } = await supabase
41
+ .from("floriday_delivery_orders_cache")
42
+ .select("floriday_id, data")
43
+ .eq("connection_id", connectionId)
44
+ .contains("data", {
45
+ fulfillmentRequests: [{ salesOrderId }],
46
+ })
47
+ .limit(1);
48
+ if (error || !data || data.length === 0)
49
+ return null;
50
+ const row = data[0];
51
+ const requests = Array.isArray(row.data?.fulfillmentRequests)
52
+ ? row.data.fulfillmentRequests.filter((r) => r?.salesOrderId === salesOrderId)
53
+ : [];
54
+ return {
55
+ deliveryOrderId: row.floriday_id,
56
+ data: row.data,
57
+ fulfillmentRequests: requests,
58
+ };
59
+ }
60
+ async function hasFulfillmentOrder(supabase, connectionId, deliveryOrderId) {
61
+ const { data } = await supabase
62
+ .from("floriday_fulfillment_orders_cache")
63
+ .select("floriday_id")
64
+ .eq("connection_id", connectionId)
65
+ .eq("delivery_order_id", deliveryOrderId)
66
+ .maybeSingle();
67
+ return data?.floriday_id ?? null;
68
+ }
69
+ async function loadAfhandelingSettings(supabase) {
70
+ const { data } = await supabase
71
+ .from("afhandeling_settings")
72
+ .select("auto_process_paused, pick_weekdays, max_workdays_earlier_pick, extra_pick_days_with_stickers")
73
+ .order("updated_at", { ascending: false })
74
+ .limit(1)
75
+ .maybeSingle();
76
+ return (data ?? {
77
+ auto_process_paused: false,
78
+ pick_weekdays: [1, 2, 3, 4, 5],
79
+ max_workdays_earlier_pick: 1,
80
+ extra_pick_days_with_stickers: 1,
81
+ });
82
+ }
83
+ async function logPhase(supabase, connectionId, salesOrderId, status, extra = {}) {
84
+ await supabase.rpc("upsert_auto_fulfillment_log", {
85
+ p_connection_id: connectionId,
86
+ p_sales_order_id: salesOrderId,
87
+ p_status: status,
88
+ p_reason: extra.reason ?? null,
89
+ p_error: extra.error ?? null,
90
+ p_fulfillment_order_id: extra.fulfillmentOrderId ?? null,
91
+ });
92
+ }
93
+ /**
94
+ * Haal `numberOfPackages` uit een fulfillmentRequest, met een reeks
95
+ * fallbacks voor licht afwijkende schema-varianten.
96
+ */
97
+ function extractPackageCount(req) {
98
+ const candidates = [
99
+ req?.numberOfPackages,
100
+ req?.packageCount,
101
+ req?.numberOfLoadCarriers,
102
+ req?.quantity,
103
+ ];
104
+ for (const c of candidates) {
105
+ const n = Number(c);
106
+ if (Number.isFinite(n) && n > 0)
107
+ return n;
108
+ }
109
+ return 1;
110
+ }
111
+ // ─── Publieke API ───────────────────────────────────────────────────────────
112
+ /**
113
+ * Voert de state-machine één stap uit voor een enkele sales-order. Idempotent
114
+ * en veilig te herhalen; alleen `sending_fulfillment` doet een write naar
115
+ * Floriday.
116
+ */
117
+ export async function runAutoProcess(supabase, input) {
118
+ const salesOrderId = input.salesOrderId.trim();
119
+ if (!salesOrderId) {
120
+ return {
121
+ ok: false,
122
+ status: "error",
123
+ salesOrderId,
124
+ error: "salesOrderId is verplicht",
125
+ };
126
+ }
127
+ const env = input.environment ?? (await getActiveFlorydayEnvironment(supabase));
128
+ const conn = await getActiveFlorydayCredentials(supabase, env);
129
+ if (!conn) {
130
+ return {
131
+ ok: false,
132
+ status: "error",
133
+ salesOrderId,
134
+ error: `Geen actieve Floriday-koppeling voor ${env}`,
135
+ };
136
+ }
137
+ const cacheRow = await readSalesOrderFromCache(supabase, conn.id, salesOrderId);
138
+ if (!cacheRow) {
139
+ await logPhase(supabase, conn.id, salesOrderId, "error", {
140
+ error: "Sales order niet in cache",
141
+ });
142
+ return {
143
+ ok: false,
144
+ status: "error",
145
+ salesOrderId,
146
+ error: "Sales order niet gevonden in cache",
147
+ };
148
+ }
149
+ const raw = cacheRow.data ?? {};
150
+ // ─ Guard 1: Order geannuleerd? ────────────────────────────────────────────
151
+ if (raw.automaticallyCancelledOn) {
152
+ return {
153
+ ok: false,
154
+ status: "error",
155
+ salesOrderId,
156
+ error: `Order automatisch geannuleerd op ${raw.automaticallyCancelledOn}`,
157
+ };
158
+ }
159
+ // ─ Guard 2: Cancellation-deadline nog niet verstreken ────────────────────
160
+ const cancellationDeadline = raw.cancellationDeadline ?? raw.commitmentDeadline ?? null;
161
+ if (!input.force && cancellationDeadline) {
162
+ const deadline = new Date(cancellationDeadline);
163
+ if (Number.isFinite(deadline.getTime()) && deadline.getTime() > Date.now()) {
164
+ await logPhase(supabase, conn.id, salesOrderId, "waiting_for_commit", {
165
+ reason: `deadline_at_${cancellationDeadline}`,
166
+ });
167
+ return {
168
+ ok: true,
169
+ status: "waiting_for_commit",
170
+ salesOrderId,
171
+ cancellationDeadline,
172
+ reason: `deadline_at_${cancellationDeadline}`,
173
+ };
174
+ }
175
+ }
176
+ // ─ Guard 3: Delivery-order al door Floriday gepushed? ─────────────────────
177
+ const doMatch = await findDeliveryOrderForSalesOrder(supabase, conn.id, salesOrderId);
178
+ if (!doMatch) {
179
+ await logPhase(supabase, conn.id, salesOrderId, "waiting_for_delivery_order", {
180
+ reason: "no_delivery_order_in_cache_yet",
181
+ });
182
+ return {
183
+ ok: true,
184
+ status: "waiting_for_delivery_order",
185
+ salesOrderId,
186
+ cancellationDeadline,
187
+ reason: "no_delivery_order_in_cache_yet",
188
+ };
189
+ }
190
+ const deliveryOrderId = doMatch.deliveryOrderId;
191
+ // ─ Idempotency: fulfillment-order al verzonden? ──────────────────────────
192
+ const existingFo = await hasFulfillmentOrder(supabase, conn.id, deliveryOrderId);
193
+ if (existingFo) {
194
+ return {
195
+ ok: true,
196
+ status: "done",
197
+ salesOrderId,
198
+ deliveryOrderId,
199
+ fulfillmentOrderId: existingFo,
200
+ reason: "already_sent",
201
+ };
202
+ }
203
+ // ─ Guard 4: Timing (autoProcessMoment) ───────────────────────────────────
204
+ const deliveryDate = raw.deliveryDate ?? raw.requestedDeliveryDate ?? doMatch.data?.deliveryDate ?? null;
205
+ const requiresStickers = Boolean(raw.requiresStickers);
206
+ if (!input.force && deliveryDate) {
207
+ const settings = await loadAfhandelingSettings(supabase);
208
+ if (settings.auto_process_paused) {
209
+ return {
210
+ ok: true,
211
+ status: "waiting",
212
+ salesOrderId,
213
+ deliveryOrderId,
214
+ reason: "auto_process_paused",
215
+ };
216
+ }
217
+ const { moment, reason } = computeAutoProcessDate({
218
+ deliveryDate,
219
+ maxWorkdaysEarlierPick: settings.max_workdays_earlier_pick ?? 1,
220
+ pickWeekdays: settings.pick_weekdays ?? [1, 2, 3, 4, 5],
221
+ extraStickerDaysWithStickers: settings.extra_pick_days_with_stickers ?? 0,
222
+ requiresStickers,
223
+ });
224
+ if (moment && moment.getTime() > Date.now()) {
225
+ return {
226
+ ok: true,
227
+ status: "waiting",
228
+ salesOrderId,
229
+ deliveryOrderId,
230
+ reason: reason ?? "before_auto_process_moment",
231
+ };
232
+ }
233
+ }
234
+ // ─ Actie: POST /fulfillment-orders ───────────────────────────────────────
235
+ await logPhase(supabase, conn.id, salesOrderId, "sending_fulfillment");
236
+ const fulfillmentRequests = doMatch.fulfillmentRequests
237
+ .map((r) => {
238
+ const fulfillmentRequestId = r?.fulfillmentRequestId ?? r?.id ?? r?.requestId;
239
+ if (!fulfillmentRequestId)
240
+ return null;
241
+ return {
242
+ deliveryOrderId,
243
+ fulfillmentRequestId,
244
+ numberOfPackages: extractPackageCount(r),
245
+ };
246
+ })
247
+ .filter(Boolean);
248
+ if (fulfillmentRequests.length === 0) {
249
+ await logPhase(supabase, conn.id, salesOrderId, "error", {
250
+ error: "Delivery-order bevat geen fulfillmentRequests voor deze sales-order",
251
+ });
252
+ return {
253
+ ok: false,
254
+ status: "error",
255
+ salesOrderId,
256
+ deliveryOrderId,
257
+ error: "Geen fulfillmentRequests gevonden in delivery-order",
258
+ };
259
+ }
260
+ const body = {
261
+ loadCarrierItems: [{ fulfillmentRequests }],
262
+ };
263
+ const foResult = await florydayWrite(supabase, env, {
264
+ module: "auto-process",
265
+ action: "send-fulfillment-order",
266
+ method: "POST",
267
+ path: "/fulfillment-orders",
268
+ body,
269
+ referenceId: deliveryOrderId,
270
+ idempotencyKey: `do:${deliveryOrderId}:fulfillment-order`,
271
+ });
272
+ if (!foResult.ok) {
273
+ await logPhase(supabase, conn.id, salesOrderId, "error", {
274
+ error: foResult.error ?? "Versturen fulfillment-order mislukt",
275
+ });
276
+ return {
277
+ ok: false,
278
+ status: "error",
279
+ salesOrderId,
280
+ deliveryOrderId,
281
+ error: foResult.error ?? "Versturen fulfillment-order mislukt",
282
+ };
283
+ }
284
+ const fulfillmentOrderId = foResult.data?.fulfillmentOrderId ??
285
+ foResult.data?.id ??
286
+ foResult.data?.floridayId;
287
+ await logPhase(supabase, conn.id, salesOrderId, "done", { fulfillmentOrderId });
288
+ return {
289
+ ok: true,
290
+ status: "done",
291
+ salesOrderId,
292
+ deliveryOrderId,
293
+ fulfillmentOrderId,
294
+ };
295
+ }
296
+ /**
297
+ * Cron-scan: sales-orders waarvan (a) de cancellation-deadline verstreken is,
298
+ * (b) er een delivery-order in cache staat en (c) nog geen fulfillment-order
299
+ * verzonden is → trigger `runAutoProcess`.
300
+ */
301
+ export async function scanAndTriggerAutoProcess(supabase) {
302
+ const env = await getActiveFlorydayEnvironment(supabase);
303
+ const conn = await getActiveFlorydayCredentials(supabase, env);
304
+ if (!conn)
305
+ return { scanned: 0, triggered: 0, results: [] };
306
+ const settings = await loadAfhandelingSettings(supabase);
307
+ if (settings.auto_process_paused)
308
+ return { scanned: 0, triggered: 0, results: [] };
309
+ // Vandaag geen pickdag? Alleen spoedorders (computeAutoProcessDate laat die
310
+ // via `outside_pickday` alsnog door).
311
+ void isPickdayToday(settings.pick_weekdays ?? [1, 2, 3, 4, 5]);
312
+ const nowIso = new Date().toISOString();
313
+ // Filter aan de bron: order niet geannuleerd, deadline verstreken. De
314
+ // "delivery-order aanwezig?" en "fulfillment-order al verzonden?" checks
315
+ // gebeuren per-order in runAutoProcess (blijven zo idempotent).
316
+ const { data: orders } = await supabase
317
+ .from("floriday_salesorders_cache")
318
+ .select("floriday_id, data")
319
+ .eq("connection_id", conn.id)
320
+ .eq("is_deleted", false)
321
+ .is("data->>automaticallyCancelledOn", null)
322
+ .lt("data->>cancellationDeadline", nowIso)
323
+ .limit(500);
324
+ const candidates = orders ?? [];
325
+ // Circuit-breaker: sla orders over die 5× of vaker op 'error' zijn geëindigd.
326
+ // Handmatige retry via runAutoProcess({ force: true }) reset attempts vanzelf
327
+ // omdat de nieuwe status ≠ 'error' is.
328
+ const ids = candidates.map((c) => c.floriday_id);
329
+ let blocked = new Set();
330
+ if (ids.length > 0) {
331
+ const { data: logs } = await supabase
332
+ .from("floriday_auto_fulfillment_log")
333
+ .select("sales_order_id, status, attempts")
334
+ .eq("connection_id", conn.id)
335
+ .in("sales_order_id", ids);
336
+ blocked = new Set((logs ?? [])
337
+ .filter((l) => l.status === "error" && (l.attempts ?? 0) >= 5)
338
+ .map((l) => l.sales_order_id));
339
+ }
340
+ const filtered = candidates.filter((c) => !blocked.has(c.floriday_id));
341
+ const results = [];
342
+ const concurrency = 5;
343
+ let idx = 0;
344
+ async function worker() {
345
+ while (idx < filtered.length) {
346
+ const i = idx++;
347
+ const row = filtered[i];
348
+ const outcome = await runAutoProcess(supabase, {
349
+ salesOrderId: row.floriday_id,
350
+ environment: env,
351
+ }).catch((e) => ({
352
+ ok: false,
353
+ status: "error",
354
+ salesOrderId: row.floriday_id,
355
+ error: String(e?.message ?? e),
356
+ }));
357
+ results.push(outcome);
358
+ }
359
+ }
360
+ await Promise.all(Array.from({ length: concurrency }, worker));
361
+ const triggered = results.filter((r) => r.status !== "waiting" &&
362
+ r.status !== "waiting_for_commit" &&
363
+ r.status !== "waiting_for_delivery_order").length;
364
+ return { scanned: candidates.length, triggered, results };
365
+ }
@@ -0,0 +1,19 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ import { type FlorydayEnvironment } from "./floriday.functions";
3
+ export interface LookupInput {
4
+ environment: FlorydayEnvironment;
5
+ fulfillmentOrderId?: string | null;
6
+ deliveryOrderId?: string | null;
7
+ salesOrderId?: string | null;
8
+ }
9
+ export interface LookupResult {
10
+ fulfillmentOrderIds: string[];
11
+ source: "input" | "cache" | "delivery-order" | "sales-order" | "sales-order-cache";
12
+ }
13
+ /**
14
+ * Retourneert alle bekende fulfillmentOrderIds voor de gegeven referenties.
15
+ * Wanneer `fulfillmentOrderId` gegeven is wordt die direct teruggegeven —
16
+ * er wordt niets extra bevraagd.
17
+ */
18
+ export declare function resolveFulfillmentOrderIds(supabase: SupabaseClient, input: LookupInput): Promise<LookupResult>;
19
+ //# sourceMappingURL=floriday-fulfillment-lookup.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floriday-fulfillment-lookup.server.d.ts","sourceRoot":"","sources":["../../src/lib/floriday-fulfillment-lookup.server.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,mBAAmB,CAAC;IACjC,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,MAAM,EACF,OAAO,GACP,OAAO,GACP,gBAAgB,GAChB,aAAa,GACb,mBAAmB,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,YAAY,CAAC,CAoGvB"}
@@ -0,0 +1,130 @@
1
+ // ============================================================================
2
+ // Resolve een fulfillmentOrderId vanuit deliveryOrderId of salesOrderId.
3
+ // ----------------------------------------------------------------------------
4
+ // Modules als Verkoop en Afhandeling kennen doorgaans alleen de sales-order
5
+ // of delivery-order ID. Voor afleverbrieven hebben we een fulfillmentOrderId
6
+ // nodig. Deze helper zoekt hem op — eerst in de lokale cache, daarna
7
+ // on-demand via de Floriday-API.
8
+ // ============================================================================
9
+ import { SUPPLIERS_API_VERSION, florydayFetch, getActiveFlorydayCredentials, } from "./floriday.functions";
10
+ /**
11
+ * Retourneert alle bekende fulfillmentOrderIds voor de gegeven referenties.
12
+ * Wanneer `fulfillmentOrderId` gegeven is wordt die direct teruggegeven —
13
+ * er wordt niets extra bevraagd.
14
+ */
15
+ export async function resolveFulfillmentOrderIds(supabase, input) {
16
+ if (input.fulfillmentOrderId?.trim()) {
17
+ return { fulfillmentOrderIds: [input.fulfillmentOrderId.trim()], source: "input" };
18
+ }
19
+ const env = input.environment;
20
+ const conn = await getActiveFlorydayCredentials(supabase, env);
21
+ if (!conn)
22
+ throw new Error(`Geen actieve Floriday-koppeling voor ${env}`);
23
+ // 1) deliveryOrderId → cache-lookup
24
+ if (input.deliveryOrderId?.trim()) {
25
+ const deliveryOrderId = input.deliveryOrderId.trim();
26
+ const { data: cached } = await supabase
27
+ .from("floriday_fulfillment_orders_cache")
28
+ .select("floriday_id")
29
+ .eq("connection_id", conn.id)
30
+ .eq("delivery_order_id", deliveryOrderId)
31
+ .eq("is_deleted", false);
32
+ const cachedIds = (cached ?? [])
33
+ .map((r) => String(r.floriday_id))
34
+ .filter(Boolean);
35
+ if (cachedIds.length > 0) {
36
+ return { fulfillmentOrderIds: cachedIds, source: "cache" };
37
+ }
38
+ // Cache-miss → live ophalen bij Floriday
39
+ const ids = await fetchFulfillmentIdsForDeliveryOrder(supabase, env, deliveryOrderId);
40
+ if (ids.length === 0) {
41
+ throw new Error(`Geen fulfillment-order gevonden voor delivery-order ${deliveryOrderId}`);
42
+ }
43
+ return { fulfillmentOrderIds: ids, source: "delivery-order" };
44
+ }
45
+ // 2) salesOrderId → fulfillment-orders cache doorzoeken.
46
+ // In Floriday 2026v1 verwijst de fulfillment-order naar de sales-order
47
+ // (via loadCarriers[].loadCarrierItems[].salesOrderId), niet andersom.
48
+ if (input.salesOrderId?.trim()) {
49
+ const salesOrderId = input.salesOrderId.trim();
50
+ // Eerst cache raadplegen
51
+ let matched = await findFulfillmentIdsForSalesOrderInCache(supabase, conn.id, salesOrderId);
52
+ // Cache-miss? Triggert een korte on-demand delta-sync (fulfillment-orders
53
+ // groeit meestal met 0-enkele items per minuut) en probeer opnieuw. Zo
54
+ // hoeven we niet op de volgende cron-tick te wachten.
55
+ if (matched.length === 0) {
56
+ try {
57
+ const { runFloridayResourceSync } = await import("./floriday-sync.functions");
58
+ await runFloridayResourceSync(supabase, env, "fulfillment-orders");
59
+ }
60
+ catch (e) {
61
+ console.warn("[fulfillment-lookup] on-demand sync faalde:", e);
62
+ }
63
+ matched = await findFulfillmentIdsForSalesOrderInCache(supabase, conn.id, salesOrderId);
64
+ }
65
+ if (matched.length > 0) {
66
+ return { fulfillmentOrderIds: matched, source: "sales-order-cache" };
67
+ }
68
+ // Nog steeds niets. Onderscheid maken tussen "order onbekend" en
69
+ // "order bekend, maar Floriday heeft nog geen fulfillment aangemaakt".
70
+ const { data: soRow } = await supabase
71
+ .from("floriday_salesorders_cache")
72
+ .select("floriday_id")
73
+ .eq("connection_id", conn.id)
74
+ .eq("floriday_id", salesOrderId)
75
+ .maybeSingle();
76
+ if (!soRow) {
77
+ throw new Error(`Deze order (${salesOrderId}) is nog niet gesynchroniseerd vanuit Floriday. Probeer het over een minuut opnieuw.`);
78
+ }
79
+ throw new Error(`Floriday heeft voor deze order nog geen afleverbrief aangemaakt. Open de order in het Floriday-portaal en wijs de order toe aan een trolley of afleverronde. Zodra dat gedaan is verschijnt de afleverbrief hier automatisch (binnen ongeveer een minuut).`);
80
+ }
81
+ throw new Error("Geef ten minste één van fulfillmentOrderId, deliveryOrderId of salesOrderId op");
82
+ }
83
+ async function fetchFulfillmentIdsForDeliveryOrder(supabase, env, deliveryOrderId) {
84
+ const res = await florydayFetch(supabase, env, `/${SUPPLIERS_API_VERSION}/delivery-orders/${encodeURIComponent(deliveryOrderId)}`, { cache: "no-store" });
85
+ if (!res.ok) {
86
+ const body = await res.text().catch(() => "");
87
+ throw new Error(`Delivery-order ${res.status}: ${body.slice(0, 200)}`);
88
+ }
89
+ const json = await res.json();
90
+ const ids = [];
91
+ const push = (v) => {
92
+ if (typeof v === "string" && v)
93
+ ids.push(v);
94
+ };
95
+ push(json?.fulfillmentOrderId);
96
+ if (Array.isArray(json?.fulfillmentOrderIds)) {
97
+ for (const id of json.fulfillmentOrderIds)
98
+ push(id);
99
+ }
100
+ if (Array.isArray(json?.fulfillmentOrders)) {
101
+ for (const fo of json.fulfillmentOrders) {
102
+ push(fo?.fulfillmentOrderId ?? fo?.id);
103
+ }
104
+ }
105
+ return Array.from(new Set(ids));
106
+ }
107
+ async function findFulfillmentIdsForSalesOrderInCache(supabase, connectionId, salesOrderId) {
108
+ const { data: candidates } = await supabase
109
+ .from("floriday_fulfillment_orders_cache")
110
+ .select("floriday_id, data")
111
+ .eq("connection_id", connectionId)
112
+ .eq("is_deleted", false)
113
+ .ilike("data", `%${salesOrderId}%`);
114
+ const matched = new Set();
115
+ for (const row of candidates ?? []) {
116
+ const data = row.data;
117
+ const carriers = Array.isArray(data?.loadCarriers)
118
+ ? data.loadCarriers
119
+ : [];
120
+ const hit = carriers.some((lc) => Array.isArray(lc?.loadCarrierItems)
121
+ ? lc.loadCarrierItems.some((it) => it?.salesOrderId === salesOrderId)
122
+ : false);
123
+ if (hit) {
124
+ const id = String(row.floriday_id ?? data?.fulfillmentOrderId ?? "");
125
+ if (id)
126
+ matched.add(id);
127
+ }
128
+ }
129
+ return Array.from(matched);
130
+ }
@@ -0,0 +1,24 @@
1
+ import type { GatewayAuthFail, GatewayAuthOk, GatewayBinaryAuthOk } from "./floriday-gateway-hmac";
2
+ import type { SupabaseClient } from "@supabase/supabase-js";
3
+ import { type FlorydayEnvironment } from "./floriday.functions";
4
+ /**
5
+ * Resolveert welke Floriday-omgeving voor deze gateway-call gebruikt moet worden.
6
+ *
7
+ * Regels:
8
+ * 1. Als de caller een omgeving opgeeft én die heeft een actieve koppeling → gebruik die.
9
+ * 2. Anders → val terug op de in deze module actieve omgeving (`floriday_settings.active_environment`).
10
+ * 3. Als ook die geen actieve koppeling heeft → gooi een duidelijke fout.
11
+ *
12
+ * Op deze manier crasht de gateway niet meer wanneer een externe module `live`
13
+ * stuurt terwijl alleen `staging` is ingericht (of omgekeerd).
14
+ */
15
+ export declare function resolveGatewayEnvironment(supabase: SupabaseClient, requested?: FlorydayEnvironment | string | null): Promise<FlorydayEnvironment>;
16
+ export declare const GATEWAY_CORS_HEADERS: Record<string, string>;
17
+ export declare function corsPreflight(): Response;
18
+ export declare function withCors(response: Response): Response;
19
+ /**
20
+ * Accepteert HMAC of Supabase Bearer. Retourneert de raw body voor JSON.parse.
21
+ */
22
+ export declare function verifyGatewayCaller(request: Request): Promise<GatewayAuthOk | GatewayAuthFail>;
23
+ export declare function verifyGatewayBinaryCaller(request: Request): Promise<GatewayBinaryAuthOk | GatewayAuthFail>;
24
+ //# sourceMappingURL=floriday-gateway-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floriday-gateway-auth.d.ts","sourceRoot":"","sources":["../../src/lib/floriday-gateway-auth.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,mBAAmB,EACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,cAAc,EACxB,SAAS,CAAC,EAAE,mBAAmB,GAAG,MAAM,GAAG,IAAI,GAC9C,OAAO,CAAC,mBAAmB,CAAC,CAyB9B;AAED,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMvD,CAAC;AAEF,wBAAgB,aAAa,IAAI,QAAQ,CAExC;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAQrD;AAgCD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,aAAa,GAAG,eAAe,CAAC,CAuB1C;AAGD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,mBAAmB,GAAG,eAAe,CAAC,CAahD"}
@@ -0,0 +1,127 @@
1
+ // ============================================================================
2
+ // Dual-mode auth voor de PDF-gateway routes.
3
+ // ----------------------------------------------------------------------------
4
+ // Accepteert:
5
+ // - HMAC-handtekening (server-to-server callers met shared secret), of
6
+ // - Supabase Bearer JWT (ingelogde gebruiker in een andere Lovable-module).
7
+ //
8
+ // Alleen bedoeld voor de lees-/PDF-endpoints. Schrijf-endpoints blijven
9
+ // exclusief HMAC.
10
+ // ============================================================================
11
+ import { createClient } from "@supabase/supabase-js";
12
+ import { verifyGatewayRequest, verifyGatewayBinaryRequest } from "./floriday-gateway-hmac";
13
+ import { getActiveFlorydayCredentials, getActiveFlorydayEnvironment, } from "./floriday.functions";
14
+ /**
15
+ * Resolveert welke Floriday-omgeving voor deze gateway-call gebruikt moet worden.
16
+ *
17
+ * Regels:
18
+ * 1. Als de caller een omgeving opgeeft én die heeft een actieve koppeling → gebruik die.
19
+ * 2. Anders → val terug op de in deze module actieve omgeving (`floriday_settings.active_environment`).
20
+ * 3. Als ook die geen actieve koppeling heeft → gooi een duidelijke fout.
21
+ *
22
+ * Op deze manier crasht de gateway niet meer wanneer een externe module `live`
23
+ * stuurt terwijl alleen `staging` is ingericht (of omgekeerd).
24
+ */
25
+ export async function resolveGatewayEnvironment(supabase, requested) {
26
+ const wanted = requested === "staging" || requested === "live"
27
+ ? requested
28
+ : null;
29
+ if (wanted) {
30
+ const row = await getActiveFlorydayCredentials(supabase, wanted);
31
+ if (row)
32
+ return wanted;
33
+ }
34
+ const active = await getActiveFlorydayEnvironment(supabase);
35
+ const activeRow = await getActiveFlorydayCredentials(supabase, active);
36
+ if (!activeRow) {
37
+ throw new Error("Er is geen actieve Floriday-koppeling geconfigureerd. Open het beheerscherm en verbind Floriday.");
38
+ }
39
+ if (wanted && wanted !== active) {
40
+ console.warn(`[gateway] gevraagde omgeving "${wanted}" heeft geen actieve koppeling; val terug op "${active}"`);
41
+ }
42
+ return active;
43
+ }
44
+ export const GATEWAY_CORS_HEADERS = {
45
+ "Access-Control-Allow-Origin": "*",
46
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
47
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, apikey, x-floriday-timestamp, x-floriday-signature, x-floriday-caller",
48
+ "Access-Control-Max-Age": "86400",
49
+ };
50
+ export function corsPreflight() {
51
+ return new Response(null, { status: 204, headers: GATEWAY_CORS_HEADERS });
52
+ }
53
+ export function withCors(response) {
54
+ const headers = new Headers(response.headers);
55
+ for (const [k, v] of Object.entries(GATEWAY_CORS_HEADERS))
56
+ headers.set(k, v);
57
+ return new Response(response.body, {
58
+ status: response.status,
59
+ statusText: response.statusText,
60
+ headers,
61
+ });
62
+ }
63
+ async function verifySupabaseBearer(request) {
64
+ const authHeader = request.headers.get("authorization");
65
+ if (!authHeader || !authHeader.toLowerCase().startsWith("bearer "))
66
+ return null;
67
+ const token = authHeader.slice(7).trim();
68
+ if (!token)
69
+ return null;
70
+ const SUPABASE_URL = process.env.SUPABASE_URL;
71
+ const SUPABASE_PUBLISHABLE_KEY = process.env.SUPABASE_PUBLISHABLE_KEY;
72
+ if (!SUPABASE_URL || !SUPABASE_PUBLISHABLE_KEY) {
73
+ return { ok: false, status: 500, error: "Supabase niet geconfigureerd op gateway" };
74
+ }
75
+ const supabase = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
76
+ auth: { storage: undefined, persistSession: false, autoRefreshToken: false },
77
+ });
78
+ try {
79
+ const { data, error } = await supabase.auth.getClaims(token);
80
+ if (error || !data?.claims?.sub) {
81
+ return { ok: false, status: 401, error: "Ongeldige of verlopen sessie" };
82
+ }
83
+ return { ok: true, userId: String(data.claims.sub) };
84
+ }
85
+ catch {
86
+ return { ok: false, status: 401, error: "Sessie kon niet worden geverifieerd" };
87
+ }
88
+ }
89
+ /**
90
+ * Accepteert HMAC of Supabase Bearer. Retourneert de raw body voor JSON.parse.
91
+ */
92
+ export async function verifyGatewayCaller(request) {
93
+ // 1) apikey-header met publishable key (bv. pg_cron via net.http_post)?
94
+ const apiKey = request.headers.get("apikey");
95
+ if (apiKey && apiKey === process.env.SUPABASE_PUBLISHABLE_KEY) {
96
+ const rawBody = await request.text();
97
+ return { ok: true, caller: "cron:pg_cron", rawBody };
98
+ }
99
+ // 2) Supabase Bearer? Dan die weg gebruiken.
100
+ const bearer = await verifySupabaseBearer(request);
101
+ if (bearer && bearer.ok) {
102
+ const rawBody = await request.text();
103
+ return { ok: true, caller: `user:${bearer.userId}`, rawBody };
104
+ }
105
+ if (bearer && !bearer.ok) {
106
+ const hasHmac = request.headers.get("x-floriday-signature") &&
107
+ request.headers.get("x-floriday-timestamp");
108
+ if (!hasHmac)
109
+ return bearer;
110
+ }
111
+ // 3) Val terug op HMAC.
112
+ return verifyGatewayRequest(request);
113
+ }
114
+ export async function verifyGatewayBinaryCaller(request) {
115
+ const bearer = await verifySupabaseBearer(request);
116
+ if (bearer && bearer.ok) {
117
+ const bodyBytes = new Uint8Array(await request.arrayBuffer());
118
+ return { ok: true, caller: `user:${bearer.userId}`, bodyBytes };
119
+ }
120
+ if (bearer && !bearer.ok) {
121
+ const hasHmac = request.headers.get("x-floriday-signature") &&
122
+ request.headers.get("x-floriday-timestamp");
123
+ if (!hasHmac)
124
+ return bearer;
125
+ }
126
+ return verifyGatewayBinaryRequest(request);
127
+ }