@flowselections/floriday-voorraad 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 (50) hide show
  1. package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts.map +1 -1
  2. package/dist-lib/components/voorraad/ValidationOverviewTable.js +45 -14
  3. package/dist-lib/integrations/supabase/auth-middleware.d.ts +914 -19
  4. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  5. package/dist-lib/integrations/supabase/client.d.ts +914 -19
  6. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  7. package/dist-lib/integrations/supabase/client.server.d.ts +914 -19
  8. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  9. package/dist-lib/integrations/supabase/types.d.ts +914 -17
  10. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  11. package/dist-lib/lib/floricode-required-features.functions.d.ts +914 -19
  12. package/dist-lib/lib/floricode-required-features.functions.d.ts.map +1 -1
  13. package/dist-lib/lib/floriday-2026v1.server.d.ts +1 -1
  14. package/dist-lib/lib/floriday-2026v1.server.d.ts.map +1 -1
  15. package/dist-lib/lib/floriday-2026v1.server.js +4 -0
  16. package/dist-lib/lib/floriday-customer-offer.functions.d.ts +914 -19
  17. package/dist-lib/lib/floriday-customer-offer.functions.d.ts.map +1 -1
  18. package/dist-lib/lib/floriday-env.functions.d.ts +1790 -0
  19. package/dist-lib/lib/floriday-env.functions.d.ts.map +1 -1
  20. package/dist-lib/lib/floriday-gateway.functions.d.ts +5370 -0
  21. package/dist-lib/lib/floriday-gateway.functions.d.ts.map +1 -1
  22. package/dist-lib/lib/floriday-gateway.server.d.ts +7 -1
  23. package/dist-lib/lib/floriday-gateway.server.d.ts.map +1 -1
  24. package/dist-lib/lib/floriday-gateway.server.js +50 -3
  25. package/dist-lib/lib/floriday-relink.d.ts +16 -0
  26. package/dist-lib/lib/floriday-relink.d.ts.map +1 -0
  27. package/dist-lib/lib/floriday-relink.js +14 -0
  28. package/dist-lib/lib/floriday-vbn-required.functions.d.ts +1790 -0
  29. package/dist-lib/lib/floriday-vbn-required.functions.d.ts.map +1 -1
  30. package/dist-lib/lib/floriday-warehouse.functions.d.ts +2685 -0
  31. package/dist-lib/lib/floriday-warehouse.functions.d.ts.map +1 -1
  32. package/dist-lib/lib/floriday-write-gate.server.d.ts +60 -0
  33. package/dist-lib/lib/floriday-write-gate.server.d.ts.map +1 -0
  34. package/dist-lib/lib/floriday-write-gate.server.js +196 -0
  35. package/dist-lib/lib/floriday-writes.functions.d.ts +1790 -0
  36. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  37. package/dist-lib/lib/inventory-settings.functions.d.ts +1790 -0
  38. package/dist-lib/lib/inventory-settings.functions.d.ts.map +1 -1
  39. package/dist-lib/lib/logistics-buffer-events.server.d.ts +13 -0
  40. package/dist-lib/lib/logistics-buffer-events.server.d.ts.map +1 -0
  41. package/dist-lib/lib/logistics-buffer-events.server.js +365 -0
  42. package/dist-lib/lib/logistics-events.d.ts.map +1 -1
  43. package/dist-lib/lib/logistics-events.js +36 -7
  44. package/dist-lib/lib/logistics-tasks.server.d.ts.map +1 -1
  45. package/dist-lib/lib/logistics-tasks.server.js +38 -6
  46. package/dist-lib/lib/module-health.functions.d.ts +1790 -0
  47. package/dist-lib/lib/module-health.functions.d.ts.map +1 -1
  48. package/dist-lib/lib/stock-sync.functions.d.ts +1790 -0
  49. package/dist-lib/lib/stock-sync.functions.d.ts.map +1 -1
  50. package/package.json +1 -1
