@tokenoftrust/storefront-runner 2.4.63 → 2.4.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/storefront/src/lib/admin/services/mock/index.ts +0 -1
- package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +4 -114
- package/apps/storefront/src/lib/checkoutCommerce.ts +21 -1
- package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +7 -3
- package/apps/storefront/src/pages/products/[handle].astro +7 -12
- package/package.json +1 -1
- package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +0 -19
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export { getOrdersViewModel } from "./orders";
|
|
2
2
|
export { getProductsViewModel } from "./products";
|
|
3
3
|
export { getCustomersViewModel } from "./customers";
|
|
4
|
-
export { getFulfillmentViewModel } from "./fulfillment";
|
|
5
4
|
export { getReportingViewModel } from "./reporting";
|
|
6
5
|
export { getSettingsViewModel } from "./settings";
|
|
7
6
|
export { getCheckoutSettingsViewModel } from "./checkoutSettings";
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Daily batch desk. Queues, evidence, and commands are a pure projection of the
|
|
3
|
-
* commerce contract.
|
|
4
|
-
*
|
|
5
|
-
* log never carry a destination.
|
|
3
|
+
* commerce contract. Print payload and tracking links are owner-only. Public
|
|
4
|
+
* events and the desk log never carry a destination.
|
|
6
5
|
*/
|
|
7
6
|
import {
|
|
8
7
|
actorMayCommand,
|
|
@@ -224,25 +223,6 @@ export interface WorkspaceCommand {
|
|
|
224
223
|
quantityInvalid: boolean;
|
|
225
224
|
lineId: boolean;
|
|
226
225
|
exception: string | null;
|
|
227
|
-
accountConfigured: boolean | null;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export interface WorkspaceHttpRequest {
|
|
231
|
-
method: string;
|
|
232
|
-
url: string;
|
|
233
|
-
body: string | null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export interface WorkspaceHttpResponse {
|
|
237
|
-
status: number;
|
|
238
|
-
contentType: string;
|
|
239
|
-
body: string;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export interface TestFulfillmentAdapter {
|
|
243
|
-
readonly livePurchase: false;
|
|
244
|
-
state(): WorkspaceState;
|
|
245
|
-
handle(request: WorkspaceHttpRequest): WorkspaceHttpResponse;
|
|
246
226
|
}
|
|
247
227
|
|
|
248
228
|
export function defaultWorkspaceQuery(): WorkspaceQuery {
|
|
@@ -842,17 +822,6 @@ export function applyWorkspaceCommand(input: WorkspaceState, command: WorkspaceC
|
|
|
842
822
|
ids.map((orderId) => blankResult(command.command, orderId, "refused", "partial-shipment")),
|
|
843
823
|
);
|
|
844
824
|
}
|
|
845
|
-
if (command.command === "set-carrier-account") {
|
|
846
|
-
const carrierReason = explicitCarrierReason(command.carrier);
|
|
847
|
-
if (carrierReason) return finishCommand(state, command.actorName, [blankResult(command.command, "", "refused", carrierReason)]);
|
|
848
|
-
if (command.accountConfigured === null) {
|
|
849
|
-
return finishCommand(state, command.actorName, [blankResult(command.command, "", "refused", "carrier-account")]);
|
|
850
|
-
}
|
|
851
|
-
state.carrierAccountConfigured = command.accountConfigured;
|
|
852
|
-
return finishCommand(state, command.actorName, [
|
|
853
|
-
blankResult(command.command, "", "admitted", command.accountConfigured ? "configured" : "missing"),
|
|
854
|
-
]);
|
|
855
|
-
}
|
|
856
825
|
if (command.orderIds.length === 0) {
|
|
857
826
|
return finishCommand(state, command.actorName, [blankResult(command.command, "", "refused", "empty-batch")]);
|
|
858
827
|
}
|
|
@@ -883,7 +852,6 @@ const COMMANDS = new Set([
|
|
|
883
852
|
"pick-batch",
|
|
884
853
|
"packing-slip",
|
|
885
854
|
"record-exception",
|
|
886
|
-
"set-carrier-account",
|
|
887
855
|
"refund",
|
|
888
856
|
"return",
|
|
889
857
|
]);
|
|
@@ -922,8 +890,6 @@ export function parseWorkspaceCommand(params: URLSearchParams): { ok: true; comm
|
|
|
922
890
|
else quantity = Number(raw);
|
|
923
891
|
}
|
|
924
892
|
}
|
|
925
|
-
const configuredRaw = params.get("configured");
|
|
926
|
-
const accountConfigured = configuredRaw === "1" ? true : configuredRaw === "0" ? false : null;
|
|
927
893
|
const carrierRaw = params.get("carrier");
|
|
928
894
|
const purchaseRaw = params.get("purchaseKey");
|
|
929
895
|
return {
|
|
@@ -938,66 +904,6 @@ export function parseWorkspaceCommand(params: URLSearchParams): { ok: true; comm
|
|
|
938
904
|
quantityInvalid,
|
|
939
905
|
lineId: params.has("lineId"),
|
|
940
906
|
exception: params.get("exception"),
|
|
941
|
-
accountConfigured,
|
|
942
|
-
},
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
export function handleWorkspaceRequest(
|
|
947
|
-
state: WorkspaceState,
|
|
948
|
-
request: WorkspaceHttpRequest,
|
|
949
|
-
): { state: WorkspaceState; response: WorkspaceHttpResponse } {
|
|
950
|
-
let url: URL;
|
|
951
|
-
try {
|
|
952
|
-
url = new URL(request.url, "http://127.0.0.1");
|
|
953
|
-
} catch {
|
|
954
|
-
return { state, response: { status: 400, contentType: "text/plain; charset=utf-8", body: "bad url" } };
|
|
955
|
-
}
|
|
956
|
-
if (url.pathname !== SCRAPPY_FULFILLMENT_PATH) {
|
|
957
|
-
return { state, response: { status: 404, contentType: "text/plain; charset=utf-8", body: "not found" } };
|
|
958
|
-
}
|
|
959
|
-
const method = request.method.toUpperCase();
|
|
960
|
-
if (method === "GET") {
|
|
961
|
-
const view = projectWorkspace(state, parseWorkspaceQuery(url.searchParams));
|
|
962
|
-
return {
|
|
963
|
-
state,
|
|
964
|
-
response: { status: 200, contentType: "text/html; charset=utf-8", body: renderFulfillmentDocument(view) },
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
if (method === "POST") {
|
|
968
|
-
const params = new URLSearchParams(request.body ?? "");
|
|
969
|
-
const parsed = parseWorkspaceCommand(params);
|
|
970
|
-
if (!parsed.ok) {
|
|
971
|
-
return {
|
|
972
|
-
state,
|
|
973
|
-
response: {
|
|
974
|
-
status: 400,
|
|
975
|
-
contentType: "text/html; charset=utf-8",
|
|
976
|
-
body: `<!doctype html><p data-command-error="${esc(parsed.error)}">Unknown command.</p>`,
|
|
977
|
-
},
|
|
978
|
-
};
|
|
979
|
-
}
|
|
980
|
-
const next = applyWorkspaceCommand(state, parsed.command);
|
|
981
|
-
const query = parseWorkspaceQuery(params);
|
|
982
|
-
query.cursor = null;
|
|
983
|
-
const view = projectWorkspace(next, query);
|
|
984
|
-
return {
|
|
985
|
-
state: next,
|
|
986
|
-
response: { status: 200, contentType: "text/html; charset=utf-8", body: renderFulfillmentDocument(view) },
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
return { state, response: { status: 405, contentType: "text/plain; charset=utf-8", body: "method" } };
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
export function createTestFulfillmentAdapter(tenantId = "synthetic-tenant", stage: CommerceStage = "preview"): TestFulfillmentAdapter {
|
|
993
|
-
let state = initialWorkspaceState(tenantId, stage);
|
|
994
|
-
return {
|
|
995
|
-
livePurchase: false,
|
|
996
|
-
state: () => state,
|
|
997
|
-
handle(request) {
|
|
998
|
-
const outcome = handleWorkspaceRequest(state, request);
|
|
999
|
-
state = outcome.state;
|
|
1000
|
-
return outcome.response;
|
|
1001
907
|
},
|
|
1002
908
|
};
|
|
1003
909
|
}
|
|
@@ -1041,10 +947,6 @@ function hiddenFields(view: FulfillmentViewModel, command: string, extras: Array
|
|
|
1041
947
|
.join("");
|
|
1042
948
|
}
|
|
1043
949
|
|
|
1044
|
-
function resultItem(result: WorkspaceResult): string {
|
|
1045
|
-
return `<li data-result-order="${esc(result.orderId)}" data-result-command="${esc(result.command)}" data-result-status="${result.status}" data-result-reason="${esc(result.reason)}" data-cost-minor="${result.costMinor ?? ""}" data-purchase-id="${esc(result.purchaseId ?? "")}" data-live-purchase="false" data-release-reservation="${result.releaseReservation ? "true" : "false"}" data-restock="false" data-blind-repurchase="false">${esc(result.command)} ${esc(result.orderId)} ${result.status} ${esc(result.reason)}</li>`;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
950
|
function orderActions(view: FulfillmentViewModel, order: WorkspaceOrderView, action: string): string {
|
|
1049
951
|
const purchaseExtras: Array<[string, string]> = [["orderId", order.id]];
|
|
1050
952
|
if (order.purchaseKey !== null) purchaseExtras.push(["purchaseKey", order.purchaseKey]);
|
|
@@ -1075,10 +977,6 @@ export function renderFulfillmentWorkspace(view: FulfillmentViewModel): string {
|
|
|
1075
977
|
return `<a data-queue-link="${queue}" href="${esc(href(view, { queue, cursor: null }))}">${esc(label)}${count}</a>`;
|
|
1076
978
|
})
|
|
1077
979
|
.join("");
|
|
1078
|
-
const actorLinks = ACTOR_SPECS.map(
|
|
1079
|
-
(spec) =>
|
|
1080
|
-
`<a data-actor-link="${spec.name}" href="${esc(href(view, { actorName: spec.name, cursor: null }))}">${esc(spec.name)}</a>`,
|
|
1081
|
-
).join("");
|
|
1082
980
|
const action = esc(href(view, { cursor: null }));
|
|
1083
981
|
const rows = view.orders
|
|
1084
982
|
.map((order) => {
|
|
@@ -1134,12 +1032,11 @@ export function renderFulfillmentWorkspace(view: FulfillmentViewModel): string {
|
|
|
1134
1032
|
? `<a data-page-next href="${esc(href(view, { cursor: view.nextCursor }))}">Next page</a>`
|
|
1135
1033
|
: "";
|
|
1136
1034
|
const account = view.carrierAccountConfigured ? "configured" : "missing";
|
|
1137
|
-
return `<div data-scrappy-batch data-target-tenant="${esc(view.targetTenant)}" data-
|
|
1035
|
+
return `<div data-scrappy-batch data-target-tenant="${esc(view.targetTenant)}" data-live-purchase="false" data-carrier-account="${account}" data-prohibited-carrier="USPS" data-adult-signature-required="true" data-partial-shipment="unavailable" data-returns="unavailable" data-blind-repurchase="false" data-actor="${esc(view.actorName)}" data-actor-role="${esc(view.actorRole)}" data-storewide-ready="${view.storewide.ready}" data-storewide-held="${view.storewide.held}" data-storewide-in-progress="${view.storewide.in_progress}" data-storewide-shipped="${view.storewide.shipped}" data-filter-count="${view.filterCount}" data-page-count="${view.pageCount}" data-queue="${esc(view.queue)}">
|
|
1138
1036
|
<h2 id="fulfillment-title">Daily batch</h2>
|
|
1139
|
-
<p>
|
|
1037
|
+
<p>Not a live label purchase. Carrier account is configuration. USPS is prohibited. Adult signature is required. Shipment is the whole order. Partial shipment is not available. Returns are not automated.</p>
|
|
1140
1038
|
<p data-carrier-account-text>${view.carrierAccountConfigured ? "Carrier account: configured." : "Carrier account: not configured. Do not choose a carrier."}</p>
|
|
1141
1039
|
<p><a data-refresh href="${esc(href(view))}">Refresh</a></p>
|
|
1142
|
-
<nav data-actor-switch>${actorLinks}</nav>
|
|
1143
1040
|
<nav data-queue-switch>${queueLinks}</nav>
|
|
1144
1041
|
<form method="get" action="${SCRAPPY_FULFILLMENT_PATH}">
|
|
1145
1042
|
<input type="hidden" name="actor" value="${esc(view.actorName)}">
|
|
@@ -1164,12 +1061,5 @@ export function renderFulfillmentWorkspace(view: FulfillmentViewModel): string {
|
|
|
1164
1061
|
${next}
|
|
1165
1062
|
<section data-pick-list><h3>Pick list</h3><ul>${pickItems}</ul></section>
|
|
1166
1063
|
${details}
|
|
1167
|
-
<ol data-last-results>${view.lastResults.map(resultItem).join("")}</ol>
|
|
1168
|
-
<section data-public-events>${view.events.map((event) => `<pre>${esc(JSON.stringify(event))}</pre>`).join("")}</section>
|
|
1169
|
-
<pre data-adapter-log>${esc(view.log.join("\n"))}</pre>
|
|
1170
1064
|
</div>`;
|
|
1171
1065
|
}
|
|
1172
|
-
|
|
1173
|
-
export function renderFulfillmentDocument(view: FulfillmentViewModel): string {
|
|
1174
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Daily batch</title></head><body><section id="fulfillment" data-admin-panel data-agent-scope="fulfillment">${renderFulfillmentWorkspace(view)}</section></body></html>`;
|
|
1175
|
-
}
|
|
@@ -87,10 +87,30 @@ export async function readCartSecret(
|
|
|
87
87
|
* but the secret is missing, so NO cart may be built. The caller must render
|
|
88
88
|
* no buy form (never a tamperable / broken one).
|
|
89
89
|
*/
|
|
90
|
+
/** Shopper copy when checkout cannot be signed. No operator setup detail. */
|
|
91
|
+
export const CHECKOUT_UNAVAILABLE_SHOPPER_MESSAGE = "Checkout is unavailable.";
|
|
92
|
+
|
|
93
|
+
/** Product-page alert. The cart signing env var name stays in the server log. */
|
|
94
|
+
export function shopperCheckoutUnavailableHtml(): string {
|
|
95
|
+
return (
|
|
96
|
+
`<p class="mb-3 text-sm text-[var(--color-text)]" role="alert" data-cart-operator-error>` +
|
|
97
|
+
`${CHECKOUT_UNAVAILABLE_SHOPPER_MESSAGE}</p>`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Message safe to put in an HTTP body. `signing-unavailable` carries operator
|
|
103
|
+
* setup text from the cart runtime; shoppers get the neutral message instead.
|
|
104
|
+
*/
|
|
105
|
+
export function shopperCartDenialMessage(code: string, message: string): string {
|
|
106
|
+
if (code === "signing-unavailable") return CHECKOUT_UNAVAILABLE_SHOPPER_MESSAGE;
|
|
107
|
+
return message;
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
export interface CartSigningResolution {
|
|
91
111
|
enabled: boolean;
|
|
92
112
|
secret?: string;
|
|
93
|
-
/**
|
|
113
|
+
/** Operator setup detail. Do not write this into shopper HTML or an HTTP body. */
|
|
94
114
|
operatorError?: string;
|
|
95
115
|
}
|
|
96
116
|
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
type CartDenialCode,
|
|
27
27
|
} from "@tot/public-runtime";
|
|
28
28
|
import { resolveCheckoutHandoffCorrelation } from "@/lib/commerce/checkoutHandoffCorrelation";
|
|
29
|
-
import { resolveCartSigning } from "@/lib/checkoutCommerce";
|
|
29
|
+
import { resolveCartSigning, shopperCartDenialMessage } from "@/lib/checkoutCommerce";
|
|
30
30
|
|
|
31
31
|
export const prerender = false;
|
|
32
32
|
|
|
@@ -88,7 +88,11 @@ async function cartFormPost(context: APIContext): Promise<Response> {
|
|
|
88
88
|
}
|
|
89
89
|
const signing = await resolveCartSigning(commerce);
|
|
90
90
|
if (!signing.enabled) {
|
|
91
|
-
return cartDenial(
|
|
91
|
+
return cartDenial(
|
|
92
|
+
"signing-unavailable",
|
|
93
|
+
shopperCartDenialMessage("signing-unavailable", signing.operatorError ?? ""),
|
|
94
|
+
503,
|
|
95
|
+
);
|
|
92
96
|
}
|
|
93
97
|
const form = await context.request.formData();
|
|
94
98
|
const entries: Array<[string, string]> = [];
|
|
@@ -103,7 +107,7 @@ async function cartFormPost(context: APIContext): Promise<Response> {
|
|
|
103
107
|
});
|
|
104
108
|
if (!result.ok) {
|
|
105
109
|
const status = result.code === "signing-unavailable" ? 503 : 422;
|
|
106
|
-
return cartDenial(result.code, result.message, status);
|
|
110
|
+
return cartDenial(result.code, shopperCartDenialMessage(result.code, result.message), status);
|
|
107
111
|
}
|
|
108
112
|
return new Response(null, {
|
|
109
113
|
status: 303,
|
|
@@ -37,7 +37,7 @@ import type {
|
|
|
37
37
|
MembershipStackingPolicy,
|
|
38
38
|
PurchaseOptionPricingContext,
|
|
39
39
|
} from "@tot/public-runtime";
|
|
40
|
-
import { resolveCartSigning } from "@/lib/checkoutCommerce";
|
|
40
|
+
import { resolveCartSigning, shopperCheckoutUnavailableHtml } from "@/lib/checkoutCommerce";
|
|
41
41
|
import { isRegulatedTenant } from "@/lib/compliance/enforcement";
|
|
42
42
|
import { resolveMemberContext } from "@/lib/membership/eligibility";
|
|
43
43
|
import { truncate } from "@/lib/format";
|
|
@@ -223,14 +223,14 @@ let checkoutCart:
|
|
|
223
223
|
byVariantId: Record<string, Record<string, VariantCartPayload>>;
|
|
224
224
|
}
|
|
225
225
|
| undefined;
|
|
226
|
-
let
|
|
226
|
+
let checkoutUnavailable = false;
|
|
227
227
|
if (commerce) {
|
|
228
228
|
// Fail closed for production signed checkout: a signed tenant with no
|
|
229
229
|
// cart-validation secret builds NO cart (never a tamperable form). See
|
|
230
230
|
// resolveCartSigning — it also logs the misconfiguration loudly.
|
|
231
231
|
const signing = await resolveCartSigning(commerce);
|
|
232
232
|
if (!signing.enabled) {
|
|
233
|
-
|
|
233
|
+
checkoutUnavailable = true;
|
|
234
234
|
} else {
|
|
235
235
|
try {
|
|
236
236
|
const purchaseOptions = visiblePurchaseOptions(
|
|
@@ -265,10 +265,9 @@ if (commerce) {
|
|
|
265
265
|
byVariantId: Object.fromEntries(entries),
|
|
266
266
|
};
|
|
267
267
|
} catch (err) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
: undefined;
|
|
268
|
+
if (err instanceof Error && err.name === "CartSigningUnavailableError") {
|
|
269
|
+
checkoutUnavailable = true;
|
|
270
|
+
}
|
|
272
271
|
console.warn(
|
|
273
272
|
`[storefront] Invalid checkout purchase options for ${product.handle}; cart disabled for this product.`,
|
|
274
273
|
);
|
|
@@ -386,11 +385,7 @@ const jsonLd = [productJsonLd, breadcrumbLd(crumbs, canonicalBase)];
|
|
|
386
385
|
The form action is the worker cart route, so a native submit reaches
|
|
387
386
|
the server check. A clear browser preflight is not payment authority. */}
|
|
388
387
|
<div class="mt-7" data-payment-authority="none">
|
|
389
|
-
{
|
|
390
|
-
<p class="mb-3 text-sm text-[var(--color-text)]" role="alert" data-cart-operator-error>
|
|
391
|
-
{cartOperatorError}
|
|
392
|
-
</p>
|
|
393
|
-
)}
|
|
388
|
+
{checkoutUnavailable && <Fragment set:html={shopperCheckoutUnavailableHtml()} />}
|
|
394
389
|
<VariantSelector
|
|
395
390
|
client:load
|
|
396
391
|
variants={publicSelectorVariants(product.variants)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.65",
|
|
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",
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { AdminTenantEnvelope } from "../../envelope";
|
|
2
|
-
import { memoizeByEnvelope } from "../../memoizeByEnvelope";
|
|
3
|
-
import {
|
|
4
|
-
defaultWorkspaceQuery,
|
|
5
|
-
initialWorkspaceState,
|
|
6
|
-
projectWorkspace,
|
|
7
|
-
type FulfillmentViewModel,
|
|
8
|
-
} from "../../viewmodels/fulfillment";
|
|
9
|
-
|
|
10
|
-
/** Initial test-adapter snapshot. Not a seeded queue and not a live order feed. */
|
|
11
|
-
export function readTestAdapterSnapshot(targetTenant: string): FulfillmentViewModel {
|
|
12
|
-
return projectWorkspace(initialWorkspaceState(targetTenant), defaultWorkspaceQuery());
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function computeFulfillmentViewModel(envelope: AdminTenantEnvelope): FulfillmentViewModel {
|
|
16
|
-
return readTestAdapterSnapshot(envelope.targetTenant);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const getFulfillmentViewModel = memoizeByEnvelope(computeFulfillmentViewModel);
|