@flowselections/logistiek-engine 1.0.99 → 1.0.104
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/BeladingIndelingCard.d.ts.map +1 -1
- package/dist-lib/components/BeladingIndelingCard.js +5 -1
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +600 -6
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +600 -6
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/moduleClient.d.ts +600 -6
- package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +616 -6
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/cartCoverage.functions.d.ts +600 -6
- package/dist-lib/lib/cartCoverage.functions.d.ts.map +1 -1
- package/dist-lib/lib/controlScan.server.d.ts +8 -0
- package/dist-lib/lib/controlScan.server.d.ts.map +1 -1
- package/dist-lib/lib/controlScan.server.js +86 -5
- package/dist-lib/lib/floridayCancellation.server.d.ts +15 -0
- package/dist-lib/lib/floridayCancellation.server.d.ts.map +1 -0
- package/dist-lib/lib/floridayCancellation.server.js +89 -0
- package/dist-lib/lib/floridayCancellation.test.d.ts +2 -0
- package/dist-lib/lib/floridayCancellation.test.d.ts.map +1 -0
- package/dist-lib/lib/floridayCancellation.test.js +42 -0
- package/dist-lib/lib/floridayCartCarriers.d.ts +61 -0
- package/dist-lib/lib/floridayCartCarriers.d.ts.map +1 -0
- package/dist-lib/lib/floridayCartCarriers.js +113 -0
- package/dist-lib/lib/floridayCartCarriers.test.d.ts +2 -0
- package/dist-lib/lib/floridayCartCarriers.test.d.ts.map +1 -0
- package/dist-lib/lib/floridayCartCarriers.test.js +66 -0
- package/dist-lib/lib/floridayDocuments.functions.d.ts +1800 -18
- package/dist-lib/lib/floridayDocuments.functions.d.ts.map +1 -1
- package/dist-lib/lib/floridayDocuments.server.d.ts +7 -0
- package/dist-lib/lib/floridayDocuments.server.d.ts.map +1 -1
- package/dist-lib/lib/floridayDocuments.server.js +16 -1
- package/dist-lib/lib/floridayPush.functions.d.ts +600 -6
- package/dist-lib/lib/floridayPush.functions.d.ts.map +1 -1
- package/dist-lib/lib/floridayPush.server.d.ts +2 -0
- package/dist-lib/lib/floridayPush.server.d.ts.map +1 -1
- package/dist-lib/lib/floridayPush.server.js +54 -3
- package/dist-lib/lib/layerUtilization.d.ts +2 -0
- package/dist-lib/lib/layerUtilization.d.ts.map +1 -0
- package/dist-lib/lib/layerUtilization.js +11 -0
- package/dist-lib/lib/layerUtilization.test.d.ts +2 -0
- package/dist-lib/lib/layerUtilization.test.d.ts.map +1 -0
- package/dist-lib/lib/layerUtilization.test.js +11 -0
- package/dist-lib/lib/loadCarrierPlanning.functions.d.ts +3000 -30
- package/dist-lib/lib/loadCarrierPlanning.functions.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanning.server.js +16 -4
- package/dist-lib/lib/logisticsRecalcJob.functions.d.ts +1200 -12
- package/dist-lib/lib/logisticsRecalcJob.functions.d.ts.map +1 -1
- package/dist-lib/lib/logisticsRecalculate.functions.d.ts +600 -6
- package/dist-lib/lib/logisticsRecalculate.functions.d.ts.map +1 -1
- package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
- package/dist-lib/lib/modulePrompts.js +46 -0
- package/dist-lib/lib/moduleVersion.d.ts +1 -1
- package/dist-lib/lib/moduleVersion.d.ts.map +1 -1
- package/dist-lib/lib/moduleVersion.js +1 -1
- package/dist-lib/lib/plannerDiff.functions.d.ts +600 -6
- package/dist-lib/lib/plannerDiff.functions.d.ts.map +1 -1
- package/dist-lib/lib/shippingNoteQueue.server.d.ts.map +1 -1
- package/dist-lib/lib/shippingNoteQueue.server.js +40 -1
- package/package.json +4 -4
|
@@ -8,6 +8,14 @@ export interface ControlScanRun {
|
|
|
8
8
|
}
|
|
9
9
|
/** Maximaal aantal wachtrondes op Floriday voordat de controle als mislukt stopt. */
|
|
10
10
|
export declare const CONTROL_SCAN_MAX_ATTEMPTS = 60;
|
|
11
|
+
/** Maximale wachttijd sinds goedkeuring (uren). */
|
|
12
|
+
export declare const CONTROL_SCAN_MAX_HOURS = 24;
|
|
13
|
+
/** Is de wachtgrens (pogingen of tijd) bereikt? */
|
|
14
|
+
export declare function controlWaitExceeded(row: {
|
|
15
|
+
attempts?: number | null;
|
|
16
|
+
approved_at?: string | null;
|
|
17
|
+
created_at?: string | null;
|
|
18
|
+
}, now?: number): "pogingen" | "tijd" | null;
|
|
11
19
|
export declare function runControlScanQueue(sb: SupabaseClient, options?: {
|
|
12
20
|
limit?: number;
|
|
13
21
|
}): Promise<ControlScanRun>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controlScan.server.d.ts","sourceRoot":"","sources":["../../src/lib/controlScan.server.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"controlScan.server.d.ts","sourceRoot":"","sources":["../../src/lib/controlScan.server.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAK5D,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAiBD,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAC5C,mDAAmD;AACnD,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAgBzC,mDAAmD;AACnD,wBAAgB,mBAAmB,CACjC,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAC1F,GAAG,GAAE,MAAmB,GACvB,UAAU,GAAG,MAAM,GAAG,IAAI,CAK5B;AA6BD,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,cAAc,EAClB,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/B,OAAO,CAAC,cAAc,CAAC,CAsOzB"}
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
// klantwaarden in deze code; de printer komt uit de werkwijze in de database.
|
|
16
16
|
// ============================================================================
|
|
17
17
|
import { printViaLayout } from "./layoutPrint.server.js";
|
|
18
|
+
import { isFloridayCancelled, FLORIDAY_CANCELLED_REASON } from "./floridayCancellation.server.js";
|
|
19
|
+
import { trace } from "./trace.server.js";
|
|
18
20
|
/** Maximaal aantal wachtrondes op Floriday voordat de controle als mislukt stopt. */
|
|
19
21
|
export const CONTROL_SCAN_MAX_ATTEMPTS = 60;
|
|
22
|
+
/** Maximale wachttijd sinds goedkeuring (uren). */
|
|
23
|
+
export const CONTROL_SCAN_MAX_HOURS = 24;
|
|
20
24
|
function pickPdf(result) {
|
|
21
25
|
const base64 = result["pdfBase64"] ??
|
|
22
26
|
result["base64"] ??
|
|
@@ -28,20 +32,31 @@ function pickPdf(result) {
|
|
|
28
32
|
null;
|
|
29
33
|
return { base64: base64 ?? null, url: url ?? null };
|
|
30
34
|
}
|
|
35
|
+
/** Is de wachtgrens (pogingen of tijd) bereikt? */
|
|
36
|
+
export function controlWaitExceeded(row, now = Date.now()) {
|
|
37
|
+
if (Number(row.attempts ?? 0) >= CONTROL_SCAN_MAX_ATTEMPTS)
|
|
38
|
+
return "pogingen";
|
|
39
|
+
const start = Date.parse(String(row.approved_at ?? row.created_at ?? ""));
|
|
40
|
+
if (Number.isFinite(start) && now - start >= CONTROL_SCAN_MAX_HOURS * 3600000)
|
|
41
|
+
return "tijd";
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
31
44
|
/**
|
|
32
|
-
* Laat de controle wachten op Floriday.
|
|
33
|
-
*
|
|
45
|
+
* Laat de controle wachten op Floriday. Na het maximum (pogingen of 24 uur)
|
|
46
|
+
* eindigt de controle als mislukt in plaats van eeuwig te wachten.
|
|
34
47
|
* Geeft true terug als de controle nu terminaal is afgesloten.
|
|
35
48
|
*/
|
|
36
49
|
async function keepWaiting(sb, row) {
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
50
|
+
const grens = controlWaitExceeded(row);
|
|
51
|
+
if (grens) {
|
|
39
52
|
await sb.rpc("logistics_control_scan_finish", {
|
|
40
53
|
_id: row.id,
|
|
41
54
|
_ok: false,
|
|
42
55
|
_status: "mislukt",
|
|
43
56
|
_error_code: "wachtlimiet",
|
|
44
|
-
_error:
|
|
57
|
+
_error: grens === "pogingen"
|
|
58
|
+
? `Na ${row.attempts} pogingen kende Floriday deze order nog steeds niet; de controle is gestopt.`
|
|
59
|
+
: `Na ${CONTROL_SCAN_MAX_HOURS} uur kende Floriday deze order nog steeds niet; de controle is gestopt.`,
|
|
45
60
|
});
|
|
46
61
|
return true;
|
|
47
62
|
}
|
|
@@ -84,6 +99,35 @@ export async function runControlScanQueue(sb, options = {}) {
|
|
|
84
99
|
run.failed++;
|
|
85
100
|
continue;
|
|
86
101
|
}
|
|
102
|
+
// Geannuleerd in Floriday? Dan direct stoppen.
|
|
103
|
+
const annulering = await isFloridayCancelled(sb, {
|
|
104
|
+
orderId: row.order_id,
|
|
105
|
+
orderNumber: row.order_number,
|
|
106
|
+
fulfillmentOrderId: row.fulfillment_order_id ?? null,
|
|
107
|
+
});
|
|
108
|
+
if (annulering.cancelled) {
|
|
109
|
+
await sb.rpc("logistics_control_scan_finish", {
|
|
110
|
+
_id: row.id,
|
|
111
|
+
_ok: false,
|
|
112
|
+
_status: "mislukt",
|
|
113
|
+
_error_code: "floriday_geannuleerd",
|
|
114
|
+
_error: `${FLORIDAY_CANCELLED_REASON} (${annulering.bron}).`,
|
|
115
|
+
});
|
|
116
|
+
await trace(sb, {
|
|
117
|
+
orderRef: row.order_number,
|
|
118
|
+
orderSource: "floriday",
|
|
119
|
+
cartRef: row.cart_id,
|
|
120
|
+
phase: "controle",
|
|
121
|
+
step: "Afleverbrief gestopt: order geannuleerd",
|
|
122
|
+
status: "mislukt",
|
|
123
|
+
actor: "engine:controlepost",
|
|
124
|
+
errorClass: "extern",
|
|
125
|
+
reason: `${FLORIDAY_CANCELLED_REASON} (${annulering.bron}).`,
|
|
126
|
+
error: FLORIDAY_CANCELLED_REASON,
|
|
127
|
+
});
|
|
128
|
+
run.failed++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
87
131
|
// Stand van de push naar Floriday. De belading gaat via de bestaande
|
|
88
132
|
// pushwachtrij; wij wachten tot Floriday de fulfillment-order kent.
|
|
89
133
|
const { data: push } = await sb
|
|
@@ -113,6 +157,43 @@ export async function runControlScanQueue(sb, options = {}) {
|
|
|
113
157
|
run.waiting++;
|
|
114
158
|
continue;
|
|
115
159
|
}
|
|
160
|
+
// Eerst de status bij Floriday: pas bij ACCEPTED/DELIVERED bestaat het document.
|
|
161
|
+
const { getFlorydayFulfillmentStatus } = await import("./floridayDocuments.server.js");
|
|
162
|
+
const besluit = await getFlorydayFulfillmentStatus(sb, fulfillmentOrderId);
|
|
163
|
+
if (besluit.kind === "afgewezen" || besluit.kind === "geannuleerd") {
|
|
164
|
+
const reden = besluit.kind === "afgewezen"
|
|
165
|
+
? `Floriday heeft de afleverbrief afgewezen: ${besluit.reden}`
|
|
166
|
+
: "Floriday heeft deze order geannuleerd (fulfillment-order).";
|
|
167
|
+
await sb.rpc("logistics_control_scan_finish", {
|
|
168
|
+
_id: row.id,
|
|
169
|
+
_ok: false,
|
|
170
|
+
_status: "mislukt",
|
|
171
|
+
_fulfillment_order_id: fulfillmentOrderId,
|
|
172
|
+
_error_code: besluit.kind === "afgewezen" ? "floriday_afgewezen" : "floriday_geannuleerd",
|
|
173
|
+
_error: reden,
|
|
174
|
+
});
|
|
175
|
+
await trace(sb, {
|
|
176
|
+
orderRef: row.order_number,
|
|
177
|
+
orderSource: "floriday",
|
|
178
|
+
cartRef: row.cart_id,
|
|
179
|
+
phase: "controle",
|
|
180
|
+
step: "Afleverbrief gestopt: Floriday-status " + besluit.status,
|
|
181
|
+
status: "mislukt",
|
|
182
|
+
actor: "engine:controlepost",
|
|
183
|
+
errorClass: "extern",
|
|
184
|
+
reason: reden,
|
|
185
|
+
error: reden,
|
|
186
|
+
});
|
|
187
|
+
run.failed++;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (besluit.kind === "wachten") {
|
|
191
|
+
if (await keepWaiting(sb, row))
|
|
192
|
+
run.failed++;
|
|
193
|
+
else
|
|
194
|
+
run.waiting++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
116
197
|
const doc = (await getFlorydayDocument(sb, {
|
|
117
198
|
kind: "delivery-note",
|
|
118
199
|
fulfillmentOrderId,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
export type CancellationResult = {
|
|
3
|
+
cancelled: true;
|
|
4
|
+
bron: string;
|
|
5
|
+
} | {
|
|
6
|
+
cancelled: false;
|
|
7
|
+
unknown?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const FLORIDAY_CANCELLED_REASON = "Floriday heeft deze order geannuleerd";
|
|
10
|
+
export declare function isFloridayCancelled(sb: SupabaseClient, input: {
|
|
11
|
+
orderId?: string | null;
|
|
12
|
+
orderNumber?: string | null;
|
|
13
|
+
fulfillmentOrderId?: string | null;
|
|
14
|
+
}): Promise<CancellationResult>;
|
|
15
|
+
//# sourceMappingURL=floridayCancellation.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floridayCancellation.server.d.ts","sourceRoot":"","sources":["../../src/lib/floridayCancellation.server.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,MAAM,MAAM,kBAAkB,GAC1B;IAAE,SAAS,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,SAAS,EAAE,KAAK,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE5C,eAAO,MAAM,yBAAyB,0CAA0C,CAAC;AAIjF,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAClG,OAAO,CAAC,kBAAkB,CAAC,CAuE7B"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/lib/floridayCancellation.server.ts — is de order in Floriday geannuleerd?
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Leest alleen de gegevens die de Floriday-koppelmodule al periodiek ophaalt
|
|
5
|
+
// (salesorders, fulfillment-orders, delivery-orders). Geen extra API-aanroep.
|
|
6
|
+
// Niet leesbaar of geen gegevens => "onbekend": de wachtlus gaat dan gewoon
|
|
7
|
+
// door en de eindgrens vangt het op. Er wordt nooit een annulering verzonnen.
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export const FLORIDAY_CANCELLED_REASON = "Floriday heeft deze order geannuleerd";
|
|
10
|
+
const isCancelledStatus = (s) => String(s ?? "").toUpperCase() === "CANCELLED";
|
|
11
|
+
export async function isFloridayCancelled(sb, input) {
|
|
12
|
+
try {
|
|
13
|
+
let unknown = false;
|
|
14
|
+
// Salesorder-id: eerst van de order zelf, anders via de nummerkoppeling.
|
|
15
|
+
let salesOrderId = null;
|
|
16
|
+
if (input.orderId) {
|
|
17
|
+
const { data, error } = await sb
|
|
18
|
+
.from("orders")
|
|
19
|
+
.select("floriday_sales_order_id")
|
|
20
|
+
.eq("id", input.orderId)
|
|
21
|
+
.maybeSingle();
|
|
22
|
+
if (error)
|
|
23
|
+
unknown = true;
|
|
24
|
+
salesOrderId = String(data?.floriday_sales_order_id ?? "").trim() || null;
|
|
25
|
+
}
|
|
26
|
+
if (!salesOrderId && input.orderNumber) {
|
|
27
|
+
const { data, error } = await sb
|
|
28
|
+
.from("floriday_order_number_map")
|
|
29
|
+
.select("floriday_id")
|
|
30
|
+
.eq("order_number", input.orderNumber)
|
|
31
|
+
.limit(1)
|
|
32
|
+
.maybeSingle();
|
|
33
|
+
if (error)
|
|
34
|
+
unknown = true;
|
|
35
|
+
salesOrderId = String(data?.floriday_id ?? "").trim() || null;
|
|
36
|
+
}
|
|
37
|
+
if (salesOrderId) {
|
|
38
|
+
const { data, error } = await sb
|
|
39
|
+
.from("floriday_salesorders_cache")
|
|
40
|
+
.select("status, is_deleted, data")
|
|
41
|
+
.eq("floriday_id", salesOrderId)
|
|
42
|
+
.maybeSingle();
|
|
43
|
+
if (error)
|
|
44
|
+
unknown = true;
|
|
45
|
+
const so = data;
|
|
46
|
+
if (so) {
|
|
47
|
+
if (isCancelledStatus(so.status) || isCancelledStatus(so.data?.status)) {
|
|
48
|
+
return { cancelled: true, bron: "salesorder" };
|
|
49
|
+
}
|
|
50
|
+
if (so.data?.automaticallyCancelledOn)
|
|
51
|
+
return { cancelled: true, bron: "salesorder (automatisch geannuleerd)" };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (input.fulfillmentOrderId) {
|
|
55
|
+
const { data, error } = await sb
|
|
56
|
+
.from("floriday_fulfillment_orders_cache")
|
|
57
|
+
.select("status, is_deleted, delivery_order_id")
|
|
58
|
+
.eq("floriday_id", input.fulfillmentOrderId)
|
|
59
|
+
.maybeSingle();
|
|
60
|
+
if (error)
|
|
61
|
+
unknown = true;
|
|
62
|
+
const fo = data;
|
|
63
|
+
if (fo) {
|
|
64
|
+
if (isCancelledStatus(fo.status))
|
|
65
|
+
return { cancelled: true, bron: "fulfillment-order" };
|
|
66
|
+
if (fo.is_deleted === true)
|
|
67
|
+
return { cancelled: true, bron: "fulfillment-order (verwijderd)" };
|
|
68
|
+
if (fo.delivery_order_id) {
|
|
69
|
+
const { data: d, error: dErr } = await sb
|
|
70
|
+
.from("floriday_delivery_orders_cache")
|
|
71
|
+
.select("status, is_deleted")
|
|
72
|
+
.eq("floriday_id", fo.delivery_order_id)
|
|
73
|
+
.maybeSingle();
|
|
74
|
+
if (dErr)
|
|
75
|
+
unknown = true;
|
|
76
|
+
const del = d;
|
|
77
|
+
if (del && isCancelledStatus(del.status))
|
|
78
|
+
return { cancelled: true, bron: "delivery-order" };
|
|
79
|
+
if (del && del.is_deleted === true)
|
|
80
|
+
return { cancelled: true, bron: "delivery-order (verwijderd)" };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return unknown ? { cancelled: false, unknown: true } : { cancelled: false };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { cancelled: false, unknown: true };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floridayCancellation.test.d.ts","sourceRoot":"","sources":["../../src/lib/floridayCancellation.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isFloridayCancelled } from "./floridayCancellation.server";
|
|
3
|
+
import { controlWaitExceeded } from "./controlScan.server";
|
|
4
|
+
function stub(rows) {
|
|
5
|
+
return {
|
|
6
|
+
from(table) {
|
|
7
|
+
const q = {
|
|
8
|
+
select: () => q,
|
|
9
|
+
eq: () => q,
|
|
10
|
+
limit: () => q,
|
|
11
|
+
maybeSingle: async () => {
|
|
12
|
+
const r = rows[table];
|
|
13
|
+
return r === "error" ? { data: null, error: { message: "x" } } : { data: r ?? null, error: null };
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
return q;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
describe("isFloridayCancelled", () => {
|
|
21
|
+
it("geannuleerde salesorder", async () => {
|
|
22
|
+
const r = await isFloridayCancelled(stub({ orders: { floriday_sales_order_id: "so1" }, floriday_salesorders_cache: { status: "CANCELLED", data: {} } }), { orderId: "o1" });
|
|
23
|
+
expect(r.cancelled).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it("geannuleerde fulfillment-order", async () => {
|
|
26
|
+
const r = await isFloridayCancelled(stub({ orders: null, floriday_fulfillment_orders_cache: { status: "CANCELLED" } }), { orderId: "o1", fulfillmentOrderId: "fo1" });
|
|
27
|
+
expect(r).toEqual({ cancelled: true, bron: "fulfillment-order" });
|
|
28
|
+
});
|
|
29
|
+
it("onleesbare gegevens => niet geannuleerd, onbekend", async () => {
|
|
30
|
+
const r = await isFloridayCancelled(stub({ orders: "error", floriday_order_number_map: "error" }), {
|
|
31
|
+
orderId: "o1",
|
|
32
|
+
orderNumber: "FO-1",
|
|
33
|
+
});
|
|
34
|
+
expect(r).toEqual({ cancelled: false, unknown: true });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("controlWaitExceeded", () => {
|
|
38
|
+
const now = Date.parse("2026-10-02T12:00:00Z");
|
|
39
|
+
it("na 24 uur", () => expect(controlWaitExceeded({ attempts: 3, approved_at: "2026-10-01T11:00:00Z" }, now)).toBe("tijd"));
|
|
40
|
+
it("na 60 pogingen", () => expect(controlWaitExceeded({ attempts: 60 }, now)).toBe("pogingen"));
|
|
41
|
+
it("binnen grens", () => expect(controlWaitExceeded({ attempts: 5, approved_at: "2026-10-02T10:00:00Z" }, now)).toBeNull());
|
|
42
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface CartInput {
|
|
2
|
+
cartNumber: number | null;
|
|
3
|
+
layersUsed: number | null;
|
|
4
|
+
barcode?: string | null;
|
|
5
|
+
items: Array<{
|
|
6
|
+
fulfillmentRequestId: string | null;
|
|
7
|
+
crates: number | null;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface DeliveryRequestInput {
|
|
11
|
+
fulfillmentRequestId: string;
|
|
12
|
+
numberOfPackages: number | null;
|
|
13
|
+
serviceCode?: number | null;
|
|
14
|
+
packingAgentOrganizationId?: string | null;
|
|
15
|
+
deliveryRemarks?: string | null;
|
|
16
|
+
commercialInvoiceReference?: string | null;
|
|
17
|
+
alternativeDisplayName?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface LoadCarrierItem {
|
|
20
|
+
fulfillmentRequestId: string;
|
|
21
|
+
numberOfPackages: number;
|
|
22
|
+
serviceCode?: number;
|
|
23
|
+
packingAgentOrganizationId?: string;
|
|
24
|
+
deliveryRemarks?: string;
|
|
25
|
+
commercialInvoiceReference?: string;
|
|
26
|
+
alternativeDisplayName?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface LoadCarrier {
|
|
29
|
+
numberOfAdditionalLayers: number;
|
|
30
|
+
sortIndex: number;
|
|
31
|
+
loadCarrierReference: string;
|
|
32
|
+
loadCarrierBarcode?: string;
|
|
33
|
+
items: LoadCarrierItem[];
|
|
34
|
+
}
|
|
35
|
+
export type CartCarriersResult = {
|
|
36
|
+
ok: true;
|
|
37
|
+
loadCarriers: LoadCarrier[];
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
code: string;
|
|
41
|
+
error: string;
|
|
42
|
+
};
|
|
43
|
+
export declare function buildCartCarriers(carts: CartInput[], requests: DeliveryRequestInput[]): CartCarriersResult;
|
|
44
|
+
export type FulfillmentDecision = {
|
|
45
|
+
kind: "klaar";
|
|
46
|
+
status: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "afgewezen";
|
|
49
|
+
status: string;
|
|
50
|
+
reden: string;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "geannuleerd";
|
|
53
|
+
status: string;
|
|
54
|
+
} | {
|
|
55
|
+
kind: "wachten";
|
|
56
|
+
status: string | null;
|
|
57
|
+
reden: string;
|
|
58
|
+
tijdelijk: boolean;
|
|
59
|
+
};
|
|
60
|
+
export declare function decideFulfillmentStatus(result: unknown): FulfillmentDecision;
|
|
61
|
+
//# sourceMappingURL=floridayCartCarriers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floridayCartCarriers.d.ts","sourceRoot":"","sources":["../../src/lib/floridayCartCarriers.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,KAAK,CAAC;QAAE,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CAC9E;AAED,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,0BAA0B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,0BAA0B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,eAAe;IAC9B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,wBAAwB,EAAE,MAAM,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,WAAW,EAAE,CAAA;CAAE,GACzC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/C,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,EAAE,EAClB,QAAQ,EAAE,oBAAoB,EAAE,GAC/B,kBAAkB,CAuEpB;AAMD,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC;AAElF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,GAAG,mBAAmB,CA0B5E"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/lib/floridayCartCarriers.ts — eigen karindeling → `loadCarriers` voor de
|
|
3
|
+
// fulfillment-order (veld dat de Floriday-koppelmodule als karindeling leest).
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Puur en testbaar. Fail-closed: kan een karregel niet aan een
|
|
6
|
+
// fulfillmentRequest worden gekoppeld, of klopt het aantal fusten per request
|
|
7
|
+
// niet met de delivery-order, dan wordt er niets opgebouwd en komt er een
|
|
8
|
+
// duidelijke reden terug. Er wordt nooit stil teruggevallen op 0 lagen.
|
|
9
|
+
// ============================================================================
|
|
10
|
+
export function buildCartCarriers(carts, requests) {
|
|
11
|
+
if (carts.length === 0) {
|
|
12
|
+
return { ok: false, code: "geen_karindeling", error: "Er is nog geen karindeling voor deze order." };
|
|
13
|
+
}
|
|
14
|
+
const byId = new Map(requests.map((r) => [r.fulfillmentRequestId, r]));
|
|
15
|
+
const totals = new Map();
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const [index, cart] of carts.entries()) {
|
|
18
|
+
const label = cart.cartNumber ?? index + 1;
|
|
19
|
+
const layers = cart.layersUsed;
|
|
20
|
+
if (typeof layers !== "number" || !Number.isFinite(layers) || layers < 0) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
code: "lagen_onbekend",
|
|
24
|
+
error: `Kar ${label} heeft geen geldig aantal lagen; de belading wordt niet verstuurd.`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const perRequest = new Map();
|
|
28
|
+
for (const item of cart.items) {
|
|
29
|
+
const id = String(item.fulfillmentRequestId ?? "").trim();
|
|
30
|
+
if (!id || !byId.has(id)) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
code: "geen_fulfillment_request",
|
|
34
|
+
error: `Een regel op kar ${label} hoort bij geen enkel Floriday-verzoek op de delivery-order.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const crates = Number(item.crates ?? 0);
|
|
38
|
+
perRequest.set(id, (perRequest.get(id) ?? 0) + (Number.isFinite(crates) ? crates : 0));
|
|
39
|
+
}
|
|
40
|
+
const items = [];
|
|
41
|
+
for (const [id, packages] of perRequest) {
|
|
42
|
+
if (packages <= 0)
|
|
43
|
+
continue;
|
|
44
|
+
totals.set(id, (totals.get(id) ?? 0) + packages);
|
|
45
|
+
const req = byId.get(id);
|
|
46
|
+
const item = { fulfillmentRequestId: id, numberOfPackages: packages };
|
|
47
|
+
if (typeof req.serviceCode === "number")
|
|
48
|
+
item.serviceCode = req.serviceCode;
|
|
49
|
+
if (req.packingAgentOrganizationId)
|
|
50
|
+
item.packingAgentOrganizationId = req.packingAgentOrganizationId;
|
|
51
|
+
if (req.deliveryRemarks)
|
|
52
|
+
item.deliveryRemarks = req.deliveryRemarks;
|
|
53
|
+
if (req.commercialInvoiceReference)
|
|
54
|
+
item.commercialInvoiceReference = req.commercialInvoiceReference;
|
|
55
|
+
if (req.alternativeDisplayName)
|
|
56
|
+
item.alternativeDisplayName = req.alternativeDisplayName;
|
|
57
|
+
items.push(item);
|
|
58
|
+
}
|
|
59
|
+
if (items.length === 0) {
|
|
60
|
+
return { ok: false, code: "lege_kar", error: `Kar ${label} bevat geen fusten.` };
|
|
61
|
+
}
|
|
62
|
+
const carrier = {
|
|
63
|
+
numberOfAdditionalLayers: layers,
|
|
64
|
+
sortIndex: index + 1,
|
|
65
|
+
loadCarrierReference: String(label),
|
|
66
|
+
items,
|
|
67
|
+
};
|
|
68
|
+
if (cart.barcode)
|
|
69
|
+
carrier.loadCarrierBarcode = cart.barcode;
|
|
70
|
+
out.push(carrier);
|
|
71
|
+
}
|
|
72
|
+
for (const req of requests) {
|
|
73
|
+
const expected = Number(req.numberOfPackages ?? 0);
|
|
74
|
+
const got = totals.get(req.fulfillmentRequestId) ?? 0;
|
|
75
|
+
if (got !== expected) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
code: "fusten_wijken_af",
|
|
79
|
+
error: `De karindeling heeft ${got} fust(en) voor Floriday-verzoek ${req.fulfillmentRequestId}, ` +
|
|
80
|
+
`maar de Floriday-order vraagt ${expected}. De belading wordt niet verstuurd.`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { ok: true, loadCarriers: out };
|
|
85
|
+
}
|
|
86
|
+
export function decideFulfillmentStatus(result) {
|
|
87
|
+
const r = (result ?? {});
|
|
88
|
+
const payload = ((r["payload"] ?? r) ?? {});
|
|
89
|
+
if (payload["ok"] !== true) {
|
|
90
|
+
return {
|
|
91
|
+
kind: "wachten",
|
|
92
|
+
status: null,
|
|
93
|
+
tijdelijk: true,
|
|
94
|
+
reden: `Status bij Floriday niet leesbaar: ${String(payload["error"] ?? "onbekende fout")}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const status = String(payload["fulfillmentStatus"] ?? "").toUpperCase();
|
|
98
|
+
if (status === "ACCEPTED" || status === "DELIVERED")
|
|
99
|
+
return { kind: "klaar", status };
|
|
100
|
+
if (status === "FAILED") {
|
|
101
|
+
const se = payload["submissionError"];
|
|
102
|
+
const reden = typeof se === "string" ? se : se ? JSON.stringify(se) : "Floriday heeft de fulfillment-order afgewezen (geen reden meegegeven).";
|
|
103
|
+
return { kind: "afgewezen", status, reden };
|
|
104
|
+
}
|
|
105
|
+
if (status === "CANCELLED")
|
|
106
|
+
return { kind: "geannuleerd", status };
|
|
107
|
+
return {
|
|
108
|
+
kind: "wachten",
|
|
109
|
+
status: status || null,
|
|
110
|
+
tijdelijk: false,
|
|
111
|
+
reden: `Floriday verwerkt de fulfillment-order nog (status ${status || "onbekend"}).`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floridayCartCarriers.test.d.ts","sourceRoot":"","sources":["../../src/lib/floridayCartCarriers.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildCartCarriers, decideFulfillmentStatus } from "./floridayCartCarriers";
|
|
3
|
+
const reqs = [
|
|
4
|
+
{ fulfillmentRequestId: "fr-a", numberOfPackages: 50, deliveryRemarks: "achterdeur" },
|
|
5
|
+
{ fulfillmentRequestId: "fr-b", numberOfPackages: 10 },
|
|
6
|
+
];
|
|
7
|
+
describe("buildCartCarriers", () => {
|
|
8
|
+
it("geeft per kar de echte lagen en de fusten per verzoek mee", () => {
|
|
9
|
+
const r = buildCartCarriers([
|
|
10
|
+
{ cartNumber: 1, layersUsed: 4, items: [{ fulfillmentRequestId: "fr-a", crates: 30 }] },
|
|
11
|
+
{
|
|
12
|
+
cartNumber: 2,
|
|
13
|
+
layersUsed: 3,
|
|
14
|
+
items: [
|
|
15
|
+
{ fulfillmentRequestId: "fr-a", crates: 15 },
|
|
16
|
+
{ fulfillmentRequestId: "fr-a", crates: 5 },
|
|
17
|
+
{ fulfillmentRequestId: "fr-b", crates: 10 },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
], reqs);
|
|
21
|
+
expect(r.ok).toBe(true);
|
|
22
|
+
if (!r.ok)
|
|
23
|
+
return;
|
|
24
|
+
expect(r.loadCarriers.map((c) => c.numberOfAdditionalLayers)).toEqual([4, 3]);
|
|
25
|
+
expect(r.loadCarriers[1].items).toEqual([
|
|
26
|
+
{ fulfillmentRequestId: "fr-a", numberOfPackages: 20, deliveryRemarks: "achterdeur" },
|
|
27
|
+
{ fulfillmentRequestId: "fr-b", numberOfPackages: 10 },
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
it("verstuurt niet als een karregel bij geen verzoek hoort", () => {
|
|
31
|
+
const r = buildCartCarriers([{ cartNumber: 1, layersUsed: 2, items: [{ fulfillmentRequestId: null, crates: 60 }] }], reqs);
|
|
32
|
+
expect(r).toMatchObject({ ok: false, code: "geen_fulfillment_request" });
|
|
33
|
+
});
|
|
34
|
+
it("verstuurt niet als het aantal fusten afwijkt van Floriday", () => {
|
|
35
|
+
const r = buildCartCarriers([{ cartNumber: 1, layersUsed: 2, items: [{ fulfillmentRequestId: "fr-a", crates: 49 }, { fulfillmentRequestId: "fr-b", crates: 10 }] }], reqs);
|
|
36
|
+
expect(r).toMatchObject({ ok: false, code: "fusten_wijken_af" });
|
|
37
|
+
});
|
|
38
|
+
it("valt nooit terug op 0 lagen", () => {
|
|
39
|
+
const r = buildCartCarriers([{ cartNumber: 1, layersUsed: null, items: [{ fulfillmentRequestId: "fr-a", crates: 50 }] }], reqs);
|
|
40
|
+
expect(r).toMatchObject({ ok: false, code: "lagen_onbekend" });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
describe("decideFulfillmentStatus", () => {
|
|
44
|
+
const p = (payload) => ({ status: 200, payload: { ok: true, ...payload } });
|
|
45
|
+
it("FAILED → afgewezen met de letterlijke submissionError", () => {
|
|
46
|
+
expect(decideFulfillmentStatus(p({ fulfillmentStatus: "FAILED", submissionError: "Invalid load carrier" }))).toEqual({
|
|
47
|
+
kind: "afgewezen",
|
|
48
|
+
status: "FAILED",
|
|
49
|
+
reden: "Invalid load carrier",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
it("ACCEPTED en DELIVERED → klaar om te printen", () => {
|
|
53
|
+
expect(decideFulfillmentStatus(p({ fulfillmentStatus: "ACCEPTED" })).kind).toBe("klaar");
|
|
54
|
+
expect(decideFulfillmentStatus(p({ fulfillmentStatus: "DELIVERED" })).kind).toBe("klaar");
|
|
55
|
+
});
|
|
56
|
+
it("PENDING/DRAFT → wachten", () => {
|
|
57
|
+
expect(decideFulfillmentStatus(p({ fulfillmentStatus: "PENDING" })).kind).toBe("wachten");
|
|
58
|
+
expect(decideFulfillmentStatus(p({ fulfillmentStatus: "DRAFT" })).kind).toBe("wachten");
|
|
59
|
+
});
|
|
60
|
+
it("onleesbaar → wachten, tijdelijk", () => {
|
|
61
|
+
expect(decideFulfillmentStatus({ status: 502, payload: { ok: false, error: "x" } })).toMatchObject({
|
|
62
|
+
kind: "wachten",
|
|
63
|
+
tijdelijk: true,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|