@burdenoff/microfe-movethewheels 2026.1016.13 → 2026.1017.1

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.
@@ -1,103 +1,110 @@
1
- import { prepareWorkspaceRequest as e } from "@burdenoff/fe-libs/shared/graphql";
1
+ import { vehicleHoldRefusal as e } from "./orderVehicleHold.js";
2
+ import { prepareWorkspaceRequest as t } from "@burdenoff/fe-libs/shared/graphql";
2
3
  //#region src/movethewheels/utils/graphqlClient.ts
3
- function t(e) {
4
+ function n(e) {
4
5
  return e ? e.endsWith("/graphql") ? e : `${e}/workspaces/graphql` : "/workspaces/graphql";
5
6
  }
6
- var n = 3, r = 12e3;
7
- function i(e) {
7
+ var r = 3, i = 12e3;
8
+ function a(e) {
8
9
  return e ? /upstream connect|connection refused|reset before headers|delayed connect|econnrefused|econnreset|socket hang up|service unavailable|bad gateway|gateway timeout|temporarily unavailable|please try again|failed to fetch|networkerror|load failed/i.test(e) : !1;
9
10
  }
10
- function a(e) {
11
+ function o(e) {
11
12
  return e === 429 || e === 502 || e === 503 || e === 504;
12
13
  }
13
- function o(e) {
14
+ function s(e) {
14
15
  return typeof e == "object" && !!e && "name" in e && e.name === "AbortError";
15
16
  }
16
- var s = (e) => new Promise((t) => setTimeout(t, e)), c = 4, l = 0, u = [];
17
- function d() {
17
+ var c = (e) => new Promise((t) => setTimeout(t, e)), l = 4, u = 0, d = [];
18
+ function f() {
18
19
  return new Promise((e) => {
19
- l < c ? (l += 1, e()) : u.push(e);
20
+ u < l ? (u += 1, e()) : d.push(e);
20
21
  });
21
22
  }
22
- function f() {
23
- let e = u.shift();
24
- e ? e() : --l;
23
+ function p() {
24
+ let e = d.shift();
25
+ e ? e() : --u;
25
26
  }
26
- async function p(r, i, a, o = "RETRY_TRANSIENT") {
27
- let s = { ...r }, c = JSON.stringify({
27
+ async function m(e, i, a, o = "RETRY_TRANSIENT", s) {
28
+ let c = { ...e }, l = JSON.stringify({
28
29
  query: i,
29
30
  variables: a
30
- }), l = t(s.apiGatewayUrl);
31
- await d();
31
+ }), u = n(c.apiGatewayUrl);
32
+ await f();
32
33
  try {
33
- let t = typeof window < "u" ? await e({
34
- url: new URL(l, typeof document < "u" && document.baseURI || window.location.href).toString(),
35
- authToken: s.authToken ?? "",
36
- actorId: s.actorId ?? "",
37
- workspaceId: s.workspaceId ?? "",
38
- tenantId: s.tenantId,
39
- workspaceToken: s.workspaceToken,
34
+ let e = typeof window < "u" ? await t({
35
+ url: new URL(u, typeof document < "u" && document.baseURI || window.location.href).toString(),
36
+ authToken: c.authToken ?? "",
37
+ actorId: c.actorId ?? "",
38
+ workspaceId: c.workspaceId ?? "",
39
+ tenantId: c.tenantId,
40
+ workspaceToken: c.workspaceToken,
40
41
  currentQuery: i
41
- }) : null, r = t?.workspaceToken ?? s.workspaceToken;
42
- return await h(l, {
42
+ }) : null, n = e?.workspaceToken ?? c.workspaceToken;
43
+ return await _(u, {
43
44
  "content-type": "application/json",
44
- ...s.authToken ? { Authorization: `Bearer ${s.authToken}` } : {},
45
- ...r ? { "X-Workspace-Authorization": `Bearer ${r}` } : {},
46
- ...s.workspaceId ? { "x-workspace-id": s.workspaceId } : {},
47
- ...s.tenantId ? { "x-tenant-id": s.tenantId } : {},
48
- ...s.actorId ? { "x-actor-id": s.actorId } : {},
49
- ...s.actorId ? { "x-actor-type": "user" } : {}
50
- }, c, o === "SINGLE_ATTEMPT" ? 0 : n, t?.assertCurrent ?? (() => {}));
45
+ ...c.authToken ? { Authorization: `Bearer ${c.authToken}` } : {},
46
+ ...n ? { "X-Workspace-Authorization": `Bearer ${n}` } : {},
47
+ ...c.workspaceId ? { "x-workspace-id": c.workspaceId } : {},
48
+ ...c.tenantId ? { "x-tenant-id": c.tenantId } : {},
49
+ ...c.actorId ? { "x-actor-id": c.actorId } : {},
50
+ ...c.actorId ? { "x-actor-type": "user" } : {}
51
+ }, l, o === "SINGLE_ATTEMPT" ? 0 : r, e?.assertCurrent ?? (() => {}), o === "SINGLE_ATTEMPT" ? s : void 0);
51
52
  } finally {
52
- f();
53
+ p();
53
54
  }
54
55
  }
55
- async function m(e) {
56
+ async function h(t, n) {
56
57
  try {
57
- let t = ((await e.json()).errors ?? []).map((e) => e.message).filter((e) => typeof e == "string" && e.length > 0);
58
- return t.length > 0 ? t.join("; ") : null;
58
+ let r = await t.json(), i = e(r, t.status, n);
59
+ if (i) return i;
60
+ let a = (r.errors ?? []).map((e) => e.message).filter((e) => typeof e == "string" && e.length > 0);
61
+ return a.length > 0 ? Error(a.join("; ")) : null;
59
62
  } catch {
60
63
  return null;
61
64
  }
62
65
  }
63
- async function h(e, t, n, c, l) {
64
- let u = /* @__PURE__ */ Error("GraphQL request failed");
65
- for (let d = 0; d <= c; d += 1) {
66
- let f = d < c;
67
- d > 0 && await s(Math.min(250 * 2 ** (d - 1), 2e3) + Math.floor(Math.random() * 150)), l();
68
- let p = new AbortController(), h = setTimeout(() => p.abort(), r);
66
+ function g(e, t) {
67
+ return typeof e != "object" || !e || Array.isArray(e) || "errors" in e && (!Array.isArray(e.errors) || e.errors.length !== 0) || !("data" in e) || typeof e.data != "object" || e.data === null || Array.isArray(e.data) ? !1 : Object.keys(e.data).length === 1 && Object.hasOwn(e.data, t);
68
+ }
69
+ async function _(t, n, r, l, u, d) {
70
+ let f = /* @__PURE__ */ Error("GraphQL request failed");
71
+ for (let p = 0; p <= l; p += 1) {
72
+ let m = p < l;
73
+ p > 0 && await c(Math.min(250 * 2 ** (p - 1), 2e3) + Math.floor(Math.random() * 150)), u();
74
+ let _ = new AbortController(), v = setTimeout(() => _.abort(), i);
69
75
  try {
70
- let r = await fetch(e, {
76
+ let i = await fetch(t, {
71
77
  method: "POST",
72
- headers: t,
73
- body: n,
74
- signal: p.signal
78
+ headers: n,
79
+ body: r,
80
+ signal: _.signal
75
81
  });
76
- if (!r.ok) {
77
- let e = a(r.status) ? null : await m(r);
78
- if (u = Error(e ?? `GraphQL request failed with status ${r.status}`), f && a(r.status)) continue;
79
- throw u;
82
+ if (!i.ok) {
83
+ if (f = (o(i.status) ? null : await h(i, d)) ?? /* @__PURE__ */ Error(`GraphQL request failed with status ${i.status}`), m && o(i.status)) continue;
84
+ throw f;
80
85
  }
81
- let o = await r.json();
82
- if (o.errors?.length) {
83
- let e = o.errors.map((e) => e.message ?? "Unknown GraphQL error").join("; ");
84
- if (u = Error(e), f && o.errors.some((e) => i(e.message))) continue;
85
- throw u;
86
+ let s = await i.json(), c = e(s, i.status, d);
87
+ if (c) throw c;
88
+ if (d && !g(s, d)) throw Error("The order write response could not be confirmed.");
89
+ if (s.errors?.length) {
90
+ let e = s.errors.map((e) => e.message ?? "Unknown GraphQL error").join("; ");
91
+ if (f = Error(e), m && s.errors.some((e) => a(e.message))) continue;
92
+ throw f;
86
93
  }
87
- if (!o.data) {
88
- if (u = /* @__PURE__ */ Error("GraphQL response did not include data"), f) continue;
89
- throw u;
94
+ if (!s.data) {
95
+ if (f = /* @__PURE__ */ Error("GraphQL response did not include data"), m) continue;
96
+ throw f;
90
97
  }
91
- return o.data;
98
+ return s.data;
92
99
  } catch (e) {
93
- let t = o(e), n = t ? `GraphQL request timed out after ${r}ms` : e instanceof Error ? e.message : String(e);
94
- if (u = !t && e instanceof Error ? e : Error(n), f && (t || i(n) || e instanceof TypeError)) continue;
95
- throw u;
100
+ let t = s(e), n = t ? `GraphQL request timed out after ${i}ms` : e instanceof Error ? e.message : String(e);
101
+ if (f = !t && e instanceof Error ? e : Error(n), m && (t || a(n) || e instanceof TypeError)) continue;
102
+ throw f;
96
103
  } finally {
97
- clearTimeout(h);
104
+ clearTimeout(v);
98
105
  }
99
106
  }
100
- throw u;
107
+ throw f;
101
108
  }
102
109
  //#endregion
103
- export { p as executeGraphQL, t as resolveGraphqlUrl };
110
+ export { m as executeGraphQL, n as resolveGraphqlUrl };
@@ -0,0 +1,38 @@
1
+ //#region src/movethewheels/utils/orderVehicleHold.ts
2
+ var e = class extends Error {
3
+ operation;
4
+ kind;
5
+ constructor(e, t = "vehicle") {
6
+ super("The recorded eligibility prevents this assignment or movement."), this.operation = e, this.kind = t, this.name = "VehicleHoldRefusal";
7
+ }
8
+ };
9
+ function t(e) {
10
+ return typeof e == "object" && !!e && !Array.isArray(e);
11
+ }
12
+ function n(n, r, i) {
13
+ if (!i || r !== 200 && r !== 400 || !t(n) || !Array.isArray(n.errors) || n.errors.length !== 1) return null;
14
+ let a = n.errors[0];
15
+ if (!t(a) || typeof a.message != "string" || !a.message || !Array.isArray(a.path) || a.path.length !== 1 || a.path[0] !== i || !t(a.extensions) || a.extensions.code !== "BAD_USER_INPUT") return null;
16
+ let o = a.extensions.i18nKey, s = o === "route.errors.vehicleNotAssignable" ? "vehicle" : o === "route.errors.driverInactive" || o === "route.errors.driverLicenseUnknown" || o === "route.errors.driverLicenseExpired" ? "driver" : null;
17
+ return !s || n.data !== void 0 && n.data !== null && (!t(n.data) || Object.keys(n.data).length !== 1 || n.data[i] !== null) ? null : new e(i, s);
18
+ }
19
+ var r = new Set([
20
+ "PENDING",
21
+ "CONFIRMED",
22
+ "ASSIGNED",
23
+ "PICKED_UP",
24
+ "IN_TRANSIT",
25
+ "OUT_FOR_DELIVERY",
26
+ "DELIVERED",
27
+ "RETURNED",
28
+ "CANCELLED",
29
+ "FAILED"
30
+ ]);
31
+ function i(e, n) {
32
+ if (!t(e) || e.id !== n.id || typeof e.status != "string" || !r.has(e.status) || n.vehicleId !== void 0 && e.vehicleId !== n.vehicleId || n.driverId !== void 0 && e.driverId !== n.driverId || n.status !== void 0 && e.status !== n.status) throw Error("The order write could not be confirmed.");
33
+ }
34
+ function a(e) {
35
+ return e === "available" || e === "assigned";
36
+ }
37
+ //#endregion
38
+ export { e as VehicleHoldRefusal, i as assertOrderAcknowledgement, a as isVehicleAssignable, n as vehicleHoldRefusal };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-movethewheels",
3
- "version": "2026.1016.13",
3
+ "version": "2026.1017.1",
4
4
  "description": "Burdenoff MoveTheWheels Microfrontend - Logistics & Supply Chain Management Platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -26,6 +26,7 @@
26
26
  "main": "./dist/index.js",
27
27
  "types": "./dist/index.d.ts",
28
28
  "scripts": {
29
+ "verify:order-vehicle-hold": "bun test scripts/check-order-vehicle-hold.test.ts && bun scripts/verify-order-dispatch.mjs",
29
30
  "verify:workspace-predispatch": "bun test scripts/verify-workspace-predispatch.test.ts",
30
31
  "verify:multimodal-single-attempt": "bun scripts/verify-multimodal-single-attempt.mjs",
31
32
  "verify:live-logistics-contract": "bun test scripts/check-logistics-live-contract.test.mjs scripts/fetch-logistics-live-schema.test.mjs",
@@ -36,7 +37,7 @@
36
37
  "format": "prettier --write \"src/**/*.{ts,tsx}\"",
37
38
  "type:check": "bash scripts/build-limits.sh tsc --noEmit",
38
39
  "validate:schema-drift": "graphql-schema-drift",
39
- "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run validate:schema-drift && bun run lint:sanity && bun run format && bun run type:check && bun run verify:live-logistics-contract && bun run verify:empty-reuse-retry && bun run verify:empty-reuse-recovery && bun run verify:trip-utilization-input && bun run verify:trip-draft-context && bun run verify:stock-binding-input && bun run verify:stock-observation-input && bun run verify:pending-pick-input && bun run verify:pick-review-ack && bun run verify:driver-license-expiry && bun run verify:freight-input && bun run verify:freight-recovery && bun run verify:freight-commercial && bun run verify:freight-invoice && bun run verify:procedure-input && bun run verify:consolidation && bun run verify:multimodal-single-attempt && bun run verify:workspace-predispatch && bun run verify:tracking-quality-outcomes && bun run verify:shipment-status-authority && bun run verify:free-time-permissions && bun run verify:core-codegen && bun run build && bun run build-storybook'",
40
+ "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run validate:schema-drift && bun run lint:sanity && bun run format && bun run type:check && bun run verify:live-logistics-contract && bun run verify:empty-reuse-retry && bun run verify:empty-reuse-recovery && bun run verify:trip-utilization-input && bun run verify:trip-draft-context && bun run verify:stock-binding-input && bun run verify:stock-observation-input && bun run verify:pending-pick-input && bun run verify:pick-review-ack && bun run verify:driver-license-expiry && bun run verify:freight-input && bun run verify:freight-recovery && bun run verify:freight-commercial && bun run verify:freight-invoice && bun run verify:procedure-input && bun run verify:consolidation && bun run verify:multimodal-single-attempt && bun run verify:workspace-predispatch && bun run verify:tracking-quality-outcomes && bun run verify:shipment-status-authority && bun run verify:free-time-permissions && bun run verify:core-codegen && bun run verify:order-vehicle-hold && bun run build && bun run build-storybook'",
40
41
  "storybook": "storybook dev -p 6006",
41
42
  "build-storybook": "storybook build",
42
43
  "test:storybook": "vitest run --project=storybook",
@@ -1161,6 +1161,15 @@ export function useVehicle(vehicleId: string | undefined) {
1161
1161
  id: vehicleId,
1162
1162
  }
1163
1163
  );
1164
+ if (
1165
+ data.vehicle &&
1166
+ (data.vehicle.id !== vehicleId ||
1167
+ !['ACTIVE', 'IN_TRANSIT', 'MAINTENANCE', 'INACTIVE', 'DECOMMISSIONED'].includes(
1168
+ data.vehicle.status
1169
+ ))
1170
+ ) {
1171
+ throw new Error('Vehicle status could not be verified.');
1172
+ }
1164
1173
  setVehicle(data.vehicle ? mapVehicle(data.vehicle) : null);
1165
1174
  } catch (fetchError) {
1166
1175
  setError(fetchError instanceof Error ? fetchError : new Error('Failed to load vehicle'));
@@ -0,0 +1,196 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type { Order, OrderStatus } from '../types';
3
+ import type { OrderWriteInput } from './useOrders';
4
+ import { VehicleHoldRefusal } from '../utils/orderVehicleHold';
5
+ import { useTr } from '../shared/hooks/useTr';
6
+
7
+ interface DispatchOptions {
8
+ order: Order | null;
9
+ contextKey: string;
10
+ ready: boolean;
11
+ assignmentReady: boolean;
12
+ movementReady: boolean;
13
+ updateOrder: (
14
+ id: string,
15
+ input: OrderWriteInput,
16
+ attemptPolicy: 'SINGLE_ATTEMPT'
17
+ ) => Promise<Order>;
18
+ updateOrderStatus: (id: string, status: OrderStatus) => Promise<Order>;
19
+ refresh: () => void;
20
+ }
21
+
22
+ /** One order/context only. Storage retains uncertainty, never a replayable request or token. */
23
+ export function useOrderDispatch(options: DispatchOptions) {
24
+ const tr = useTr();
25
+ const { order, contextKey, updateOrder, updateOrderStatus, refresh } = options;
26
+ const storageKey = `movethewheels.order-dispatch.unconfirmed.v1:${contextKey}`;
27
+ const [blocked, setBlocked] = useState(() => {
28
+ try {
29
+ return sessionStorage.getItem(storageKey) !== null;
30
+ } catch {
31
+ return true;
32
+ }
33
+ });
34
+ const [busy, setBusy] = useState(false);
35
+ const [message, setMessage] = useState<string | null>(null);
36
+ const [knownAssignment, setKnownAssignment] = useState<{
37
+ orderId: string;
38
+ vehicleId: string;
39
+ driverId: string;
40
+ } | null>(null);
41
+ const running = useRef(false);
42
+ const active = useRef(true);
43
+ useEffect(() => {
44
+ active.current = true;
45
+ return () => {
46
+ active.current = false;
47
+ };
48
+ }, []);
49
+ const uncertain = (assignmentSaved: boolean) =>
50
+ tr(
51
+ assignmentSaved
52
+ ? 'movethewheels.order.movementUnknown'
53
+ : 'movethewheels.order.assignmentUnknown',
54
+ assignmentSaved
55
+ ? 'Assignment saved. Movement could not be confirmed. Further changes are blocked.'
56
+ : 'Assignment could not be confirmed. Further changes are blocked. Check the order before taking another action.'
57
+ );
58
+ const retain = () => {
59
+ // Recheck storage synchronously so a second mounted view cannot bypass a pending write.
60
+ if (sessionStorage.getItem(storageKey) !== null) throw new Error('Unconfirmed order write');
61
+ sessionStorage.setItem(storageKey, 'unconfirmed');
62
+ };
63
+ const confirm = () => sessionStorage.removeItem(storageKey);
64
+ const begin = (allowed: boolean) => {
65
+ if (running.current || blocked || !active.current || !options.ready || !allowed || !order)
66
+ return false;
67
+ try {
68
+ retain();
69
+ } catch {
70
+ setBlocked(true);
71
+ setMessage(
72
+ tr(
73
+ 'movethewheels.order.retentionUnavailable',
74
+ 'Further changes are blocked because this browser cannot safely retain the order write.'
75
+ )
76
+ );
77
+ return false;
78
+ }
79
+ running.current = true;
80
+ setBusy(true);
81
+ setMessage(null);
82
+ return true;
83
+ };
84
+ const finish = () => {
85
+ running.current = false;
86
+ if (active.current) setBusy(false);
87
+ };
88
+ const assign = async (vehicleId: string, driverId: string) => {
89
+ if (
90
+ !vehicleId ||
91
+ !driverId ||
92
+ !order ||
93
+ (order.vehicleId === vehicleId && order.driverId === driverId) ||
94
+ !begin(options.assignmentReady)
95
+ )
96
+ return;
97
+ let assignmentSaved = false;
98
+ try {
99
+ await updateOrder(order.id, { vehicleId, driverId }, 'SINGLE_ATTEMPT');
100
+ assignmentSaved = true;
101
+ if (active.current) setKnownAssignment({ orderId: order.id, vehicleId, driverId });
102
+ confirm();
103
+ if (!active.current) return;
104
+ const advance = !(order.vehicleId && order.driverId) && order.status === 'confirmed';
105
+ if (advance) {
106
+ retain();
107
+ await updateOrderStatus(order.id, 'assigned');
108
+ confirm();
109
+ }
110
+ if (!active.current) return;
111
+ setMessage(tr('movethewheels.order.assignmentSaved', 'Assignment saved.'));
112
+ refresh();
113
+ } catch (error) {
114
+ // The opt-in transport classifier is the only known pre-write refusal.
115
+ if (
116
+ error instanceof VehicleHoldRefusal &&
117
+ error.operation === (assignmentSaved ? 'updateDeliveryJobStatus' : 'updateDeliveryJob')
118
+ ) {
119
+ try {
120
+ confirm();
121
+ } catch {
122
+ if (active.current) setBlocked(true);
123
+ }
124
+ if (!active.current) return;
125
+ setMessage(
126
+ tr(
127
+ error.kind === 'driver'
128
+ ? assignmentSaved
129
+ ? 'movethewheels.order.driverMovementBlocked'
130
+ : 'movethewheels.order.driverAssignmentBlocked'
131
+ : assignmentSaved
132
+ ? 'movethewheels.order.movementBlocked'
133
+ : 'movethewheels.order.assignmentBlocked',
134
+ error.kind === 'driver'
135
+ ? assignmentSaved
136
+ ? 'Assignment saved. Driver eligibility blocked movement.'
137
+ : 'Assignment blocked by driver eligibility. Choose another driver.'
138
+ : assignmentSaved
139
+ ? 'Assignment saved. Movement blocked.'
140
+ : 'Assignment blocked by the recorded vehicle status. Choose another vehicle.'
141
+ )
142
+ );
143
+ refresh();
144
+ } else if (active.current) {
145
+ setBlocked(true);
146
+ setMessage(uncertain(assignmentSaved));
147
+ }
148
+ } finally {
149
+ finish();
150
+ }
151
+ };
152
+ const changeStatus = async (status: OrderStatus) => {
153
+ if (!order || ['delivered', 'returned', 'cancelled', 'failed'].includes(order.status)) return;
154
+ if (status === 'assigned' && (!order.vehicleId || !order.driverId)) return;
155
+ const movement = ['assigned', 'picked_up', 'in_transit', 'out_for_delivery'].includes(status);
156
+ if (!begin(!movement || options.movementReady) || !order) return;
157
+ try {
158
+ await updateOrderStatus(order.id, status);
159
+ confirm();
160
+ if (!active.current) return;
161
+ setMessage(tr('movethewheels.order.statusSaved', 'Order status updated.'));
162
+ refresh();
163
+ } catch (error) {
164
+ if (error instanceof VehicleHoldRefusal && error.operation === 'updateDeliveryJobStatus') {
165
+ try {
166
+ confirm();
167
+ } catch {
168
+ if (active.current) setBlocked(true);
169
+ }
170
+ if (!active.current) return;
171
+ setMessage(
172
+ tr(
173
+ error.kind === 'driver'
174
+ ? 'movethewheels.order.driverStatusBlocked'
175
+ : 'movethewheels.order.statusBlocked',
176
+ error.kind === 'driver'
177
+ ? 'Movement blocked by driver eligibility. Choose another driver.'
178
+ : 'Movement blocked by the recorded vehicle status. Choose another vehicle.'
179
+ )
180
+ );
181
+ refresh();
182
+ } else if (active.current) {
183
+ setBlocked(true);
184
+ setMessage(
185
+ tr(
186
+ 'movethewheels.order.statusUnknown',
187
+ 'Order status could not be confirmed. Further changes are blocked.'
188
+ )
189
+ );
190
+ }
191
+ } finally {
192
+ finish();
193
+ }
194
+ };
195
+ return { assign, changeStatus, busy, blocked, message, knownAssignment };
196
+ }
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
3
3
  import type { Order, OrderFilter, OrderStatus } from '../types';
4
4
  import { mapRoute } from './useRoutes';
5
- import { executeGraphQL } from '../utils/graphqlClient';
5
+ import { executeGraphQL, type GraphQLAttemptPolicy } from '../utils/graphqlClient';
6
+ import { assertOrderAcknowledgement } from '../utils/orderVehicleHold';
6
7
 
7
8
  interface UseOrdersOptions {
8
9
  filters?: OrderFilter;
@@ -30,7 +31,11 @@ interface UseOrdersResult {
30
31
  error: Error | null;
31
32
  refetch: () => void;
32
33
  createOrder: (order: OrderWriteInput) => Promise<Order>;
33
- updateOrder: (id: string, updates: OrderWriteInput) => Promise<Order>;
34
+ updateOrder: (
35
+ id: string,
36
+ updates: OrderWriteInput,
37
+ attemptPolicy?: GraphQLAttemptPolicy
38
+ ) => Promise<Order>;
34
39
  deleteOrder: (id: string) => Promise<void>;
35
40
  updateOrderStatus: (id: string, status: OrderStatus) => Promise<Order>;
36
41
  }
@@ -462,30 +467,47 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
462
467
  );
463
468
 
464
469
  const updateOrder = useCallback(
465
- async (id: string, updates: OrderWriteInput): Promise<Order> => {
470
+ async (
471
+ id: string,
472
+ updates: OrderWriteInput,
473
+ attemptPolicy: GraphQLAttemptPolicy = 'RETRY_TRANSIENT'
474
+ ): Promise<Order> => {
466
475
  const data = await executeGraphQL<{
467
476
  updateDeliveryJob: ApiOrder;
468
- }>(graphQLOptions, UPDATE_ORDER_MUTATION, {
469
- id,
470
- input: {
471
- status: updates.status ? toApiStatus(updates.status) : undefined,
472
- priority: updates.priority ? toApiPriority(updates.priority) : undefined,
473
- customerId: updates.customerId,
474
- pickupAddress: updates.pickupAddress,
475
- deliveryAddress: updates.deliveryAddress,
476
- cargoItems: updates.items,
477
- pricing: updates.pricing,
478
- specialInstructions: updates.specialInstructions,
479
- trackingNumber: updates.trackingNumber,
480
- // `null` clears the booked slot; `undefined` leaves it untouched.
481
- scheduledDelivery:
482
- updates.scheduledDelivery === null ? null : updates.scheduledDelivery?.toISOString(),
483
- vehicleId: updates.vehicleId,
484
- driverId: updates.driverId,
485
- routeId: updates.routeId,
477
+ }>(
478
+ graphQLOptions,
479
+ UPDATE_ORDER_MUTATION,
480
+ {
481
+ id,
482
+ input: {
483
+ status: updates.status ? toApiStatus(updates.status) : undefined,
484
+ priority: updates.priority ? toApiPriority(updates.priority) : undefined,
485
+ customerId: updates.customerId,
486
+ pickupAddress: updates.pickupAddress,
487
+ deliveryAddress: updates.deliveryAddress,
488
+ cargoItems: updates.items,
489
+ pricing: updates.pricing,
490
+ specialInstructions: updates.specialInstructions,
491
+ trackingNumber: updates.trackingNumber,
492
+ // `null` clears the booked slot; `undefined` leaves it untouched.
493
+ scheduledDelivery:
494
+ updates.scheduledDelivery === null ? null : updates.scheduledDelivery?.toISOString(),
495
+ vehicleId: updates.vehicleId,
496
+ driverId: updates.driverId,
497
+ routeId: updates.routeId,
498
+ },
486
499
  },
487
- });
500
+ attemptPolicy,
501
+ attemptPolicy === 'SINGLE_ATTEMPT' ? 'updateDeliveryJob' : undefined
502
+ );
488
503
 
504
+ if (attemptPolicy === 'SINGLE_ATTEMPT')
505
+ assertOrderAcknowledgement(data.updateDeliveryJob, {
506
+ id,
507
+ vehicleId: updates.vehicleId,
508
+ driverId: updates.driverId,
509
+ status: updates.status ? toApiStatus(updates.status) : undefined,
510
+ });
489
511
  const updated = mapOrder(data.updateDeliveryJob);
490
512
  setOrders((previous) => previous.map((item) => (item.id === id ? updated : item)));
491
513
  return updated;
@@ -507,13 +529,20 @@ export function useOrders(options: UseOrdersOptions = {}): UseOrdersResult {
507
529
  async (id: string, status: OrderStatus): Promise<Order> => {
508
530
  const data = await executeGraphQL<{
509
531
  updateDeliveryJobStatus: ApiOrder;
510
- }>(graphQLOptions, UPDATE_ORDER_STATUS_MUTATION, {
511
- id,
512
- input: {
513
- status: toApiStatus(status),
532
+ }>(
533
+ graphQLOptions,
534
+ UPDATE_ORDER_STATUS_MUTATION,
535
+ {
536
+ id,
537
+ input: {
538
+ status: toApiStatus(status),
539
+ },
514
540
  },
515
- });
541
+ 'SINGLE_ATTEMPT',
542
+ 'updateDeliveryJobStatus'
543
+ );
516
544
 
545
+ assertOrderAcknowledgement(data.updateDeliveryJobStatus, { id, status: toApiStatus(status) });
517
546
  const updated = mapOrder(data.updateDeliveryJobStatus);
518
547
  setOrders((previous) => previous.map((item) => (item.id === id ? updated : item)));
519
548
  return updated;
@@ -567,6 +596,9 @@ export function useOrder(orderId: string | undefined) {
567
596
  deliveryJob: ApiOrder | null;
568
597
  }>(graphQLOptions, GET_ORDER_QUERY, { id: orderId });
569
598
 
599
+ if (data.deliveryJob && data.deliveryJob.id !== orderId) {
600
+ throw new Error('The requested order could not be verified.');
601
+ }
570
602
  setOrder(data.deliveryJob ? mapOrder(data.deliveryJob) : null);
571
603
  } catch (fetchError) {
572
604
  setError(fetchError instanceof Error ? fetchError : new Error('Failed to load order'));