@tokenoftrust/storefront-runner 2.4.54 → 2.4.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/storefront/src/lib/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
|
@@ -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.55",
|
|
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",
|