@flowselections/floriday-authenticatie-module 1.0.40 → 1.0.42

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 (36) hide show
  1. package/dist-lib/integrations/supabase/auth-middleware.d.ts +283 -0
  2. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  3. package/dist-lib/integrations/supabase/client.d.ts +283 -0
  4. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  5. package/dist-lib/integrations/supabase/client.server.d.ts +283 -0
  6. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  7. package/dist-lib/integrations/supabase/types.d.ts +290 -0
  8. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  9. package/dist-lib/lib/floricode.functions.d.ts +1981 -0
  10. package/dist-lib/lib/floricode.functions.d.ts.map +1 -1
  11. package/dist-lib/lib/floriday-auction-letters.functions.d.ts +283 -0
  12. package/dist-lib/lib/floriday-auction-letters.functions.d.ts.map +1 -1
  13. package/dist-lib/lib/floriday-auto-process.server.d.ts +2 -0
  14. package/dist-lib/lib/floriday-auto-process.server.d.ts.map +1 -1
  15. package/dist-lib/lib/floriday-auto-process.server.js +64 -33
  16. package/dist-lib/lib/floriday-labels.functions.d.ts +283 -0
  17. package/dist-lib/lib/floriday-labels.functions.d.ts.map +1 -1
  18. package/dist-lib/lib/floriday-public.functions.d.ts +1698 -0
  19. package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -1
  20. package/dist-lib/lib/floriday-stickers.functions.d.ts +283 -0
  21. package/dist-lib/lib/floriday-stickers.functions.d.ts.map +1 -1
  22. package/dist-lib/lib/floriday-sync.functions.d.ts +8773 -0
  23. package/dist-lib/lib/floriday-sync.functions.d.ts.map +1 -1
  24. package/dist-lib/lib/floriday-writes.functions.d.ts +1698 -0
  25. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  26. package/dist-lib/lib/floriday.functions.d.ts +5377 -0
  27. package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
  28. package/dist-lib/lib/gateway/fulfillment-order-body.d.ts +57 -0
  29. package/dist-lib/lib/gateway/fulfillment-order-body.d.ts.map +1 -0
  30. package/dist-lib/lib/gateway/fulfillment-order-body.js +361 -0
  31. package/dist-lib/lib/gateway/fulfillment-order-send.d.ts.map +1 -1
  32. package/dist-lib/lib/gateway/fulfillment-order-send.js +94 -67
  33. package/dist-lib/lib/gateway/index.d.ts +1 -0
  34. package/dist-lib/lib/gateway/index.d.ts.map +1 -1
  35. package/dist-lib/lib/gateway/index.js +1 -0
  36. package/package.json +1 -1
