@burdenoff/microfe-movethewheels 2026.1016.12 → 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.
@@ -50,7 +50,11 @@ import { useOrder, useOrders } from '../hooks/useOrders';
50
50
  import { useVehicles, useVehicle, useDrivers, useDriver } from '../hooks/useFleet';
51
51
  import { useDebouncedValue } from '../hooks/useDebouncedValue';
52
52
  import { FormError } from '../components/FormError';
53
- import { Notice, useNotice } from '../components/Notice';
53
+ import { Notice } from '../components/Notice';
54
+ import { useOrderDispatch } from '../hooks/useOrderDispatch';
55
+ import { isVehicleAssignable } from '../utils/orderVehicleHold';
56
+ import { reuseRecoveryScope, reuseRecoveryContextKey } from '../utils/reuseRecovery';
57
+ import { useTr } from '../shared/hooks/useTr';
54
58
  import { DocumentsCard } from '../components/DocumentsCard';
55
59
  import { ProofOfDeliveryCard } from '../components/ProofOfDeliveryCard';
56
60
  import { useMoveTheWheels } from '../providers/MoveTheWheelsProvider';
@@ -94,6 +98,28 @@ function nextStatus(current: OrderStatus): OrderStatus | null {
94
98
  }
95
99
 
96
100
  export default function OrderDetailsPage() {
101
+ const context = useMoveTheWheels();
102
+ const { orderId } = useParams<{ orderId: string }>();
103
+ const scope = reuseRecoveryScope(
104
+ context.currentUser?.id,
105
+ context.workspaceId,
106
+ context.tenantId,
107
+ context.apiGatewayUrl,
108
+ context.workspaceToken
109
+ );
110
+ const contextKey = JSON.stringify([
111
+ reuseRecoveryContextKey(scope),
112
+ context.currentUser?.id,
113
+ context.workspaceId,
114
+ context.tenantId,
115
+ context.apiGatewayUrl,
116
+ orderId,
117
+ ]);
118
+ return <OrderDetails key={contextKey} contextKey={contextKey} contextReady={scope !== null} />;
119
+ }
120
+
121
+ function OrderDetails({ contextKey, contextReady }: { contextKey: string; contextReady: boolean }) {
122
+ const tr = useTr();
97
123
  const { orderId } = useParams<{ orderId: string }>();
98
124
  const { basePath, navigate } = useMoveTheWheels();
99
125
  const { order, isLoading, error, refetch } = useOrder(orderId);
@@ -101,9 +127,6 @@ export default function OrderDetailsPage() {
101
127
  const [fleetSearch, setFleetSearch] = useState('');
102
128
  const [selectedVehicle, setSelectedVehicle] = useState('');
103
129
  const [selectedDriver, setSelectedDriver] = useState('');
104
- const [assigning, setAssigning] = useState(false);
105
- const [advancing, setAdvancing] = useState(false);
106
- const [actionError, setActionError] = useState<string | null>(null);
107
130
  const debouncedFleetSearch = useDebouncedValue(fleetSearch);
108
131
  const {
109
132
  vehicles,
@@ -111,6 +134,8 @@ export default function OrderDetailsPage() {
111
134
  isLoadingMore: vehiclesLoadingMore,
112
135
  hasMore: vehiclesHaveMore,
113
136
  totalVehicles,
137
+ error: vehiclesError,
138
+ refetch: refreshVehicles,
114
139
  loadMore: loadMoreVehicles,
115
140
  } = useVehicles({
116
141
  limit: 20,
@@ -122,6 +147,8 @@ export default function OrderDetailsPage() {
122
147
  isLoadingMore: driversLoadingMore,
123
148
  hasMore: driversHaveMore,
124
149
  totalDrivers,
150
+ error: driversError,
151
+ refetch: refreshDrivers,
125
152
  loadMore: loadMoreDrivers,
126
153
  } = useDrivers({
127
154
  limit: 20,
@@ -130,76 +157,106 @@ export default function OrderDetailsPage() {
130
157
  // Resolved independently of the (searched, 20-row) lists above so the
131
158
  // trigger still shows the right label once a pick scrolls out of the
132
159
  // current search results.
133
- const { vehicle: selectedVehicleRecord, isLoading: selectedVehicleLoading } = useVehicle(
134
- selectedVehicle || undefined
135
- );
136
- const { driver: selectedDriverRecord, isLoading: selectedDriverLoading } = useDriver(
137
- selectedDriver || undefined
138
- );
139
- const { message: notice, notify } = useNotice();
160
+ const {
161
+ vehicle: selectedVehicleRecord,
162
+ isLoading: selectedVehicleLoading,
163
+ error: vehicleError,
164
+ refetch: refreshVehicle,
165
+ } = useVehicle(selectedVehicle || order?.vehicleId || undefined);
166
+ const {
167
+ driver: selectedDriverRecord,
168
+ isLoading: selectedDriverLoading,
169
+ error: driverError,
170
+ refetch: refreshDriver,
171
+ } = useDriver(selectedDriver || order?.driverId || undefined);
140
172
  const availableVehicles = vehicles.filter(
141
- (vehicle) => vehicle.status === 'available' || vehicle.id === order?.vehicleId
173
+ (vehicle) => isVehicleAssignable(vehicle.status) || vehicle.id === order?.vehicleId
142
174
  );
143
175
  const availableDrivers = drivers.filter(
144
176
  (driver) => driver.status === 'available' || driver.id === order?.driverId
145
177
  );
146
178
 
147
- const handleAssign = async () => {
148
- if (!orderId || !order) {
149
- setActionError('This order is no longer available. Refresh and try again.');
150
- return;
151
- }
152
-
153
- const vehicleId = selectedVehicle || order.vehicleId;
154
- const driverId = selectedDriver || order.driverId;
155
- if (!vehicleId || !driverId) {
156
- setActionError('Select both a compatible vehicle and driver before dispatching.');
157
- return;
158
- }
159
-
160
- const wasFullyAssigned = Boolean(order.vehicleId && order.driverId);
161
- setAssigning(true);
162
- setActionError(null);
163
- try {
164
- await updateOrder(orderId, { vehicleId, driverId });
165
- if (!wasFullyAssigned && order.status === 'confirmed') {
166
- await updateOrderStatus(orderId, 'assigned');
167
- }
168
- notify(
169
- !wasFullyAssigned && order.status === 'confirmed'
170
- ? 'Fleet assigned and order advanced to Assigned.'
171
- : 'Assignment updated.'
172
- );
173
- setSelectedVehicle('');
174
- setSelectedDriver('');
175
- refetch();
176
- } catch (err) {
177
- setActionError(err instanceof Error ? err.message : 'Failed to update assignment.');
178
- } finally {
179
- setAssigning(false);
180
- }
181
- };
182
-
183
- const handleStatusChange = async (status: OrderStatus, successMessage: string) => {
184
- if (!orderId || !order) return;
185
- if (status === 'assigned' && (!order.vehicleId || !order.driverId)) {
186
- setActionError('Assign both a compatible vehicle and driver before advancing to Assigned.');
187
- return;
188
- }
189
- if (TERMINAL_STATUSES.has(order.status)) return;
190
-
191
- setAdvancing(true);
192
- setActionError(null);
193
- try {
194
- await updateOrderStatus(orderId, status);
195
- notify(successMessage);
196
- refetch();
197
- } catch (err) {
198
- setActionError(err instanceof Error ? err.message : 'Failed to update status.');
199
- } finally {
200
- setAdvancing(false);
201
- }
179
+ const {
180
+ vehicle: assignedVehicle,
181
+ isLoading: assignedVehicleLoading,
182
+ error: assignedVehicleError,
183
+ refetch: refreshAssignedVehicle,
184
+ } = useVehicle(order?.vehicleId);
185
+ const assignedVehicleVerified =
186
+ !order?.vehicleId ||
187
+ (!assignedVehicleLoading && !assignedVehicleError && assignedVehicle?.id === order.vehicleId);
188
+ const assignedVehicleHeld = Boolean(
189
+ order?.vehicleId &&
190
+ assignedVehicleVerified &&
191
+ assignedVehicle &&
192
+ !isVehicleAssignable(assignedVehicle.status)
193
+ );
194
+ const effectiveVehicleId = selectedVehicle || order?.vehicleId;
195
+ const effectiveDriverId = selectedDriver || order?.driverId;
196
+ const vehicleVerified =
197
+ !effectiveVehicleId ||
198
+ (!selectedVehicleLoading && !vehicleError && selectedVehicleRecord?.id === effectiveVehicleId);
199
+ const driverVerified =
200
+ !effectiveDriverId ||
201
+ (!selectedDriverLoading && !driverError && selectedDriverRecord?.id === effectiveDriverId);
202
+ const vehicleHeld = Boolean(
203
+ effectiveVehicleId &&
204
+ vehicleVerified &&
205
+ selectedVehicleRecord &&
206
+ !isVehicleAssignable(selectedVehicleRecord.status)
207
+ );
208
+ const assignmentEntersMovement = Boolean(
209
+ order && !(order.vehicleId && order.driverId) && order.status === 'confirmed'
210
+ );
211
+ const heldAssignmentBlocked =
212
+ vehicleHeld && (effectiveVehicleId !== order?.vehicleId || assignmentEntersMovement);
213
+ const refreshAssignment = () => {
214
+ refetch();
215
+ refreshVehicle();
216
+ refreshAssignedVehicle();
217
+ refreshDriver();
218
+ refreshVehicles();
219
+ refreshDrivers();
202
220
  };
221
+ const dispatch = useOrderDispatch({
222
+ order,
223
+ contextKey,
224
+ ready: contextReady && !isLoading && !error && Boolean(order),
225
+ assignmentReady:
226
+ Boolean(effectiveVehicleId && effectiveDriverId) &&
227
+ vehicleVerified &&
228
+ driverVerified &&
229
+ !heldAssignmentBlocked &&
230
+ !vehiclesLoading &&
231
+ !driversLoading &&
232
+ !vehiclesError &&
233
+ !driversError,
234
+ movementReady: assignedVehicleVerified && !assignedVehicleHeld,
235
+ updateOrder,
236
+ updateOrderStatus,
237
+ refresh: refreshAssignment,
238
+ });
239
+ const assignmentChanged =
240
+ effectiveVehicleId !== order?.vehicleId || effectiveDriverId !== order?.driverId;
241
+ const actionUnavailable =
242
+ dispatch.busy || dispatch.blocked || !contextReady || isLoading || Boolean(error);
243
+ const handleAssign = () => dispatch.assign(effectiveVehicleId ?? '', effectiveDriverId ?? '');
244
+ const handleStatusChange = (status: OrderStatus) => dispatch.changeStatus(status);
245
+ const heldMessage =
246
+ selectedVehicleRecord?.status === 'maintenance'
247
+ ? tr(
248
+ 'movethewheels.order.vehicleMaintenance',
249
+ 'Vehicle marked for maintenance. Choose another vehicle.'
250
+ )
251
+ : selectedVehicleRecord?.status === 'decommissioned'
252
+ ? tr(
253
+ 'movethewheels.order.vehicleDecommissioned',
254
+ 'Vehicle decommissioned. Choose another vehicle.'
255
+ )
256
+ : tr(
257
+ 'movethewheels.order.vehicleInactive',
258
+ 'Vehicle marked inactive. Choose another vehicle.'
259
+ );
203
260
 
204
261
  const goTo = (path: string) => {
205
262
  if (navigate) {
@@ -213,15 +270,18 @@ export default function OrderDetailsPage() {
213
270
  }
214
271
  };
215
272
 
216
- if (isLoading) {
273
+ if (isLoading && !order) {
217
274
  return <LoadingState message="Loading order details..." />;
218
275
  }
219
276
 
220
- if (error || !order) {
277
+ if (!order) {
221
278
  return (
222
279
  <ErrorState
223
- title="Order not found"
224
- message="The order you're looking for doesn't exist or has been removed."
280
+ title={tr('movethewheels.order.unavailable', 'Order unavailable')}
281
+ message={tr(
282
+ 'movethewheels.order.loadUnavailable',
283
+ 'The requested order could not be loaded. Refresh before making changes.'
284
+ )}
225
285
  onRetry={refetch}
226
286
  />
227
287
  );
@@ -558,8 +618,65 @@ export default function OrderDetailsPage() {
558
618
  </CardHeader>
559
619
  <CardContent>
560
620
  <div className="space-y-4">
561
- {actionError && <FormError message={actionError} />}
562
- {notice && <Notice message={notice} />}
621
+ {isLoading && (
622
+ <p role="status" className="text-sm">
623
+ {tr(
624
+ 'movethewheels.order.refreshing',
625
+ 'Refreshing order details. Changes are unavailable.'
626
+ )}
627
+ </p>
628
+ )}
629
+ {error && (
630
+ <FormError
631
+ message={tr(
632
+ 'movethewheels.order.refreshFailed',
633
+ 'Order details could not be refreshed. Existing values are read-only.'
634
+ )}
635
+ />
636
+ )}
637
+ {dispatch.message && <Notice variant="info" message={dispatch.message} />}
638
+ {dispatch.knownAssignment && (
639
+ <p className="text-sm break-words" role="status">
640
+ {tr(
641
+ 'movethewheels.order.knownAssignment',
642
+ 'Saved assignment: vehicle {{vehicle}}, driver {{driver}}.',
643
+ {
644
+ vehicle:
645
+ selectedVehicleRecord?.id === dispatch.knownAssignment.vehicleId
646
+ ? selectedVehicleRecord.vehicleNumber
647
+ : dispatch.knownAssignment.vehicleId,
648
+ driver:
649
+ selectedDriverRecord?.id === dispatch.knownAssignment.driverId
650
+ ? selectedDriverRecord.fullName
651
+ : dispatch.knownAssignment.driverId,
652
+ }
653
+ )}
654
+ </p>
655
+ )}
656
+ {dispatch.blocked && (
657
+ <FormError
658
+ message={tr(
659
+ 'movethewheels.order.blocked',
660
+ 'An order write is unconfirmed. Refresh is read-only and will not unlock another write.'
661
+ )}
662
+ />
663
+ )}
664
+ {vehicleHeld && <FormError message={heldMessage} />}
665
+ {(!vehicleVerified ||
666
+ !driverVerified ||
667
+ !assignedVehicleVerified ||
668
+ vehiclesError ||
669
+ driversError) && (
670
+ <FormError
671
+ message={tr(
672
+ 'movethewheels.order.unverifiedFleet',
673
+ 'Vehicle status could not be verified. Refresh before assigning or advancing.'
674
+ )}
675
+ />
676
+ )}
677
+ <Button variant="outline" onClick={refreshAssignment} disabled={dispatch.busy}>
678
+ {tr('movethewheels.order.refreshAssignment', 'Refresh assignment')}
679
+ </Button>
563
680
 
564
681
  <div>
565
682
  <p className="text-sm text-text-secondary">Tracking Number</p>
@@ -576,6 +693,7 @@ export default function OrderDetailsPage() {
576
693
  <Input
577
694
  id="assign-fleet-search"
578
695
  value={fleetSearch}
696
+ disabled={actionUnavailable}
579
697
  onChange={(event) => setFleetSearch(event.target.value)}
580
698
  placeholder="Vehicle number, driver name or employee ID"
581
699
  className="pl-9"
@@ -586,7 +704,8 @@ export default function OrderDetailsPage() {
586
704
  <Label htmlFor="assign-vehicle">Vehicle</Label>
587
705
  <EntityCombobox
588
706
  id="assign-vehicle"
589
- value={selectedVehicle}
707
+ disabled={actionUnavailable}
708
+ value={effectiveVehicleId ?? ''}
590
709
  onChange={setSelectedVehicle}
591
710
  options={availableVehicles}
592
711
  loading={vehiclesLoading}
@@ -597,8 +716,8 @@ export default function OrderDetailsPage() {
597
716
  {vehicle.vehicleNumber} ({vehicle.type})
598
717
  </>
599
718
  )}
600
- selectedOption={selectedVehicleRecord}
601
- selectedLoading={selectedVehicleLoading}
719
+ selectedOption={vehicleVerified ? selectedVehicleRecord : null}
720
+ selectedLoading={!vehicleVerified}
602
721
  hasMore={vehiclesHaveMore}
603
722
  totalCount={totalVehicles}
604
723
  onLoadMore={loadMoreVehicles}
@@ -617,7 +736,8 @@ export default function OrderDetailsPage() {
617
736
  <Label htmlFor="assign-driver">Driver</Label>
618
737
  <EntityCombobox
619
738
  id="assign-driver"
620
- value={selectedDriver}
739
+ disabled={actionUnavailable}
740
+ value={effectiveDriverId ?? ''}
621
741
  onChange={setSelectedDriver}
622
742
  options={availableDrivers}
623
743
  loading={driversLoading}
@@ -628,8 +748,8 @@ export default function OrderDetailsPage() {
628
748
  {driver.fullName} ({driver.employeeId})
629
749
  </>
630
750
  )}
631
- selectedOption={selectedDriverRecord}
632
- selectedLoading={selectedDriverLoading}
751
+ selectedOption={driverVerified ? selectedDriverRecord : null}
752
+ selectedLoading={!driverVerified}
633
753
  hasMore={driversHaveMore}
634
754
  totalCount={totalDrivers}
635
755
  onLoadMore={loadMoreDrivers}
@@ -645,10 +765,19 @@ export default function OrderDetailsPage() {
645
765
  <Button
646
766
  className="w-full"
647
767
  onClick={handleAssign}
648
- disabled={assigning || (!selectedVehicle && !selectedDriver)}
768
+ disabled={
769
+ actionUnavailable ||
770
+ !assignmentChanged ||
771
+ heldAssignmentBlocked ||
772
+ !vehicleVerified ||
773
+ !driverVerified ||
774
+ vehiclesLoading ||
775
+ driversLoading ||
776
+ Boolean(vehiclesError || driversError)
777
+ }
649
778
  >
650
779
  <UserCheck size={16} />
651
- {assigning ? 'Assigning…' : 'Assign driver & vehicle'}
780
+ {dispatch.busy ? 'Assigning…' : 'Assign driver & vehicle'}
652
781
  </Button>
653
782
  </div>
654
783
 
@@ -659,6 +788,14 @@ export default function OrderDetailsPage() {
659
788
  Current status:{' '}
660
789
  <span className="font-medium">{STATUS_LABELS[order.status]}</span>
661
790
  </p>
791
+ {assignedVehicleHeld && (
792
+ <p className="text-sm text-status-error-text" role="status">
793
+ {tr(
794
+ 'movethewheels.order.currentVehicleHeld',
795
+ 'The currently assigned vehicle is on hold. Save a replacement before advancing movement.'
796
+ )}
797
+ </p>
798
+ )}
662
799
  {(() => {
663
800
  const upcoming = nextStatus(order.status);
664
801
  if (!upcoming) return null;
@@ -669,16 +806,18 @@ export default function OrderDetailsPage() {
669
806
  <Button
670
807
  variant="outline"
671
808
  className="w-full"
672
- onClick={() =>
673
- handleStatusChange(
674
- upcoming,
675
- `Order advanced to ${STATUS_LABELS[upcoming]}.`
676
- )
809
+ onClick={() => handleStatusChange(upcoming)}
810
+ disabled={
811
+ actionUnavailable ||
812
+ missingDispatchPair ||
813
+ (['assigned', 'picked_up', 'in_transit', 'out_for_delivery'].includes(
814
+ upcoming
815
+ ) &&
816
+ (!assignedVehicleVerified || assignedVehicleHeld))
677
817
  }
678
- disabled={advancing || missingDispatchPair}
679
818
  >
680
819
  <ArrowRight size={16} />
681
- {advancing ? 'Updating…' : `Advance to ${STATUS_LABELS[upcoming]}`}
820
+ {dispatch.busy ? 'Updating…' : `Advance to ${STATUS_LABELS[upcoming]}`}
682
821
  </Button>
683
822
  {missingDispatchPair && (
684
823
  <p className="text-xs text-text-secondary">
@@ -693,8 +832,8 @@ export default function OrderDetailsPage() {
693
832
  <Button
694
833
  variant="outline"
695
834
  className="w-full text-status-error-text"
696
- onClick={() => handleStatusChange('cancelled', 'Order cancelled.')}
697
- disabled={advancing}
835
+ onClick={() => handleStatusChange('cancelled')}
836
+ disabled={actionUnavailable}
698
837
  >
699
838
  <XCircle size={16} />
700
839
  Cancel order
@@ -1,3 +1,4 @@
1
+ import { vehicleHoldRefusal, type VehicleHoldMutation } from './orderVehicleHold';
1
2
  import { prepareWorkspaceRequest } from '@burdenoff/fe-libs/shared/graphql';
2
3
 
3
4
  export type GraphQLAttemptPolicy = 'RETRY_TRANSIENT' | 'SINGLE_ATTEMPT';
@@ -111,7 +112,8 @@ export async function executeGraphQL<TData>(
111
112
  options: GraphQLRequestOptions,
112
113
  query: string,
113
114
  variables?: Record<string, unknown>,
114
- attemptPolicy: GraphQLAttemptPolicy = 'RETRY_TRANSIENT'
115
+ attemptPolicy: GraphQLAttemptPolicy = 'RETRY_TRANSIENT',
116
+ holdRefusalField?: VehicleHoldMutation
115
117
  ): Promise<TData> {
116
118
  // Capture the original intent before queueing or asynchronously renewing authority.
117
119
  const request = { ...options };
@@ -152,7 +154,8 @@ export async function executeGraphQL<TData>(
152
154
  headers,
153
155
  body,
154
156
  attemptPolicy === 'SINGLE_ATTEMPT' ? 0 : MAX_RETRIES,
155
- authority?.assertCurrent ?? (() => {})
157
+ authority?.assertCurrent ?? (() => {}),
158
+ attemptPolicy === 'SINGLE_ATTEMPT' ? holdRefusalField : undefined
156
159
  );
157
160
  } finally {
158
161
  releaseSlot();
@@ -160,24 +163,45 @@ export async function executeGraphQL<TData>(
160
163
  }
161
164
 
162
165
  /** Best-effort: the joined `errors[].message` of a non-2xx GraphQL response, or null. */
163
- async function readGraphqlErrorMessage(response: Response): Promise<string | null> {
166
+ async function readGraphqlErrorMessage(
167
+ response: Response,
168
+ holdRefusalField?: VehicleHoldMutation
169
+ ): Promise<Error | null> {
164
170
  try {
165
171
  const payload = (await response.json()) as { errors?: Array<{ message?: string }> };
172
+ const refusal = vehicleHoldRefusal(payload, response.status, holdRefusalField);
173
+ if (refusal) return refusal;
166
174
  const messages = (payload.errors ?? [])
167
175
  .map((item) => item.message)
168
176
  .filter((message): message is string => typeof message === 'string' && message.length > 0);
169
- return messages.length > 0 ? messages.join('; ') : null;
177
+ return messages.length > 0 ? new Error(messages.join('; ')) : null;
170
178
  } catch {
171
179
  return null;
172
180
  }
173
181
  }
174
182
 
183
+ /** A write ACK must have one owning root and an absent or empty errors array. */
184
+ function validWriteEnvelope(payload: unknown, field: VehicleHoldMutation): boolean {
185
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) return false;
186
+ if ('errors' in payload && (!Array.isArray(payload.errors) || payload.errors.length !== 0))
187
+ return false;
188
+ if (
189
+ !('data' in payload) ||
190
+ typeof payload.data !== 'object' ||
191
+ payload.data === null ||
192
+ Array.isArray(payload.data)
193
+ )
194
+ return false;
195
+ return Object.keys(payload.data).length === 1 && Object.hasOwn(payload.data, field);
196
+ }
197
+
175
198
  async function runWithRetries<TData>(
176
199
  url: string,
177
200
  headers: Record<string, string>,
178
201
  body: string,
179
202
  retries: number,
180
- assertCurrent: () => void
203
+ assertCurrent: () => void,
204
+ holdRefusalField?: VehicleHoldMutation
181
205
  ): Promise<TData> {
182
206
  let lastError: Error = new Error('GraphQL request failed');
183
207
 
@@ -204,8 +228,8 @@ async function runWithRetries<TData>(
204
228
  // rule rejection) — show that message rather than a bare status code.
205
229
  const detail = isTransientStatus(response.status)
206
230
  ? null
207
- : await readGraphqlErrorMessage(response);
208
- lastError = new Error(detail ?? `GraphQL request failed with status ${response.status}`);
231
+ : await readGraphqlErrorMessage(response, holdRefusalField);
232
+ lastError = detail ?? new Error(`GraphQL request failed with status ${response.status}`);
209
233
  if (canRetry && isTransientStatus(response.status)) continue;
210
234
  throw lastError;
211
235
  }
@@ -215,6 +239,11 @@ async function runWithRetries<TData>(
215
239
  errors?: Array<{ message?: string }>;
216
240
  };
217
241
 
242
+ const refusal = vehicleHoldRefusal(payload, response.status, holdRefusalField);
243
+ if (refusal) throw refusal;
244
+ if (holdRefusalField && !validWriteEnvelope(payload, holdRefusalField))
245
+ throw new Error('The order write response could not be confirmed.');
246
+
218
247
  if (payload.errors?.length) {
219
248
  const message = payload.errors
220
249
  .map((item) => item.message ?? 'Unknown GraphQL error')
@@ -0,0 +1,84 @@
1
+ export type VehicleHoldMutation = 'updateDeliveryJob' | 'updateDeliveryJobStatus';
2
+
3
+ export class VehicleHoldRefusal extends Error {
4
+ constructor(
5
+ readonly operation: VehicleHoldMutation,
6
+ readonly kind: 'vehicle' | 'driver' = 'vehicle'
7
+ ) {
8
+ super('The recorded eligibility prevents this assignment or movement.');
9
+ this.name = 'VehicleHoldRefusal';
10
+ }
11
+ }
12
+
13
+ function object(value: unknown): value is Record<string, unknown> {
14
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
15
+ }
16
+
17
+ /** Only the owning pre-write refusal is known to have made no job/history write. */
18
+ export function vehicleHoldRefusal(
19
+ payload: unknown,
20
+ status: number,
21
+ operation?: VehicleHoldMutation
22
+ ): VehicleHoldRefusal | null {
23
+ if (!operation || (status !== 200 && status !== 400) || !object(payload)) return null;
24
+ if (!Array.isArray(payload.errors) || payload.errors.length !== 1) return null;
25
+ const error: unknown = payload.errors[0];
26
+ if (!object(error) || typeof error.message !== 'string' || !error.message) return null;
27
+ if (!Array.isArray(error.path) || error.path.length !== 1 || error.path[0] !== operation)
28
+ return null;
29
+ if (!object(error.extensions) || error.extensions.code !== 'BAD_USER_INPUT') return null;
30
+ const key = error.extensions.i18nKey;
31
+ const kind =
32
+ key === 'route.errors.vehicleNotAssignable'
33
+ ? 'vehicle'
34
+ : key === 'route.errors.driverInactive' ||
35
+ key === 'route.errors.driverLicenseUnknown' ||
36
+ key === 'route.errors.driverLicenseExpired'
37
+ ? 'driver'
38
+ : null;
39
+ if (!kind) return null;
40
+ if (
41
+ payload.data !== undefined &&
42
+ payload.data !== null &&
43
+ (!object(payload.data) ||
44
+ Object.keys(payload.data).length !== 1 ||
45
+ payload.data[operation] !== null)
46
+ )
47
+ return null;
48
+ return new VehicleHoldRefusal(operation, kind);
49
+ }
50
+
51
+ const API_STATUSES = new Set([
52
+ 'PENDING',
53
+ 'CONFIRMED',
54
+ 'ASSIGNED',
55
+ 'PICKED_UP',
56
+ 'IN_TRANSIT',
57
+ 'OUT_FOR_DELIVERY',
58
+ 'DELIVERED',
59
+ 'RETURNED',
60
+ 'CANCELLED',
61
+ 'FAILED',
62
+ ]);
63
+
64
+ /** Validate the returned write identity before mapping or dispatching a dependent mutation. */
65
+ export function assertOrderAcknowledgement(
66
+ value: unknown,
67
+ expected: { id: string; vehicleId?: string; driverId?: string; status?: string }
68
+ ): void {
69
+ if (
70
+ !object(value) ||
71
+ value.id !== expected.id ||
72
+ typeof value.status !== 'string' ||
73
+ !API_STATUSES.has(value.status) ||
74
+ (expected.vehicleId !== undefined && value.vehicleId !== expected.vehicleId) ||
75
+ (expected.driverId !== undefined && value.driverId !== expected.driverId) ||
76
+ (expected.status !== undefined && value.status !== expected.status)
77
+ )
78
+ throw new Error('The order write could not be confirmed.');
79
+ }
80
+
81
+ export function isVehicleAssignable(status: string): boolean {
82
+ // These are the existing view-model mappings of ACTIVE and IN_TRANSIT.
83
+ return status === 'available' || status === 'assigned';
84
+ }