@@ -0,0 +1,13 @@
1
+ type Admin = any;
2
+ export interface BufferEventSummary {
3
+ events: number;
4
+ closed: number;
5
+ detached: number;
6
+ recalculated: number;
7
+ errors: string[];
8
+ }
9
+ export declare function runBufferEvents(): Promise<BufferEventSummary>;
10
+ /** True wanneer een taak niet meer uitgevoerd mag worden (buffer dicht/af). */
11
+ export declare function isBufferClosed(admin: Admin, bufferRef: string | null): Promise<boolean>;
12
+ export {};
13
+ //# sourceMappingURL=logistics-buffer-events.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logistics-buffer-events.server.d.ts","sourceRoot":"","sources":["../../src/lib/logistics-buffer-events.server.ts"],"names":[],"mappings":"AA0BA,KAAK,KAAK,GAAG,GAAG,CAAC;AAEjB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAqUD,wBAAsB,eAAe,IAAI,OAAO,CAAC,kBAAkB,CAAC,CA6EnE;AAED,+EAA+E;AAC/E,wBAAsB,cAAc,CAClC,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,OAAO,CAAC,OAAO,CAAC,CAQlB"}
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Verwerkt de seintjes van de Logistics Engine over orderbuffers.
3
+ *
4
+ * De Engine sluit een buffer zodra alle karren afgehandeld zijn
5
+ * (`buffer.closed`) en koppelt orders die er nog aan hingen los
6
+ * (`buffer.order_detached`). Deze module beslist niets over buffers: ze
7
+ * herberekent alleen reserveringen en beschikbare voorraad.
8
+ *
9
+ * Eigenschappen:
10
+ * - Idempotent: elk seintje wordt hoogstens één keer doorgerekend
11
+ * (`voorraad_logistics_event_log`, primaire sleutel = event-id).
12
+ * - Nooit terugboeken: `physical_stock` wordt niet aangeraakt; al afgeboekte
13
+ * voorraad van afgehandelde karren blijft staan.
14
+ * - Reserveringen worden absoluut gezet uit de nog openstaande taken, dus een
15
+ * herhaalde run kan niet dubbeltellen.
16
+ * - Gesloten of afgehandelde buffers krijgen niets meer toegewezen.
17
+ *
18
+ * Server-only: nooit importeren vanuit componenten.
19
+ */
20
+ import { OWNER_MODULE, parseStockReservePlan } from "./logistics-engine";
21
+ const BUFFER_EVENT_TYPES = ["buffer.closed", "buffer.order_detached"];
22
+ const LOOKBACK_DAYS = 7;
23
+ const EVENT_BATCH = 50;
24
+ const ACTIVE_TASK_STATUSES = ["planned", "ready", "in_progress"];
25
+ function str(value) {
26
+ const s = String(value ?? "").trim();
27
+ return s ? s : null;
28
+ }
29
+ /** Alle manieren waarop een buffer in `logistics_tasks.buffer_ref` kan staan. */
30
+ function bufferRefsFrom(payload, prefix) {
31
+ const pick = (base) => {
32
+ if (prefix === "")
33
+ return payload?.[base];
34
+ const key = prefix + base.charAt(0).toUpperCase() + base.slice(1);
35
+ return payload?.[key];
36
+ };
37
+ return [str(pick("bufferId")), str(pick("bufferKey")), str(pick("bufferRef"))].filter((v) => Boolean(v));
38
+ }
39
+ /**
40
+ * Verwijzingen die als "dicht" gelden.
41
+ *
42
+ * Een buffer-id is uniek, dus die telt altijd. Een buffersleutel wordt door
43
+ * meerdere buffers gedeeld: die telt alleen mee wanneer er geen enkele nog
44
+ * open buffer met dezelfde sleutel bekend is. Zo blokkeert het sluiten van
45
+ * één buffer nooit de andere buffers met dezelfde sleutel.
46
+ */
47
+ async function closedBufferRefs(admin) {
48
+ const { data } = await admin
49
+ .from("voorraad_buffer_state")
50
+ .select("buffer_id,buffer_key,closed_at");
51
+ const rows = (data ?? []);
52
+ const set = new Set();
53
+ const openKeys = new Set();
54
+ for (const row of rows) {
55
+ if (row.closed_at)
56
+ set.add(String(row.buffer_id));
57
+ else if (row.buffer_key)
58
+ openKeys.add(row.buffer_key);
59
+ }
60
+ for (const row of rows) {
61
+ if (row.closed_at && row.buffer_key && !openKeys.has(row.buffer_key)) {
62
+ set.add(row.buffer_key);
63
+ }
64
+ }
65
+ return set;
66
+ }
67
+ /**
68
+ * Toets bij de Engine of een buffer echt afgehandeld is. Deze module bepaalt
69
+ * dat nooit zelf; bij twijfel behandelen we de buffer als open.
70
+ */
71
+ async function bufferHandled(admin, bufferId) {
72
+ if (!bufferId)
73
+ return false;
74
+ try {
75
+ const { data, error } = await admin.rpc("logistics_buffer_handled", { p_buffer: bufferId });
76
+ if (error)
77
+ return false;
78
+ return data === true;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ /** Artikelsleutels uit alle taken van een buffer (ongeacht status). */
85
+ async function keysForBuffer(admin, refs) {
86
+ if (refs.length === 0)
87
+ return [];
88
+ const { data } = await admin
89
+ .from("logistics_tasks")
90
+ .select("payload")
91
+ .eq("owner_module", OWNER_MODULE)
92
+ .in("buffer_ref", refs);
93
+ const keys = new Set();
94
+ for (const row of (data ?? [])) {
95
+ for (const line of parseStockReservePlan(row.payload).lines)
96
+ keys.add(line.tradeItemKey);
97
+ }
98
+ return [...keys];
99
+ }
100
+ /** Artikelsleutels uit alle taken van één order. */
101
+ async function keysForOrder(admin, orderRef) {
102
+ const { data } = await admin
103
+ .from("logistics_tasks")
104
+ .select("payload")
105
+ .eq("owner_module", OWNER_MODULE)
106
+ .eq("order_ref", orderRef);
107
+ const keys = new Set();
108
+ for (const row of (data ?? [])) {
109
+ for (const line of parseStockReservePlan(row.payload).lines)
110
+ keys.add(line.tradeItemKey);
111
+ }
112
+ return [...keys];
113
+ }
114
+ /**
115
+ * Zet de reservering van de opgegeven artikelen opnieuw op de som van de nog
116
+ * openstaande taken. Taken die klaar (`done`) zijn horen bij afgehandelde
117
+ * karren: die voorraad is al geboekt en telt hier bewust niet mee, zodat er
118
+ * niets wordt teruggedraaid of dubbel geteld. Taken op een gesloten buffer
119
+ * tellen evenmin mee.
120
+ */
121
+ async function recalculateReservations(admin, keys, closed, connectionId) {
122
+ if (keys.length === 0)
123
+ return [];
124
+ const { data: tasks } = await admin
125
+ .from("logistics_tasks")
126
+ .select("order_ref,buffer_ref,payload,status")
127
+ .eq("owner_module", OWNER_MODULE)
128
+ .in("status", ACTIVE_TASK_STATUSES);
129
+ const totals = new Map();
130
+ const orderRefs = new Map();
131
+ for (const key of keys) {
132
+ totals.set(key, 0);
133
+ orderRefs.set(key, new Set());
134
+ }
135
+ for (const task of (tasks ?? [])) {
136
+ if (task.buffer_ref && closed.has(task.buffer_ref))
137
+ continue;
138
+ for (const line of parseStockReservePlan(task.payload).lines) {
139
+ if (!totals.has(line.tradeItemKey))
140
+ continue;
141
+ totals.set(line.tradeItemKey, (totals.get(line.tradeItemKey) ?? 0) + line.quantity);
142
+ orderRefs.get(line.tradeItemKey).add(task.order_ref);
143
+ }
144
+ }
145
+ let query = admin
146
+ .from("trade_item_stock")
147
+ .select("connection_id,trade_item_key,physical_stock,reserved_stock")
148
+ .in("trade_item_key", keys);
149
+ if (connectionId)
150
+ query = query.eq("connection_id", connectionId);
151
+ const { data: rows } = await query;
152
+ const result = [];
153
+ for (const row of (rows ?? [])) {
154
+ const target = totals.get(row.trade_item_key) ?? 0;
155
+ if (target !== row.reserved_stock) {
156
+ // Absolute waarde: idempotent, en `physical_stock` blijft ongemoeid.
157
+ await admin
158
+ .from("trade_item_stock")
159
+ .update({ reserved_stock: target })
160
+ .eq("connection_id", row.connection_id)
161
+ .eq("trade_item_key", row.trade_item_key);
162
+ }
163
+ result.push({
164
+ tradeItemKey: row.trade_item_key,
165
+ reserved: target,
166
+ physical: row.physical_stock,
167
+ orderRefs: [...(orderRefs.get(row.trade_item_key) ?? [])],
168
+ });
169
+ }
170
+ return result;
171
+ }
172
+ /** Terugmelden aan de Engine met het ordernummer van de (losgekoppelde) order. */
173
+ async function emitStockChanged(admin, orderRef, bufferRef, lines) {
174
+ if (!orderRef || lines.length === 0)
175
+ return;
176
+ const rows = lines.map((line) => ({
177
+ source_module: OWNER_MODULE,
178
+ event_type: "stock.changed",
179
+ order_ref: orderRef,
180
+ shadow: false,
181
+ payload: {
182
+ tradeItemKey: line.tradeItemKey,
183
+ physicalStock: line.physical,
184
+ reservedStock: line.reserved,
185
+ availableStock: Math.max(0, line.physical - line.reserved),
186
+ bufferRef,
187
+ reason: "buffer_recalculated",
188
+ },
189
+ }));
190
+ await admin.from("logistics_events").insert(rows);
191
+ }
192
+ async function handleBufferClosed(admin, event, closed, connectionId) {
193
+ const payload = event.payload ?? {};
194
+ const bufferId = str(payload.bufferId);
195
+ const refs = bufferRefsFrom(payload, "");
196
+ if (bufferId) {
197
+ await admin.from("voorraad_buffer_state").upsert({
198
+ buffer_id: bufferId,
199
+ buffer_key: str(payload.bufferKey) ?? str(payload.bufferRef),
200
+ closed_at: event.created_at ?? new Date().toISOString(),
201
+ reason: str(payload.reason) ?? "buffer_closed",
202
+ updated_at: new Date().toISOString(),
203
+ }, { onConflict: "buffer_id" });
204
+ }
205
+ // Alleen verwijzingen die écht dicht zijn tellen mee: het buffer-id altijd,
206
+ // een gedeelde buffersleutel pas wanneer er geen open buffer meer op staat.
207
+ const fresh = await closedBufferRefs(admin);
208
+ for (const ref of fresh)
209
+ closed.add(ref);
210
+ const closedRefs = refs.filter((r) => closed.has(r));
211
+ const keys = await keysForBuffer(admin, refs);
212
+ // Nog niet gestarte reserveringen voor deze buffer worden niet meer
213
+ // uitgevoerd: een gesloten buffer krijgt geen voorraad meer toegewezen.
214
+ if (closedRefs.length > 0) {
215
+ await admin
216
+ .from("logistics_tasks")
217
+ .update({
218
+ status: "skipped",
219
+ claimed_by: null,
220
+ claimed_at: null,
221
+ next_attempt_at: null,
222
+ completed_at: new Date().toISOString(),
223
+ updated_at: new Date().toISOString(),
224
+ result: { reason: "buffer_closed", bufferId },
225
+ })
226
+ .eq("owner_module", OWNER_MODULE)
227
+ .in("buffer_ref", closedRefs)
228
+ .in("status", ["planned", "ready"]);
229
+ }
230
+ const lines = await recalculateReservations(admin, keys, closed, connectionId);
231
+ const orderRef = str(event.order_ref);
232
+ if (orderRef)
233
+ await emitStockChanged(admin, orderRef, refs[0] ?? null, lines);
234
+ return { keys: keys.length, lines: lines.length };
235
+ }
236
+ async function handleOrderDetached(admin, event, closed, connectionId) {
237
+ const payload = event.payload ?? {};
238
+ const orderRef = str(payload.orderRef) ?? str(event.order_ref);
239
+ if (!orderRef)
240
+ return { keys: 0, lines: 0 };
241
+ const newRefs = bufferRefsFrom(payload, "new");
242
+ const previousRefs = bufferRefsFrom(payload, "previous");
243
+ for (const ref of previousRefs) {
244
+ if (await bufferHandled(admin, str(payload.previousBufferId)))
245
+ closed.add(ref);
246
+ }
247
+ const newBufferId = str(payload.newBufferId);
248
+ const newBufferRef = newRefs[0] ?? null;
249
+ // De order staat vanaf nu op zichzelf (of in de nieuwe buffer): openstaande
250
+ // taken volgen mee, zodat de reservering onder het eigen ordernummer hangt.
251
+ await admin
252
+ .from("logistics_tasks")
253
+ .update({ buffer_ref: newBufferRef, updated_at: new Date().toISOString() })
254
+ .eq("owner_module", OWNER_MODULE)
255
+ .eq("order_ref", orderRef)
256
+ .in("status", ACTIVE_TASK_STATUSES);
257
+ if (newBufferId) {
258
+ await admin.from("voorraad_buffer_state").upsert({
259
+ buffer_id: newBufferId,
260
+ buffer_key: str(payload.newBufferKey),
261
+ updated_at: new Date().toISOString(),
262
+ }, { onConflict: "buffer_id" });
263
+ }
264
+ const previousBufferId = str(payload.previousBufferId);
265
+ if (previousBufferId) {
266
+ const { data: state } = await admin
267
+ .from("voorraad_buffer_state")
268
+ .select("detached_orders")
269
+ .eq("buffer_id", previousBufferId)
270
+ .maybeSingle();
271
+ const list = new Set(Array.isArray(state?.detached_orders) ? state.detached_orders.map(String) : []);
272
+ list.add(orderRef);
273
+ await admin.from("voorraad_buffer_state").upsert({
274
+ buffer_id: previousBufferId,
275
+ buffer_key: str(payload.previousBufferKey),
276
+ detached_orders: [...list],
277
+ updated_at: new Date().toISOString(),
278
+ }, { onConflict: "buffer_id" });
279
+ }
280
+ const keys = await keysForOrder(admin, orderRef);
281
+ const lines = await recalculateReservations(admin, keys, closed, connectionId);
282
+ await emitStockChanged(admin, orderRef, newBufferRef, lines);
283
+ return { keys: keys.length, lines: lines.length };
284
+ }
285
+ export async function runBufferEvents() {
286
+ const summary = {
287
+ events: 0,
288
+ closed: 0,
289
+ detached: 0,
290
+ recalculated: 0,
291
+ errors: [],
292
+ };
293
+ const { supabaseAdmin } = await import("../integrations/supabase/client.server");
294
+ const admin = supabaseAdmin;
295
+ const since = new Date(Date.now() - LOOKBACK_DAYS * 86400000).toISOString();
296
+ const { data: events, error } = await admin
297
+ .from("logistics_events")
298
+ .select("id,event_type,order_ref,payload,created_at")
299
+ .in("event_type", BUFFER_EVENT_TYPES)
300
+ .gte("created_at", since)
301
+ .order("created_at", { ascending: true })
302
+ .limit(EVENT_BATCH * 4);
303
+ if (error)
304
+ throw new Error(`seintjes lezen: ${error.message}`);
305
+ const all = (events ?? []);
306
+ if (all.length === 0)
307
+ return summary;
308
+ const { data: seenRows } = await admin
309
+ .from("voorraad_logistics_event_log")
310
+ .select("event_id")
311
+ .in("event_id", all.map((e) => e.id));
312
+ const seen = new Set((seenRows ?? []).map((r) => String(r.event_id)));
313
+ const todo = all.filter((e) => !seen.has(String(e.id))).slice(0, EVENT_BATCH);
314
+ if (todo.length === 0)
315
+ return summary;
316
+ let connectionId = null;
317
+ try {
318
+ const { resolveActiveFloridayConnection } = await import("./floriday-env.server");
319
+ connectionId = (await resolveActiveFloridayConnection(admin)).id;
320
+ }
321
+ catch {
322
+ connectionId = null;
323
+ }
324
+ const closed = await closedBufferRefs(admin);
325
+ for (const event of todo) {
326
+ try {
327
+ const result = event.event_type === "buffer.closed"
328
+ ? await handleBufferClosed(admin, event, closed, connectionId)
329
+ : await handleOrderDetached(admin, event, closed, connectionId);
330
+ if (event.event_type === "buffer.closed")
331
+ summary.closed++;
332
+ else
333
+ summary.detached++;
334
+ summary.recalculated += result.lines;
335
+ summary.events++;
336
+ await admin.from("voorraad_logistics_event_log").upsert({
337
+ event_id: event.id,
338
+ event_type: event.event_type,
339
+ order_ref: event.order_ref ?? null,
340
+ result,
341
+ processed_at: new Date().toISOString(),
342
+ }, { onConflict: "event_id" });
343
+ }
344
+ catch (e) {
345
+ const message = String(e?.message ?? e).slice(0, 500);
346
+ summary.errors.push(`${event.id}: ${message}`);
347
+ console.warn(`[voorraad] bufferseintje ${event.id} mislukt: ${message}`);
348
+ // Niet loggen als verwerkt: de volgende ronde probeert het opnieuw.
349
+ }
350
+ }
351
+ return summary;
352
+ }
353
+ /** True wanneer een taak niet meer uitgevoerd mag worden (buffer dicht/af). */
354
+ export async function isBufferClosed(admin, bufferRef) {
355
+ if (!bufferRef)
356
+ return false;
357
+ const closed = await closedBufferRefs(admin);
358
+ if (closed.has(bufferRef))
359
+ return true;
360
+ // Onbekend bij ons? Dan heeft de Engine het laatste woord.
361
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(bufferRef);
362
+ if (isUuid)
363
+ return await bufferHandled(admin, bufferRef);
364
+ return false;
365
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"logistics-events.d.ts","sourceRoot":"","sources":["../../src/lib/logistics-events.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,gBAAgB,GAAG,eAAe,GAAG,eAAe,GAAG,MAAM,CAAC;IACzE,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,mBAAmB,iBAUpE;AAED,UAAU,iBAAiB;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AA6BD,6CAA6C;AAC7C,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,iBAAiB,iBAmBnE"}
1
+ {"version":3,"file":"logistics-events.d.ts","sourceRoot":"","sources":["../../src/lib/logistics-events.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,gBAAgB,GAAG,eAAe,GAAG,eAAe,GAAG,MAAM,CAAC;IACzE,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,mBAAmB,iBAUpE;AAED,UAAU,iBAAiB;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AA6DD,6CAA6C;AAC7C,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,iBAAiB,iBAmBnE"}
@@ -19,18 +19,47 @@ export async function insertLogisticsEvent(input) {
19
19
  if (error)
20
20
  throw new Error(error.message);
21
21
  }
22
+ /**
23
+ * Buffers die door de Engine gesloten zijn; die krijgen niets meer. Een
24
+ * gedeelde buffersleutel telt pas mee als er geen open buffer meer op staat.
25
+ */
26
+ async function closedBufferRefs() {
27
+ const { data } = await supabase
28
+ .from("voorraad_buffer_state")
29
+ .select("buffer_id,buffer_key,closed_at");
30
+ const rows = (data ?? []);
31
+ const set = new Set();
32
+ const openKeys = new Set();
33
+ for (const row of rows) {
34
+ if (row.closed_at)
35
+ set.add(String(row.buffer_id));
36
+ else if (row.buffer_key)
37
+ openKeys.add(row.buffer_key);
38
+ }
39
+ for (const row of rows) {
40
+ if (row.closed_at && row.buffer_key && !openKeys.has(row.buffer_key))
41
+ set.add(row.buffer_key);
42
+ }
43
+ return set;
44
+ }
22
45
  /**
23
46
  * Zoekt de openstaande Engine-taak die dit artikel raakt, zodat het
24
- * stock.changed-event dezelfde order_ref (en context) draagt.
47
+ * stock.changed-event dezelfde order_ref (en context) draagt. Taken op een
48
+ * gesloten buffer tellen niet mee.
25
49
  */
26
50
  async function findOpenTaskFor(tradeItemKey) {
27
- const { data } = await supabase
28
- .from("logistics_tasks")
29
- .select("order_ref,strategy_id,shadow,payload,buffer_ref")
30
- .eq("owner_module", OWNER_MODULE)
31
- .in("status", ["planned", "ready", "in_progress"])
32
- .order("planned_start", { ascending: true });
51
+ const [{ data }, closed] = await Promise.all([
52
+ supabase
53
+ .from("logistics_tasks")
54
+ .select("order_ref,strategy_id,shadow,payload,buffer_ref")
55
+ .eq("owner_module", OWNER_MODULE)
56
+ .in("status", ["planned", "ready", "in_progress"])
57
+ .order("planned_start", { ascending: true }),
58
+ closedBufferRefs(),
59
+ ]);
33
60
  for (const task of (data ?? [])) {
61
+ if (task.buffer_ref && closed.has(task.buffer_ref))
62
+ continue;
34
63
  const plan = parseStockReservePlan(task.payload);
35
64
  if (plan.lines.some((l) => l.tradeItemKey === tradeItemKey)) {
36
65
  return { task, loadCarriers: plan.loadCarriers };
@@ -1 +1 @@
1
- {"version":3,"file":"logistics-tasks.server.d.ts","sourceRoot":"","sources":["../../src/lib/logistics-tasks.server.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,SAAS,kBAAkB,CAAC;AACzC,eAAO,MAAM,YAAY,IAAI,CAAC;AAQ9B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,SAAS,QAAQ;CAC3B;AA0BD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AA0FD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CAuJ7D;AAgBD,wBAAgB,uBAAuB,IAAI,IAAI,CA2B9C"}
1
+ {"version":3,"file":"logistics-tasks.server.d.ts","sourceRoot":"","sources":["../../src/lib/logistics-tasks.server.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,SAAS,kBAAkB,CAAC;AACzC,eAAO,MAAM,YAAY,IAAI,CAAC;AAQ9B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,SAAS,QAAQ;CAC3B;AA0BD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAuGD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CA2K7D;AAgBD,wBAAgB,uBAAuB,IAAI,IAAI,CAiC9C"}
@@ -63,6 +63,17 @@ function appendAttempt(task, entry) {
63
63
  * of er opnieuw geprobeerd wordt.
64
64
  */
65
65
  async function executeTask(supabaseAdmin, task, connectionId) {
66
+ // Een gesloten of volledig afgehandelde buffer krijgt geen voorraad meer
67
+ // toegewezen; de taak wordt overgeslagen in plaats van uitgevoerd.
68
+ const { isBufferClosed } = await import("./logistics-buffer-events.server");
69
+ if (await isBufferClosed(supabaseAdmin, task.buffer_ref)) {
70
+ return {
71
+ applied: false,
72
+ skipped: true,
73
+ diff: [],
74
+ message: "Buffer is gesloten of afgehandeld; reservering overgeslagen.",
75
+ };
76
+ }
66
77
  const plan = parseStockReservePlan(task.payload);
67
78
  if (plan.lines.length === 0) {
68
79
  return {
@@ -130,6 +141,18 @@ export async function runLogisticsTasks() {
130
141
  durationMs: 0,
131
142
  };
132
143
  const { supabaseAdmin } = await import("../integrations/supabase/client.server");
144
+ // Eerst de seintjes over gesloten en losgekoppelde buffers verwerken, zodat
145
+ // taken op een dichte buffer daarna niet meer worden uitgevoerd.
146
+ try {
147
+ const { runBufferEvents } = await import("./logistics-buffer-events.server");
148
+ const buffers = await runBufferEvents();
149
+ if (buffers.events > 0) {
150
+ console.info(`[voorraad] bufferseintjes verwerkt: ${buffers.closed} gesloten, ${buffers.detached} losgekoppeld, ${buffers.recalculated} artikel(en) herberekend`);
151
+ }
152
+ }
153
+ catch (e) {
154
+ console.warn("[voorraad] bufferseintjes verwerken mislukt:", e?.message ?? e);
155
+ }
133
156
  // Actieve koppeling is optioneel: zonder koppeling reserveren we op alle
134
157
  // rijen van het artikel. Faalt de resolutie, dan blokkeren we niet.
135
158
  let connectionId = null;
@@ -181,7 +204,7 @@ export async function runLogisticsTasks() {
181
204
  await supabaseAdmin
182
205
  .from("logistics_tasks")
183
206
  .update({
184
- status: "done",
207
+ status: result.skipped ? "skipped" : "done",
185
208
  attempts: attempt,
186
209
  attempt_log: log,
187
210
  last_error: null,
@@ -200,9 +223,14 @@ export async function runLogisticsTasks() {
200
223
  updated_at: nowIso,
201
224
  })
202
225
  .eq("id", task.id);
203
- if (task.shadow)
204
- summary.shadow++;
205
- summary.done++;
226
+ if (result.skipped) {
227
+ summary.skipped++;
228
+ }
229
+ else {
230
+ if (task.shadow)
231
+ summary.shadow++;
232
+ summary.done++;
233
+ }
206
234
  }
207
235
  catch (e) {
208
236
  const message = errorMessage(e);
@@ -289,9 +317,13 @@ export function maybeTickLogisticsTasks() {
289
317
  .or(`next_attempt_at.is.null,next_attempt_at.lte.${new Date().toISOString()}`);
290
318
  if (error)
291
319
  throw new Error(error.message);
292
- if (!count)
320
+ if (count) {
321
+ await runLogisticsTasks(); // verwerkt zelf ook de bufferseintjes
293
322
  return;
294
- await runLogisticsTasks();
323
+ }
324
+ // Geen openstaande taken, maar er kunnen wel nieuwe bufferseintjes zijn.
325
+ const { runBufferEvents } = await import("./logistics-buffer-events.server");
326
+ await runBufferEvents();
295
327
  }
296
328
  catch (e) {
297
329
  console.warn("[voorraad] achtergrond-tick mislukt:", e?.message ?? e);