@tokenoftrust/storefront-runner 2.4.51 → 2.4.53
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/apps/storefront/src/lib/admin/orderClient.ts +45 -11
- package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +10 -53
- package/apps/storefront/src/lib/admin/services/orders.ts +524 -87
- package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +1159 -33
- package/apps/storefront/src/lib/admin/viewmodels/orders.ts +37 -1
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +20 -5
- package/apps/storefront/src/pages/api/admin/orders.ts +8 -12
- package/package.json +1 -1
|
@@ -1,49 +1,1175 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* into).
|
|
2
|
+
* Daily batch desk. Queues, evidence, and commands are a pure projection of the
|
|
3
|
+
* commerce contract. The test adapter is not a carrier and does not buy postage.
|
|
4
|
+
* Print payload and tracking links are owner-only. Public events and the desk
|
|
5
|
+
* log never carry a destination.
|
|
7
6
|
*/
|
|
7
|
+
import {
|
|
8
|
+
actorMayCommand,
|
|
9
|
+
admitTrackingRead,
|
|
10
|
+
decideCommerceCommand,
|
|
11
|
+
paginateOrders,
|
|
12
|
+
publicCommerceEvent,
|
|
13
|
+
readOwnerAddress,
|
|
14
|
+
type CommerceActor,
|
|
15
|
+
type CommerceEvidence,
|
|
16
|
+
type CommerceStage,
|
|
17
|
+
type ComplianceState,
|
|
18
|
+
type FulfillmentState,
|
|
19
|
+
type LabelState,
|
|
20
|
+
type PaymentState,
|
|
21
|
+
type ProviderOutcome,
|
|
22
|
+
type PublicCommerceEvent,
|
|
23
|
+
type StockState,
|
|
24
|
+
} from "../../../../../../packages/public-runtime/src/checkout-orders.js";
|
|
8
25
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
| "packed"
|
|
14
|
-
| "label_needed"
|
|
15
|
-
| "shipped"
|
|
16
|
-
| "exceptions";
|
|
26
|
+
export const SCRAPPY_FULFILLMENT_PATH = "/scrappy-fulfillment";
|
|
27
|
+
export const WORKSPACE_PAGE_LIMIT = 4;
|
|
28
|
+
export const TEST_ADAPTER_QUOTE_MINOR = 640;
|
|
29
|
+
export const TRACKING_HOSTS = ["tracking.example"] as const;
|
|
17
30
|
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
export const WORKSPACE_QUEUES = ["ready", "held", "in_progress", "shipped"] as const;
|
|
32
|
+
export type WorkspaceQueue = (typeof WORKSPACE_QUEUES)[number];
|
|
33
|
+
export type WorkspaceQueueFilter = WorkspaceQueue | "all";
|
|
34
|
+
|
|
35
|
+
export const WORKSPACE_EXCEPTIONS = [
|
|
36
|
+
"carrier-outage",
|
|
37
|
+
"refund-stop",
|
|
38
|
+
"unreadable-destination",
|
|
39
|
+
"other",
|
|
40
|
+
] as const;
|
|
41
|
+
export type WorkspaceException = (typeof WORKSPACE_EXCEPTIONS)[number];
|
|
42
|
+
|
|
43
|
+
export const QUEUE_LABEL: Record<WorkspaceQueue, string> = {
|
|
44
|
+
ready: "Ready",
|
|
45
|
+
held: "Held",
|
|
46
|
+
in_progress: "In progress",
|
|
47
|
+
shipped: "Shipped",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ACTOR_SPECS = [
|
|
51
|
+
{ name: "owner", role: "owner" as const, sameTenant: true, sameStage: true },
|
|
52
|
+
{ name: "caller", role: "caller" as const, sameTenant: true, sameStage: true },
|
|
53
|
+
{ name: "denied", role: "denied" as const, sameTenant: true, sameStage: true },
|
|
54
|
+
{ name: "other-tenant", role: "owner" as const, sameTenant: false, sameStage: true },
|
|
55
|
+
{ name: "wrong-stage", role: "owner" as const, sameTenant: true, sameStage: false },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const FRESH: CommerceEvidence = {
|
|
59
|
+
verification: "fresh",
|
|
60
|
+
tax: "fresh",
|
|
61
|
+
risk: "fresh",
|
|
62
|
+
stock: "fresh",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const STALE: CommerceEvidence = {
|
|
66
|
+
verification: "UNKNOWN",
|
|
67
|
+
tax: "stale",
|
|
68
|
+
risk: "unreadable",
|
|
69
|
+
stock: "UNKNOWN",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const TOTALS = { subtotalMinor: 4000, shippingMinor: 0, taxMinor: 0, totalMinor: 4000 } as const;
|
|
73
|
+
|
|
74
|
+
type PurchaseScript = "admit" | "pending" | "unknown";
|
|
75
|
+
type VoidScript = "approved" | "pending" | "denied";
|
|
76
|
+
type SignatureScript = "accept" | "refuse";
|
|
77
|
+
|
|
78
|
+
export interface WorkspaceLine {
|
|
79
|
+
sku: string;
|
|
80
|
+
quantity: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface WorkspaceOrder {
|
|
84
|
+
id: string;
|
|
85
|
+
placedAt: string;
|
|
86
|
+
lines: WorkspaceLine[];
|
|
87
|
+
currency: "USD";
|
|
88
|
+
payment: PaymentState;
|
|
89
|
+
compliance: ComplianceState;
|
|
90
|
+
stock: StockState;
|
|
91
|
+
fulfillment: FulfillmentState;
|
|
92
|
+
label: LabelState;
|
|
93
|
+
providerOutcome: ProviderOutcome | null;
|
|
94
|
+
evidence: CommerceEvidence;
|
|
95
|
+
picked: boolean;
|
|
96
|
+
packed: boolean;
|
|
97
|
+
manualException: WorkspaceException | null;
|
|
98
|
+
adultSignature: "required" | "accepted" | "refused";
|
|
99
|
+
purchaseKey: string | null;
|
|
100
|
+
purchaseId: string | null;
|
|
101
|
+
labelCostMinor: number | null;
|
|
102
|
+
trackingNumber: string | null;
|
|
103
|
+
trackingUrl: string | null;
|
|
104
|
+
ownerDestination: string;
|
|
105
|
+
purchaseScript: PurchaseScript;
|
|
106
|
+
voidScript: VoidScript;
|
|
107
|
+
signatureScript: SignatureScript;
|
|
108
|
+
slipIssued: boolean;
|
|
109
|
+
results: WorkspaceResult[];
|
|
22
110
|
}
|
|
23
111
|
|
|
24
|
-
export interface
|
|
112
|
+
export interface WorkspaceResult {
|
|
25
113
|
orderId: string;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
114
|
+
command: string;
|
|
115
|
+
status: "admitted" | "pending" | "unknown" | "refused";
|
|
116
|
+
reason: string;
|
|
117
|
+
livePurchase: false;
|
|
118
|
+
purchaseId: string | null;
|
|
119
|
+
costMinor: number | null;
|
|
120
|
+
releaseReservation: boolean;
|
|
121
|
+
restock: false;
|
|
34
122
|
}
|
|
35
123
|
|
|
36
|
-
export
|
|
124
|
+
export interface WorkspaceState {
|
|
125
|
+
tenantId: string;
|
|
126
|
+
stage: CommerceStage;
|
|
127
|
+
carrierAccountConfigured: boolean;
|
|
128
|
+
prohibitedCarriers: readonly ["USPS"];
|
|
129
|
+
adultSignatureRequired: true;
|
|
130
|
+
livePurchase: false;
|
|
131
|
+
currency: "USD";
|
|
132
|
+
quoteMinor: number;
|
|
133
|
+
orders: WorkspaceOrder[];
|
|
134
|
+
events: PublicCommerceEvent[];
|
|
135
|
+
log: string[];
|
|
136
|
+
lastResults: WorkspaceResult[];
|
|
137
|
+
}
|
|
37
138
|
|
|
38
|
-
export interface
|
|
139
|
+
export interface WorkspaceQuery {
|
|
140
|
+
actorName: string;
|
|
141
|
+
queue: WorkspaceQueueFilter;
|
|
142
|
+
q: string;
|
|
143
|
+
cursor: string | null;
|
|
144
|
+
limit: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface OwnerPrintPayload {
|
|
148
|
+
purchaseId: string;
|
|
149
|
+
format: "pdf";
|
|
150
|
+
adultSignature: "required";
|
|
151
|
+
livePurchase: false;
|
|
152
|
+
destination: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface PackingSlip {
|
|
39
156
|
orderId: string;
|
|
40
|
-
|
|
41
|
-
|
|
157
|
+
lines: WorkspaceLine[];
|
|
158
|
+
wholeOrder: true;
|
|
159
|
+
destination: string | null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface WorkspaceOrderView {
|
|
163
|
+
id: string;
|
|
164
|
+
placedAt: string;
|
|
165
|
+
lines: WorkspaceLine[];
|
|
166
|
+
unitCount: number;
|
|
167
|
+
currency: "USD";
|
|
168
|
+
payment: PaymentState;
|
|
169
|
+
stock: StockState;
|
|
170
|
+
fulfillment: FulfillmentState;
|
|
171
|
+
label: LabelState;
|
|
172
|
+
providerOutcome: ProviderOutcome | null;
|
|
173
|
+
evidence: CommerceEvidence;
|
|
174
|
+
picked: boolean;
|
|
175
|
+
packed: boolean;
|
|
176
|
+
manualException: WorkspaceException | null;
|
|
177
|
+
adultSignature: "required" | "accepted" | "refused";
|
|
178
|
+
purchaseKey: string | null;
|
|
179
|
+
purchaseId: string | null;
|
|
180
|
+
labelCostMinor: number | null;
|
|
181
|
+
trackingNumber: string | null;
|
|
182
|
+
trackingUrl: string | null;
|
|
183
|
+
queue: WorkspaceQueue;
|
|
184
|
+
holdReasons: string[];
|
|
185
|
+
results: WorkspaceResult[];
|
|
186
|
+
slip: PackingSlip | null;
|
|
187
|
+
printPayload: OwnerPrintPayload | null;
|
|
42
188
|
}
|
|
43
189
|
|
|
44
190
|
export interface FulfillmentViewModel {
|
|
45
191
|
targetTenant: string;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
192
|
+
stage: CommerceStage;
|
|
193
|
+
actorName: string;
|
|
194
|
+
actorRole: CommerceActor["role"];
|
|
195
|
+
livePurchase: false;
|
|
196
|
+
carrierAccountConfigured: boolean;
|
|
197
|
+
prohibitedCarriers: readonly ["USPS"];
|
|
198
|
+
adultSignatureRequired: true;
|
|
199
|
+
quoteMinor: number;
|
|
200
|
+
currency: "USD";
|
|
201
|
+
queue: WorkspaceQueueFilter;
|
|
202
|
+
q: string;
|
|
203
|
+
limit: number;
|
|
204
|
+
cursor: string | null;
|
|
205
|
+
nextCursor: string | null;
|
|
206
|
+
invalidCursor: boolean;
|
|
207
|
+
storewide: Record<WorkspaceQueue, number>;
|
|
208
|
+
filterCount: number;
|
|
209
|
+
pageCount: number;
|
|
210
|
+
orders: WorkspaceOrderView[];
|
|
211
|
+
pickList: Array<{ orderId: string; sku: string; quantity: number; packed: boolean }>;
|
|
212
|
+
events: PublicCommerceEvent[];
|
|
213
|
+
log: string[];
|
|
214
|
+
lastResults: WorkspaceResult[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface WorkspaceCommand {
|
|
218
|
+
actorName: string;
|
|
219
|
+
command: string;
|
|
220
|
+
orderIds: string[];
|
|
221
|
+
purchaseKey: string | null;
|
|
222
|
+
carrier: string | null;
|
|
223
|
+
quantity: number | null;
|
|
224
|
+
quantityInvalid: boolean;
|
|
225
|
+
lineId: boolean;
|
|
226
|
+
exception: string | null;
|
|
227
|
+
accountConfigured: boolean | null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface WorkspaceHttpRequest {
|
|
231
|
+
method: string;
|
|
232
|
+
url: string;
|
|
233
|
+
body: string | null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface WorkspaceHttpResponse {
|
|
237
|
+
status: number;
|
|
238
|
+
contentType: string;
|
|
239
|
+
body: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface TestFulfillmentAdapter {
|
|
243
|
+
readonly livePurchase: false;
|
|
244
|
+
state(): WorkspaceState;
|
|
245
|
+
handle(request: WorkspaceHttpRequest): WorkspaceHttpResponse;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function defaultWorkspaceQuery(): WorkspaceQuery {
|
|
249
|
+
return {
|
|
250
|
+
actorName: "owner",
|
|
251
|
+
queue: "ready",
|
|
252
|
+
q: "",
|
|
253
|
+
cursor: null,
|
|
254
|
+
limit: WORKSPACE_PAGE_LIMIT,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function workspaceActor(tenantId: string, stage: CommerceStage, name: string): CommerceActor {
|
|
259
|
+
const known = ACTOR_SPECS.find((spec) => spec.name === name);
|
|
260
|
+
if (!known) return { role: "denied", tenantId, stage };
|
|
261
|
+
return {
|
|
262
|
+
role: known.role,
|
|
263
|
+
tenantId: known.sameTenant ? tenantId : `${tenantId}-b`,
|
|
264
|
+
stage: known.sameStage ? stage : stage === "preview" ? "publication" : "preview",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function holdReasons(order: {
|
|
269
|
+
fulfillment: FulfillmentState;
|
|
270
|
+
manualException: WorkspaceException | null;
|
|
271
|
+
adultSignature: "required" | "accepted" | "refused";
|
|
272
|
+
evidence: CommerceEvidence;
|
|
273
|
+
payment: PaymentState;
|
|
274
|
+
stock: StockState;
|
|
275
|
+
label: LabelState;
|
|
276
|
+
providerOutcome: ProviderOutcome | null;
|
|
277
|
+
}): string[] {
|
|
278
|
+
if (order.fulfillment === "handed_off") return [];
|
|
279
|
+
const reasons: string[] = [];
|
|
280
|
+
if (order.fulfillment === "cancelled") reasons.push("Cancelled. The shipment is the whole order.");
|
|
281
|
+
if (order.fulfillment === "held") reasons.push("Fulfillment is held.");
|
|
282
|
+
if (order.fulfillment === "UNKNOWN") reasons.push("Fulfillment is unknown. Reconcile before shipping.");
|
|
283
|
+
if (order.manualException) reasons.push(`Manual exception: ${order.manualException}.`);
|
|
284
|
+
if (order.adultSignature === "refused") reasons.push("Adult signature was refused. The order does not ship.");
|
|
285
|
+
const stale = (["verification", "tax", "risk", "stock"] as const).filter((key) => order.evidence[key] !== "fresh");
|
|
286
|
+
if (stale.length > 0) {
|
|
287
|
+
reasons.push(`Evidence is not fresh: ${stale.map((key) => `${key} ${order.evidence[key]}`).join(", ")}.`);
|
|
288
|
+
}
|
|
289
|
+
if (order.payment === "refund_requested" || order.payment === "refund_unknown") {
|
|
290
|
+
reasons.push("Refund stop. Reconcile the refund before shipping. Returns are not automated.");
|
|
291
|
+
} else if (order.payment === "capture_unknown") {
|
|
292
|
+
reasons.push("Capture is unknown. Reconcile before shipping.");
|
|
293
|
+
} else if (order.payment !== "captured") {
|
|
294
|
+
reasons.push("Payment is not captured.");
|
|
295
|
+
}
|
|
296
|
+
if (order.stock !== "confirmed") reasons.push("Stock is not confirmed.");
|
|
297
|
+
if (order.label === "UNKNOWN" || order.providerOutcome === "unknown") {
|
|
298
|
+
reasons.push("Label outcome is unknown. Do not buy another label.");
|
|
299
|
+
}
|
|
300
|
+
if (order.label === "intent" || order.providerOutcome === "intent") {
|
|
301
|
+
reasons.push("Label purchase is pending. Do not buy another label.");
|
|
302
|
+
}
|
|
303
|
+
if (order.label === "void_requested") reasons.push("Void is pending. Do not buy another label.");
|
|
304
|
+
return reasons;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function workspaceQueue(order: {
|
|
308
|
+
fulfillment: FulfillmentState;
|
|
309
|
+
manualException: WorkspaceException | null;
|
|
310
|
+
adultSignature: "required" | "accepted" | "refused";
|
|
311
|
+
evidence: CommerceEvidence;
|
|
312
|
+
payment: PaymentState;
|
|
313
|
+
stock: StockState;
|
|
314
|
+
label: LabelState;
|
|
315
|
+
providerOutcome: ProviderOutcome | null;
|
|
316
|
+
picked: boolean;
|
|
317
|
+
packed: boolean;
|
|
318
|
+
}): WorkspaceQueue {
|
|
319
|
+
if (order.fulfillment === "handed_off") return "shipped";
|
|
320
|
+
if (holdReasons(order).length > 0) return "held";
|
|
321
|
+
if (order.picked || order.packed || order.label === "purchased" || order.label === "printed") return "in_progress";
|
|
322
|
+
return "ready";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function unitCount(order: WorkspaceOrder): number {
|
|
326
|
+
return order.lines.reduce((sum, line) => sum + line.quantity, 0);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function blankResult(command: string, orderId: string, status: WorkspaceResult["status"], reason: string): WorkspaceResult {
|
|
330
|
+
return {
|
|
331
|
+
orderId,
|
|
332
|
+
command,
|
|
333
|
+
status,
|
|
334
|
+
reason,
|
|
335
|
+
livePurchase: false,
|
|
336
|
+
purchaseId: null,
|
|
337
|
+
costMinor: null,
|
|
338
|
+
releaseReservation: false,
|
|
339
|
+
restock: false,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function initialWorkspaceState(tenantId: string, stage: CommerceStage = "preview"): WorkspaceState {
|
|
344
|
+
const shippable = {
|
|
345
|
+
payment: "captured" as const,
|
|
346
|
+
compliance: "fresh" as const,
|
|
347
|
+
stock: "confirmed" as const,
|
|
348
|
+
evidence: FRESH,
|
|
349
|
+
fulfillment: "ready" as const,
|
|
350
|
+
};
|
|
351
|
+
const order = (
|
|
352
|
+
id: string,
|
|
353
|
+
placedAt: string,
|
|
354
|
+
sku: string,
|
|
355
|
+
quantity: number,
|
|
356
|
+
extra: Partial<WorkspaceOrder> = {},
|
|
357
|
+
): WorkspaceOrder => ({
|
|
358
|
+
id,
|
|
359
|
+
placedAt,
|
|
360
|
+
lines: [{ sku, quantity }],
|
|
361
|
+
currency: "USD",
|
|
362
|
+
payment: extra.payment ?? shippable.payment,
|
|
363
|
+
compliance: extra.compliance ?? shippable.compliance,
|
|
364
|
+
stock: extra.stock ?? shippable.stock,
|
|
365
|
+
fulfillment: extra.fulfillment ?? shippable.fulfillment,
|
|
366
|
+
label: extra.label ?? "none",
|
|
367
|
+
providerOutcome: extra.providerOutcome ?? null,
|
|
368
|
+
evidence: extra.evidence ?? shippable.evidence,
|
|
369
|
+
picked: extra.picked ?? false,
|
|
370
|
+
packed: extra.packed ?? false,
|
|
371
|
+
manualException: extra.manualException ?? null,
|
|
372
|
+
adultSignature: extra.adultSignature ?? "required",
|
|
373
|
+
purchaseKey: extra.purchaseKey ?? null,
|
|
374
|
+
purchaseId: extra.purchaseId ?? null,
|
|
375
|
+
labelCostMinor: extra.labelCostMinor ?? null,
|
|
376
|
+
trackingNumber: extra.trackingNumber ?? null,
|
|
377
|
+
trackingUrl: extra.trackingUrl ?? null,
|
|
378
|
+
ownerDestination: `dest-${id}`,
|
|
379
|
+
purchaseScript: extra.purchaseScript ?? "admit",
|
|
380
|
+
voidScript: extra.voidScript ?? "approved",
|
|
381
|
+
signatureScript: extra.signatureScript ?? "accept",
|
|
382
|
+
slipIssued: false,
|
|
383
|
+
results: [],
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
tenantId,
|
|
388
|
+
stage,
|
|
389
|
+
carrierAccountConfigured: true,
|
|
390
|
+
prohibitedCarriers: ["USPS"],
|
|
391
|
+
adultSignatureRequired: true,
|
|
392
|
+
livePurchase: false,
|
|
393
|
+
currency: "USD",
|
|
394
|
+
quoteMinor: TEST_ADAPTER_QUOTE_MINOR,
|
|
395
|
+
orders: [
|
|
396
|
+
order("ord-ready-a", "2026-10-15T18:00:00.000Z", "sku-ready", 2),
|
|
397
|
+
order("ord-ready-b", "2026-10-15T17:00:00.000Z", "sku-ready", 1),
|
|
398
|
+
order("ord-ready-pending", "2026-10-15T16:00:00.000Z", "sku-ready", 1, { purchaseScript: "pending" }),
|
|
399
|
+
order("ord-ready-unknown", "2026-10-15T15:30:00.000Z", "sku-ready", 1, { purchaseScript: "unknown" }),
|
|
400
|
+
order("ord-ready-signature", "2026-10-15T15:00:00.000Z", "sku-ready", 1, { signatureScript: "refuse" }),
|
|
401
|
+
order("ord-packed", "2026-10-15T14:00:00.000Z", "sku-pack", 1, { picked: true, packed: true }),
|
|
402
|
+
order("ord-labeled", "2026-10-15T13:00:00.000Z", "sku-label", 1, {
|
|
403
|
+
picked: true,
|
|
404
|
+
packed: true,
|
|
405
|
+
label: "purchased",
|
|
406
|
+
providerOutcome: "accepted",
|
|
407
|
+
adultSignature: "accepted",
|
|
408
|
+
purchaseKey: "key-labeled",
|
|
409
|
+
purchaseId: "test-key-labeled",
|
|
410
|
+
labelCostMinor: TEST_ADAPTER_QUOTE_MINOR,
|
|
411
|
+
}),
|
|
412
|
+
order("ord-void-pending", "2026-10-15T12:40:00.000Z", "sku-void", 1, {
|
|
413
|
+
picked: true,
|
|
414
|
+
packed: true,
|
|
415
|
+
label: "purchased",
|
|
416
|
+
providerOutcome: "accepted",
|
|
417
|
+
adultSignature: "accepted",
|
|
418
|
+
purchaseKey: "key-void-pending",
|
|
419
|
+
purchaseId: "test-key-void-pending",
|
|
420
|
+
labelCostMinor: TEST_ADAPTER_QUOTE_MINOR,
|
|
421
|
+
voidScript: "pending",
|
|
422
|
+
}),
|
|
423
|
+
order("ord-void-denied", "2026-10-15T12:20:00.000Z", "sku-void", 1, {
|
|
424
|
+
picked: true,
|
|
425
|
+
packed: true,
|
|
426
|
+
label: "purchased",
|
|
427
|
+
providerOutcome: "accepted",
|
|
428
|
+
adultSignature: "accepted",
|
|
429
|
+
purchaseKey: "key-void-denied",
|
|
430
|
+
purchaseId: "test-key-void-denied",
|
|
431
|
+
labelCostMinor: TEST_ADAPTER_QUOTE_MINOR,
|
|
432
|
+
voidScript: "denied",
|
|
433
|
+
}),
|
|
434
|
+
order("ord-intent", "2026-10-15T12:00:00.000Z", "sku-intent", 1, {
|
|
435
|
+
label: "intent",
|
|
436
|
+
providerOutcome: "intent",
|
|
437
|
+
purchaseKey: "key-intent",
|
|
438
|
+
}),
|
|
439
|
+
order("ord-unknown-label", "2026-10-15T11:30:00.000Z", "sku-unknown", 1, {
|
|
440
|
+
label: "UNKNOWN",
|
|
441
|
+
providerOutcome: "unknown",
|
|
442
|
+
purchaseKey: "key-unknown",
|
|
443
|
+
}),
|
|
444
|
+
order("ord-evidence", "2026-10-15T11:00:00.000Z", "sku-evidence", 1, {
|
|
445
|
+
payment: "authorized",
|
|
446
|
+
compliance: "UNKNOWN",
|
|
447
|
+
stock: "reserved",
|
|
448
|
+
fulfillment: "held",
|
|
449
|
+
evidence: STALE,
|
|
450
|
+
}),
|
|
451
|
+
order("ord-refund", "2026-10-15T10:00:00.000Z", "sku-refund", 1, { payment: "refund_requested" }),
|
|
452
|
+
order("ord-refund-unknown", "2026-10-15T09:30:00.000Z", "sku-refund-unknown", 1, { payment: "refund_unknown" }),
|
|
453
|
+
order("ord-signature", "2026-10-15T09:00:00.000Z", "sku-signature", 1, {
|
|
454
|
+
adultSignature: "refused",
|
|
455
|
+
signatureScript: "refuse",
|
|
456
|
+
}),
|
|
457
|
+
order("ord-cancelled", "2026-10-15T08:30:00.000Z", "sku-cancelled", 1, { fulfillment: "cancelled" }),
|
|
458
|
+
order("ord-shipped", "2026-10-15T08:00:00.000Z", "sku-shipped", 1, {
|
|
459
|
+
picked: true,
|
|
460
|
+
packed: true,
|
|
461
|
+
fulfillment: "handed_off",
|
|
462
|
+
label: "printed",
|
|
463
|
+
providerOutcome: "accepted",
|
|
464
|
+
adultSignature: "accepted",
|
|
465
|
+
purchaseKey: "key-shipped",
|
|
466
|
+
purchaseId: "test-key-shipped",
|
|
467
|
+
labelCostMinor: TEST_ADAPTER_QUOTE_MINOR,
|
|
468
|
+
trackingNumber: "TRK-ord-shipped",
|
|
469
|
+
trackingUrl: "https://tracking.example/label/ord-shipped",
|
|
470
|
+
}),
|
|
471
|
+
],
|
|
472
|
+
events: [],
|
|
473
|
+
log: [],
|
|
474
|
+
lastResults: [],
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function matchesQuery(order: WorkspaceOrder, q: string): boolean {
|
|
479
|
+
if (q === "") return true;
|
|
480
|
+
const needle = q.toLowerCase();
|
|
481
|
+
if (order.id.toLowerCase().includes(needle)) return true;
|
|
482
|
+
return order.lines.some((line) => line.sku.toLowerCase().includes(needle));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function projectOrder(state: WorkspaceState, order: WorkspaceOrder, actor: CommerceActor): WorkspaceOrderView {
|
|
486
|
+
const scope = { tenantId: state.tenantId, stage: state.stage };
|
|
487
|
+
const queue = workspaceQueue(order);
|
|
488
|
+
let trackingUrl: string | null = null;
|
|
489
|
+
if (order.trackingUrl !== null) {
|
|
490
|
+
const admitted = admitTrackingRead({
|
|
491
|
+
actor,
|
|
492
|
+
resource: scope,
|
|
493
|
+
trackingUrl: order.trackingUrl,
|
|
494
|
+
allowedHosts: TRACKING_HOSTS,
|
|
495
|
+
});
|
|
496
|
+
if (admitted.ok) trackingUrl = order.trackingUrl;
|
|
497
|
+
}
|
|
498
|
+
const address = readOwnerAddress(actor, scope, () => order.ownerDestination);
|
|
499
|
+
const printPayload =
|
|
500
|
+
address.ok && order.purchaseId !== null && (order.label === "purchased" || order.label === "printed")
|
|
501
|
+
? {
|
|
502
|
+
purchaseId: order.purchaseId,
|
|
503
|
+
format: "pdf" as const,
|
|
504
|
+
adultSignature: "required" as const,
|
|
505
|
+
livePurchase: false as const,
|
|
506
|
+
destination: address.address,
|
|
507
|
+
}
|
|
508
|
+
: null;
|
|
509
|
+
const slip: PackingSlip | null = order.slipIssued
|
|
510
|
+
? {
|
|
511
|
+
orderId: order.id,
|
|
512
|
+
lines: order.lines.map((line) => ({ sku: line.sku, quantity: line.quantity })),
|
|
513
|
+
wholeOrder: true,
|
|
514
|
+
destination: address.ok ? address.address : null,
|
|
515
|
+
}
|
|
516
|
+
: null;
|
|
517
|
+
return {
|
|
518
|
+
id: order.id,
|
|
519
|
+
placedAt: order.placedAt,
|
|
520
|
+
lines: order.lines.map((line) => ({ sku: line.sku, quantity: line.quantity })),
|
|
521
|
+
unitCount: unitCount(order),
|
|
522
|
+
currency: "USD",
|
|
523
|
+
payment: order.payment,
|
|
524
|
+
stock: order.stock,
|
|
525
|
+
fulfillment: order.fulfillment,
|
|
526
|
+
label: order.label,
|
|
527
|
+
providerOutcome: order.providerOutcome,
|
|
528
|
+
evidence: { ...order.evidence },
|
|
529
|
+
picked: order.picked,
|
|
530
|
+
packed: order.packed,
|
|
531
|
+
manualException: order.manualException,
|
|
532
|
+
adultSignature: order.adultSignature,
|
|
533
|
+
purchaseKey: order.purchaseKey,
|
|
534
|
+
purchaseId: order.purchaseId,
|
|
535
|
+
labelCostMinor: order.labelCostMinor,
|
|
536
|
+
trackingNumber: order.trackingNumber,
|
|
537
|
+
trackingUrl,
|
|
538
|
+
queue,
|
|
539
|
+
holdReasons: holdReasons(order),
|
|
540
|
+
results: order.results.map((result) => ({ ...result })),
|
|
541
|
+
slip,
|
|
542
|
+
printPayload,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function projectWorkspace(state: WorkspaceState, query: WorkspaceQuery): FulfillmentViewModel {
|
|
547
|
+
const actor = workspaceActor(state.tenantId, state.stage, query.actorName);
|
|
548
|
+
const storewide: Record<WorkspaceQueue, number> = { ready: 0, held: 0, in_progress: 0, shipped: 0 };
|
|
549
|
+
for (const order of state.orders) storewide[workspaceQueue(order)] += 1;
|
|
550
|
+
const filtered = state.orders.filter((order) => {
|
|
551
|
+
if (query.queue !== "all" && workspaceQueue(order) !== query.queue) return false;
|
|
552
|
+
return matchesQuery(order, query.q);
|
|
553
|
+
});
|
|
554
|
+
const page = paginateOrders(filtered, query.limit, query.cursor);
|
|
555
|
+
const pickList: FulfillmentViewModel["pickList"] = [];
|
|
556
|
+
for (const order of state.orders) {
|
|
557
|
+
if (!order.picked || order.fulfillment === "handed_off" || order.fulfillment === "cancelled") continue;
|
|
558
|
+
for (const line of order.lines) {
|
|
559
|
+
pickList.push({ orderId: order.id, sku: line.sku, quantity: line.quantity, packed: order.packed });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
targetTenant: state.tenantId,
|
|
564
|
+
stage: state.stage,
|
|
565
|
+
actorName: query.actorName,
|
|
566
|
+
actorRole: actor.role,
|
|
567
|
+
livePurchase: false,
|
|
568
|
+
carrierAccountConfigured: state.carrierAccountConfigured,
|
|
569
|
+
prohibitedCarriers: state.prohibitedCarriers,
|
|
570
|
+
adultSignatureRequired: true,
|
|
571
|
+
quoteMinor: state.quoteMinor,
|
|
572
|
+
currency: "USD",
|
|
573
|
+
queue: query.queue,
|
|
574
|
+
q: query.q,
|
|
575
|
+
limit: query.limit,
|
|
576
|
+
cursor: query.cursor,
|
|
577
|
+
nextCursor: page.nextCursor,
|
|
578
|
+
invalidCursor: page.invalidCursor,
|
|
579
|
+
storewide,
|
|
580
|
+
filterCount: filtered.length,
|
|
581
|
+
pageCount: page.orders.length,
|
|
582
|
+
orders: page.orders.map((order) => projectOrder(state, order, actor)),
|
|
583
|
+
pickList,
|
|
584
|
+
events: state.events.map((event) => ({ ...event, totals: { ...event.totals }, verificationDetail: { ...event.verificationDetail } })),
|
|
585
|
+
log: [...state.log],
|
|
586
|
+
lastResults: state.lastResults.map((result) => ({ ...result })),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function explicitCarrierReason(carrier: string | null): "usps-prohibited" | "carrier-not-selectable" | null {
|
|
591
|
+
if (carrier === null || carrier.trim() === "") return null;
|
|
592
|
+
if (carrier.trim().toUpperCase() === "USPS") return "usps-prohibited";
|
|
593
|
+
return "carrier-not-selectable";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function isException(value: string | null): value is WorkspaceException {
|
|
597
|
+
return WORKSPACE_EXCEPTIONS.some((item) => item === value);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function commerceStamp(order: WorkspaceOrder): string {
|
|
601
|
+
return [
|
|
602
|
+
order.payment,
|
|
603
|
+
order.compliance,
|
|
604
|
+
order.stock,
|
|
605
|
+
order.fulfillment,
|
|
606
|
+
order.label,
|
|
607
|
+
order.evidence.verification,
|
|
608
|
+
order.evidence.tax,
|
|
609
|
+
order.evidence.risk,
|
|
610
|
+
order.evidence.stock,
|
|
611
|
+
].join("|");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function emitIfChanged(state: WorkspaceState, order: WorkspaceOrder, before: string): void {
|
|
615
|
+
if (commerceStamp(order) === before) return;
|
|
616
|
+
const eventVersion = state.events.filter((event) => event.orderId === order.id).length + 1;
|
|
617
|
+
state.events.push(
|
|
618
|
+
publicCommerceEvent({
|
|
619
|
+
eventVersion,
|
|
620
|
+
orderId: order.id,
|
|
621
|
+
placedAt: order.placedAt,
|
|
622
|
+
currency: order.currency,
|
|
623
|
+
payment: order.payment,
|
|
624
|
+
compliance: order.compliance,
|
|
625
|
+
stock: order.stock,
|
|
626
|
+
fulfillment: order.fulfillment,
|
|
627
|
+
label: order.label,
|
|
628
|
+
totals: TOTALS,
|
|
629
|
+
verificationDetail: order.evidence,
|
|
630
|
+
}),
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function attemptKey(order: WorkspaceOrder, requested: string | null): string {
|
|
635
|
+
if (order.label === "voided") {
|
|
636
|
+
if (requested !== null && requested !== order.purchaseKey) return requested;
|
|
637
|
+
return `${order.purchaseKey ?? `pk-${order.id}`}-2`;
|
|
638
|
+
}
|
|
639
|
+
if (order.purchaseKey !== null) return order.purchaseKey;
|
|
640
|
+
if (requested !== null && requested.trim() !== "") return requested;
|
|
641
|
+
return `pk-${order.id}`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function applyToOrder(state: WorkspaceState, order: WorkspaceOrder, command: WorkspaceCommand): WorkspaceResult {
|
|
645
|
+
const before = commerceStamp(order);
|
|
646
|
+
const finish = (result: WorkspaceResult): WorkspaceResult => {
|
|
647
|
+
order.results.push(result);
|
|
648
|
+
emitIfChanged(state, order, before);
|
|
649
|
+
return result;
|
|
650
|
+
};
|
|
651
|
+
const refuse = (reason: string, extra: Partial<WorkspaceResult> = {}): WorkspaceResult =>
|
|
652
|
+
finish({ ...blankResult(command.command, order.id, "refused", reason), purchaseId: order.purchaseId, ...extra, livePurchase: false, restock: false });
|
|
653
|
+
|
|
654
|
+
if (command.quantityInvalid) return refuse("quantity");
|
|
655
|
+
if (command.quantity !== null && command.quantity !== unitCount(order)) return refuse("partial-shipment");
|
|
656
|
+
|
|
657
|
+
if (command.command === "quote-label") {
|
|
658
|
+
const carrierReason = explicitCarrierReason(command.carrier);
|
|
659
|
+
if (carrierReason) return refuse(carrierReason);
|
|
660
|
+
if (!state.carrierAccountConfigured) return refuse("carrier-account");
|
|
661
|
+
return finish({
|
|
662
|
+
...blankResult(command.command, order.id, "admitted", "quote"),
|
|
663
|
+
costMinor: state.quoteMinor,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (command.command === "purchase-label") {
|
|
668
|
+
const carrierReason = explicitCarrierReason(command.carrier);
|
|
669
|
+
if (carrierReason) return refuse(carrierReason);
|
|
670
|
+
if (!state.carrierAccountConfigured) return refuse("carrier-account");
|
|
671
|
+
const decision = decideCommerceCommand({
|
|
672
|
+
command: "purchase-label",
|
|
673
|
+
payment: order.payment,
|
|
674
|
+
stock: order.stock,
|
|
675
|
+
fulfillment: order.fulfillment,
|
|
676
|
+
label: order.label,
|
|
677
|
+
evidence: order.evidence,
|
|
678
|
+
providerOutcome: order.providerOutcome,
|
|
679
|
+
});
|
|
680
|
+
if (!decision.admit) return refuse(decision.reason);
|
|
681
|
+
if (order.adultSignature === "refused" || order.signatureScript === "refuse") {
|
|
682
|
+
order.adultSignature = "refused";
|
|
683
|
+
return refuse("adult-signature");
|
|
684
|
+
}
|
|
685
|
+
const key = attemptKey(order, command.purchaseKey);
|
|
686
|
+
order.purchaseKey = key;
|
|
687
|
+
if (order.purchaseScript === "pending") {
|
|
688
|
+
order.label = "intent";
|
|
689
|
+
order.providerOutcome = "intent";
|
|
690
|
+
return finish({ ...blankResult(command.command, order.id, "pending", "pending"), purchaseId: null });
|
|
691
|
+
}
|
|
692
|
+
if (order.purchaseScript === "unknown") {
|
|
693
|
+
order.label = "UNKNOWN";
|
|
694
|
+
order.providerOutcome = "unknown";
|
|
695
|
+
return finish({ ...blankResult(command.command, order.id, "unknown", "unknown"), purchaseId: null });
|
|
696
|
+
}
|
|
697
|
+
order.label = "purchased";
|
|
698
|
+
order.providerOutcome = "accepted";
|
|
699
|
+
order.adultSignature = "accepted";
|
|
700
|
+
order.purchaseId = `test-${key}`;
|
|
701
|
+
order.labelCostMinor = state.quoteMinor;
|
|
702
|
+
return finish({
|
|
703
|
+
...blankResult(command.command, order.id, "admitted", "purchased"),
|
|
704
|
+
purchaseId: order.purchaseId,
|
|
705
|
+
costMinor: state.quoteMinor,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (command.command === "print-label" || command.command === "reprint-label") {
|
|
710
|
+
const decision = decideCommerceCommand({
|
|
711
|
+
command: "print-label",
|
|
712
|
+
payment: order.payment,
|
|
713
|
+
stock: order.stock,
|
|
714
|
+
fulfillment: order.fulfillment,
|
|
715
|
+
label: order.label,
|
|
716
|
+
evidence: order.evidence,
|
|
717
|
+
providerOutcome: order.providerOutcome,
|
|
718
|
+
});
|
|
719
|
+
if (!decision.admit) return refuse(decision.reason);
|
|
720
|
+
const reason = command.command === "reprint-label" || order.label === "printed" ? "reprint" : "printed";
|
|
721
|
+
order.label = "printed";
|
|
722
|
+
return finish({ ...blankResult(command.command, order.id, "admitted", reason), purchaseId: order.purchaseId });
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (command.command === "void-label") {
|
|
726
|
+
if (order.fulfillment === "handed_off") return refuse("fulfillment");
|
|
727
|
+
if (
|
|
728
|
+
order.label === "UNKNOWN" ||
|
|
729
|
+
order.label === "intent" ||
|
|
730
|
+
order.label === "void_requested" ||
|
|
731
|
+
order.providerOutcome === "unknown" ||
|
|
732
|
+
order.providerOutcome === "intent"
|
|
733
|
+
) {
|
|
734
|
+
return refuse("reconcile");
|
|
735
|
+
}
|
|
736
|
+
if (order.label !== "purchased" && order.label !== "printed") return refuse("label");
|
|
737
|
+
if (order.voidScript === "pending") {
|
|
738
|
+
order.label = "void_requested";
|
|
739
|
+
return finish({ ...blankResult(command.command, order.id, "pending", "void-pending"), purchaseId: order.purchaseId });
|
|
740
|
+
}
|
|
741
|
+
if (order.voidScript === "denied") return refuse("void-denied");
|
|
742
|
+
order.label = "voided";
|
|
743
|
+
order.providerOutcome = "voided";
|
|
744
|
+
return finish({ ...blankResult(command.command, order.id, "admitted", "voided"), purchaseId: order.purchaseId });
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (command.command === "hand-off") {
|
|
748
|
+
if (!order.packed) return refuse("not-packed");
|
|
749
|
+
if (order.adultSignature !== "accepted") return refuse("adult-signature");
|
|
750
|
+
const decision = decideCommerceCommand({
|
|
751
|
+
command: "hand-off",
|
|
752
|
+
payment: order.payment,
|
|
753
|
+
stock: order.stock,
|
|
754
|
+
fulfillment: order.fulfillment,
|
|
755
|
+
label: order.label,
|
|
756
|
+
evidence: order.evidence,
|
|
757
|
+
providerOutcome: order.providerOutcome,
|
|
758
|
+
});
|
|
759
|
+
if (!decision.admit) return refuse(decision.reason);
|
|
760
|
+
order.fulfillment = "handed_off";
|
|
761
|
+
order.trackingNumber = `TRK-${order.id}`;
|
|
762
|
+
order.trackingUrl = `https://tracking.example/label/${encodeURIComponent(order.id)}`;
|
|
763
|
+
return finish({ ...blankResult(command.command, order.id, "admitted", "handed-off"), purchaseId: order.purchaseId });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (command.command === "cancel") {
|
|
767
|
+
const decision = decideCommerceCommand({
|
|
768
|
+
command: "cancel",
|
|
769
|
+
payment: order.payment,
|
|
770
|
+
stock: order.stock,
|
|
771
|
+
fulfillment: order.fulfillment,
|
|
772
|
+
label: order.label,
|
|
773
|
+
evidence: order.evidence,
|
|
774
|
+
providerOutcome: order.providerOutcome,
|
|
775
|
+
});
|
|
776
|
+
if (!decision.admit) return refuse(decision.reason);
|
|
777
|
+
order.fulfillment = "cancelled";
|
|
778
|
+
return finish({
|
|
779
|
+
...blankResult(command.command, order.id, "admitted", "cancelled"),
|
|
780
|
+
purchaseId: order.purchaseId,
|
|
781
|
+
releaseReservation: decision.releaseReservation,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (command.command === "pick-batch") {
|
|
786
|
+
if (workspaceQueue(order) !== "ready") return refuse("not-ready");
|
|
787
|
+
order.picked = true;
|
|
788
|
+
return finish(blankResult(command.command, order.id, "admitted", "picked"));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (command.command === "confirm-pack") {
|
|
792
|
+
if (order.fulfillment === "handed_off" || order.fulfillment === "cancelled") return refuse("fulfillment");
|
|
793
|
+
if (holdReasons(order).length > 0) return refuse("held");
|
|
794
|
+
if (!order.picked) return refuse("not-picked");
|
|
795
|
+
if (order.packed) return refuse("already-packed");
|
|
796
|
+
order.packed = true;
|
|
797
|
+
return finish(blankResult(command.command, order.id, "admitted", "packed"));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (command.command === "packing-slip") {
|
|
801
|
+
if (order.fulfillment === "cancelled") return refuse("fulfillment");
|
|
802
|
+
order.slipIssued = true;
|
|
803
|
+
return finish(blankResult(command.command, order.id, "admitted", "packing-slip"));
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (command.command === "record-exception") {
|
|
807
|
+
if (order.fulfillment === "handed_off") return refuse("fulfillment");
|
|
808
|
+
if (!isException(command.exception)) return refuse("exception");
|
|
809
|
+
order.manualException = command.exception;
|
|
810
|
+
return finish(blankResult(command.command, order.id, "admitted", command.exception));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (command.command === "refund" || command.command === "return") return refuse("returns-not-automated");
|
|
814
|
+
return refuse("unknown-command");
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function finishCommand(state: WorkspaceState, actorName: string, results: WorkspaceResult[]): WorkspaceState {
|
|
818
|
+
state.lastResults = results;
|
|
819
|
+
for (const result of results) {
|
|
820
|
+
state.log.push(`${actorName} ${result.command} ${result.orderId} ${result.status} ${result.reason}`);
|
|
821
|
+
}
|
|
822
|
+
return state;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export function applyWorkspaceCommand(input: WorkspaceState, command: WorkspaceCommand): WorkspaceState {
|
|
826
|
+
const state = structuredClone(input);
|
|
827
|
+
const actor = workspaceActor(state.tenantId, state.stage, command.actorName);
|
|
828
|
+
const scope = { tenantId: state.tenantId, stage: state.stage };
|
|
829
|
+
if (!actorMayCommand(actor, scope)) {
|
|
830
|
+
const ids = command.orderIds.length > 0 ? command.orderIds : [""];
|
|
831
|
+
return finishCommand(
|
|
832
|
+
state,
|
|
833
|
+
command.actorName,
|
|
834
|
+
ids.map((orderId) => blankResult(command.command, orderId, "refused", "denied")),
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
if (command.lineId) {
|
|
838
|
+
const ids = command.orderIds.length > 0 ? command.orderIds : [""];
|
|
839
|
+
return finishCommand(
|
|
840
|
+
state,
|
|
841
|
+
command.actorName,
|
|
842
|
+
ids.map((orderId) => blankResult(command.command, orderId, "refused", "partial-shipment")),
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (command.command === "set-carrier-account") {
|
|
846
|
+
const carrierReason = explicitCarrierReason(command.carrier);
|
|
847
|
+
if (carrierReason) return finishCommand(state, command.actorName, [blankResult(command.command, "", "refused", carrierReason)]);
|
|
848
|
+
if (command.accountConfigured === null) {
|
|
849
|
+
return finishCommand(state, command.actorName, [blankResult(command.command, "", "refused", "carrier-account")]);
|
|
850
|
+
}
|
|
851
|
+
state.carrierAccountConfigured = command.accountConfigured;
|
|
852
|
+
return finishCommand(state, command.actorName, [
|
|
853
|
+
blankResult(command.command, "", "admitted", command.accountConfigured ? "configured" : "missing"),
|
|
854
|
+
]);
|
|
855
|
+
}
|
|
856
|
+
if (command.orderIds.length === 0) {
|
|
857
|
+
return finishCommand(state, command.actorName, [blankResult(command.command, "", "refused", "empty-batch")]);
|
|
858
|
+
}
|
|
859
|
+
const results: WorkspaceResult[] = [];
|
|
860
|
+
const seen = new Set<string>();
|
|
861
|
+
for (const orderId of command.orderIds) {
|
|
862
|
+
if (seen.has(orderId)) continue;
|
|
863
|
+
seen.add(orderId);
|
|
864
|
+
const order = state.orders.find((item) => item.id === orderId);
|
|
865
|
+
if (order === undefined) {
|
|
866
|
+
results.push(blankResult(command.command, orderId, "refused", "missing"));
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
results.push(applyToOrder(state, order, command));
|
|
870
|
+
}
|
|
871
|
+
return finishCommand(state, command.actorName, results);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const COMMANDS = new Set([
|
|
875
|
+
"quote-label",
|
|
876
|
+
"purchase-label",
|
|
877
|
+
"print-label",
|
|
878
|
+
"reprint-label",
|
|
879
|
+
"void-label",
|
|
880
|
+
"hand-off",
|
|
881
|
+
"cancel",
|
|
882
|
+
"confirm-pack",
|
|
883
|
+
"pick-batch",
|
|
884
|
+
"packing-slip",
|
|
885
|
+
"record-exception",
|
|
886
|
+
"set-carrier-account",
|
|
887
|
+
"refund",
|
|
888
|
+
"return",
|
|
889
|
+
]);
|
|
890
|
+
|
|
891
|
+
export function parseWorkspaceQuery(params: URLSearchParams): WorkspaceQuery {
|
|
892
|
+
const queueRaw = params.get("queue");
|
|
893
|
+
const queue: WorkspaceQueueFilter =
|
|
894
|
+
queueRaw === "all" || queueRaw === "ready" || queueRaw === "held" || queueRaw === "in_progress" || queueRaw === "shipped"
|
|
895
|
+
? queueRaw
|
|
896
|
+
: "ready";
|
|
897
|
+
const limitRaw = params.get("limit");
|
|
898
|
+
let limit = WORKSPACE_PAGE_LIMIT;
|
|
899
|
+
if (limitRaw !== null && /^\d+$/.test(limitRaw)) {
|
|
900
|
+
const parsed = Number(limitRaw);
|
|
901
|
+
if (parsed >= 1 && parsed <= 50) limit = parsed;
|
|
902
|
+
}
|
|
903
|
+
const cursorRaw = params.get("cursor");
|
|
904
|
+
return {
|
|
905
|
+
actorName: (params.get("actor") ?? "owner").trim() || "owner",
|
|
906
|
+
queue,
|
|
907
|
+
q: (params.get("q") ?? "").trim(),
|
|
908
|
+
cursor: cursorRaw !== null && cursorRaw.trim() !== "" ? cursorRaw : null,
|
|
909
|
+
limit,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
export function parseWorkspaceCommand(params: URLSearchParams): { ok: true; command: WorkspaceCommand } | { ok: false; error: string } {
|
|
914
|
+
const command = (params.get("command") ?? "").trim();
|
|
915
|
+
if (!COMMANDS.has(command)) return { ok: false, error: "unknown-command" };
|
|
916
|
+
let quantity: number | null = null;
|
|
917
|
+
let quantityInvalid = false;
|
|
918
|
+
if (params.has("quantity")) {
|
|
919
|
+
const raw = (params.get("quantity") ?? "").trim();
|
|
920
|
+
if (raw !== "") {
|
|
921
|
+
if (!/^\d+$/.test(raw)) quantityInvalid = true;
|
|
922
|
+
else quantity = Number(raw);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const configuredRaw = params.get("configured");
|
|
926
|
+
const accountConfigured = configuredRaw === "1" ? true : configuredRaw === "0" ? false : null;
|
|
927
|
+
const carrierRaw = params.get("carrier");
|
|
928
|
+
const purchaseRaw = params.get("purchaseKey");
|
|
929
|
+
return {
|
|
930
|
+
ok: true,
|
|
931
|
+
command: {
|
|
932
|
+
actorName: (params.get("actor") ?? "owner").trim() || "owner",
|
|
933
|
+
command,
|
|
934
|
+
orderIds: params.getAll("orderId").map((id) => id.trim()).filter((id) => id !== ""),
|
|
935
|
+
purchaseKey: purchaseRaw !== null && purchaseRaw.trim() !== "" ? purchaseRaw.trim() : null,
|
|
936
|
+
carrier: carrierRaw !== null && carrierRaw.trim() !== "" ? carrierRaw : null,
|
|
937
|
+
quantity,
|
|
938
|
+
quantityInvalid,
|
|
939
|
+
lineId: params.has("lineId"),
|
|
940
|
+
exception: params.get("exception"),
|
|
941
|
+
accountConfigured,
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
export function handleWorkspaceRequest(
|
|
947
|
+
state: WorkspaceState,
|
|
948
|
+
request: WorkspaceHttpRequest,
|
|
949
|
+
): { state: WorkspaceState; response: WorkspaceHttpResponse } {
|
|
950
|
+
let url: URL;
|
|
951
|
+
try {
|
|
952
|
+
url = new URL(request.url, "http://127.0.0.1");
|
|
953
|
+
} catch {
|
|
954
|
+
return { state, response: { status: 400, contentType: "text/plain; charset=utf-8", body: "bad url" } };
|
|
955
|
+
}
|
|
956
|
+
if (url.pathname !== SCRAPPY_FULFILLMENT_PATH) {
|
|
957
|
+
return { state, response: { status: 404, contentType: "text/plain; charset=utf-8", body: "not found" } };
|
|
958
|
+
}
|
|
959
|
+
const method = request.method.toUpperCase();
|
|
960
|
+
if (method === "GET") {
|
|
961
|
+
const view = projectWorkspace(state, parseWorkspaceQuery(url.searchParams));
|
|
962
|
+
return {
|
|
963
|
+
state,
|
|
964
|
+
response: { status: 200, contentType: "text/html; charset=utf-8", body: renderFulfillmentDocument(view) },
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
if (method === "POST") {
|
|
968
|
+
const params = new URLSearchParams(request.body ?? "");
|
|
969
|
+
const parsed = parseWorkspaceCommand(params);
|
|
970
|
+
if (!parsed.ok) {
|
|
971
|
+
return {
|
|
972
|
+
state,
|
|
973
|
+
response: {
|
|
974
|
+
status: 400,
|
|
975
|
+
contentType: "text/html; charset=utf-8",
|
|
976
|
+
body: `<!doctype html><p data-command-error="${esc(parsed.error)}">Unknown command.</p>`,
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
const next = applyWorkspaceCommand(state, parsed.command);
|
|
981
|
+
const query = parseWorkspaceQuery(params);
|
|
982
|
+
query.cursor = null;
|
|
983
|
+
const view = projectWorkspace(next, query);
|
|
984
|
+
return {
|
|
985
|
+
state: next,
|
|
986
|
+
response: { status: 200, contentType: "text/html; charset=utf-8", body: renderFulfillmentDocument(view) },
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
return { state, response: { status: 405, contentType: "text/plain; charset=utf-8", body: "method" } };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
export function createTestFulfillmentAdapter(tenantId = "synthetic-tenant", stage: CommerceStage = "preview"): TestFulfillmentAdapter {
|
|
993
|
+
let state = initialWorkspaceState(tenantId, stage);
|
|
994
|
+
return {
|
|
995
|
+
livePurchase: false,
|
|
996
|
+
state: () => state,
|
|
997
|
+
handle(request) {
|
|
998
|
+
const outcome = handleWorkspaceRequest(state, request);
|
|
999
|
+
state = outcome.state;
|
|
1000
|
+
return outcome.response;
|
|
1001
|
+
},
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function esc(value: string): string {
|
|
1006
|
+
return value
|
|
1007
|
+
.replace(/&/g, "&")
|
|
1008
|
+
.replace(/</g, "<")
|
|
1009
|
+
.replace(/>/g, ">")
|
|
1010
|
+
.replace(/"/g, """);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function href(view: FulfillmentViewModel, over: Partial<WorkspaceQuery> = {}): string {
|
|
1014
|
+
const next: WorkspaceQuery = {
|
|
1015
|
+
actorName: over.actorName ?? view.actorName,
|
|
1016
|
+
queue: over.queue ?? view.queue,
|
|
1017
|
+
q: over.q ?? view.q,
|
|
1018
|
+
limit: over.limit ?? view.limit,
|
|
1019
|
+
cursor: "cursor" in over ? (over.cursor ?? null) : view.cursor,
|
|
1020
|
+
};
|
|
1021
|
+
const params = new URLSearchParams();
|
|
1022
|
+
params.set("actor", next.actorName);
|
|
1023
|
+
params.set("queue", next.queue);
|
|
1024
|
+
params.set("limit", String(next.limit));
|
|
1025
|
+
if (next.q !== "") params.set("q", next.q);
|
|
1026
|
+
if (next.cursor !== null) params.set("cursor", next.cursor);
|
|
1027
|
+
return `${SCRAPPY_FULFILLMENT_PATH}?${params.toString()}`;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function hiddenFields(view: FulfillmentViewModel, command: string, extras: Array<[string, string]> = []): string {
|
|
1031
|
+
const fields: Array<[string, string]> = [
|
|
1032
|
+
["actor", view.actorName],
|
|
1033
|
+
["queue", view.queue],
|
|
1034
|
+
["q", view.q],
|
|
1035
|
+
["limit", String(view.limit)],
|
|
1036
|
+
["command", command],
|
|
1037
|
+
...extras,
|
|
1038
|
+
];
|
|
1039
|
+
return fields
|
|
1040
|
+
.map(([name, value]) => `<input type="hidden" name="${esc(name)}" value="${esc(value)}">`)
|
|
1041
|
+
.join("");
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function resultItem(result: WorkspaceResult): string {
|
|
1045
|
+
return `<li data-result-order="${esc(result.orderId)}" data-result-command="${esc(result.command)}" data-result-status="${result.status}" data-result-reason="${esc(result.reason)}" data-cost-minor="${result.costMinor ?? ""}" data-purchase-id="${esc(result.purchaseId ?? "")}" data-live-purchase="false" data-release-reservation="${result.releaseReservation ? "true" : "false"}" data-restock="false" data-blind-repurchase="false">${esc(result.command)} ${esc(result.orderId)} ${result.status} ${esc(result.reason)}</li>`;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function orderActions(view: FulfillmentViewModel, order: WorkspaceOrderView, action: string): string {
|
|
1049
|
+
const purchaseExtras: Array<[string, string]> = [["orderId", order.id]];
|
|
1050
|
+
if (order.purchaseKey !== null) purchaseExtras.push(["purchaseKey", order.purchaseKey]);
|
|
1051
|
+
const one = (command: string, label: string, extras: Array<[string, string]> = [["orderId", order.id]]) =>
|
|
1052
|
+
`<form method="post" action="${action}">${hiddenFields(view, command, extras)}<button type="submit">${esc(label)}</button></form>`;
|
|
1053
|
+
const exceptionOptions = WORKSPACE_EXCEPTIONS.map(
|
|
1054
|
+
(reason) => `<option value="${reason}">${esc(reason)}</option>`,
|
|
1055
|
+
).join("");
|
|
1056
|
+
return `<div data-order-actions="${esc(order.id)}">
|
|
1057
|
+
${one("quote-label", "Quote cost")}
|
|
1058
|
+
${one("purchase-label", "Purchase label", purchaseExtras)}
|
|
1059
|
+
${one("print-label", "Print label")}
|
|
1060
|
+
${one("reprint-label", "Reprint label")}
|
|
1061
|
+
${one("void-label", "Void label")}
|
|
1062
|
+
${one("hand-off", "Hand off")}
|
|
1063
|
+
${one("confirm-pack", "Confirm pack")}
|
|
1064
|
+
${one("packing-slip", "Packing slip")}
|
|
1065
|
+
${one("cancel", "Cancel order")}
|
|
1066
|
+
<form method="post" action="${action}">${hiddenFields(view, "record-exception", [["orderId", order.id]])}<label>Manual exception <select name="exception">${exceptionOptions}</select></label><button type="submit">Record manual exception</button></form>
|
|
1067
|
+
</div>`;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
export function renderFulfillmentWorkspace(view: FulfillmentViewModel): string {
|
|
1071
|
+
const queueLinks = (["ready", "held", "in_progress", "shipped", "all"] as const)
|
|
1072
|
+
.map((queue) => {
|
|
1073
|
+
const count = queue === "all" ? "" : ` (${view.storewide[queue]})`;
|
|
1074
|
+
const label = queue === "all" ? "All" : QUEUE_LABEL[queue];
|
|
1075
|
+
return `<a data-queue-link="${queue}" href="${esc(href(view, { queue, cursor: null }))}">${esc(label)}${count}</a>`;
|
|
1076
|
+
})
|
|
1077
|
+
.join("");
|
|
1078
|
+
const actorLinks = ACTOR_SPECS.map(
|
|
1079
|
+
(spec) =>
|
|
1080
|
+
`<a data-actor-link="${spec.name}" href="${esc(href(view, { actorName: spec.name, cursor: null }))}">${esc(spec.name)}</a>`,
|
|
1081
|
+
).join("");
|
|
1082
|
+
const action = esc(href(view, { cursor: null }));
|
|
1083
|
+
const rows = view.orders
|
|
1084
|
+
.map((order) => {
|
|
1085
|
+
const pick = order.queue === "ready"
|
|
1086
|
+
? `<input form="scrappy-pick" type="checkbox" name="orderId" value="${esc(order.id)}" data-pick-box="${esc(order.id)}">`
|
|
1087
|
+
: "";
|
|
1088
|
+
const evidence = `verification ${order.evidence.verification}, tax ${order.evidence.tax}, risk ${order.evidence.risk}, stock ${order.evidence.stock}`;
|
|
1089
|
+
return `<tr data-order-row="${esc(order.id)}" data-queue="${order.queue}" data-label="${esc(order.label)}" data-payment="${esc(order.payment)}" data-fulfillment="${esc(order.fulfillment)}" data-packed="${order.packed ? "true" : "false"}" data-picked="${order.picked ? "true" : "false"}" data-adult-signature="${order.adultSignature}" data-purchase-id="${esc(order.purchaseId ?? "")}" data-purchase-key="${esc(order.purchaseKey ?? "")}" data-whole-order="true">
|
|
1090
|
+
<td>${pick}</td>
|
|
1091
|
+
<td>${esc(order.id)}</td>
|
|
1092
|
+
<td>${esc(QUEUE_LABEL[order.queue])}</td>
|
|
1093
|
+
<td>${order.lines.map((line) => `${esc(line.sku)} × ${line.quantity}`).join(", ")}</td>
|
|
1094
|
+
<td>${esc(order.label)}</td>
|
|
1095
|
+
<td>${esc(evidence)}</td>
|
|
1096
|
+
<td data-hold="${esc(order.holdReasons.join(" "))}">${order.holdReasons.length === 0 ? "None" : esc(order.holdReasons.join(" "))}</td>
|
|
1097
|
+
<td>${esc(order.trackingNumber ?? "")}</td>
|
|
1098
|
+
</tr>`;
|
|
1099
|
+
})
|
|
1100
|
+
.join("");
|
|
1101
|
+
const details = view.orders
|
|
1102
|
+
.map((order) => {
|
|
1103
|
+
const payload = order.printPayload
|
|
1104
|
+
? `<pre data-print-payload data-order-id="${esc(order.id)}">${esc(JSON.stringify(order.printPayload))}</pre>`
|
|
1105
|
+
: order.label === "purchased" || order.label === "printed"
|
|
1106
|
+
? `<p data-print-denied="owner-only" data-order-id="${esc(order.id)}">Print payload is owner-only.</p>`
|
|
1107
|
+
: "";
|
|
1108
|
+
const slip = order.slip
|
|
1109
|
+
? `<div data-packing-slip data-order-id="${esc(order.id)}" data-whole-order="true"><ul>${order.slip.lines
|
|
1110
|
+
.map((line) => `<li data-slip-sku="${esc(line.sku)}" data-slip-quantity="${line.quantity}">${esc(line.sku)} ${line.quantity}</li>`)
|
|
1111
|
+
.join("")}</ul>${
|
|
1112
|
+
order.slip.destination
|
|
1113
|
+
? `<p data-slip-destination>${esc(order.slip.destination)}</p>`
|
|
1114
|
+
: `<p data-slip-destination-denied="owner-only">Destination is owner-only.</p>`
|
|
1115
|
+
}</div>`
|
|
1116
|
+
: "";
|
|
1117
|
+
const tracking = order.trackingNumber
|
|
1118
|
+
? `<p data-tracking-number data-order-id="${esc(order.id)}">${esc(order.trackingNumber)}</p>${
|
|
1119
|
+
order.trackingUrl
|
|
1120
|
+
? `<a data-tracking-url data-order-id="${esc(order.id)}" href="${esc(order.trackingUrl)}" referrerpolicy="no-referrer">Tracking</a>`
|
|
1121
|
+
: `<p data-tracking-denied="owner-only" data-order-id="${esc(order.id)}">Tracking link is owner-only.</p>`
|
|
1122
|
+
}`
|
|
1123
|
+
: "";
|
|
1124
|
+
return `<section data-order-detail="${esc(order.id)}">${payload}${slip}${tracking}${orderActions(view, order, action)}</section>`;
|
|
1125
|
+
})
|
|
1126
|
+
.join("");
|
|
1127
|
+
const pickItems = view.pickList
|
|
1128
|
+
.map(
|
|
1129
|
+
(line) =>
|
|
1130
|
+
`<li data-pick-order="${esc(line.orderId)}" data-sku="${esc(line.sku)}" data-quantity="${line.quantity}" data-packed="${line.packed ? "true" : "false"}">${esc(line.orderId)} ${esc(line.sku)} ${line.quantity}</li>`,
|
|
1131
|
+
)
|
|
1132
|
+
.join("");
|
|
1133
|
+
const next = view.nextCursor
|
|
1134
|
+
? `<a data-page-next href="${esc(href(view, { cursor: view.nextCursor }))}">Next page</a>`
|
|
1135
|
+
: "";
|
|
1136
|
+
const account = view.carrierAccountConfigured ? "configured" : "missing";
|
|
1137
|
+
return `<div data-scrappy-batch data-target-tenant="${esc(view.targetTenant)}" data-test-adapter="1" data-live-purchase="false" data-carrier-account="${account}" data-prohibited-carrier="USPS" data-adult-signature-required="true" data-partial-shipment="unavailable" data-returns="unavailable" data-blind-repurchase="false" data-actor="${esc(view.actorName)}" data-actor-role="${esc(view.actorRole)}" data-storewide-ready="${view.storewide.ready}" data-storewide-held="${view.storewide.held}" data-storewide-in-progress="${view.storewide.in_progress}" data-storewide-shipped="${view.storewide.shipped}" data-filter-count="${view.filterCount}" data-page-count="${view.pageCount}" data-queue="${esc(view.queue)}">
|
|
1138
|
+
<h2 id="fulfillment-title">Daily batch</h2>
|
|
1139
|
+
<p>This desk reads the test adapter. Not a live label purchase. Carrier account is configuration. USPS is prohibited. Adult signature is required. Shipment is the whole order. Partial shipment is not available. Returns are not automated.</p>
|
|
1140
|
+
<p data-carrier-account-text>${view.carrierAccountConfigured ? "Carrier account: configured." : "Carrier account: not configured. Do not choose a carrier."}</p>
|
|
1141
|
+
<p><a data-refresh href="${esc(href(view))}">Refresh</a></p>
|
|
1142
|
+
<nav data-actor-switch>${actorLinks}</nav>
|
|
1143
|
+
<nav data-queue-switch>${queueLinks}</nav>
|
|
1144
|
+
<form method="get" action="${SCRAPPY_FULFILLMENT_PATH}">
|
|
1145
|
+
<input type="hidden" name="actor" value="${esc(view.actorName)}">
|
|
1146
|
+
<input type="hidden" name="limit" value="${view.limit}">
|
|
1147
|
+
<label>Queue <select name="queue">
|
|
1148
|
+
<option value="ready"${view.queue === "ready" ? " selected" : ""}>Ready</option>
|
|
1149
|
+
<option value="held"${view.queue === "held" ? " selected" : ""}>Held</option>
|
|
1150
|
+
<option value="in_progress"${view.queue === "in_progress" ? " selected" : ""}>In progress</option>
|
|
1151
|
+
<option value="shipped"${view.queue === "shipped" ? " selected" : ""}>Shipped</option>
|
|
1152
|
+
<option value="all"${view.queue === "all" ? " selected" : ""}>All</option>
|
|
1153
|
+
</select></label>
|
|
1154
|
+
<label>Filter <input name="q" value="${esc(view.q)}"></label>
|
|
1155
|
+
<button type="submit">Apply filter</button>
|
|
1156
|
+
</form>
|
|
1157
|
+
<p data-counts>Storewide ready ${view.storewide.ready}. Held ${view.storewide.held}. In progress ${view.storewide.in_progress}. Shipped ${view.storewide.shipped}. Filter ${view.filterCount}. This page ${view.pageCount}.</p>
|
|
1158
|
+
${view.invalidCursor ? `<p data-invalid-cursor="1">Unknown page cursor. No rows on this page.</p>` : ""}
|
|
1159
|
+
<form id="scrappy-pick" method="post" action="${action}">${hiddenFields(view, "pick-batch")}<button type="submit">Build pick list</button></form>
|
|
1160
|
+
<table>
|
|
1161
|
+
<thead><tr><th>Pick</th><th>Order</th><th>Queue</th><th>Lines</th><th>Label</th><th>Evidence</th><th>Hold</th><th>Tracking number</th></tr></thead>
|
|
1162
|
+
<tbody>${rows}</tbody>
|
|
1163
|
+
</table>
|
|
1164
|
+
${next}
|
|
1165
|
+
<section data-pick-list><h3>Pick list</h3><ul>${pickItems}</ul></section>
|
|
1166
|
+
${details}
|
|
1167
|
+
<ol data-last-results>${view.lastResults.map(resultItem).join("")}</ol>
|
|
1168
|
+
<section data-public-events>${view.events.map((event) => `<pre>${esc(JSON.stringify(event))}</pre>`).join("")}</section>
|
|
1169
|
+
<pre data-adapter-log>${esc(view.log.join("\n"))}</pre>
|
|
1170
|
+
</div>`;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export function renderFulfillmentDocument(view: FulfillmentViewModel): string {
|
|
1174
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Daily batch</title></head><body><section id="fulfillment" data-admin-panel data-agent-scope="fulfillment">${renderFulfillmentWorkspace(view)}</section></body></html>`;
|
|
49
1175
|
}
|