@tokenoftrust/storefront-runner 2.4.54 → 2.4.56

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.
@@ -0,0 +1,1654 @@
1
+ /**
2
+ * Whole-order fulfillment commands.
3
+ *
4
+ * `decideCommerceCommand` is the commerce decision core. This service stores the
5
+ * result. It does not call a carrier, does not select a vendor, and does not
6
+ * read the admin fulfillment mock. Label purchase stays an intent row for a
7
+ * separate adapter. UI fields are not source truth.
8
+ */
9
+ import type { APIContext } from "astro";
10
+ import {
11
+ COMPLIANCE_STATES,
12
+ EVIDENCE_STATES,
13
+ FULFILLMENT_STATES,
14
+ LABEL_STATES,
15
+ PAYMENT_STATES,
16
+ STOCK_STATES,
17
+ actorMayCommand,
18
+ decideCommerceCommand,
19
+ publicCommerceEvent,
20
+ readOwnerAddress,
21
+ totalsMatch,
22
+ type CommerceActor,
23
+ type CommerceEvidence,
24
+ type CommerceStage,
25
+ type ComplianceState,
26
+ type FulfillmentState,
27
+ type LabelState,
28
+ type MinorTotals,
29
+ type PaymentState,
30
+ type ProviderOutcome,
31
+ type StockState,
32
+ } from "@tot/public-runtime";
33
+ import { readD1 } from "@/lib/env";
34
+ import { decideAdminApiAccess } from "@/lib/auth/adminApiGuard";
35
+ import { readViewerSession } from "@/lib/auth/route";
36
+ import type { SessionRecord } from "@/lib/auth/session";
37
+ import {
38
+ canRelease,
39
+ carrierPolicyForTenant,
40
+ carrierSelectionRefusal,
41
+ handoffRefusal,
42
+ releaseHolds,
43
+ type CarrierPolicy,
44
+ type ReadinessInput,
45
+ type ReleaseHold,
46
+ } from "./readiness.js";
47
+ import {
48
+ fulfillmentDbFromD1,
49
+ type FulfillmentDatabase,
50
+ type SqlStatement,
51
+ } from "./store.js";
52
+
53
+ export const OPAQUE_ID = /^[A-Za-z0-9._:-]{1,128}$/;
54
+
55
+ export function isOpaqueId(value: string): boolean {
56
+ return OPAQUE_ID.test(value) && !value.includes("@");
57
+ }
58
+
59
+ export interface FulfillmentActor extends CommerceActor {
60
+ actorId: string;
61
+ }
62
+
63
+ export interface SourceOrderSnapshot {
64
+ tenantId: string;
65
+ stage: CommerceStage;
66
+ orderId: string;
67
+ source: string;
68
+ sourceOrderId: string;
69
+ currency: string;
70
+ placedAt: string;
71
+ payment: PaymentState;
72
+ compliance: ComplianceState;
73
+ stock: StockState;
74
+ fulfillment: FulfillmentState;
75
+ label: LabelState;
76
+ amountMinor: number;
77
+ totals: MinorTotals;
78
+ evidence: CommerceEvidence;
79
+ lines: readonly { sku: string; quantity: number }[];
80
+ address: Record<string, unknown> | null;
81
+ }
82
+
83
+ export interface PackingSlip {
84
+ orderId: string;
85
+ currency: string;
86
+ lines: { sku: string; quantity: number }[];
87
+ adultSignatureRequired: boolean;
88
+ prohibitedCarriers: string[];
89
+ }
90
+
91
+ export type BatchPhase = "claimed" | "picked" | "packed";
92
+
93
+ export interface ShipmentIntentView {
94
+ scope: "whole-order";
95
+ purchaseId: string | null;
96
+ labelPurchaseState: string;
97
+ handoffState: string;
98
+ vendorSelected: false;
99
+ carrierAccountId: string | null;
100
+ adultSignatureRequired: boolean;
101
+ prohibitedCarriers: string[];
102
+ }
103
+
104
+ export interface FulfillmentView {
105
+ orderId: string;
106
+ tenantId: string;
107
+ stage: CommerceStage;
108
+ eventVersion: number;
109
+ payment: PaymentState;
110
+ compliance: ComplianceState;
111
+ stock: StockState;
112
+ fulfillment: FulfillmentState;
113
+ label: LabelState;
114
+ holds: ReleaseHold[];
115
+ canRelease: boolean;
116
+ canHandOff: boolean;
117
+ batch: { batchId: string; phase: BatchPhase } | null;
118
+ shipment: ShipmentIntentView | null;
119
+ packingSlip: PackingSlip | null;
120
+ }
121
+
122
+ export interface FulfillmentBatch {
123
+ batchId: string;
124
+ orders: { orderId: string; phase: BatchPhase }[];
125
+ }
126
+
127
+ export interface FulfillmentCommandOptions {
128
+ expectedVersion?: number;
129
+ scope?: string;
130
+ carrier?: string;
131
+ lines?: unknown;
132
+ }
133
+
134
+ export interface LabelOutcomeInput {
135
+ outcome: "purchased" | "unknown" | "voided";
136
+ adultSignature: "accepted" | "refused" | "absent";
137
+ carrier?: string;
138
+ expectedVersion?: number;
139
+ }
140
+
141
+ export type FulfillmentResult<T> =
142
+ | { ok: true; value: T }
143
+ | { ok: false; code: string; holds?: readonly ReleaseHold[] };
144
+
145
+ interface LoadedOrder {
146
+ order: {
147
+ tenantId: string;
148
+ stage: CommerceStage;
149
+ orderId: string;
150
+ source: string;
151
+ sourceOrderId: string;
152
+ currency: string;
153
+ placedAt: string;
154
+ payment: PaymentState;
155
+ compliance: ComplianceState;
156
+ stock: StockState;
157
+ fulfillment: FulfillmentState;
158
+ label: LabelState;
159
+ amountMinor: number;
160
+ eventVersion: number;
161
+ };
162
+ evidence: CommerceEvidence;
163
+ lines: { sku: string; quantity: number }[];
164
+ totals: MinorTotals | null;
165
+ batch: { batchId: string; phase: BatchPhase } | null;
166
+ operatorHold: boolean;
167
+ cancelPending: boolean;
168
+ packingSlip: PackingSlip | null;
169
+ shipment: { labelPurchaseState: string; handoffState: string } | null;
170
+ provider: { purchaseId: string; outcome: ProviderOutcome } | null;
171
+ labelOutcome: { adultSignature: "accepted" | "refused" | "absent" } | null;
172
+ carrierAccountId: string | null;
173
+ prohibitedCarriers: string[];
174
+ adultSignatureRequired: boolean;
175
+ auditCount: number;
176
+ view: FulfillmentView;
177
+ }
178
+
179
+ type CommitPlan =
180
+ | { type: "done"; view: FulfillmentView }
181
+ | { type: "refuse"; code: string; holds?: readonly ReleaseHold[] }
182
+ | { type: "write"; statements: SqlStatement[]; minimumChanges: number };
183
+
184
+ const FAIL_CLOSED_EVIDENCE: CommerceEvidence = {
185
+ verification: "unreadable",
186
+ tax: "unreadable",
187
+ risk: "unreadable",
188
+ stock: "unreadable",
189
+ };
190
+
191
+ const LABEL_RANK: Record<LabelState, number> = {
192
+ UNKNOWN: 0,
193
+ none: 1,
194
+ voided: 1,
195
+ intent: 2,
196
+ void_requested: 3,
197
+ purchased: 4,
198
+ printed: 5,
199
+ };
200
+
201
+ const FORBIDDEN_BODY_KEYS = new Set([
202
+ "address",
203
+ "email",
204
+ "phone",
205
+ "line1",
206
+ "line2",
207
+ "postalCode",
208
+ "name",
209
+ "customer",
210
+ ]);
211
+
212
+ function refuse(code: string, holds?: readonly ReleaseHold[]): FulfillmentResult<never> {
213
+ return holds && holds.length > 0 ? { ok: false, code, holds } : { ok: false, code };
214
+ }
215
+
216
+ function ok<T>(value: T): FulfillmentResult<T> {
217
+ return { ok: true, value };
218
+ }
219
+
220
+ function isRecord(value: unknown): value is Record<string, unknown> {
221
+ return typeof value === "object" && value !== null && !Array.isArray(value);
222
+ }
223
+
224
+ function oneOf<T extends string>(value: unknown, allowed: readonly T[]): value is T {
225
+ return typeof value === "string" && (allowed as readonly string[]).includes(value);
226
+ }
227
+
228
+ function phaseRank(phase: BatchPhase | null): number {
229
+ if (phase === "packed") return 3;
230
+ if (phase === "picked") return 2;
231
+ if (phase === "claimed") return 1;
232
+ return 0;
233
+ }
234
+
235
+ function stamp(now: string, n: number): string {
236
+ return `${now}#${String(n).padStart(6, "0")}`;
237
+ }
238
+
239
+ function jsonText(value: unknown): string {
240
+ return JSON.stringify(value);
241
+ }
242
+
243
+ function containsForbiddenKey(value: unknown): boolean {
244
+ if (!isRecord(value) && !Array.isArray(value)) return false;
245
+ for (const [key, child] of Object.entries(value)) {
246
+ if (FORBIDDEN_BODY_KEYS.has(key) || containsForbiddenKey(child)) return true;
247
+ }
248
+ return false;
249
+ }
250
+
251
+ async function digestHex(content: string): Promise<string> {
252
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content));
253
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
254
+ }
255
+
256
+ async function purchaseIdFor(tenantId: string, stage: CommerceStage, orderId: string): Promise<string> {
257
+ const digest = await digestHex(`scrappy-label-intent-v1\n${tenantId}\n${stage}\n${orderId}`);
258
+ return `label_${digest.slice(0, 32)}`;
259
+ }
260
+
261
+ function observedEvent(input: {
262
+ eventVersion: number;
263
+ orderId: string;
264
+ placedAt: string;
265
+ currency: string;
266
+ payment: PaymentState;
267
+ compliance: ComplianceState;
268
+ stock: StockState;
269
+ fulfillment: FulfillmentState;
270
+ label: LabelState;
271
+ totals: MinorTotals;
272
+ evidence: CommerceEvidence;
273
+ address: unknown;
274
+ }): string {
275
+ return jsonText(
276
+ publicCommerceEvent({
277
+ eventVersion: input.eventVersion,
278
+ orderId: input.orderId,
279
+ placedAt: input.placedAt,
280
+ currency: input.currency,
281
+ payment: input.payment,
282
+ compliance: input.compliance,
283
+ stock: input.stock,
284
+ fulfillment: input.fulfillment,
285
+ label: input.label,
286
+ totals: input.totals,
287
+ verificationDetail: input.evidence,
288
+ address: input.address,
289
+ }),
290
+ );
291
+ }
292
+
293
+ function mergePayment(local: PaymentState | null, source: PaymentState): PaymentState {
294
+ if (local === "refund_requested" && (source === "captured" || source === "authorized" || source === "pending")) {
295
+ return "refund_requested";
296
+ }
297
+ return source;
298
+ }
299
+
300
+ function mergeLabel(local: LabelState | null, source: LabelState): LabelState {
301
+ if (!local) return source;
302
+ return LABEL_RANK[local] >= LABEL_RANK[source] ? local : source;
303
+ }
304
+
305
+ function parseEvidence(value: unknown): CommerceEvidence | null {
306
+ if (!isRecord(value)) return null;
307
+ const verification = value.verification;
308
+ const tax = value.tax;
309
+ const risk = value.risk;
310
+ const stock = value.stock;
311
+ if (!oneOf(verification, EVIDENCE_STATES) || !oneOf(tax, EVIDENCE_STATES) || !oneOf(risk, EVIDENCE_STATES) || !oneOf(stock, EVIDENCE_STATES)) {
312
+ return null;
313
+ }
314
+ return { verification, tax, risk, stock };
315
+ }
316
+
317
+ function parseLines(value: unknown): { sku: string; quantity: number }[] | null {
318
+ if (!Array.isArray(value)) return null;
319
+ const lines: { sku: string; quantity: number }[] = [];
320
+ for (const entry of value) {
321
+ if (!isRecord(entry) || typeof entry.sku !== "string" || !isOpaqueId(entry.sku)) return null;
322
+ if (!Number.isSafeInteger(entry.quantity) || Number(entry.quantity) < 1) return null;
323
+ lines.push({ sku: entry.sku, quantity: Number(entry.quantity) });
324
+ }
325
+ return lines;
326
+ }
327
+
328
+ function parseTotals(value: unknown): MinorTotals | null {
329
+ if (!isRecord(value)) return null;
330
+ const totals = {
331
+ subtotalMinor: Number(value.subtotalMinor),
332
+ shippingMinor: Number(value.shippingMinor),
333
+ taxMinor: Number(value.taxMinor),
334
+ totalMinor: Number(value.totalMinor),
335
+ };
336
+ if (
337
+ !Number.isSafeInteger(totals.subtotalMinor) ||
338
+ !Number.isSafeInteger(totals.shippingMinor) ||
339
+ !Number.isSafeInteger(totals.taxMinor) ||
340
+ !Number.isSafeInteger(totals.totalMinor)
341
+ ) {
342
+ return null;
343
+ }
344
+ return totals;
345
+ }
346
+
347
+ function auditStatement(
348
+ state: { order: LoadedOrder["order"] },
349
+ actorId: string,
350
+ kind: string,
351
+ fromState: string,
352
+ toState: string,
353
+ version: number,
354
+ at: string,
355
+ ): SqlStatement {
356
+ return {
357
+ sql: `INSERT INTO scrappy_shipment_audit (
358
+ audit_id, tenant_id, stage, order_id, at, actor_id, kind, from_state, to_state
359
+ )
360
+ SELECT ?, tenant_id, stage, order_id, ?, ?, ?, ?, ?
361
+ FROM scrappy_commerce_orders
362
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?`,
363
+ params: [
364
+ `audit_${crypto.randomUUID()}`,
365
+ at,
366
+ actorId,
367
+ kind,
368
+ fromState,
369
+ toState,
370
+ state.order.tenantId,
371
+ state.order.stage,
372
+ state.order.orderId,
373
+ version,
374
+ ],
375
+ };
376
+ }
377
+
378
+ export function commerceStageForEnv(env: string | undefined): CommerceStage | null {
379
+ if (env === "live") return "publication";
380
+ if (env === "test") return "preview";
381
+ return null;
382
+ }
383
+
384
+ export function decideFulfillmentAccess(input: {
385
+ record: SessionRecord | null | undefined;
386
+ hostResource: string;
387
+ nowSeconds: number;
388
+ customizationEnv: string | undefined;
389
+ subject: string | undefined;
390
+ bodyTenantId?: string | null;
391
+ }): { ok: true; actor: FulfillmentActor } | { ok: false; status: 401 | 403 | 409; code: string } {
392
+ const access = decideAdminApiAccess({
393
+ record: input.record,
394
+ hostResource: input.hostResource,
395
+ nowSeconds: input.nowSeconds,
396
+ requireOwner: true,
397
+ });
398
+ if (!access.ok) return { ok: false, status: access.status, code: access.reason };
399
+ const stage = commerceStageForEnv(input.customizationEnv);
400
+ if (!stage) return { ok: false, status: 409, code: "stage-unknown" };
401
+ const host = input.hostResource.trim();
402
+ if (!isOpaqueId(host) && host.length === 0) return { ok: false, status: 403, code: "denied" };
403
+ if (!host) return { ok: false, status: 403, code: "denied" };
404
+ if (input.bodyTenantId && input.bodyTenantId !== host) return { ok: false, status: 403, code: "denied" };
405
+ if (!input.subject || !isOpaqueId(input.subject)) return { ok: false, status: 403, code: "denied" };
406
+ const actor: FulfillmentActor = { role: "owner", tenantId: host, stage, actorId: input.subject };
407
+ if (!actorMayCommand(actor, { tenantId: host, stage })) return { ok: false, status: 403, code: "denied" };
408
+ return { ok: true, actor };
409
+ }
410
+
411
+ function jsonResponse(body: unknown, status: number): Response {
412
+ return new Response(JSON.stringify(body), {
413
+ status,
414
+ headers: {
415
+ "content-type": "application/json; charset=utf-8",
416
+ "cache-control": "no-store",
417
+ "x-robots-tag": "noindex, nofollow",
418
+ },
419
+ });
420
+ }
421
+
422
+ export function fulfillmentResultResponse<T>(result: FulfillmentResult<T>, body: (value: T) => unknown): Response {
423
+ if (!result.ok) {
424
+ const status = result.code === "not-found" ? 404 : result.code === "denied" ? 403 : result.code === "unavailable" ? 503 : result.code === "malformed" ? 400 : 409;
425
+ return jsonResponse({ error: result.code, ...(result.holds ? { holds: result.holds } : {}) }, status);
426
+ }
427
+ return jsonResponse(body(result.value), 200);
428
+ }
429
+
430
+ export async function openFulfillment(
431
+ context: APIContext,
432
+ bodyTenantId?: string | null,
433
+ ): Promise<{ ok: false; response: Response } | { ok: true; service: FulfillmentService; actor: FulfillmentActor }> {
434
+ const url = new URL(context.request.url);
435
+ if (url.searchParams.has("tenant") || url.searchParams.has("tenantId") || url.searchParams.has("stage")) {
436
+ return { ok: false, response: jsonResponse({ error: "denied" }, 403) };
437
+ }
438
+ const tenant = context.locals.tenant as
439
+ | {
440
+ appDomain?: string;
441
+ tenant_id?: string;
442
+ compliance?: { ruleProfile?: string };
443
+ carrierAccountId?: unknown;
444
+ }
445
+ | undefined;
446
+ const host = tenant?.appDomain?.trim() ?? "";
447
+ const record = await readViewerSession(context);
448
+ const decision = decideFulfillmentAccess({
449
+ record,
450
+ hostResource: host,
451
+ nowSeconds: Math.floor(Date.now() / 1000),
452
+ customizationEnv: context.locals.customizationEnv,
453
+ subject: record?.subject,
454
+ bodyTenantId,
455
+ });
456
+ if (!decision.ok) return { ok: false, response: jsonResponse({ error: decision.code }, decision.status) };
457
+ const d1 = await readD1("STOREFRONT_APPS_DB");
458
+ if (!d1) return { ok: false, response: jsonResponse({ error: "unavailable" }, 503) };
459
+ const policy = carrierPolicyForTenant({
460
+ tenant_id: tenant?.tenant_id || host,
461
+ compliance: tenant?.compliance,
462
+ carrierAccountId: tenant?.carrierAccountId,
463
+ });
464
+ return { ok: true, actor: decision.actor, service: new FulfillmentService(fulfillmentDbFromD1(d1), { policy }) };
465
+ }
466
+
467
+ export function parseFulfillmentBody(body: unknown): FulfillmentResult<{
468
+ action: string;
469
+ expectedVersion?: number;
470
+ scope?: string;
471
+ carrier?: string;
472
+ lines?: unknown;
473
+ outcome?: string;
474
+ adultSignature?: string;
475
+ orderIds?: string[];
476
+ tenantId?: string;
477
+ expectedVersions?: Record<string, number>;
478
+ }> {
479
+ if (!isRecord(body) || containsForbiddenKey(body)) return refuse("malformed");
480
+ const action = typeof body.action === "string" ? body.action : "";
481
+ if (body.action !== undefined && action === "") return refuse("malformed");
482
+ if (body.expectedVersion !== undefined && (!Number.isSafeInteger(body.expectedVersion) || Number(body.expectedVersion) < 1)) {
483
+ return refuse("malformed");
484
+ }
485
+ if (body.orderIds !== undefined) {
486
+ if (!Array.isArray(body.orderIds) || body.orderIds.some((id) => typeof id !== "string" || !isOpaqueId(id))) {
487
+ return refuse("malformed");
488
+ }
489
+ }
490
+ let expectedVersions: Record<string, number> | undefined;
491
+ if (body.expectedVersions !== undefined) {
492
+ if (!isRecord(body.expectedVersions)) return refuse("malformed");
493
+ expectedVersions = {};
494
+ for (const [key, value] of Object.entries(body.expectedVersions)) {
495
+ if (!isOpaqueId(key) || !Number.isSafeInteger(value) || Number(value) < 1) return refuse("malformed");
496
+ expectedVersions[key] = Number(value);
497
+ }
498
+ }
499
+ const tenantId = typeof body.tenantId === "string" ? body.tenantId : undefined;
500
+ return ok({
501
+ action,
502
+ ...(body.expectedVersion !== undefined ? { expectedVersion: Number(body.expectedVersion) } : {}),
503
+ ...(typeof body.scope === "string" ? { scope: body.scope } : {}),
504
+ ...(typeof body.carrier === "string" ? { carrier: body.carrier } : {}),
505
+ ...(body.lines !== undefined ? { lines: body.lines } : {}),
506
+ ...(typeof body.outcome === "string" ? { outcome: body.outcome } : {}),
507
+ ...(typeof body.adultSignature === "string" ? { adultSignature: body.adultSignature } : {}),
508
+ ...(Array.isArray(body.orderIds) ? { orderIds: body.orderIds as string[] } : {}),
509
+ ...(tenantId ? { tenantId } : {}),
510
+ ...(expectedVersions ? { expectedVersions } : {}),
511
+ });
512
+ }
513
+
514
+ export async function readJsonBody(request: Request): Promise<FulfillmentResult<unknown>> {
515
+ const text = await request.text();
516
+ if (text.trim() === "") return ok({});
517
+ try {
518
+ return ok(JSON.parse(text) as unknown);
519
+ } catch {
520
+ return refuse("malformed");
521
+ }
522
+ }
523
+
524
+ export class FulfillmentService {
525
+ private readonly now: () => string;
526
+
527
+ constructor(
528
+ private readonly db: FulfillmentDatabase,
529
+ private readonly options: { policy: CarrierPolicy; now?: () => string },
530
+ ) {
531
+ this.now = options.now ?? (() => new Date().toISOString());
532
+ }
533
+
534
+ get policy(): CarrierPolicy {
535
+ return this.options.policy;
536
+ }
537
+
538
+ async get(actor: FulfillmentActor, orderId: string): Promise<FulfillmentResult<FulfillmentView>> {
539
+ const scope = this.commandScope(actor);
540
+ if (!scope) return refuse("denied");
541
+ if (!isOpaqueId(orderId)) return refuse("not-found");
542
+ const state = await this.load(scope.tenantId, scope.stage, orderId);
543
+ return state ? ok(state.view) : refuse("not-found");
544
+ }
545
+
546
+ async batch(actor: FulfillmentActor, batchId: string): Promise<FulfillmentResult<FulfillmentBatch>> {
547
+ const scope = this.commandScope(actor);
548
+ if (!scope) return refuse("denied");
549
+ if (!isOpaqueId(batchId)) return refuse("not-found");
550
+ const rows = await this.db.all<{ order_id: string; to_state: string }>(
551
+ `SELECT order_id, to_state FROM scrappy_shipment_audit
552
+ WHERE tenant_id = ? AND stage = ? AND kind = 'batch'
553
+ ORDER BY at ASC, audit_id ASC`,
554
+ [scope.tenantId, scope.stage],
555
+ );
556
+ const latest = new Map<string, { batchId: string; phase: BatchPhase }>();
557
+ for (const row of rows) {
558
+ const parsed = parseBatchState(row.to_state);
559
+ if (parsed && isOpaqueId(row.order_id)) latest.set(row.order_id, parsed);
560
+ }
561
+ const orders = [...latest.entries()]
562
+ .filter(([, value]) => value.batchId === batchId)
563
+ .map(([orderId, value]) => ({ orderId, phase: value.phase }));
564
+ if (orders.length === 0) return refuse("not-found");
565
+ return ok({ batchId, orders });
566
+ }
567
+
568
+ async readAddress(
569
+ actor: FulfillmentActor,
570
+ orderId: string,
571
+ ): Promise<FulfillmentResult<Record<string, unknown>>> {
572
+ const scope = { tenantId: actor.tenantId, stage: actor.stage };
573
+ let opened = false;
574
+ const gate = readOwnerAddress(actor, scope, () => {
575
+ opened = true;
576
+ return true;
577
+ });
578
+ if (!gate.ok || !opened || !isOpaqueId(actor.actorId)) return refuse("denied");
579
+ if (!isOpaqueId(orderId)) return refuse("not-found");
580
+ const row = await this.db.get<{ address_json: string }>(
581
+ `SELECT address_json FROM scrappy_order_addresses WHERE tenant_id = ? AND stage = ? AND order_id = ?`,
582
+ [scope.tenantId, scope.stage, orderId],
583
+ );
584
+ if (!row) return refuse("not-found");
585
+ try {
586
+ const parsed = JSON.parse(row.address_json) as unknown;
587
+ if (!isRecord(parsed)) return refuse("unreadable");
588
+ return ok(parsed);
589
+ } catch {
590
+ return refuse("unreadable");
591
+ }
592
+ }
593
+
594
+ async applySourceSnapshot(
595
+ actor: FulfillmentActor,
596
+ snapshot: SourceOrderSnapshot,
597
+ ): Promise<FulfillmentResult<FulfillmentView>> {
598
+ const scope = this.commandScope(actor);
599
+ if (!scope) return refuse("denied");
600
+ if (snapshot.tenantId !== scope.tenantId || snapshot.stage !== scope.stage) return refuse("denied");
601
+ const invalid = validateSnapshot(snapshot);
602
+ if (invalid) return refuse(invalid);
603
+ const existing = await this.load(scope.tenantId, scope.stage, snapshot.orderId);
604
+ if (!existing) {
605
+ const inserted = await this.insertSnapshot(actor, snapshot);
606
+ if (inserted !== "exists") return inserted;
607
+ const raced = await this.load(scope.tenantId, scope.stage, snapshot.orderId);
608
+ if (!raced) return refuse("unavailable");
609
+ return this.updateSnapshot(actor, raced, snapshot);
610
+ }
611
+ return this.updateSnapshot(actor, existing, snapshot);
612
+ }
613
+
614
+ async claim(
615
+ actor: FulfillmentActor,
616
+ orderIds: readonly string[],
617
+ expectedVersions?: Readonly<Record<string, number>>,
618
+ ): Promise<FulfillmentResult<FulfillmentBatch>> {
619
+ const scope = this.commandScope(actor);
620
+ if (!scope) return refuse("denied");
621
+ if (orderIds.length === 0 || new Set(orderIds).size !== orderIds.length) return refuse("malformed");
622
+ if (orderIds.some((id) => !isOpaqueId(id))) return refuse("malformed");
623
+ for (let attempt = 0; attempt < 5; attempt++) {
624
+ const states: LoadedOrder[] = [];
625
+ for (const orderId of orderIds) {
626
+ const state = await this.load(scope.tenantId, scope.stage, orderId);
627
+ if (!state) return refuse("not-found");
628
+ states.push(state);
629
+ }
630
+ if (expectedVersions && states.some((state) => expectedVersions[state.order.orderId] !== state.order.eventVersion)) {
631
+ return refuse("conflict");
632
+ }
633
+ const blocked = states.find((state) => state.order.fulfillment === "handed_off" || state.order.fulfillment === "cancelled");
634
+ if (blocked) return refuse("fulfillment");
635
+ const batches = new Set(states.map((state) => state.batch?.batchId).filter((id): id is string => !!id));
636
+ if (states.every((state) => state.batch) && batches.size === 1) {
637
+ return this.batch(actor, [...batches][0] ?? "");
638
+ }
639
+ if (batches.size > 0) return refuse("conflict");
640
+ const batchId = `batch_${crypto.randomUUID()}`;
641
+ const statements: SqlStatement[] = [claimUpdate(states)];
642
+ states.forEach((state, index) => {
643
+ statements.push(
644
+ auditStatement(
645
+ state,
646
+ actor.actorId,
647
+ "batch",
648
+ "none",
649
+ `claimed|${batchId}`,
650
+ state.order.eventVersion + 1,
651
+ stamp(this.now(), state.auditCount + 1 + index),
652
+ ),
653
+ );
654
+ });
655
+ const changes = await this.write(statements);
656
+ if (!changes) return refuse("unavailable");
657
+ if ((changes[0] ?? 0) === states.length) return this.batch(actor, batchId);
658
+ if (expectedVersions) return refuse("conflict");
659
+ }
660
+ return refuse("conflict");
661
+ }
662
+
663
+ async pick(actor: FulfillmentActor, orderId: string, options: FulfillmentCommandOptions = {}): Promise<FulfillmentResult<FulfillmentView>> {
664
+ return this.advance(actor, orderId, "picked", options);
665
+ }
666
+
667
+ async pack(actor: FulfillmentActor, orderId: string, options: FulfillmentCommandOptions = {}): Promise<FulfillmentResult<FulfillmentView>> {
668
+ return this.advance(actor, orderId, "packed", options);
669
+ }
670
+
671
+ async release(actor: FulfillmentActor, orderId: string, options: FulfillmentCommandOptions = {}): Promise<FulfillmentResult<FulfillmentView>> {
672
+ return this.advance(actor, orderId, null, options);
673
+ }
674
+
675
+ async hold(actor: FulfillmentActor, orderId: string, options: FulfillmentCommandOptions = {}): Promise<FulfillmentResult<FulfillmentView>> {
676
+ return this.commit(actor, orderId, options.expectedVersion, (state) => {
677
+ if (state.order.fulfillment === "handed_off" || state.order.fulfillment === "cancelled") return { type: "refuse", code: "fulfillment" };
678
+ if (state.operatorHold) return { type: "done", view: state.view };
679
+ const version = state.order.eventVersion + 1;
680
+ return {
681
+ type: "write",
682
+ minimumChanges: 1,
683
+ statements: [
684
+ {
685
+ sql: `UPDATE scrappy_commerce_orders
686
+ SET fulfillment_state = 'held', event_version = event_version + 1
687
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
688
+ AND fulfillment_state IN ('ready', 'held')`,
689
+ params: [state.order.tenantId, state.order.stage, state.order.orderId, state.order.eventVersion],
690
+ },
691
+ auditStatement(state, actor.actorId, "operator-hold", state.order.fulfillment, "open", version, stamp(this.now(), state.auditCount + 1)),
692
+ ],
693
+ };
694
+ });
695
+ }
696
+
697
+ async clearOperatorHold(actor: FulfillmentActor, orderId: string, options: FulfillmentCommandOptions = {}): Promise<FulfillmentResult<FulfillmentView>> {
698
+ return this.commit(actor, orderId, options.expectedVersion, (state) => {
699
+ if (!state.operatorHold) return { type: "done", view: state.view };
700
+ if (state.order.fulfillment === "handed_off") return { type: "refuse", code: "fulfillment" };
701
+ const next = projectedFulfillment(state, false, state.cancelPending);
702
+ const version = state.order.eventVersion + 1;
703
+ return {
704
+ type: "write",
705
+ minimumChanges: 1,
706
+ statements: [
707
+ {
708
+ sql: `UPDATE scrappy_commerce_orders
709
+ SET fulfillment_state = ?, event_version = event_version + 1
710
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
711
+ AND fulfillment_state = 'held'`,
712
+ params: [next, state.order.tenantId, state.order.stage, state.order.orderId, state.order.eventVersion],
713
+ },
714
+ auditStatement(state, actor.actorId, "operator-hold", "open", "cleared", version, stamp(this.now(), state.auditCount + 1)),
715
+ ],
716
+ };
717
+ });
718
+ }
719
+
720
+ async requestCancel(actor: FulfillmentActor, orderId: string, options: FulfillmentCommandOptions = {}): Promise<FulfillmentResult<FulfillmentView>> {
721
+ return this.commit(actor, orderId, options.expectedVersion, (state) => {
722
+ if (state.cancelPending || state.order.fulfillment === "cancelled") return { type: "done", view: state.view };
723
+ const decision = decideCommerceCommand({
724
+ command: "cancel",
725
+ payment: state.order.payment,
726
+ stock: state.order.stock,
727
+ fulfillment: state.order.fulfillment,
728
+ label: state.order.label,
729
+ evidence: state.evidence,
730
+ providerOutcome: state.provider?.outcome ?? null,
731
+ });
732
+ if (!decision.admit) return { type: "refuse", code: decision.reason };
733
+ const version = state.order.eventVersion + 1;
734
+ return {
735
+ type: "write",
736
+ minimumChanges: 1,
737
+ statements: [
738
+ {
739
+ sql: `UPDATE scrappy_commerce_orders
740
+ SET fulfillment_state = 'held', event_version = event_version + 1
741
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
742
+ AND fulfillment_state IN ('ready', 'held')`,
743
+ params: [state.order.tenantId, state.order.stage, state.order.orderId, state.order.eventVersion],
744
+ },
745
+ auditStatement(
746
+ state,
747
+ actor.actorId,
748
+ "pending-cancel",
749
+ decision.releaseReservation ? "release-reservation" : "keep-reservation",
750
+ "open",
751
+ version,
752
+ stamp(this.now(), state.auditCount + 1),
753
+ ),
754
+ ],
755
+ };
756
+ });
757
+ }
758
+
759
+ async requestRefund(actor: FulfillmentActor, orderId: string, options: FulfillmentCommandOptions = {}): Promise<FulfillmentResult<FulfillmentView>> {
760
+ return this.commit(actor, orderId, options.expectedVersion, (state) => {
761
+ if (state.order.payment === "refund_requested" || state.order.payment === "refunded") {
762
+ return { type: "done", view: state.view };
763
+ }
764
+ if (state.order.fulfillment === "handed_off" || state.order.fulfillment === "cancelled") {
765
+ return { type: "refuse", code: "fulfillment" };
766
+ }
767
+ const decision = decideCommerceCommand({
768
+ command: "refund",
769
+ payment: state.order.payment,
770
+ stock: state.order.stock,
771
+ fulfillment: state.order.fulfillment,
772
+ label: state.order.label,
773
+ evidence: state.evidence,
774
+ providerOutcome: state.provider?.outcome ?? null,
775
+ });
776
+ if (!decision.admit) return { type: "refuse", code: decision.reason };
777
+ const version = state.order.eventVersion + 1;
778
+ return {
779
+ type: "write",
780
+ minimumChanges: 1,
781
+ statements: [
782
+ {
783
+ sql: `UPDATE scrappy_commerce_orders
784
+ SET payment_state = 'refund_requested', fulfillment_state = 'held', event_version = event_version + 1
785
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
786
+ AND payment_state = 'captured'
787
+ AND fulfillment_state NOT IN ('handed_off', 'cancelled')`,
788
+ params: [state.order.tenantId, state.order.stage, state.order.orderId, state.order.eventVersion],
789
+ },
790
+ auditStatement(state, actor.actorId, "pending-refund", state.order.payment, "open", version, stamp(this.now(), state.auditCount + 1)),
791
+ ],
792
+ };
793
+ });
794
+ }
795
+
796
+ async applyLabelOutcome(actor: FulfillmentActor, orderId: string, input: LabelOutcomeInput): Promise<FulfillmentResult<FulfillmentView>> {
797
+ const selected = carrierSelectionRefusal(this.policy, input.carrier);
798
+ if (selected) return refuse(selected);
799
+ if (!oneOf(input.outcome, ["purchased", "unknown", "voided"] as const)) return refuse("malformed");
800
+ if (!oneOf(input.adultSignature, ["accepted", "refused", "absent"] as const)) return refuse("malformed");
801
+ const mapped = labelOutcomeColumns(input.outcome);
802
+ return this.commit(actor, orderId, input.expectedVersion, (state) => {
803
+ if (!state.shipment || !state.provider) return { type: "refuse", code: "label" };
804
+ if (state.order.fulfillment === "handed_off") return { type: "refuse", code: "fulfillment" };
805
+ if (state.shipment.labelPurchaseState === mapped.shipment && state.labelOutcome?.adultSignature === input.adultSignature) {
806
+ return { type: "done", view: state.view };
807
+ }
808
+ const version = state.order.eventVersion + 1;
809
+ return {
810
+ type: "write",
811
+ minimumChanges: 1,
812
+ statements: [
813
+ {
814
+ sql: `UPDATE scrappy_commerce_orders
815
+ SET label_state = ?, event_version = event_version + 1
816
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
817
+ AND fulfillment_state != 'handed_off'
818
+ AND label_state IN ('intent', 'purchased', 'UNKNOWN', 'voided', 'void_requested')`,
819
+ params: [mapped.order, state.order.tenantId, state.order.stage, state.order.orderId, state.order.eventVersion],
820
+ },
821
+ {
822
+ sql: `UPDATE scrappy_shipments
823
+ SET label_purchase_state = ?
824
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND handoff_state = 'none'
825
+ AND EXISTS (
826
+ SELECT 1 FROM scrappy_commerce_orders
827
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
828
+ )`,
829
+ params: [
830
+ mapped.shipment,
831
+ state.order.tenantId,
832
+ state.order.stage,
833
+ state.order.orderId,
834
+ state.order.tenantId,
835
+ state.order.stage,
836
+ state.order.orderId,
837
+ version,
838
+ ],
839
+ },
840
+ {
841
+ sql: `UPDATE scrappy_provider_operations
842
+ SET outcome = ?
843
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND kind = 'label_purchase'
844
+ AND provider = 'unselected' AND purchase_id = ?
845
+ AND EXISTS (
846
+ SELECT 1 FROM scrappy_commerce_orders
847
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
848
+ )`,
849
+ params: [
850
+ mapped.provider,
851
+ state.order.tenantId,
852
+ state.order.stage,
853
+ state.order.orderId,
854
+ state.provider.purchaseId,
855
+ state.order.tenantId,
856
+ state.order.stage,
857
+ state.order.orderId,
858
+ version,
859
+ ],
860
+ },
861
+ auditStatement(
862
+ state,
863
+ actor.actorId,
864
+ "label-outcome",
865
+ state.order.label,
866
+ jsonText({ outcome: input.outcome, adultSignature: input.adultSignature }),
867
+ version,
868
+ stamp(this.now(), state.auditCount + 1),
869
+ ),
870
+ ],
871
+ };
872
+ });
873
+ }
874
+
875
+ async handOff(actor: FulfillmentActor, orderId: string, options: FulfillmentCommandOptions = {}): Promise<FulfillmentResult<FulfillmentView>> {
876
+ const selected = carrierSelectionRefusal(this.policy, options.carrier);
877
+ if (selected) return refuse(selected);
878
+ return this.commit(actor, orderId, options.expectedVersion, (state) => {
879
+ if (state.shipment?.handoffState === "handed_off" || state.order.fulfillment === "handed_off") {
880
+ return { type: "done", view: state.view };
881
+ }
882
+ const code = handoffRefusal(this.readiness(state));
883
+ if (code) return { type: "refuse", code, holds: releaseHolds(this.readiness(state)) };
884
+ if (state.shipment?.labelPurchaseState !== "purchased" || state.provider?.outcome !== "accepted") {
885
+ return { type: "refuse", code: "label" };
886
+ }
887
+ const version = state.order.eventVersion + 1;
888
+ return {
889
+ type: "write",
890
+ minimumChanges: 1,
891
+ statements: [
892
+ {
893
+ sql: `UPDATE scrappy_commerce_orders
894
+ SET fulfillment_state = 'handed_off', event_version = event_version + 1
895
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
896
+ AND payment_state = 'captured' AND stock_state = 'confirmed'
897
+ AND fulfillment_state = 'ready' AND label_state IN ('purchased', 'printed')
898
+ AND EXISTS (
899
+ SELECT 1 FROM scrappy_shipments
900
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
901
+ AND scope = 'whole-order' AND label_purchase_state = 'purchased' AND handoff_state = 'none'
902
+ )`,
903
+ params: [
904
+ state.order.tenantId,
905
+ state.order.stage,
906
+ state.order.orderId,
907
+ state.order.eventVersion,
908
+ state.order.tenantId,
909
+ state.order.stage,
910
+ state.order.orderId,
911
+ ],
912
+ },
913
+ {
914
+ sql: `UPDATE scrappy_shipments
915
+ SET handoff_state = 'handed_off'
916
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
917
+ AND scope = 'whole-order' AND label_purchase_state = 'purchased' AND handoff_state = 'none'
918
+ AND EXISTS (
919
+ SELECT 1 FROM scrappy_commerce_orders
920
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
921
+ )`,
922
+ params: [
923
+ state.order.tenantId,
924
+ state.order.stage,
925
+ state.order.orderId,
926
+ state.order.tenantId,
927
+ state.order.stage,
928
+ state.order.orderId,
929
+ version,
930
+ ],
931
+ },
932
+ auditStatement(state, actor.actorId, "transition", state.order.fulfillment, "handed_off", version, stamp(this.now(), state.auditCount + 1)),
933
+ ],
934
+ };
935
+ });
936
+ }
937
+
938
+ /** Returns are not automated. This records nothing. */
939
+ createReturn(actor: FulfillmentActor, _orderId: string): FulfillmentResult<never> {
940
+ if (!this.commandScope(actor)) return refuse("denied");
941
+ return refuse("returns-not-automated");
942
+ }
943
+
944
+ private commandScope(actor: FulfillmentActor): { tenantId: string; stage: CommerceStage } | null {
945
+ if (!isOpaqueId(actor.actorId)) return null;
946
+ const scope = { tenantId: actor.tenantId, stage: actor.stage };
947
+ return actorMayCommand(actor, scope) ? scope : null;
948
+ }
949
+
950
+ private readiness(state: LoadedOrder): ReadinessInput {
951
+ return {
952
+ payment: state.order.payment,
953
+ stock: state.order.stock,
954
+ fulfillment: state.order.fulfillment,
955
+ label: state.order.label,
956
+ evidence: state.evidence,
957
+ operatorHold: state.operatorHold,
958
+ cancelPending: state.cancelPending,
959
+ providerOutcome: state.provider?.outcome ?? null,
960
+ adultSignature: state.labelOutcome?.adultSignature ?? "absent",
961
+ adultSignatureRequired: state.adultSignatureRequired || this.policy.adultSignatureRequired,
962
+ };
963
+ }
964
+
965
+ private async advance(
966
+ actor: FulfillmentActor,
967
+ orderId: string,
968
+ phase: BatchPhase | null,
969
+ options: FulfillmentCommandOptions,
970
+ ): Promise<FulfillmentResult<FulfillmentView>> {
971
+ const selected = carrierSelectionRefusal(this.policy, options.carrier);
972
+ if (selected) return refuse(selected);
973
+ if (options.scope !== undefined && options.scope !== "whole-order") return refuse("partial-shipment");
974
+ if (options.lines !== undefined) return refuse("partial-shipment");
975
+ return this.commit(actor, orderId, options.expectedVersion, async (state) => {
976
+ if (!state.batch) return { type: "refuse", code: "batch" };
977
+ if (state.order.fulfillment === "handed_off" || state.order.fulfillment === "cancelled") {
978
+ return { type: "refuse", code: "fulfillment" };
979
+ }
980
+ const raising = phase !== null && phaseRank(state.batch.phase) < phaseRank(phase);
981
+ const wantSlip = phase === "packed" && !state.packingSlip;
982
+ const readiness = this.readiness(state);
983
+ const releasable = canRelease(readiness);
984
+ const labelAcceptsIntent = state.order.label === "none" || state.order.label === "voided" || state.order.label === "intent";
985
+ const wantIntent = (phase === null || phase === "picked" || phase === "packed") && releasable && !state.shipment && labelAcceptsIntent;
986
+ if (phase === null && !state.shipment && !wantIntent) {
987
+ if (!releasable) {
988
+ const holds = releaseHolds(readiness);
989
+ return { type: "refuse", code: holds[0] ?? "hold", holds };
990
+ }
991
+ const labelCode = state.order.label === "UNKNOWN" || state.order.label === "void_requested" ? "reconcile" : "label";
992
+ return { type: "refuse", code: labelCode };
993
+ }
994
+ if (!raising && !wantSlip && !wantIntent) {
995
+ if (state.shipment || phase !== null) return { type: "done", view: state.view };
996
+ }
997
+ const version = state.order.eventVersion + 1;
998
+ const statements: SqlStatement[] = [touchOrder(state, wantIntent)];
999
+ let n = state.auditCount;
1000
+ if (raising && phase) {
1001
+ n += 1;
1002
+ statements.push(auditStatement(state, actor.actorId, "batch", state.batch.phase, `${phase}|${state.batch.batchId}`, version, stamp(this.now(), n)));
1003
+ }
1004
+ if (wantSlip) {
1005
+ n += 1;
1006
+ const slip = packingSlip(state, this.policy);
1007
+ statements.push(auditStatement(state, actor.actorId, "packing-slip", "none", jsonText(slip), version, stamp(this.now(), n)));
1008
+ }
1009
+ if (wantIntent) {
1010
+ const intent = await this.intentStatements(state, actor.actorId, version, () => {
1011
+ n += 1;
1012
+ return stamp(this.now(), n);
1013
+ });
1014
+ statements.push(...intent);
1015
+ }
1016
+ return { type: "write", statements, minimumChanges: 1 };
1017
+ });
1018
+ }
1019
+
1020
+ private async intentStatements(
1021
+ state: LoadedOrder,
1022
+ actorId: string,
1023
+ version: number,
1024
+ nextStamp: () => string,
1025
+ ): Promise<SqlStatement[]> {
1026
+ const purchaseId = state.provider?.purchaseId ?? (await purchaseIdFor(state.order.tenantId, state.order.stage, state.order.orderId));
1027
+ const intent = {
1028
+ purchaseId,
1029
+ adultSignatureRequired: this.policy.adultSignatureRequired,
1030
+ prohibitedCarriers: [...this.policy.prohibitedCarriers],
1031
+ carrierAccountId: this.policy.carrierAccountId,
1032
+ vendorSelected: false as const,
1033
+ };
1034
+ const { order } = state;
1035
+ return [
1036
+ {
1037
+ sql: `INSERT INTO scrappy_shipments (
1038
+ tenant_id, stage, order_id, scope, label_purchase_state, label_print_state, handoff_state
1039
+ )
1040
+ SELECT tenant_id, stage, order_id, 'whole-order', 'intent', 'none', 'none'
1041
+ FROM scrappy_commerce_orders AS o
1042
+ WHERE o.tenant_id = ? AND o.stage = ? AND o.order_id = ? AND o.event_version = ?
1043
+ AND NOT EXISTS (
1044
+ SELECT 1 FROM scrappy_shipments AS s
1045
+ WHERE s.tenant_id = o.tenant_id AND s.stage = o.stage AND s.order_id = o.order_id
1046
+ )`,
1047
+ params: [order.tenantId, order.stage, order.orderId, version],
1048
+ },
1049
+ {
1050
+ sql: `INSERT INTO scrappy_provider_operations (
1051
+ tenant_id, stage, provider, purchase_id, order_id, kind, outcome, amount_minor
1052
+ )
1053
+ SELECT tenant_id, stage, 'unselected', ?, order_id, 'label_purchase', 'intent', amount_minor
1054
+ FROM scrappy_commerce_orders AS o
1055
+ WHERE o.tenant_id = ? AND o.stage = ? AND o.order_id = ? AND o.event_version = ?
1056
+ AND NOT EXISTS (
1057
+ SELECT 1 FROM scrappy_provider_operations AS p
1058
+ WHERE p.tenant_id = o.tenant_id AND p.stage = o.stage AND p.order_id = o.order_id
1059
+ AND p.kind = 'label_purchase' AND p.outcome IN ('intent', 'accepted', 'unknown')
1060
+ )`,
1061
+ params: [purchaseId, order.tenantId, order.stage, order.orderId, version],
1062
+ },
1063
+ auditStatement(state, actorId, "label-intent", state.order.label, jsonText(intent), version, nextStamp()),
1064
+ ];
1065
+ }
1066
+
1067
+ private async commit(
1068
+ actor: FulfillmentActor,
1069
+ orderId: string,
1070
+ expectedVersion: number | undefined,
1071
+ build: (state: LoadedOrder) => CommitPlan | Promise<CommitPlan>,
1072
+ ): Promise<FulfillmentResult<FulfillmentView>> {
1073
+ const scope = this.commandScope(actor);
1074
+ if (!scope) return refuse("denied");
1075
+ if (!isOpaqueId(orderId)) return refuse("not-found");
1076
+ for (let attempt = 0; attempt < 5; attempt++) {
1077
+ const state = await this.load(scope.tenantId, scope.stage, orderId);
1078
+ if (!state) return refuse("not-found");
1079
+ const plan = await build(state);
1080
+ if (plan.type === "done") return ok(plan.view);
1081
+ if (plan.type === "refuse") return refuse(plan.code, plan.holds);
1082
+ if (expectedVersion !== undefined && state.order.eventVersion !== expectedVersion) return refuse("conflict");
1083
+ const changes = await this.write(plan.statements);
1084
+ if (!changes) return refuse("unavailable");
1085
+ if ((changes[0] ?? 0) >= plan.minimumChanges) {
1086
+ const next = await this.load(scope.tenantId, scope.stage, orderId);
1087
+ return next ? ok(next.view) : refuse("not-found");
1088
+ }
1089
+ if (expectedVersion !== undefined) return refuse("conflict");
1090
+ }
1091
+ return refuse("conflict");
1092
+ }
1093
+
1094
+ private async write(statements: readonly SqlStatement[]): Promise<number[] | null> {
1095
+ try {
1096
+ return await this.db.batch(statements);
1097
+ } catch (error) {
1098
+ if (error instanceof Error && error.message === "storage") return null;
1099
+ return null;
1100
+ }
1101
+ }
1102
+
1103
+ private async insertSnapshot(actor: FulfillmentActor, snapshot: SourceOrderSnapshot): Promise<FulfillmentResult<FulfillmentView> | "exists"> {
1104
+ const payment = mergePayment(null, snapshot.payment);
1105
+ const label = mergeLabel(null, snapshot.label);
1106
+ const fulfillment = projectedFromSnapshot(null, snapshot, payment, false, false);
1107
+ const version = 1;
1108
+ const at = stamp(this.now(), 1);
1109
+ const statements: SqlStatement[] = [
1110
+ {
1111
+ sql: `INSERT INTO scrappy_commerce_orders (
1112
+ tenant_id, stage, order_id, source, source_order_id, currency, placed_at,
1113
+ payment_state, compliance_state, stock_state, fulfillment_state, label_state,
1114
+ amount_minor, event_version
1115
+ )
1116
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1
1117
+ WHERE NOT EXISTS (
1118
+ SELECT 1 FROM scrappy_commerce_orders WHERE tenant_id = ? AND stage = ? AND order_id = ?
1119
+ )`,
1120
+ params: [
1121
+ snapshot.tenantId,
1122
+ snapshot.stage,
1123
+ snapshot.orderId,
1124
+ snapshot.source,
1125
+ snapshot.sourceOrderId,
1126
+ snapshot.currency,
1127
+ snapshot.placedAt,
1128
+ payment,
1129
+ snapshot.compliance,
1130
+ snapshot.stock,
1131
+ fulfillment,
1132
+ label,
1133
+ snapshot.amountMinor,
1134
+ snapshot.tenantId,
1135
+ snapshot.stage,
1136
+ snapshot.orderId,
1137
+ ],
1138
+ },
1139
+ ];
1140
+ const gate = `tenant_id = ? AND stage = ? AND order_id = ? AND event_version = 1 AND source = ? AND source_order_id = ?`;
1141
+ const gateParams = [snapshot.tenantId, snapshot.stage, snapshot.orderId, snapshot.source, snapshot.sourceOrderId];
1142
+ if (snapshot.address) {
1143
+ statements.push({
1144
+ sql: `INSERT INTO scrappy_order_addresses (tenant_id, stage, order_id, address_json)
1145
+ SELECT tenant_id, stage, order_id, ?
1146
+ FROM scrappy_commerce_orders
1147
+ WHERE ${gate}
1148
+ AND NOT EXISTS (
1149
+ SELECT 1 FROM scrappy_order_addresses AS a
1150
+ WHERE a.tenant_id = scrappy_commerce_orders.tenant_id
1151
+ AND a.stage = scrappy_commerce_orders.stage
1152
+ AND a.order_id = scrappy_commerce_orders.order_id
1153
+ )`,
1154
+ params: [jsonText(snapshot.address), ...gateParams],
1155
+ });
1156
+ }
1157
+ const stub = {
1158
+ order: {
1159
+ tenantId: snapshot.tenantId,
1160
+ stage: snapshot.stage,
1161
+ orderId: snapshot.orderId,
1162
+ source: snapshot.source,
1163
+ sourceOrderId: snapshot.sourceOrderId,
1164
+ currency: snapshot.currency,
1165
+ placedAt: snapshot.placedAt,
1166
+ payment,
1167
+ compliance: snapshot.compliance,
1168
+ stock: snapshot.stock,
1169
+ fulfillment,
1170
+ label,
1171
+ amountMinor: snapshot.amountMinor,
1172
+ eventVersion: version,
1173
+ },
1174
+ };
1175
+ statements.push(
1176
+ gatedAudit(stub, actor.actorId, "evidence", "none", jsonText(snapshot.evidence), at, gate, gateParams),
1177
+ gatedAudit(stub, actor.actorId, "lines", "none", jsonText(snapshot.lines), stamp(this.now(), 2), gate, gateParams),
1178
+ gatedAudit(stub, actor.actorId, "totals", "none", jsonText(snapshot.totals), stamp(this.now(), 3), gate, gateParams),
1179
+ gatedAudit(stub, actor.actorId, "address-present", "none", snapshot.address ? "yes" : "no", stamp(this.now(), 4), gate, gateParams),
1180
+ gatedAudit(
1181
+ stub,
1182
+ actor.actorId,
1183
+ "public-event",
1184
+ "none",
1185
+ observedEvent({
1186
+ eventVersion: version,
1187
+ orderId: snapshot.orderId,
1188
+ placedAt: snapshot.placedAt,
1189
+ currency: snapshot.currency,
1190
+ payment,
1191
+ compliance: snapshot.compliance,
1192
+ stock: snapshot.stock,
1193
+ fulfillment,
1194
+ label,
1195
+ totals: snapshot.totals,
1196
+ evidence: snapshot.evidence,
1197
+ address: snapshot.address,
1198
+ }),
1199
+ stamp(this.now(), 5),
1200
+ gate,
1201
+ gateParams,
1202
+ ),
1203
+ );
1204
+ const changes = await this.write(statements);
1205
+ if (!changes) return refuse("unavailable");
1206
+ if ((changes[0] ?? 0) !== 1) return "exists";
1207
+ const loaded = await this.load(snapshot.tenantId, snapshot.stage, snapshot.orderId);
1208
+ return loaded ? ok(loaded.view) : refuse("unavailable");
1209
+ }
1210
+
1211
+ private updateSnapshot(
1212
+ actor: FulfillmentActor,
1213
+ _existing: LoadedOrder,
1214
+ snapshot: SourceOrderSnapshot,
1215
+ ): Promise<FulfillmentResult<FulfillmentView>> {
1216
+ return this.commit(actor, snapshot.orderId, undefined, (state) => {
1217
+ const payment = mergePayment(state.order.payment, snapshot.payment);
1218
+ const label = mergeLabel(state.order.label, snapshot.label);
1219
+ const confirmedCancel = snapshot.fulfillment === "cancelled" || payment === "voided" || payment === "refunded";
1220
+ const cancelPending = state.cancelPending && !confirmedCancel;
1221
+ const fulfillment = projectedFromSnapshot(state.order.fulfillment, snapshot, payment, state.operatorHold, cancelPending);
1222
+ const version = state.order.eventVersion + 1;
1223
+ const statements: SqlStatement[] = [
1224
+ {
1225
+ sql: `UPDATE scrappy_commerce_orders
1226
+ SET payment_state = ?, compliance_state = ?, stock_state = ?, fulfillment_state = ?,
1227
+ label_state = ?, amount_minor = ?, event_version = event_version + 1
1228
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
1229
+ AND (? = 'handed_off' OR fulfillment_state != 'handed_off' OR ? = 'handed_off')`,
1230
+ params: [
1231
+ payment,
1232
+ snapshot.compliance,
1233
+ snapshot.stock,
1234
+ state.order.fulfillment === "handed_off" ? "handed_off" : fulfillment,
1235
+ label,
1236
+ snapshot.amountMinor,
1237
+ state.order.tenantId,
1238
+ state.order.stage,
1239
+ state.order.orderId,
1240
+ state.order.eventVersion,
1241
+ state.order.fulfillment,
1242
+ fulfillment,
1243
+ ],
1244
+ },
1245
+ ];
1246
+ let n = state.auditCount;
1247
+ const nextVersion = version;
1248
+ n += 1;
1249
+ statements.push(auditStatement(state, actor.actorId, "evidence", "source", jsonText(snapshot.evidence), nextVersion, stamp(this.now(), n)));
1250
+ n += 1;
1251
+ statements.push(auditStatement(state, actor.actorId, "lines", "source", jsonText(snapshot.lines), nextVersion, stamp(this.now(), n)));
1252
+ n += 1;
1253
+ statements.push(auditStatement(state, actor.actorId, "totals", "source", jsonText(snapshot.totals), nextVersion, stamp(this.now(), n)));
1254
+ if (snapshot.address) {
1255
+ statements.push({
1256
+ sql: `INSERT INTO scrappy_order_addresses (tenant_id, stage, order_id, address_json)
1257
+ SELECT ?, ?, ?, ?
1258
+ WHERE EXISTS (
1259
+ SELECT 1 FROM scrappy_commerce_orders
1260
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
1261
+ )
1262
+ ON CONFLICT(tenant_id, stage, order_id) DO UPDATE SET address_json = excluded.address_json
1263
+ WHERE EXISTS (
1264
+ SELECT 1 FROM scrappy_commerce_orders
1265
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
1266
+ )`,
1267
+ params: [
1268
+ snapshot.tenantId,
1269
+ snapshot.stage,
1270
+ snapshot.orderId,
1271
+ jsonText(snapshot.address),
1272
+ snapshot.tenantId,
1273
+ snapshot.stage,
1274
+ snapshot.orderId,
1275
+ nextVersion,
1276
+ snapshot.tenantId,
1277
+ snapshot.stage,
1278
+ snapshot.orderId,
1279
+ nextVersion,
1280
+ ],
1281
+ });
1282
+ }
1283
+ const storedFulfillment = state.order.fulfillment === "handed_off" ? "handed_off" : fulfillment;
1284
+ n += 1;
1285
+ statements.push(
1286
+ auditStatement(
1287
+ state,
1288
+ actor.actorId,
1289
+ "public-event",
1290
+ "source",
1291
+ observedEvent({
1292
+ eventVersion: nextVersion,
1293
+ orderId: state.order.orderId,
1294
+ placedAt: state.order.placedAt,
1295
+ currency: state.order.currency,
1296
+ payment,
1297
+ compliance: snapshot.compliance,
1298
+ stock: snapshot.stock,
1299
+ fulfillment: storedFulfillment,
1300
+ label,
1301
+ totals: snapshot.totals,
1302
+ evidence: snapshot.evidence,
1303
+ address: snapshot.address,
1304
+ }),
1305
+ nextVersion,
1306
+ stamp(this.now(), n),
1307
+ ),
1308
+ );
1309
+ if (confirmedCancel && state.order.fulfillment !== "handed_off") {
1310
+ n += 1;
1311
+ statements.push(auditStatement(state, actor.actorId, "pending-cancel", "open", "confirmed", nextVersion, stamp(this.now(), n)));
1312
+ }
1313
+ return { type: "write", statements, minimumChanges: 1 };
1314
+ });
1315
+ }
1316
+
1317
+ private async load(tenantId: string, stage: CommerceStage, orderId: string): Promise<LoadedOrder | null> {
1318
+ const row = await this.db.get<Record<string, unknown>>(
1319
+ `SELECT tenant_id, stage, order_id, source, source_order_id, currency, placed_at,
1320
+ payment_state, compliance_state, stock_state, fulfillment_state, label_state,
1321
+ amount_minor, event_version
1322
+ FROM scrappy_commerce_orders
1323
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?`,
1324
+ [tenantId, stage, orderId],
1325
+ );
1326
+ if (!row) return null;
1327
+ if (
1328
+ !oneOf(row.payment_state, PAYMENT_STATES) ||
1329
+ !oneOf(row.compliance_state, COMPLIANCE_STATES) ||
1330
+ !oneOf(row.stock_state, STOCK_STATES) ||
1331
+ !oneOf(row.fulfillment_state, FULFILLMENT_STATES) ||
1332
+ !oneOf(row.label_state, LABEL_STATES) ||
1333
+ !oneOf(row.stage, ["preview", "publication"] as const) ||
1334
+ typeof row.order_id !== "string" ||
1335
+ typeof row.currency !== "string" ||
1336
+ typeof row.placed_at !== "string" ||
1337
+ typeof row.source !== "string" ||
1338
+ typeof row.source_order_id !== "string"
1339
+ ) {
1340
+ return null;
1341
+ }
1342
+ const audits = await this.db.all<{ kind: string; from_state: string; to_state: string }>(
1343
+ `SELECT kind, from_state, to_state FROM scrappy_shipment_audit
1344
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?
1345
+ ORDER BY at ASC, audit_id ASC`,
1346
+ [tenantId, stage, orderId],
1347
+ );
1348
+ const shipment = await this.db.get<{ label_purchase_state: string; handoff_state: string; scope: string }>(
1349
+ `SELECT scope, label_purchase_state, handoff_state FROM scrappy_shipments
1350
+ WHERE tenant_id = ? AND stage = ? AND order_id = ?`,
1351
+ [tenantId, stage, orderId],
1352
+ );
1353
+ const providers = await this.db.all<{ purchase_id: string; outcome: string }>(
1354
+ `SELECT purchase_id, outcome FROM scrappy_provider_operations
1355
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND kind = 'label_purchase'`,
1356
+ [tenantId, stage, orderId],
1357
+ );
1358
+ let evidence = FAIL_CLOSED_EVIDENCE;
1359
+ let lines: { sku: string; quantity: number }[] = [];
1360
+ let totals: MinorTotals | null = null;
1361
+ let batch: { batchId: string; phase: BatchPhase } | null = null;
1362
+ let operatorHold = false;
1363
+ let cancelPending = false;
1364
+ let packingSlip: PackingSlip | null = null;
1365
+ let labelOutcome: LoadedOrder["labelOutcome"] = null;
1366
+ let intentAccount: string | null = null;
1367
+ let intentProhibited: string[] = [...this.policy.prohibitedCarriers];
1368
+ let intentAdult = this.policy.adultSignatureRequired;
1369
+ for (const audit of audits) {
1370
+ if (audit.kind === "evidence") evidence = parseEvidence(parseJson(audit.to_state)) ?? FAIL_CLOSED_EVIDENCE;
1371
+ else if (audit.kind === "lines") lines = parseLines(parseJson(audit.to_state)) ?? [];
1372
+ else if (audit.kind === "totals") totals = parseTotals(parseJson(audit.to_state));
1373
+ else if (audit.kind === "batch") batch = parseBatchState(audit.to_state);
1374
+ else if (audit.kind === "operator-hold") operatorHold = audit.to_state === "open";
1375
+ else if (audit.kind === "pending-cancel") cancelPending = audit.to_state === "open";
1376
+ else if (audit.kind === "packing-slip") packingSlip = parseSlip(parseJson(audit.to_state));
1377
+ else if (audit.kind === "label-outcome") labelOutcome = parseLabelOutcome(parseJson(audit.to_state));
1378
+ else if (audit.kind === "label-intent") {
1379
+ const intent = parseJson(audit.to_state);
1380
+ if (isRecord(intent)) {
1381
+ intentAccount = typeof intent.carrierAccountId === "string" ? intent.carrierAccountId : null;
1382
+ intentAdult = intent.adultSignatureRequired === true;
1383
+ intentProhibited = Array.isArray(intent.prohibitedCarriers)
1384
+ ? intent.prohibitedCarriers.filter((item): item is string => typeof item === "string")
1385
+ : intentProhibited;
1386
+ }
1387
+ }
1388
+ }
1389
+ const provider = pickProvider(providers);
1390
+ const order = {
1391
+ tenantId,
1392
+ stage: row.stage,
1393
+ orderId: row.order_id,
1394
+ source: row.source,
1395
+ sourceOrderId: row.source_order_id,
1396
+ currency: row.currency,
1397
+ placedAt: row.placed_at,
1398
+ payment: row.payment_state,
1399
+ compliance: row.compliance_state,
1400
+ stock: row.stock_state,
1401
+ fulfillment: row.fulfillment_state,
1402
+ label: row.label_state,
1403
+ amountMinor: Number(row.amount_minor),
1404
+ eventVersion: Number(row.event_version),
1405
+ };
1406
+ const loaded: LoadedOrder = {
1407
+ order,
1408
+ evidence,
1409
+ lines,
1410
+ totals,
1411
+ batch,
1412
+ operatorHold,
1413
+ cancelPending,
1414
+ packingSlip,
1415
+ shipment: shipment && shipment.scope === "whole-order"
1416
+ ? { labelPurchaseState: shipment.label_purchase_state, handoffState: shipment.handoff_state }
1417
+ : null,
1418
+ provider,
1419
+ labelOutcome,
1420
+ carrierAccountId: intentAccount,
1421
+ prohibitedCarriers: intentProhibited,
1422
+ adultSignatureRequired: intentAdult,
1423
+ auditCount: audits.length,
1424
+ view: {
1425
+ orderId: order.orderId,
1426
+ tenantId: order.tenantId,
1427
+ stage: order.stage,
1428
+ eventVersion: order.eventVersion,
1429
+ payment: order.payment,
1430
+ compliance: order.compliance,
1431
+ stock: order.stock,
1432
+ fulfillment: order.fulfillment,
1433
+ label: order.label,
1434
+ holds: [],
1435
+ canRelease: false,
1436
+ canHandOff: false,
1437
+ batch,
1438
+ shipment: null,
1439
+ packingSlip,
1440
+ },
1441
+ };
1442
+ const readiness = this.readiness(loaded);
1443
+ const holds = releaseHolds(readiness);
1444
+ loaded.view.holds = holds;
1445
+ loaded.view.canRelease = canRelease(readiness) && !loaded.shipment;
1446
+ loaded.view.canHandOff = handoffRefusal(readiness) === null && loaded.shipment?.handoffState !== "handed_off";
1447
+ loaded.view.shipment = loaded.shipment
1448
+ ? {
1449
+ scope: "whole-order",
1450
+ purchaseId: provider?.purchaseId ?? null,
1451
+ labelPurchaseState: loaded.shipment.labelPurchaseState,
1452
+ handoffState: loaded.shipment.handoffState,
1453
+ vendorSelected: false,
1454
+ carrierAccountId: loaded.carrierAccountId,
1455
+ adultSignatureRequired: loaded.adultSignatureRequired,
1456
+ prohibitedCarriers: loaded.prohibitedCarriers,
1457
+ }
1458
+ : null;
1459
+ return loaded;
1460
+ }
1461
+ }
1462
+
1463
+ function touchOrder(state: LoadedOrder, wantIntent: boolean): SqlStatement {
1464
+ return {
1465
+ sql: `UPDATE scrappy_commerce_orders
1466
+ SET event_version = event_version + 1,
1467
+ label_state = CASE WHEN ? = 1 THEN 'intent' ELSE label_state END
1468
+ WHERE tenant_id = ? AND stage = ? AND order_id = ? AND event_version = ?
1469
+ AND fulfillment_state NOT IN ('handed_off', 'cancelled')
1470
+ AND (? = 0 OR (
1471
+ payment_state = 'captured' AND stock_state = 'confirmed' AND fulfillment_state = 'ready'
1472
+ AND label_state IN ('none', 'voided', 'intent')
1473
+ ))`,
1474
+ params: [
1475
+ wantIntent ? 1 : 0,
1476
+ state.order.tenantId,
1477
+ state.order.stage,
1478
+ state.order.orderId,
1479
+ state.order.eventVersion,
1480
+ wantIntent ? 1 : 0,
1481
+ ],
1482
+ };
1483
+ }
1484
+
1485
+ function claimUpdate(states: readonly LoadedOrder[]): SqlStatement {
1486
+ const first = states[0];
1487
+ if (!first) return { sql: "SELECT 0", params: [] };
1488
+ const match = states.map(() => "(order_id = ? AND event_version = ?)").join(" OR ");
1489
+ const versionParams = states.flatMap((state) => [state.order.orderId, state.order.eventVersion]);
1490
+ return {
1491
+ sql: `UPDATE scrappy_commerce_orders
1492
+ SET event_version = event_version + 1
1493
+ WHERE tenant_id = ? AND stage = ? AND (${match})
1494
+ AND fulfillment_state NOT IN ('handed_off', 'cancelled')
1495
+ AND (
1496
+ SELECT COUNT(*) FROM scrappy_commerce_orders
1497
+ WHERE tenant_id = ? AND stage = ? AND (${match})
1498
+ AND fulfillment_state NOT IN ('handed_off', 'cancelled')
1499
+ ) = ?`,
1500
+ params: [first.order.tenantId, first.order.stage, ...versionParams, first.order.tenantId, first.order.stage, ...versionParams, states.length],
1501
+ };
1502
+ }
1503
+
1504
+ function gatedAudit(
1505
+ state: { order: LoadedOrder["order"] },
1506
+ actorId: string,
1507
+ kind: string,
1508
+ fromState: string,
1509
+ toState: string,
1510
+ at: string,
1511
+ gate: string,
1512
+ gateParams: readonly unknown[],
1513
+ ): SqlStatement {
1514
+ return {
1515
+ sql: `INSERT INTO scrappy_shipment_audit (
1516
+ audit_id, tenant_id, stage, order_id, at, actor_id, kind, from_state, to_state
1517
+ )
1518
+ SELECT ?, tenant_id, stage, order_id, ?, ?, ?, ?, ?
1519
+ FROM scrappy_commerce_orders
1520
+ WHERE ${gate}
1521
+ AND NOT EXISTS (
1522
+ SELECT 1 FROM scrappy_shipment_audit AS existing
1523
+ WHERE existing.tenant_id = scrappy_commerce_orders.tenant_id
1524
+ AND existing.stage = scrappy_commerce_orders.stage
1525
+ AND existing.order_id = scrappy_commerce_orders.order_id
1526
+ AND existing.kind = ?
1527
+ )`,
1528
+ params: [`audit_${crypto.randomUUID()}`, at, actorId, kind, fromState, toState, ...gateParams, kind],
1529
+ };
1530
+ }
1531
+
1532
+ function packingSlip(state: LoadedOrder, policy: CarrierPolicy): PackingSlip {
1533
+ return {
1534
+ orderId: state.order.orderId,
1535
+ currency: state.order.currency,
1536
+ lines: state.lines.map((line) => ({ sku: line.sku, quantity: line.quantity })),
1537
+ adultSignatureRequired: policy.adultSignatureRequired,
1538
+ prohibitedCarriers: [...policy.prohibitedCarriers],
1539
+ };
1540
+ }
1541
+
1542
+ function projectedFulfillment(
1543
+ state: LoadedOrder,
1544
+ operatorHold: boolean,
1545
+ cancelPending: boolean,
1546
+ ): FulfillmentState {
1547
+ return projectedFromSnapshot(
1548
+ state.order.fulfillment,
1549
+ {
1550
+ fulfillment: state.order.fulfillment,
1551
+ stock: state.order.stock,
1552
+ label: state.order.label,
1553
+ evidence: state.evidence,
1554
+ },
1555
+ state.order.payment,
1556
+ operatorHold,
1557
+ cancelPending,
1558
+ );
1559
+ }
1560
+
1561
+ function projectedFromSnapshot(
1562
+ current: FulfillmentState | null,
1563
+ snapshot: Pick<SourceOrderSnapshot, "fulfillment" | "stock" | "label" | "evidence">,
1564
+ payment: PaymentState,
1565
+ operatorHold: boolean,
1566
+ cancelPending: boolean,
1567
+ ): FulfillmentState {
1568
+ if (current === "handed_off") return "handed_off";
1569
+ if (snapshot.fulfillment === "cancelled" || payment === "voided" || payment === "refunded") return "cancelled";
1570
+ if (snapshot.fulfillment === "UNKNOWN") return "UNKNOWN";
1571
+ const holds = releaseHolds({
1572
+ payment,
1573
+ stock: snapshot.stock,
1574
+ fulfillment: "ready",
1575
+ label: snapshot.label,
1576
+ evidence: snapshot.evidence,
1577
+ operatorHold,
1578
+ cancelPending,
1579
+ providerOutcome: null,
1580
+ adultSignature: "absent",
1581
+ adultSignatureRequired: false,
1582
+ });
1583
+ if (holds.length > 0 || operatorHold || cancelPending) return "held";
1584
+ return "ready";
1585
+ }
1586
+
1587
+ function validateSnapshot(snapshot: SourceOrderSnapshot): string | null {
1588
+ if (!isOpaqueId(snapshot.orderId) || !isOpaqueId(snapshot.source) || !isOpaqueId(snapshot.sourceOrderId)) return "malformed";
1589
+ if (!/^[A-Z]{3}$/.test(snapshot.currency)) return "malformed";
1590
+ if (snapshot.placedAt.trim() === "" || snapshot.placedAt.includes("\n")) return "malformed";
1591
+ if (!Number.isSafeInteger(snapshot.amountMinor) || snapshot.amountMinor < 0) return "malformed";
1592
+ if (!oneOf(snapshot.payment, PAYMENT_STATES) || !oneOf(snapshot.compliance, COMPLIANCE_STATES)) return "malformed";
1593
+ if (!oneOf(snapshot.stock, STOCK_STATES) || !oneOf(snapshot.fulfillment, FULFILLMENT_STATES)) return "malformed";
1594
+ if (!oneOf(snapshot.label, LABEL_STATES) || !oneOf(snapshot.stage, ["preview", "publication"] as const)) return "malformed";
1595
+ if (!parseEvidence(snapshot.evidence) || !parseLines(snapshot.lines) || !parseTotals(snapshot.totals)) return "malformed";
1596
+ if (snapshot.amountMinor !== snapshot.totals.totalMinor || !totalsMatch(snapshot.totals)) return "malformed";
1597
+ if (snapshot.address !== null && !isRecord(snapshot.address)) return "malformed";
1598
+ return null;
1599
+ }
1600
+
1601
+ function labelOutcomeColumns(outcome: "purchased" | "unknown" | "voided"): {
1602
+ order: LabelState;
1603
+ shipment: string;
1604
+ provider: ProviderOutcome;
1605
+ } {
1606
+ if (outcome === "purchased") return { order: "purchased", shipment: "purchased", provider: "accepted" };
1607
+ if (outcome === "voided") return { order: "voided", shipment: "voided", provider: "voided" };
1608
+ return { order: "UNKNOWN", shipment: "unknown", provider: "unknown" };
1609
+ }
1610
+
1611
+ function parseBatchState(value: string): { batchId: string; phase: BatchPhase } | null {
1612
+ const split = value.indexOf("|");
1613
+ if (split <= 0) return null;
1614
+ const phase = value.slice(0, split);
1615
+ const batchId = value.slice(split + 1);
1616
+ if ((phase !== "claimed" && phase !== "picked" && phase !== "packed") || !isOpaqueId(batchId)) return null;
1617
+ return { phase, batchId };
1618
+ }
1619
+
1620
+ function parseSlip(value: unknown): PackingSlip | null {
1621
+ if (!isRecord(value) || typeof value.orderId !== "string" || typeof value.currency !== "string") return null;
1622
+ const lines = parseLines(value.lines);
1623
+ if (!lines) return null;
1624
+ return {
1625
+ orderId: value.orderId,
1626
+ currency: value.currency,
1627
+ lines,
1628
+ adultSignatureRequired: value.adultSignatureRequired === true,
1629
+ prohibitedCarriers: Array.isArray(value.prohibitedCarriers)
1630
+ ? value.prohibitedCarriers.filter((item): item is string => typeof item === "string")
1631
+ : [],
1632
+ };
1633
+ }
1634
+
1635
+ function parseLabelOutcome(value: unknown): { adultSignature: "accepted" | "refused" | "absent" } | null {
1636
+ if (!isRecord(value)) return null;
1637
+ if (value.adultSignature !== "accepted" && value.adultSignature !== "refused" && value.adultSignature !== "absent") return null;
1638
+ return { adultSignature: value.adultSignature };
1639
+ }
1640
+
1641
+ function parseJson(value: string): unknown {
1642
+ try {
1643
+ return JSON.parse(value) as unknown;
1644
+ } catch {
1645
+ return null;
1646
+ }
1647
+ }
1648
+
1649
+ function pickProvider(rows: readonly { purchase_id: string; outcome: string }[]): LoadedOrder["provider"] {
1650
+ const open = rows.find((row) => row.outcome === "intent" || row.outcome === "accepted" || row.outcome === "unknown");
1651
+ const chosen = open ?? rows[0];
1652
+ if (!chosen || !oneOf(chosen.outcome, ["intent", "accepted", "unknown", "voided", "refunded"] as const)) return null;
1653
+ return { purchaseId: chosen.purchase_id, outcome: chosen.outcome };
1654
+ }