@@ -0,0 +1,57 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ export declare function deriveFulfillmentOrderId(deliveryOrderId: string): Promise<string>;
3
+ export declare function resolvePackageCount(req: any): number | null;
4
+ export interface SkippedFulfillmentRequest {
5
+ fulfillmentRequestId: string;
6
+ reason: "load_carrier_none" | "already_claimed";
7
+ }
8
+ export type BuildFulfillmentOrderBodyResult = {
9
+ kind: "body";
10
+ body: Record<string, unknown>;
11
+ fulfillmentOrderId: string;
12
+ usedRequestIds: string[];
13
+ skipped: SkippedFulfillmentRequest[];
14
+ } | {
15
+ kind: "already_fulfilled";
16
+ code: "already_fulfilled";
17
+ source: FulfillmentDetectionSource;
18
+ message: string;
19
+ fulfillmentOrderId?: string;
20
+ skipped: SkippedFulfillmentRequest[];
21
+ } | {
22
+ kind: "error";
23
+ error: string;
24
+ /** false = tijdelijk (opnieuw proberen kan slagen na sync). */
25
+ permanent: boolean;
26
+ };
27
+ export interface BuildFulfillmentOrderBodyInput {
28
+ connectionId: string;
29
+ deliveryOrderId: string;
30
+ /** De `data`-kolom van floriday_delivery_orders_cache. */
31
+ deliveryOrderData: any;
32
+ /** Optionele filter op fulfillmentRequestId (gateway-pad). */
33
+ fulfillmentRequestIds?: string[] | null;
34
+ }
35
+ export type FulfillmentDetectionSource = "delivery_order_claims" | "fulfillment_orders_cache";
36
+ export type FulfillmentDetection = {
37
+ code: "already_fulfilled";
38
+ source: FulfillmentDetectionSource;
39
+ message: string;
40
+ fulfillmentOrderId?: string;
41
+ skipped: SkippedFulfillmentRequest[];
42
+ } | {
43
+ code: "delivery_order_not_cached";
44
+ message: string;
45
+ } | {
46
+ code: "not_fulfilled";
47
+ };
48
+ export interface DetectFulfillmentStateInput {
49
+ connectionId: string;
50
+ deliveryOrderId: string;
51
+ /** Optioneel: de al gelezen `data`-kolom. Ontbreekt hij, dan lezen we zelf. */
52
+ deliveryOrderData?: any;
53
+ fulfillmentRequestIds?: string[] | null;
54
+ }
55
+ export declare function detectFulfillmentState(supabase: SupabaseClient, input: DetectFulfillmentStateInput): Promise<FulfillmentDetection>;
56
+ export declare function buildFulfillmentOrderBody(supabase: SupabaseClient, input: BuildFulfillmentOrderBodyInput): Promise<BuildFulfillmentOrderBodyResult>;
57
+ //# sourceMappingURL=fulfillment-order-body.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fulfillment-order-body.d.ts","sourceRoot":"","sources":["../../../src/lib/gateway/fulfillment-order-body.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAqD5D,wBAAgB,wBAAwB,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjF;AAWD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,IAAI,CAO3D;AAMD,MAAM,WAAW,yBAAyB;IACxC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,mBAAmB,GAAG,iBAAiB,CAAC;CACjD;AAED,MAAM,MAAM,+BAA+B,GACvC;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,EAAE,yBAAyB,EAAE,CAAC;CACtC,GACD;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,MAAM,EAAE,0BAA0B,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,yBAAyB,EAAE,CAAC;CACtC,GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAEN,MAAM,WAAW,8BAA8B;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,iBAAiB,EAAE,GAAG,CAAC;IACvB,8DAA8D;IAC9D,qBAAqB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACzC;AAqBD,MAAM,MAAM,0BAA0B,GAClC,uBAAuB,GACvB,0BAA0B,CAAC;AAE/B,MAAM,MAAM,oBAAoB,GAC5B;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,MAAM,EAAE,0BAA0B,CAAC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,yBAAyB,EAAE,CAAC;CACtC,GACD;IACE,IAAI,EAAE,2BAA2B,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC;AAyC9B,MAAM,WAAW,2BAA2B;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,GAAG,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACzC;AAED,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,2BAA2B,GACjC,OAAO,CAAC,oBAAoB,CAAC,CA0D/B;AAOD,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,8BAA8B,GACpC,OAAO,CAAC,+BAA+B,CAAC,CA2N1C"}
@@ -0,0 +1,361 @@
1
+ // Gedeelde bodybouwer voor de Floriday fulfillment-order push (2026v1).
2
+ //
3
+ // Eén implementatie voor beide paden:
4
+ // - src/lib/gateway/fulfillment-order-send.ts (gateway-endpoint)
5
+ // - src/lib/floriday-auto-process.server.ts (auto-process / cron-scan)
6
+ //
7
+ // De body wordt volledig opgebouwd uit de delivery-order-cache plus een join
8
+ // op de sales-order-cache; er wordt niets verzonnen. Ontbrekende gegevens
9
+ // leiden tot een expliciete fout, nooit tot een stille aanname.
10
+ // ---------------------------------------------------------------------------
11
+ // fulfillmentOrderId — deterministische UUIDv5
12
+ // ---------------------------------------------------------------------------
13
+ //
14
+ // WAAROM DETERMINISTISCH:
15
+ // Floriday vereist een client-side `fulfillmentOrderId` met een non-default
16
+ // waarde. Een random UUID zou bij elke retry een nieuwe fulfillment-order
17
+ // aanmaken: een netwerkfout of timeout na een geslaagde POST zou dan tot een
18
+ // dubbele fulfilment leiden. Door de GUID deterministisch af te leiden uit de
19
+ // deliveryOrderId (UUIDv5, vaste namespace hieronder) levert elke retry exact
20
+ // dezelfde waarde op en dedupliceert Floriday zelf. Er is daarom géén extra
21
+ // databasekolom en géén opslag nodig.
22
+ //
23
+ // BEPERKING:
24
+ // Deze afleiding geeft precies één fulfillment-order per delivery-order.
25
+ // Wil je ooit een delivery-order in meerdere fulfillment-orders splitsen
26
+ // (bijvoorbeeld per ladingdrager in aparte pushes), dan moet de afleiding
27
+ // worden uitgebreid met een discriminator én moet de gekozen waarde worden
28
+ // vastgelegd, anders overschrijven de pushes elkaars identiteit.
29
+ const FULFILLMENT_ORDER_NAMESPACE = "6f9a1e2c-7d34-4c1b-9a5e-3b8f0d2c4a71";
30
+ // Web Crypto in plaats van node:crypto — dit bestand wordt ook client-side
31
+ // gebundeld; Node-builtins bestaan daar niet.
32
+ function toHex(bytes) {
33
+ return Array.from(bytes)
34
+ .map((b) => b.toString(16).padStart(2, "0"))
35
+ .join("");
36
+ }
37
+ async function uuidV5(name, namespace) {
38
+ const hex = namespace.replace(/-/g, "");
39
+ if (hex.length !== 32)
40
+ throw new Error("Ongeldige namespace-GUID");
41
+ const nsBytes = new Uint8Array(16);
42
+ for (let i = 0; i < 16; i++) {
43
+ nsBytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
44
+ }
45
+ const nameBytes = new TextEncoder().encode(name);
46
+ const input = new Uint8Array(nsBytes.length + nameBytes.length);
47
+ input.set(nsBytes, 0);
48
+ input.set(nameBytes, nsBytes.length);
49
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-1", input));
50
+ const bytes = digest.slice(0, 16);
51
+ bytes[6] = (bytes[6] & 0x0f) | 0x50; // versie 5
52
+ bytes[8] = (bytes[8] & 0x3f) | 0x80; // RFC 4122 variant
53
+ const h = toHex(bytes);
54
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
55
+ }
56
+ export function deriveFulfillmentOrderId(deliveryOrderId) {
57
+ return uuidV5(deliveryOrderId, FULFILLMENT_ORDER_NAMESPACE);
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // numberOfPackages
61
+ // ---------------------------------------------------------------------------
62
+ //
63
+ // In de delivery-order-cache is `numberOfPackages` in alle rijen 0; het
64
+ // werkelijke aantal staat in `initialNumberOfPackages` (en wordt bevestigd
65
+ // door fulfillmentOrderClaims[].claimedNumberOfPackages). Deze functie valt
66
+ // NOOIT stil terug op 1: een ontbrekend aantal is een fout.
67
+ export function resolvePackageCount(req) {
68
+ const candidates = [req?.initialNumberOfPackages, req?.numberOfPackages];
69
+ for (const c of candidates) {
70
+ const n = Number(c);
71
+ if (Number.isFinite(n) && n > 0)
72
+ return n;
73
+ }
74
+ return null;
75
+ }
76
+ /** Pure scan van de claims op de (gefilterde) fulfillmentRequests. */
77
+ function scanClaims(doData, filterIds) {
78
+ const requests = Array.isArray(doData?.fulfillmentRequests)
79
+ ? doData.fulfillmentRequests
80
+ : [];
81
+ const claimed = [];
82
+ let unclaimedCount = 0;
83
+ let consideredCount = 0;
84
+ let fulfillmentOrderId;
85
+ for (const r of requests) {
86
+ const id = r?.fulfillmentRequestId ?? r?.id;
87
+ if (!id)
88
+ continue;
89
+ if (filterIds && !filterIds.includes(String(id)))
90
+ continue;
91
+ consideredCount += 1;
92
+ const claims = Array.isArray(r?.fulfillmentOrderClaims)
93
+ ? r.fulfillmentOrderClaims
94
+ : [];
95
+ if (claims.length > 0) {
96
+ claimed.push({ fulfillmentRequestId: String(id), reason: "already_claimed" });
97
+ if (!fulfillmentOrderId) {
98
+ const fromClaim = claims.find((c) => c?.fulfillmentOrderId)?.fulfillmentOrderId;
99
+ if (fromClaim)
100
+ fulfillmentOrderId = String(fromClaim);
101
+ }
102
+ }
103
+ else {
104
+ unclaimedCount += 1;
105
+ }
106
+ }
107
+ return { claimed, unclaimedCount, consideredCount, fulfillmentOrderId };
108
+ }
109
+ export async function detectFulfillmentState(supabase, input) {
110
+ const { connectionId, deliveryOrderId } = input;
111
+ const filterIds = Array.isArray(input.fulfillmentRequestIds)
112
+ ? input.fulfillmentRequestIds.map((s) => String(s))
113
+ : null;
114
+ let doData = input.deliveryOrderData ?? null;
115
+ if (!doData && deliveryOrderId) {
116
+ const { data: doRow } = await supabase
117
+ .from("floriday_delivery_orders_cache")
118
+ .select("floriday_id, data")
119
+ .eq("connection_id", connectionId)
120
+ .eq("floriday_id", deliveryOrderId)
121
+ .maybeSingle();
122
+ doData = doRow?.data ?? null;
123
+ }
124
+ // ─ Bron (a): claims op de delivery-order-cache ───────────────────────────
125
+ if (doData) {
126
+ const scan = scanClaims(doData, filterIds);
127
+ if (scan.consideredCount > 0 && scan.unclaimedCount === 0) {
128
+ return {
129
+ code: "already_fulfilled",
130
+ source: "delivery_order_claims",
131
+ message: "Al afgehandeld bij Floriday",
132
+ fulfillmentOrderId: scan.fulfillmentOrderId,
133
+ skipped: scan.claimed,
134
+ };
135
+ }
136
+ }
137
+ // ─ Bron (b): fulfillment-order-cache (zie het commentaarblok hierboven) ──
138
+ if (deliveryOrderId) {
139
+ const { data: existing } = await supabase
140
+ .from("floriday_fulfillment_orders_cache")
141
+ .select("floriday_id")
142
+ .eq("connection_id", connectionId)
143
+ .eq("delivery_order_id", deliveryOrderId)
144
+ .maybeSingle();
145
+ if (existing?.floriday_id) {
146
+ return {
147
+ code: "already_fulfilled",
148
+ source: "fulfillment_orders_cache",
149
+ message: "Al afgehandeld bij Floriday",
150
+ fulfillmentOrderId: String(existing.floriday_id),
151
+ skipped: [],
152
+ };
153
+ }
154
+ }
155
+ if (!doData) {
156
+ return {
157
+ code: "delivery_order_not_cached",
158
+ message: "Delivery-order niet in cache — sync eerst",
159
+ };
160
+ }
161
+ return { code: "not_fulfilled" };
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // De bodybouwer
165
+ // ---------------------------------------------------------------------------
166
+ export async function buildFulfillmentOrderBody(supabase, input) {
167
+ const { connectionId, deliveryOrderId, deliveryOrderData } = input;
168
+ const doData = deliveryOrderData ?? {};
169
+ const allRequests = Array.isArray(doData.fulfillmentRequests)
170
+ ? doData.fulfillmentRequests
171
+ : [];
172
+ if (allRequests.length === 0) {
173
+ return {
174
+ kind: "error",
175
+ error: "Delivery-order bevat geen fulfillmentRequests",
176
+ permanent: true,
177
+ };
178
+ }
179
+ const filterIds = Array.isArray(input.fulfillmentRequestIds)
180
+ ? input.fulfillmentRequestIds.map((s) => String(s))
181
+ : null;
182
+ const skipped = [];
183
+ const candidates = [];
184
+ for (const r of allRequests) {
185
+ const id = r?.fulfillmentRequestId ?? r?.id;
186
+ if (!id) {
187
+ return {
188
+ kind: "error",
189
+ error: "fulfillmentRequest zonder fulfillmentRequestId in delivery-order",
190
+ permanent: true,
191
+ };
192
+ }
193
+ if (filterIds && !filterIds.includes(String(id)))
194
+ continue;
195
+ // Veiligheidscheck: bestaat er al een fulfillment-order bij Floriday?
196
+ const claims = Array.isArray(r?.fulfillmentOrderClaims)
197
+ ? r.fulfillmentOrderClaims
198
+ : [];
199
+ if (claims.length > 0) {
200
+ skipped.push({ fulfillmentRequestId: String(id), reason: "already_claimed" });
201
+ continue;
202
+ }
203
+ // Ladingdrager komt per request uit de delivery-order; "NONE" slaan we over.
204
+ const loadCarrier = String(r?.loadCarrier ?? "").trim();
205
+ if (!loadCarrier || loadCarrier === "NONE") {
206
+ skipped.push({ fulfillmentRequestId: String(id), reason: "load_carrier_none" });
207
+ continue;
208
+ }
209
+ candidates.push(r);
210
+ }
211
+ if (candidates.length === 0) {
212
+ const allClaimed = skipped.length > 0 && skipped.every((s) => s.reason === "already_claimed");
213
+ if (allClaimed) {
214
+ return {
215
+ kind: "already_fulfilled",
216
+ code: "already_fulfilled",
217
+ source: "delivery_order_claims",
218
+ message: "Al afgehandeld bij Floriday",
219
+ fulfillmentOrderId: scanClaims(doData, filterIds).fulfillmentOrderId,
220
+ skipped,
221
+ };
222
+ }
223
+ return {
224
+ kind: "error",
225
+ error: "Geen verstuurbare fulfillmentRequests: alle requests zijn overgeslagen " +
226
+ `(${skipped.map((s) => `${s.fulfillmentRequestId}=${s.reason}`).join(", ") || "geen"})`,
227
+ permanent: true,
228
+ };
229
+ }
230
+ // ─ Join: tradeItemId + organisatie-id's uit de sales-order-cache ─────────
231
+ const salesOrderIds = Array.from(new Set(candidates
232
+ .map((r) => (r?.salesOrderId ? String(r.salesOrderId) : ""))
233
+ .filter(Boolean)));
234
+ if (salesOrderIds.length === 0) {
235
+ return {
236
+ kind: "error",
237
+ error: "fulfillmentRequests zonder salesOrderId — tradeItemId niet te bepalen",
238
+ permanent: true,
239
+ };
240
+ }
241
+ const { data: soRows, error: soError } = await supabase
242
+ .from("floriday_salesorders_cache")
243
+ .select("floriday_id, data")
244
+ .eq("connection_id", connectionId)
245
+ .in("floriday_id", salesOrderIds);
246
+ if (soError) {
247
+ return {
248
+ kind: "error",
249
+ error: `Sales-orders lezen mislukt: ${soError.message}`,
250
+ permanent: false,
251
+ };
252
+ }
253
+ const soById = new Map();
254
+ for (const row of (soRows ?? [])) {
255
+ soById.set(String(row.floriday_id), row.data ?? {});
256
+ }
257
+ const missing = salesOrderIds.filter((id) => !soById.has(id));
258
+ if (missing.length > 0) {
259
+ return {
260
+ kind: "error",
261
+ error: `Sales-order(s) niet in cache — sync eerst: ${missing.join(", ")}`,
262
+ permanent: false,
263
+ };
264
+ }
265
+ const firstSo = soById.get(String(candidates[0].salesOrderId));
266
+ const supplierOrganizationId = firstSo?.supplierOrganizationId ?? null;
267
+ if (!supplierOrganizationId) {
268
+ return {
269
+ kind: "error",
270
+ error: "supplierOrganizationId ontbreekt op de gekoppelde sales-order",
271
+ permanent: false,
272
+ };
273
+ }
274
+ // ─ Groeperen per loadCarrier-waarde ──────────────────────────────────────
275
+ const groups = new Map();
276
+ for (const r of candidates) {
277
+ const key = String(r.loadCarrier).trim();
278
+ const list = groups.get(key);
279
+ if (list)
280
+ list.push(r);
281
+ else
282
+ groups.set(key, [r]);
283
+ }
284
+ const usedRequestIds = [];
285
+ const loadCarriers = [];
286
+ let carrierIndex = 0;
287
+ for (const [loadCarrierType, reqs] of groups) {
288
+ carrierIndex += 1;
289
+ const items = [];
290
+ let itemIndex = 0;
291
+ for (const r of reqs) {
292
+ const id = String(r?.fulfillmentRequestId ?? r?.id);
293
+ const count = resolvePackageCount(r);
294
+ if (count === null) {
295
+ return {
296
+ kind: "error",
297
+ error: `Aantal pakketten ontbreekt op fulfillmentRequest ${id}`,
298
+ permanent: true,
299
+ };
300
+ }
301
+ const so = soById.get(String(r.salesOrderId));
302
+ const tradeItemId = so?.tradeItemId ?? null;
303
+ if (!tradeItemId) {
304
+ return {
305
+ kind: "error",
306
+ error: `tradeItemId ontbreekt op sales-order ${r.salesOrderId}`,
307
+ permanent: false,
308
+ };
309
+ }
310
+ itemIndex += 1;
311
+ items.push({
312
+ sortIndex: itemIndex,
313
+ fulfillmentRequestId: id,
314
+ salesOrderId: String(r.salesOrderId),
315
+ tradeItemId,
316
+ numberOfPackages: count,
317
+ });
318
+ usedRequestIds.push(id);
319
+ }
320
+ loadCarriers.push({
321
+ sortIndex: carrierIndex,
322
+ loadCarrierType,
323
+ numberOfAdditionalLayers: 0,
324
+ loadCarrierItems: items,
325
+ });
326
+ }
327
+ const fulfillmentOrderId = await deriveFulfillmentOrderId(deliveryOrderId);
328
+ const body = {
329
+ type: "DIRECT_SALES",
330
+ fulfillmentOrderId,
331
+ supplierOrganizationId,
332
+ fulfillingOrganizationId: supplierOrganizationId,
333
+ despatchWarehouseId: doData.despatchWarehouseId ?? null,
334
+ latestDeliveryDateTime: doData.deliveryDateTime ?? null,
335
+ oneLabelOnly: false,
336
+ destination: doData.destination ?? null,
337
+ loadCarriers,
338
+ };
339
+ if (!body.despatchWarehouseId) {
340
+ return {
341
+ kind: "error",
342
+ error: "despatchWarehouseId ontbreekt in de delivery-order-cache",
343
+ permanent: false,
344
+ };
345
+ }
346
+ if (!body.latestDeliveryDateTime) {
347
+ return {
348
+ kind: "error",
349
+ error: "deliveryDateTime ontbreekt in de delivery-order-cache",
350
+ permanent: false,
351
+ };
352
+ }
353
+ if (!body.destination) {
354
+ return {
355
+ kind: "error",
356
+ error: "destination ontbreekt in de delivery-order-cache",
357
+ permanent: false,
358
+ };
359
+ }
360
+ return { kind: "body", body, fulfillmentOrderId, usedRequestIds, skipped };
361
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"fulfillment-order-send.d.ts","sourceRoot":"","sources":["../../../src/lib/gateway/fulfillment-order-send.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAkB5D,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,0BAA0B,CAAC,CA8GrC;AAcD,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,QAAQ,CAAC,CAmCnB"}
1
+ {"version":3,"file":"fulfillment-order-send.d.ts","sourceRoot":"","sources":["../../../src/lib/gateway/fulfillment-order-send.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAQ5D,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,0BAA0B,CAAC,CA0JrC;AAeD,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,QAAQ,CAAC,CAmCnB"}
@@ -2,20 +2,7 @@
2
2
  // Spiegel van src/routes/api/public/gateway/fulfillment-order.send.ts.
3
3
  import { resolveGatewayEnvironment } from "./shared.js";
4
4
  import { florydayWrite, getActiveFlorydayCredentials } from "../floriday.functions.js";
5
- function extractPackageCount(req) {
6
- const candidates = [
7
- req?.numberOfPackages,
8
- req?.packageCount,
9
- req?.numberOfLoadCarriers,
10
- req?.quantity,
11
- ];
12
- for (const c of candidates) {
13
- const n = Number(c);
14
- if (Number.isFinite(n) && n > 0)
15
- return n;
16
- }
17
- return 1;
18
- }
5
+ import { buildFulfillmentOrderBody, detectFulfillmentState, } from "./fulfillment-order-body.js";
19
6
  export async function runFulfillmentOrderSend(supabase, body, caller) {
20
7
  const deliveryOrderId = String(body?.deliveryOrderId ?? "").trim();
21
8
  const explicitLoadCarrierItems = Array.isArray(body?.loadCarrierItems)
@@ -29,28 +16,53 @@ export async function runFulfillmentOrderSend(supabase, body, caller) {
29
16
  payload: { ok: false, error: `Geen actieve Floriday-koppeling voor ${env}` },
30
17
  };
31
18
  }
32
- // Idempotency: is er al een fulfillment-order voor deze delivery?
33
- if (deliveryOrderId) {
34
- const { data: existing } = await supabase
35
- .from("floriday_fulfillment_orders_cache")
36
- .select("floriday_id")
37
- .eq("connection_id", conn.id)
38
- .eq("delivery_order_id", deliveryOrderId)
39
- .maybeSingle();
40
- if (existing?.floriday_id) {
41
- return {
42
- status: 200,
43
- payload: {
44
- ok: true,
45
- skipped: true,
46
- fulfillmentOrderId: existing.floriday_id,
47
- },
48
- };
49
- }
19
+ const fulfillmentRequestIds = Array.isArray(body?.fulfillmentRequestIds)
20
+ ? body.fulfillmentRequestIds.map((s) => String(s))
21
+ : null;
22
+ // Detectie "al afgehandeld" — één functie, twee bronnen, één uitkomst.
23
+ // Deze check geldt voor BEIDE paden: ook een caller die zelf
24
+ // `loadCarrierItems` meestuurt (passthrough) komt hier langs, zodat er langs
25
+ // geen enkel pad een dubbele fulfillment-order bij Floriday kan ontstaan.
26
+ const detection = await detectFulfillmentState(supabase, {
27
+ connectionId: conn.id,
28
+ deliveryOrderId,
29
+ fulfillmentRequestIds,
30
+ });
31
+ if (detection.code === "already_fulfilled") {
32
+ return {
33
+ status: 200,
34
+ payload: {
35
+ ok: true,
36
+ code: "already_fulfilled",
37
+ // `skipped` blijft staan als alias van het codeveld.
38
+ skipped: true,
39
+ source: detection.source,
40
+ reason: detection.message,
41
+ fulfillmentOrderId: detection.fulfillmentOrderId,
42
+ skippedRequests: detection.skipped,
43
+ },
44
+ };
50
45
  }
51
- // Body opbouwen — hetzij passthrough, hetzij vanuit cache.
52
- let loadCarrierItems = explicitLoadCarrierItems;
53
- if (!loadCarrierItems) {
46
+ if (detection.code === "delivery_order_not_cached") {
47
+ // Niet stilzwijgend versturen: zonder cache kan de claim-check niet worden
48
+ // uitgevoerd, ook niet op het passthrough-pad.
49
+ return {
50
+ status: 409,
51
+ payload: {
52
+ ok: false,
53
+ code: "delivery_order_not_cached",
54
+ error: detection.message,
55
+ },
56
+ };
57
+ }
58
+ // Body opbouwen — hetzij passthrough (caller geeft zelf loadCarrierItems),
59
+ // hetzij via de gedeelde bodybouwer vanuit de cache.
60
+ let writeBody;
61
+ let skipped = [];
62
+ if (explicitLoadCarrierItems) {
63
+ writeBody = { loadCarrierItems: explicitLoadCarrierItems };
64
+ }
65
+ else {
54
66
  const { data: doRow } = await supabase
55
67
  .from("floriday_delivery_orders_cache")
56
68
  .select("floriday_id, data")
@@ -59,49 +71,49 @@ export async function runFulfillmentOrderSend(supabase, body, caller) {
59
71
  .maybeSingle();
60
72
  if (!doRow) {
61
73
  return {
62
- status: 404,
63
- payload: { ok: false, error: "Delivery-order niet in cache — sync eerst" },
74
+ status: 409,
75
+ payload: {
76
+ ok: false,
77
+ code: "delivery_order_not_cached",
78
+ error: "Delivery-order niet in cache — sync eerst",
79
+ },
64
80
  };
65
81
  }
66
- const allRequests = Array.isArray(doRow.data?.fulfillmentRequests)
67
- ? doRow.data.fulfillmentRequests
68
- : [];
69
- const filterIds = Array.isArray(body?.fulfillmentRequestIds)
70
- ? body.fulfillmentRequestIds.map((s) => String(s))
71
- : null;
72
- const requests = allRequests
73
- .filter((r) => {
74
- const id = r?.fulfillmentRequestId ?? r?.id;
75
- return !filterIds || filterIds.includes(String(id));
76
- })
77
- .map((r) => {
78
- const id = r?.fulfillmentRequestId ?? r?.id;
79
- if (!id)
80
- return null;
82
+ const built = await buildFulfillmentOrderBody(supabase, {
83
+ connectionId: conn.id,
84
+ deliveryOrderId,
85
+ deliveryOrderData: doRow.data,
86
+ fulfillmentRequestIds,
87
+ });
88
+ if (built.kind === "already_fulfilled") {
81
89
  return {
82
- deliveryOrderId,
83
- fulfillmentRequestId: id,
84
- numberOfPackages: extractPackageCount(r),
85
- };
86
- })
87
- .filter(Boolean);
88
- if (requests.length === 0) {
89
- return {
90
- status: 400,
90
+ status: 200,
91
91
  payload: {
92
- ok: false,
93
- error: "Geen fulfillmentRequests gevonden in delivery-order",
92
+ ok: true,
93
+ code: "already_fulfilled",
94
+ skipped: true,
95
+ source: built.source,
96
+ reason: built.message,
97
+ fulfillmentOrderId: built.fulfillmentOrderId,
98
+ skippedRequests: built.skipped,
94
99
  },
95
100
  };
96
101
  }
97
- loadCarrierItems = [{ fulfillmentRequests: requests }];
102
+ if (built.kind === "error") {
103
+ return {
104
+ status: built.permanent ? 400 : 409,
105
+ payload: { ok: false, error: built.error },
106
+ };
107
+ }
108
+ writeBody = built.body;
109
+ skipped = built.skipped;
98
110
  }
99
111
  const result = await florydayWrite(supabase, env, {
100
112
  module: "auto-process",
101
113
  action: "send-fulfillment-order",
102
114
  method: "POST",
103
115
  path: "/fulfillment-orders",
104
- body: { loadCarrierItems },
116
+ body: writeBody,
105
117
  referenceId: deliveryOrderId || "batch",
106
118
  idempotencyKey: deliveryOrderId
107
119
  ? `do:${deliveryOrderId}:fulfillment-order`
@@ -111,11 +123,26 @@ export async function runFulfillmentOrderSend(supabase, body, caller) {
111
123
  const fulfillmentOrderId = result.data?.fulfillmentOrderId ??
112
124
  result.data?.id ??
113
125
  result.data?.floridayId;
126
+ if (result.ok) {
127
+ // Geen `error` in een ok:true-payload.
128
+ return {
129
+ status: 200,
130
+ payload: {
131
+ ok: true,
132
+ code: "created",
133
+ skipped: false,
134
+ skippedRequests: skipped,
135
+ fulfillmentOrderId,
136
+ },
137
+ };
138
+ }
114
139
  return {
115
- status: result.ok ? 200 : 502,
140
+ status: 502,
116
141
  payload: {
117
- ok: result.ok,
142
+ ok: false,
143
+ code: "write_failed",
118
144
  skipped: false,
145
+ skippedRequests: skipped,
119
146
  fulfillmentOrderId,
120
147
  error: result.error,
121
148
  },
@@ -18,4 +18,5 @@ export * from "./supply-line.js";
18
18
  export * from "./auto-process.js";
19
19
  export * from "./cron-scan.js";
20
20
  export * from "./documents.js";
21
+ export * from "./fulfillment-order-body.js";
21
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/gateway/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/gateway/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,YAAY,CAAC;AAC3B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC"}
@@ -18,3 +18,4 @@ export * from "./supply-line.js";
18
18
  export * from "./auto-process.js";
19
19
  export * from "./cron-scan.js";
20
20
  export * from "./documents.js";
21
+ export * from "./fulfillment-order-body.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/floriday-authenticatie-module",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "main": "./dist-lib/index.js",