@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.
- package/apps/storefront/package.json +1 -0
- package/apps/storefront/src/lib/fulfillment/fulfillmentService.ts +1654 -0
- package/apps/storefront/src/lib/fulfillment/readiness.ts +150 -0
- package/apps/storefront/src/lib/fulfillment/store.ts +109 -0
- package/apps/storefront/src/pages/api/admin/fulfillment/batches.ts +59 -0
- package/apps/storefront/src/pages/api/admin/fulfillment/orders/[id]/address.ts +32 -0
- package/apps/storefront/src/pages/api/admin/fulfillment/orders/[id].ts +117 -0
- package/package.json +1 -1
- package/pnpm-lock.runner.yaml +17 -14
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release and hand-off gates for whole-order fulfillment.
|
|
3
|
+
*
|
|
4
|
+
* Evidence, payment, stock, refund, and cancel are rechecked from stored source
|
|
5
|
+
* truth. Compliance `fresh` is not evidence. A carrier account is tenant
|
|
6
|
+
* configuration: its absence is not a hold, and this module does not select a vendor.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
decideCommerceCommand,
|
|
10
|
+
paymentIsCaptured,
|
|
11
|
+
type CommerceEvidence,
|
|
12
|
+
type ComplianceRuleProfile,
|
|
13
|
+
type EvidenceState,
|
|
14
|
+
type FulfillmentState,
|
|
15
|
+
type LabelState,
|
|
16
|
+
type PaymentState,
|
|
17
|
+
type ProviderOutcome,
|
|
18
|
+
type StockState,
|
|
19
|
+
} from "@tot/public-runtime";
|
|
20
|
+
import { COMPLIANCE_RULE_PROFILES } from "@/config/compliance/rulesets";
|
|
21
|
+
|
|
22
|
+
export const RELEASE_HOLDS = [
|
|
23
|
+
"evidence-unknown",
|
|
24
|
+
"evidence-stale",
|
|
25
|
+
"evidence-unreadable",
|
|
26
|
+
"evidence-absent",
|
|
27
|
+
"operator-hold",
|
|
28
|
+
"payment-pending",
|
|
29
|
+
"payment-unknown",
|
|
30
|
+
"payment-not-captured",
|
|
31
|
+
"refund-pending",
|
|
32
|
+
"refunded",
|
|
33
|
+
"refund-unknown",
|
|
34
|
+
"cancel-pending",
|
|
35
|
+
"cancelled",
|
|
36
|
+
"stock-unknown",
|
|
37
|
+
"stock-unconfirmed",
|
|
38
|
+
"fulfillment-unknown",
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
export type ReleaseHold = (typeof RELEASE_HOLDS)[number];
|
|
42
|
+
|
|
43
|
+
export interface CarrierPolicy {
|
|
44
|
+
adultSignatureRequired: boolean;
|
|
45
|
+
prohibitedCarriers: readonly string[];
|
|
46
|
+
/** Null when the tenant has not configured an account. Absence is not a hold. */
|
|
47
|
+
carrierAccountId: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ReadinessInput {
|
|
51
|
+
payment: PaymentState;
|
|
52
|
+
stock: StockState;
|
|
53
|
+
fulfillment: FulfillmentState;
|
|
54
|
+
label: LabelState;
|
|
55
|
+
evidence: CommerceEvidence;
|
|
56
|
+
operatorHold: boolean;
|
|
57
|
+
cancelPending: boolean;
|
|
58
|
+
providerOutcome: ProviderOutcome | null;
|
|
59
|
+
adultSignature: "accepted" | "refused" | "absent";
|
|
60
|
+
adultSignatureRequired: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const EVIDENCE_HOLDS: Record<EvidenceState, ReleaseHold | null> = {
|
|
64
|
+
fresh: null,
|
|
65
|
+
UNKNOWN: "evidence-unknown",
|
|
66
|
+
stale: "evidence-stale",
|
|
67
|
+
unreadable: "evidence-unreadable",
|
|
68
|
+
absent: "evidence-absent",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function add(holds: ReleaseHold[], hold: ReleaseHold | null): void {
|
|
72
|
+
if (hold && !holds.includes(hold)) holds.push(hold);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Holds that forbid creating a shipment intent. Compliance is not an input. */
|
|
76
|
+
export function releaseHolds(input: ReadinessInput): ReleaseHold[] {
|
|
77
|
+
const holds: ReleaseHold[] = [];
|
|
78
|
+
for (const field of ["verification", "tax", "risk", "stock"] as const) {
|
|
79
|
+
const value = input.evidence[field];
|
|
80
|
+
add(holds, value in EVIDENCE_HOLDS ? EVIDENCE_HOLDS[value as EvidenceState] : "evidence-unreadable");
|
|
81
|
+
}
|
|
82
|
+
if (input.operatorHold) add(holds, "operator-hold");
|
|
83
|
+
if (input.cancelPending) add(holds, "cancel-pending");
|
|
84
|
+
if (input.fulfillment === "cancelled") add(holds, "cancelled");
|
|
85
|
+
if (input.fulfillment === "UNKNOWN") add(holds, "fulfillment-unknown");
|
|
86
|
+
if (input.payment === "pending" || input.payment === "authorized") add(holds, "payment-pending");
|
|
87
|
+
else if (input.payment === "UNKNOWN" || input.payment === "capture_unknown") add(holds, "payment-unknown");
|
|
88
|
+
else if (input.payment === "refund_requested") add(holds, "refund-pending");
|
|
89
|
+
else if (input.payment === "refunded") add(holds, "refunded");
|
|
90
|
+
else if (input.payment === "refund_unknown") add(holds, "refund-unknown");
|
|
91
|
+
else if (input.payment === "voided") add(holds, "cancelled");
|
|
92
|
+
else if (!paymentIsCaptured(input.payment)) add(holds, "payment-not-captured");
|
|
93
|
+
if (input.stock === "UNKNOWN") add(holds, "stock-unknown");
|
|
94
|
+
else if (input.stock !== "confirmed") add(holds, "stock-unconfirmed");
|
|
95
|
+
return holds;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function canRelease(input: ReadinessInput): boolean {
|
|
99
|
+
return input.fulfillment === "ready" && releaseHolds(input).length === 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function handoffRefusal(input: ReadinessInput): ReleaseHold | "adult-signature" | "label" | "reconcile" | "fulfillment" | "evidence" | "payment" | "stock" | "reuse" | null {
|
|
103
|
+
const holds = releaseHolds(input);
|
|
104
|
+
if (holds.length > 0) return holds[0] ?? "fulfillment";
|
|
105
|
+
const decision = decideCommerceCommand({
|
|
106
|
+
command: "hand-off",
|
|
107
|
+
payment: input.payment,
|
|
108
|
+
stock: input.stock,
|
|
109
|
+
fulfillment: input.fulfillment,
|
|
110
|
+
label: input.label,
|
|
111
|
+
evidence: input.evidence,
|
|
112
|
+
providerOutcome: input.providerOutcome,
|
|
113
|
+
});
|
|
114
|
+
if (!decision.admit) return decision.reason === "admitted" ? "fulfillment" : decision.reason;
|
|
115
|
+
if (input.adultSignatureRequired && input.adultSignature !== "accepted") return "adult-signature";
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function carrierPolicyForTenant(tenant: {
|
|
120
|
+
tenant_id: string;
|
|
121
|
+
compliance?: { ruleProfile?: string };
|
|
122
|
+
carrierAccountId?: unknown;
|
|
123
|
+
}): CarrierPolicy {
|
|
124
|
+
const profile = tenant.compliance?.ruleProfile;
|
|
125
|
+
const build =
|
|
126
|
+
profile && Object.hasOwn(COMPLIANCE_RULE_PROFILES, profile)
|
|
127
|
+
? COMPLIANCE_RULE_PROFILES[profile as ComplianceRuleProfile]
|
|
128
|
+
: undefined;
|
|
129
|
+
const ruleset = build ? build(tenant.tenant_id) : null;
|
|
130
|
+
const raw = tenant.carrierAccountId;
|
|
131
|
+
const carrierAccountId = typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
|
|
132
|
+
const enabled = ruleset?.pactAct.enabled === true;
|
|
133
|
+
return {
|
|
134
|
+
adultSignatureRequired: enabled && ruleset.pactAct.adultSignatureRequired === true,
|
|
135
|
+
prohibitedCarriers: enabled ? [...ruleset.pactAct.prohibitedCarriers] : [],
|
|
136
|
+
carrierAccountId,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A supplied carrier is never a selection. USPS is refused when the policy prohibits it. */
|
|
141
|
+
export function carrierSelectionRefusal(
|
|
142
|
+
policy: CarrierPolicy,
|
|
143
|
+
carrier: string | undefined,
|
|
144
|
+
): "carrier-prohibited" | "vendor-not-selected" | null {
|
|
145
|
+
if (carrier === undefined) return null;
|
|
146
|
+
const normalized = carrier.trim().toUpperCase();
|
|
147
|
+
if (normalized === "") return "vendor-not-selected";
|
|
148
|
+
if (policy.prohibitedCarriers.some((item) => item.toUpperCase() === normalized)) return "carrier-prohibited";
|
|
149
|
+
return "vendor-not-selected";
|
|
150
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant-scoped SQL seam for scrappy commerce tables.
|
|
3
|
+
*
|
|
4
|
+
* Batch phase, holds, packing slips, and label intents are rows in
|
|
5
|
+
* `scrappy_shipment_audit` (append-only). The commerce migration has no batch
|
|
6
|
+
* table; this store does not create one.
|
|
7
|
+
*/
|
|
8
|
+
import type { D1Like } from "../d1/catalog.js";
|
|
9
|
+
|
|
10
|
+
export interface SqlStatement {
|
|
11
|
+
sql: string;
|
|
12
|
+
params: readonly unknown[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FulfillmentDatabase {
|
|
16
|
+
get<T extends Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<T | undefined>;
|
|
17
|
+
all<T extends Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<T[]>;
|
|
18
|
+
/** One transaction. A unique-constraint loss returns zeros so the caller can re-read. */
|
|
19
|
+
batch(statements: readonly SqlStatement[]): Promise<number[]>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SqliteStatement {
|
|
23
|
+
all(...params: unknown[]): Array<Record<string, unknown>>;
|
|
24
|
+
get(...params: unknown[]): Record<string, unknown> | undefined;
|
|
25
|
+
run(...params: unknown[]): { changes?: number | bigint };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SqliteDatabase {
|
|
29
|
+
prepare(sql: string): SqliteStatement;
|
|
30
|
+
exec(sql: string): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function changesOf(result: { changes?: number | bigint } | undefined): number {
|
|
34
|
+
const changes = result?.changes ?? 0;
|
|
35
|
+
return typeof changes === "bigint" ? Number(changes) : changes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function lostRace(error: unknown): boolean {
|
|
39
|
+
return error instanceof Error && error.message.includes("UNIQUE constraint failed");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function fulfillmentDbFromSqlite(db: SqliteDatabase): FulfillmentDatabase {
|
|
43
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
44
|
+
let chain: Promise<unknown> = Promise.resolve();
|
|
45
|
+
const enqueue = <T>(fn: () => T): Promise<T> => {
|
|
46
|
+
const run = chain.then(
|
|
47
|
+
() => fn(),
|
|
48
|
+
() => fn(),
|
|
49
|
+
);
|
|
50
|
+
chain = run.then(
|
|
51
|
+
() => undefined,
|
|
52
|
+
() => undefined,
|
|
53
|
+
);
|
|
54
|
+
return run;
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
get: (sql, params = []) => enqueue(() => db.prepare(sql).get(...params) as never),
|
|
58
|
+
all: (sql, params = []) => enqueue(() => db.prepare(sql).all(...params) as never),
|
|
59
|
+
batch: (statements) =>
|
|
60
|
+
enqueue(() => {
|
|
61
|
+
if (statements.length === 0) return [];
|
|
62
|
+
db.exec("BEGIN IMMEDIATE");
|
|
63
|
+
const changes: number[] = [];
|
|
64
|
+
try {
|
|
65
|
+
for (const statement of statements) {
|
|
66
|
+
changes.push(changesOf(db.prepare(statement.sql).run(...statement.params)));
|
|
67
|
+
}
|
|
68
|
+
db.exec("COMMIT");
|
|
69
|
+
return changes;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
try {
|
|
72
|
+
db.exec("ROLLBACK");
|
|
73
|
+
} catch {
|
|
74
|
+
/* the transaction is already closed */
|
|
75
|
+
}
|
|
76
|
+
if (lostRace(error)) return statements.map(() => 0);
|
|
77
|
+
throw new Error("storage", { cause: error });
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function fulfillmentDbFromD1(db: D1Like): FulfillmentDatabase {
|
|
84
|
+
return {
|
|
85
|
+
async get(sql, params = []) {
|
|
86
|
+
const row = await db.prepare(sql).bind(...params).first();
|
|
87
|
+
return (row ?? undefined) as never;
|
|
88
|
+
},
|
|
89
|
+
async all(sql, params = []) {
|
|
90
|
+
const result = await db.prepare(sql).bind(...params).all();
|
|
91
|
+
return (result.results ?? []) as never;
|
|
92
|
+
},
|
|
93
|
+
async batch(statements) {
|
|
94
|
+
if (statements.length === 0) return [];
|
|
95
|
+
if (!db.batch) throw new Error("storage");
|
|
96
|
+
try {
|
|
97
|
+
const results = await db.batch(statements.map((statement) => db.prepare(statement.sql).bind(...statement.params)));
|
|
98
|
+
return results.map((result) => {
|
|
99
|
+
if (!result || typeof result !== "object") return 0;
|
|
100
|
+
const meta = Reflect.get(result, "meta") as { changes?: number } | undefined;
|
|
101
|
+
return Number(meta?.changes ?? 0);
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (lostRace(error)) return statements.map(() => 0);
|
|
105
|
+
throw new Error("storage", { cause: error });
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authenticated owner API for whole-order batch claim and batch read.
|
|
3
|
+
*/
|
|
4
|
+
import type { APIContext } from "astro";
|
|
5
|
+
import { requireAdminApiEntry } from "@/lib/auth/adminApiGuard";
|
|
6
|
+
import {
|
|
7
|
+
fulfillmentResultResponse,
|
|
8
|
+
isOpaqueId,
|
|
9
|
+
openFulfillment,
|
|
10
|
+
parseFulfillmentBody,
|
|
11
|
+
readJsonBody,
|
|
12
|
+
} from "@/lib/fulfillment/fulfillmentService";
|
|
13
|
+
|
|
14
|
+
export const prerender = false;
|
|
15
|
+
|
|
16
|
+
function ownerRefusal(status: number, reason: string): Response {
|
|
17
|
+
return new Response(JSON.stringify({ error: reason }), {
|
|
18
|
+
status,
|
|
19
|
+
headers: {
|
|
20
|
+
"content-type": "application/json; charset=utf-8",
|
|
21
|
+
"cache-control": "no-store",
|
|
22
|
+
"x-robots-tag": "noindex, nofollow",
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function requireOwner(context: APIContext): Promise<Response | null> {
|
|
28
|
+
const access = await requireAdminApiEntry(context, { requireOwner: true });
|
|
29
|
+
if (access.ok) return null;
|
|
30
|
+
return ownerRefusal(access.status, access.reason);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
34
|
+
const refused = await requireOwner(context);
|
|
35
|
+
if (refused) return refused;
|
|
36
|
+
const opened = await openFulfillment(context);
|
|
37
|
+
if (!opened.ok) return opened.response;
|
|
38
|
+
const batchId = new URL(context.request.url).searchParams.get("batchId") ?? "";
|
|
39
|
+
if (!isOpaqueId(batchId)) return fulfillmentResultResponse({ ok: false, code: "not-found" }, () => ({}));
|
|
40
|
+
return fulfillmentResultResponse(await opened.service.batch(opened.actor, batchId), (batch) => batch);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
44
|
+
const refused = await requireOwner(context);
|
|
45
|
+
if (refused) return refused;
|
|
46
|
+
const raw = await readJsonBody(context.request);
|
|
47
|
+
if (!raw.ok) return fulfillmentResultResponse(raw, () => ({}));
|
|
48
|
+
const parsed = parseFulfillmentBody(raw.value);
|
|
49
|
+
if (!parsed.ok) return fulfillmentResultResponse(parsed, () => ({}));
|
|
50
|
+
if (!parsed.value.orderIds || parsed.value.orderIds.length === 0) {
|
|
51
|
+
return fulfillmentResultResponse({ ok: false, code: "malformed" }, () => ({}));
|
|
52
|
+
}
|
|
53
|
+
const opened = await openFulfillment(context, parsed.value.tenantId ?? null);
|
|
54
|
+
if (!opened.ok) return opened.response;
|
|
55
|
+
return fulfillmentResultResponse(
|
|
56
|
+
await opened.service.claim(opened.actor, parsed.value.orderIds, parsed.value.expectedVersions),
|
|
57
|
+
(batch) => batch,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-only retrieval of a stored ship-to address.
|
|
3
|
+
* The read runs only after the owner gate admits the caller.
|
|
4
|
+
*/
|
|
5
|
+
import type { APIContext } from "astro";
|
|
6
|
+
import { requireAdminApiEntry } from "@/lib/auth/adminApiGuard";
|
|
7
|
+
import {
|
|
8
|
+
fulfillmentResultResponse,
|
|
9
|
+
isOpaqueId,
|
|
10
|
+
openFulfillment,
|
|
11
|
+
} from "@/lib/fulfillment/fulfillmentService";
|
|
12
|
+
|
|
13
|
+
export const prerender = false;
|
|
14
|
+
|
|
15
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
16
|
+
const access = await requireAdminApiEntry(context, { requireOwner: true });
|
|
17
|
+
if (!access.ok) {
|
|
18
|
+
return new Response(JSON.stringify({ error: access.reason }), {
|
|
19
|
+
status: access.status,
|
|
20
|
+
headers: {
|
|
21
|
+
"content-type": "application/json; charset=utf-8",
|
|
22
|
+
"cache-control": "no-store",
|
|
23
|
+
"x-robots-tag": "noindex, nofollow",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const orderId = context.params.id ?? "";
|
|
28
|
+
if (!isOpaqueId(orderId)) return fulfillmentResultResponse({ ok: false, code: "not-found" }, () => ({}));
|
|
29
|
+
const opened = await openFulfillment(context);
|
|
30
|
+
if (!opened.ok) return opened.response;
|
|
31
|
+
return fulfillmentResultResponse(await opened.service.readAddress(opened.actor, orderId), (address) => address);
|
|
32
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authenticated owner API for one order's fulfillment commands.
|
|
3
|
+
* The response is state codes and the fulfillment view. It never returns an address.
|
|
4
|
+
*/
|
|
5
|
+
import type { APIContext } from "astro";
|
|
6
|
+
import { requireAdminApiEntry } from "@/lib/auth/adminApiGuard";
|
|
7
|
+
import {
|
|
8
|
+
fulfillmentResultResponse,
|
|
9
|
+
isOpaqueId,
|
|
10
|
+
openFulfillment,
|
|
11
|
+
parseFulfillmentBody,
|
|
12
|
+
readJsonBody,
|
|
13
|
+
type FulfillmentCommandOptions,
|
|
14
|
+
type LabelOutcomeInput,
|
|
15
|
+
} from "@/lib/fulfillment/fulfillmentService";
|
|
16
|
+
|
|
17
|
+
export const prerender = false;
|
|
18
|
+
|
|
19
|
+
function ownerRefusal(status: number, reason: string): Response {
|
|
20
|
+
return new Response(JSON.stringify({ error: reason }), {
|
|
21
|
+
status,
|
|
22
|
+
headers: {
|
|
23
|
+
"content-type": "application/json; charset=utf-8",
|
|
24
|
+
"cache-control": "no-store",
|
|
25
|
+
"x-robots-tag": "noindex, nofollow",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function requireOwner(context: APIContext): Promise<Response | null> {
|
|
31
|
+
const access = await requireAdminApiEntry(context, { requireOwner: true });
|
|
32
|
+
if (access.ok) return null;
|
|
33
|
+
return ownerRefusal(access.status, access.reason);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ACTIONS = new Set([
|
|
37
|
+
"pick",
|
|
38
|
+
"pack",
|
|
39
|
+
"release",
|
|
40
|
+
"hold",
|
|
41
|
+
"clear-hold",
|
|
42
|
+
"cancel",
|
|
43
|
+
"refund",
|
|
44
|
+
"label-outcome",
|
|
45
|
+
"hand-off",
|
|
46
|
+
"return",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function fail(code: string): Response {
|
|
50
|
+
return fulfillmentResultResponse({ ok: false, code }, () => ({}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
54
|
+
const refused = await requireOwner(context);
|
|
55
|
+
if (refused) return refused;
|
|
56
|
+
const orderId = context.params.id ?? "";
|
|
57
|
+
if (!isOpaqueId(orderId)) return fail("not-found");
|
|
58
|
+
const opened = await openFulfillment(context);
|
|
59
|
+
if (!opened.ok) return opened.response;
|
|
60
|
+
return fulfillmentResultResponse(await opened.service.get(opened.actor, orderId), (view) => view);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
64
|
+
const refused = await requireOwner(context);
|
|
65
|
+
if (refused) return refused;
|
|
66
|
+
const orderId = context.params.id ?? "";
|
|
67
|
+
if (!isOpaqueId(orderId)) return fail("not-found");
|
|
68
|
+
const raw = await readJsonBody(context.request);
|
|
69
|
+
if (!raw.ok) return fulfillmentResultResponse(raw, () => ({}));
|
|
70
|
+
const parsed = parseFulfillmentBody(raw.value);
|
|
71
|
+
if (!parsed.ok) return fulfillmentResultResponse(parsed, () => ({}));
|
|
72
|
+
if (!ACTIONS.has(parsed.value.action)) return fail("malformed");
|
|
73
|
+
const opened = await openFulfillment(context, parsed.value.tenantId ?? null);
|
|
74
|
+
if (!opened.ok) return opened.response;
|
|
75
|
+
const { service, actor } = opened;
|
|
76
|
+
const options: FulfillmentCommandOptions = {
|
|
77
|
+
...(parsed.value.expectedVersion !== undefined ? { expectedVersion: parsed.value.expectedVersion } : {}),
|
|
78
|
+
...(parsed.value.scope !== undefined ? { scope: parsed.value.scope } : {}),
|
|
79
|
+
...(parsed.value.carrier !== undefined ? { carrier: parsed.value.carrier } : {}),
|
|
80
|
+
...(parsed.value.lines !== undefined ? { lines: parsed.value.lines } : {}),
|
|
81
|
+
};
|
|
82
|
+
switch (parsed.value.action) {
|
|
83
|
+
case "pick":
|
|
84
|
+
return fulfillmentResultResponse(await service.pick(actor, orderId, options), (view) => view);
|
|
85
|
+
case "pack":
|
|
86
|
+
return fulfillmentResultResponse(await service.pack(actor, orderId, options), (view) => view);
|
|
87
|
+
case "release":
|
|
88
|
+
return fulfillmentResultResponse(await service.release(actor, orderId, options), (view) => view);
|
|
89
|
+
case "hold":
|
|
90
|
+
return fulfillmentResultResponse(await service.hold(actor, orderId, options), (view) => view);
|
|
91
|
+
case "clear-hold":
|
|
92
|
+
return fulfillmentResultResponse(await service.clearOperatorHold(actor, orderId, options), (view) => view);
|
|
93
|
+
case "cancel":
|
|
94
|
+
return fulfillmentResultResponse(await service.requestCancel(actor, orderId, options), (view) => view);
|
|
95
|
+
case "refund":
|
|
96
|
+
return fulfillmentResultResponse(await service.requestRefund(actor, orderId, options), (view) => view);
|
|
97
|
+
case "hand-off":
|
|
98
|
+
return fulfillmentResultResponse(await service.handOff(actor, orderId, options), (view) => view);
|
|
99
|
+
case "return":
|
|
100
|
+
return fulfillmentResultResponse(service.createReturn(actor, orderId), () => ({}));
|
|
101
|
+
case "label-outcome": {
|
|
102
|
+
const outcome = parsed.value.outcome;
|
|
103
|
+
const adultSignature = parsed.value.adultSignature;
|
|
104
|
+
if (outcome !== "purchased" && outcome !== "unknown" && outcome !== "voided") return fail("malformed");
|
|
105
|
+
if (adultSignature !== "accepted" && adultSignature !== "refused" && adultSignature !== "absent") return fail("malformed");
|
|
106
|
+
const input: LabelOutcomeInput = {
|
|
107
|
+
outcome,
|
|
108
|
+
adultSignature,
|
|
109
|
+
...(parsed.value.carrier !== undefined ? { carrier: parsed.value.carrier } : {}),
|
|
110
|
+
...(parsed.value.expectedVersion !== undefined ? { expectedVersion: parsed.value.expectedVersion } : {}),
|
|
111
|
+
};
|
|
112
|
+
return fulfillmentResultResponse(await service.applyLabelOutcome(actor, orderId, input), (view) => view);
|
|
113
|
+
}
|
|
114
|
+
default:
|
|
115
|
+
return fail("malformed");
|
|
116
|
+
}
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.56",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|
package/pnpm-lock.runner.yaml
CHANGED
|
@@ -87,7 +87,7 @@ importers:
|
|
|
87
87
|
version: 4.12.1(playwright-core@1.61.1)
|
|
88
88
|
'@cloudflare/vitest-pool-workers':
|
|
89
89
|
specifier: 0.12.21
|
|
90
|
-
version: 0.12.21(@vitest/runner@2.1.9)(@vitest/snapshot@2.1.9)(vitest@2.1.8(@types/node@24.13.3)(happy-dom@20.
|
|
90
|
+
version: 0.12.21(@vitest/runner@2.1.9)(@vitest/snapshot@2.1.9)(vitest@2.1.8(@types/node@24.13.3)(happy-dom@20.11.2)(lightningcss@1.32.0))
|
|
91
91
|
'@playwright/test':
|
|
92
92
|
specifier: ^1.61.1
|
|
93
93
|
version: 1.61.1
|
|
@@ -106,6 +106,9 @@ importers:
|
|
|
106
106
|
esbuild:
|
|
107
107
|
specifier: 0.28.1
|
|
108
108
|
version: 0.28.1
|
|
109
|
+
happy-dom:
|
|
110
|
+
specifier: 20.11.2
|
|
111
|
+
version: 20.11.2
|
|
109
112
|
tsx:
|
|
110
113
|
specifier: ^4.22.4
|
|
111
114
|
version: 4.22.4
|
|
@@ -114,7 +117,7 @@ importers:
|
|
|
114
117
|
version: 5.9.3
|
|
115
118
|
vitest:
|
|
116
119
|
specifier: 2.1.8
|
|
117
|
-
version: 2.1.8(@types/node@24.13.3)(happy-dom@20.
|
|
120
|
+
version: 2.1.8(@types/node@24.13.3)(happy-dom@20.11.2)(lightningcss@1.32.0)
|
|
118
121
|
wrangler:
|
|
119
122
|
specifier: ^4.105.0
|
|
120
123
|
version: 4.105.0
|
|
@@ -139,7 +142,7 @@ importers:
|
|
|
139
142
|
version: 5.9.3
|
|
140
143
|
vitest:
|
|
141
144
|
specifier: ^2.1.8
|
|
142
|
-
version: 2.1.8(@types/node@24.13.3)(happy-dom@20.
|
|
145
|
+
version: 2.1.8(@types/node@24.13.3)(happy-dom@20.11.2)(lightningcss@1.32.0)
|
|
143
146
|
|
|
144
147
|
packages:
|
|
145
148
|
|
|
@@ -2789,8 +2792,8 @@ packages:
|
|
|
2789
2792
|
h3@1.15.11:
|
|
2790
2793
|
resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==}
|
|
2791
2794
|
|
|
2792
|
-
happy-dom@20.
|
|
2793
|
-
resolution: {integrity: sha512-
|
|
2795
|
+
happy-dom@20.11.2:
|
|
2796
|
+
resolution: {integrity: sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw==}
|
|
2794
2797
|
engines: {node: '>=20.0.0'}
|
|
2795
2798
|
|
|
2796
2799
|
has-flag@4.0.0:
|
|
@@ -4437,14 +4440,14 @@ snapshots:
|
|
|
4437
4440
|
- utf-8-validate
|
|
4438
4441
|
- workerd
|
|
4439
4442
|
|
|
4440
|
-
'@cloudflare/vitest-pool-workers@0.12.21(@vitest/runner@2.1.9)(@vitest/snapshot@2.1.9)(vitest@2.1.8(@types/node@24.13.3)(happy-dom@20.
|
|
4443
|
+
'@cloudflare/vitest-pool-workers@0.12.21(@vitest/runner@2.1.9)(@vitest/snapshot@2.1.9)(vitest@2.1.8(@types/node@24.13.3)(happy-dom@20.11.2)(lightningcss@1.32.0))':
|
|
4441
4444
|
dependencies:
|
|
4442
4445
|
'@vitest/runner': 2.1.9
|
|
4443
4446
|
'@vitest/snapshot': 2.1.9
|
|
4444
4447
|
cjs-module-lexer: 1.4.3
|
|
4445
4448
|
esbuild: 0.27.3
|
|
4446
4449
|
miniflare: 4.20260310.0
|
|
4447
|
-
vitest: 2.1.8(@types/node@24.13.3)(happy-dom@20.
|
|
4450
|
+
vitest: 2.1.8(@types/node@24.13.3)(happy-dom@20.11.2)(lightningcss@1.32.0)
|
|
4448
4451
|
wrangler: 4.72.0
|
|
4449
4452
|
transitivePeerDependencies:
|
|
4450
4453
|
- '@cloudflare/workers-types'
|
|
@@ -5372,11 +5375,11 @@ snapshots:
|
|
|
5372
5375
|
'@tiptap/extension-text': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
|
|
5373
5376
|
'@tiptap/extension-text-style': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
|
|
5374
5377
|
'@tiptap/extension-underline': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))
|
|
5375
|
-
'@tiptap/html': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(happy-dom@20.
|
|
5378
|
+
'@tiptap/html': 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(happy-dom@20.11.2)
|
|
5376
5379
|
'@tiptap/pm': 3.27.1
|
|
5377
5380
|
'@tiptap/suggestion': 3.27.1(@floating-ui/dom@1.7.6)(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)
|
|
5378
5381
|
emojibase: 17.0.0
|
|
5379
|
-
happy-dom: 20.
|
|
5382
|
+
happy-dom: 20.11.2
|
|
5380
5383
|
markdown-it: 14.2.0
|
|
5381
5384
|
transitivePeerDependencies:
|
|
5382
5385
|
- '@floating-ui/dom'
|
|
@@ -5558,11 +5561,11 @@ snapshots:
|
|
|
5558
5561
|
dependencies:
|
|
5559
5562
|
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
|
|
5560
5563
|
|
|
5561
|
-
'@tiptap/html@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(happy-dom@20.
|
|
5564
|
+
'@tiptap/html@3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1))(@tiptap/pm@3.27.1)(happy-dom@20.11.2)':
|
|
5562
5565
|
dependencies:
|
|
5563
5566
|
'@tiptap/core': 3.27.1(@tiptap/pm@3.27.1)
|
|
5564
5567
|
'@tiptap/pm': 3.27.1
|
|
5565
|
-
happy-dom: 20.
|
|
5568
|
+
happy-dom: 20.11.2
|
|
5566
5569
|
|
|
5567
5570
|
'@tiptap/pm@3.27.1':
|
|
5568
5571
|
dependencies:
|
|
@@ -6349,7 +6352,7 @@ snapshots:
|
|
|
6349
6352
|
ufo: 1.6.4
|
|
6350
6353
|
uncrypto: 0.1.3
|
|
6351
6354
|
|
|
6352
|
-
happy-dom@20.
|
|
6355
|
+
happy-dom@20.11.2:
|
|
6353
6356
|
dependencies:
|
|
6354
6357
|
'@types/node': 24.13.3
|
|
6355
6358
|
'@types/whatwg-mimetype': 3.0.2
|
|
@@ -7330,7 +7333,7 @@ snapshots:
|
|
|
7330
7333
|
optionalDependencies:
|
|
7331
7334
|
vite: 8.1.0(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)
|
|
7332
7335
|
|
|
7333
|
-
vitest@2.1.8(@types/node@24.13.3)(happy-dom@20.
|
|
7336
|
+
vitest@2.1.8(@types/node@24.13.3)(happy-dom@20.11.2)(lightningcss@1.32.0):
|
|
7334
7337
|
dependencies:
|
|
7335
7338
|
'@vitest/expect': 2.1.8
|
|
7336
7339
|
'@vitest/mocker': 2.1.8(vite@5.4.21(@types/node@24.13.3)(lightningcss@1.32.0))
|
|
@@ -7354,7 +7357,7 @@ snapshots:
|
|
|
7354
7357
|
why-is-node-running: 2.3.0
|
|
7355
7358
|
optionalDependencies:
|
|
7356
7359
|
'@types/node': 24.13.3
|
|
7357
|
-
happy-dom: 20.
|
|
7360
|
+
happy-dom: 20.11.2
|
|
7358
7361
|
transitivePeerDependencies:
|
|
7359
7362
|
- less
|
|
7360
7363
|
- lightningcss
|