@tokenoftrust/storefront-runner 2.4.90 → 2.5.0
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/.runner-dependency-cruiser.cjs +38 -0
- package/apps/storefront/migrations-apps/0017_commerce_durable_projection.sql +28 -0
- package/apps/storefront/migrations-apps/0018_commerce_provider_cart_identity.sql +13 -0
- package/apps/storefront/package.json +2 -1
- package/apps/storefront/src/config/declaredTenantConfig.ts +5 -0
- package/apps/storefront/src/config/devTenantSeed.ts +3 -2
- package/apps/storefront/src/lib/checkout/commerce/commerceAdmission.ts +108 -0
- package/apps/storefront/src/lib/checkout/commerce/finalCartAuthorization.ts +1 -1
- package/apps/storefront/src/lib/checkout/shared/checkoutCommerce.ts +6 -4
- package/apps/storefront/src/lib/fulfillment/admin/commerceDesk.ts +111 -0
- package/apps/storefront/src/lib/fulfillment/admin/viewmodels/fulfillment.ts +1 -1
- package/apps/storefront/src/lib/fulfillment/commerceProjection.ts +146 -0
- package/apps/storefront/src/lib/fulfillment/commerceReadback.ts +60 -0
- package/apps/storefront/src/lib/fulfillment/commerceRecovery.ts +65 -0
- package/apps/storefront/src/lib/fulfillment/commerceRuntime.ts +13 -0
- package/apps/storefront/src/lib/fulfillment/fulfillmentService.ts +50 -4
- package/apps/storefront/src/lib/fulfillment/readiness.ts +1 -0
- package/apps/storefront/src/lib/fulfillment/store.ts +15 -0
- package/apps/storefront/src/lib/integrations/webhooks/deliveryEngine.ts +6 -3
- package/apps/storefront/src/lib/integrations/webhooks/deliveryStore.ts +6 -5
- package/apps/storefront/src/lib/inventory/allocations.ts +58 -9
- package/apps/storefront/src/lib/inventory/mirrorStock.ts +121 -0
- package/apps/storefront/src/lib/orders/admin/services/orders.ts +8 -8
- package/apps/storefront/src/lib/orders/data/orderMapper.ts +22 -1
- package/apps/storefront/src/lib/orders/data/ordersStore.ts +12 -4
- package/apps/storefront/src/pages/api/admin/commerce-readiness.ts +56 -26
- package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +34 -2
- package/apps/storefront/src/pages/api/commerce/final-cart-authorization.ts +158 -306
- package/apps/storefront/src/pages/products/[handle].astro +13 -8
- package/apps/storefront/test/pipeline/harness.ts +6 -0
- package/apps/storefront/test/pipeline/pipeline-host.ts +29 -0
- package/package.json +1 -1
- package/packages/public-runtime/src/checkout-orders.ts +45 -7
- package/packages/public-runtime/src/checkout.ts +37 -9
- package/packages/public-runtime/src/tenant.ts +3 -0
|
@@ -136,6 +136,10 @@ const UNKNOWN_EVIDENCE: CommerceEvidence = {
|
|
|
136
136
|
};
|
|
137
137
|
|
|
138
138
|
export interface CommerceSourceEvent {
|
|
139
|
+
lines?: readonly { sku: string; quantity: number; unitMinor: number }[];
|
|
140
|
+
admissionReference?: string;
|
|
141
|
+
providerCartDigest?: string;
|
|
142
|
+
destinationDigest?: string;
|
|
139
143
|
eventVersion: number;
|
|
140
144
|
command: CommerceCommandName;
|
|
141
145
|
orderId: string;
|
|
@@ -219,7 +223,7 @@ function readEvidence(value: unknown): CommerceEvidence {
|
|
|
219
223
|
if (!isRecord(value)) return { ...UNKNOWN_EVIDENCE };
|
|
220
224
|
const pick = (key: keyof CommerceEvidence): CommerceEvidence[keyof CommerceEvidence] => {
|
|
221
225
|
const found = value[key];
|
|
222
|
-
if (found === "fresh" || found === "UNKNOWN" || found === "stale" || found === "unreadable" || found === "absent") {
|
|
226
|
+
if (found === "fresh" || found === "not-required" || found === "UNKNOWN" || found === "stale" || found === "unreadable" || found === "absent") {
|
|
223
227
|
return found;
|
|
224
228
|
}
|
|
225
229
|
return "UNKNOWN";
|
|
@@ -260,7 +264,19 @@ export function normalizeCommerceEvent(value: unknown): CommerceSourceEvent | "m
|
|
|
260
264
|
if (!(STOCK_STATES as readonly string[]).includes(stock)) return "malformed";
|
|
261
265
|
if (!(FULFILLMENT_STATES as readonly string[]).includes(fulfillment)) return "malformed";
|
|
262
266
|
if (!(LABEL_STATES as readonly string[]).includes(label)) return "malformed";
|
|
267
|
+
const lines = value.lines;
|
|
268
|
+
if (lines !== undefined && (!Array.isArray(lines) || lines.some(line => !isRecord(line) ||
|
|
269
|
+
typeof line.sku !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(line.sku) ||
|
|
270
|
+
!Number.isSafeInteger(line.quantity) || Number(line.quantity) < 1 ||
|
|
271
|
+
!Number.isSafeInteger(line.unitMinor) || Number(line.unitMinor) < 0))) return "malformed";
|
|
272
|
+
for (const key of ["admissionReference", "providerCartDigest", "destinationDigest"] as const) {
|
|
273
|
+
if (value[key] !== undefined && (typeof value[key] !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value[key] as string))) return "malformed";
|
|
274
|
+
}
|
|
263
275
|
return {
|
|
276
|
+
...(lines !== undefined ? { lines: lines as {sku:string;quantity:number;unitMinor:number}[] } : {}),
|
|
277
|
+
...(typeof value.admissionReference === "string" ? { admissionReference:value.admissionReference } : {}),
|
|
278
|
+
...(typeof value.providerCartDigest === "string" ? { providerCartDigest:value.providerCartDigest } : {}),
|
|
279
|
+
...(typeof value.destinationDigest === "string" ? { destinationDigest:value.destinationDigest } : {}),
|
|
264
280
|
eventVersion,
|
|
265
281
|
command,
|
|
266
282
|
orderId,
|
|
@@ -351,6 +367,11 @@ export function foldCommerceEvents(events: readonly CommerceSourceEvent[]): Comm
|
|
|
351
367
|
payment = "captured";
|
|
352
368
|
paymentVersion = event.eventVersion;
|
|
353
369
|
}
|
|
370
|
+
if (event.command === "capture" && event.eventVersion >= fulfillmentVersion &&
|
|
371
|
+
fulfillment !== "cancelled" && fulfillment !== "handed_off") {
|
|
372
|
+
fulfillment = event.fulfillment;
|
|
373
|
+
fulfillmentVersion = event.eventVersion;
|
|
374
|
+
}
|
|
354
375
|
if (event.command === "refund" && event.eventVersion >= paymentVersion) {
|
|
355
376
|
const decision = decideCommerceCommand({
|
|
356
377
|
command: "refund",
|
|
@@ -335,13 +335,21 @@ export class ScrappyOrderLedger {
|
|
|
335
335
|
source_order_id = excluded.source_order_id,
|
|
336
336
|
currency = excluded.currency,
|
|
337
337
|
placed_at = excluded.placed_at,
|
|
338
|
-
payment_state =
|
|
338
|
+
payment_state = CASE WHEN EXISTS(SELECT 1 FROM scrappy_shipment_audit a WHERE a.tenant_id=scrappy_commerce_orders.tenant_id
|
|
339
|
+
AND a.stage=scrappy_commerce_orders.stage AND a.order_id=scrappy_commerce_orders.order_id) AND scrappy_commerce_orders.payment_state IN ('refund_requested','refund_unknown','refunded','voided')
|
|
340
|
+
THEN scrappy_commerce_orders.payment_state ELSE excluded.payment_state END,
|
|
339
341
|
compliance_state = excluded.compliance_state,
|
|
340
342
|
stock_state = excluded.stock_state,
|
|
341
|
-
fulfillment_state =
|
|
342
|
-
|
|
343
|
+
fulfillment_state = CASE WHEN scrappy_commerce_orders.fulfillment_state IN ('handed_off','cancelled')
|
|
344
|
+
THEN scrappy_commerce_orders.fulfillment_state
|
|
345
|
+
WHEN EXISTS(SELECT 1 FROM scrappy_shipment_audit a WHERE a.tenant_id=scrappy_commerce_orders.tenant_id
|
|
346
|
+
AND a.stage=scrappy_commerce_orders.stage AND a.order_id=scrappy_commerce_orders.order_id)
|
|
347
|
+
THEN 'held' ELSE excluded.fulfillment_state END,
|
|
348
|
+
label_state = CASE WHEN scrappy_commerce_orders.label_state IN ('intent','purchased','printed','void_requested','UNKNOWN')
|
|
349
|
+
THEN scrappy_commerce_orders.label_state ELSE excluded.label_state END,
|
|
343
350
|
amount_minor = excluded.amount_minor,
|
|
344
|
-
event_version =
|
|
351
|
+
event_version = CASE WHEN EXISTS(SELECT 1 FROM scrappy_shipment_audit a WHERE a.tenant_id=scrappy_commerce_orders.tenant_id
|
|
352
|
+
AND a.stage=scrappy_commerce_orders.stage AND a.order_id=scrappy_commerce_orders.order_id) THEN MAX(scrappy_commerce_orders.event_version + 1, excluded.event_version) ELSE excluded.event_version END`,
|
|
345
353
|
params: [
|
|
346
354
|
tenantId,
|
|
347
355
|
stage,
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
* The exercised comparison is scripts/commerce/scrappy-reconcile.mjs. This
|
|
9
|
-
* surface has no ledger, so configured, absent, and unreadable stay unhealthy.
|
|
10
|
-
*/
|
|
1
|
+
import { openFulfillment } from "@/lib/fulfillment/fulfillmentService";
|
|
2
|
+
import { readCommerceDurability } from "@/lib/fulfillment/commerceReadback";
|
|
3
|
+
import { readD1 } from "@/lib/publication/shared/env";
|
|
4
|
+
import { fromD1 } from "@/lib/catalog/d1/catalog";
|
|
5
|
+
import { commercePipelineFor } from "@/lib/fulfillment/commerceRuntime";
|
|
6
|
+
/** Owner-only persisted projection health and durable replay. External payment reconciliation remains separate. */
|
|
11
7
|
import type { APIContext } from "astro";
|
|
12
8
|
import { requireAdminApiEntry } from "@/lib/access/auth/adminApiGuard";
|
|
13
9
|
import { workerCommit } from "@/lib/activity/workerCommit";
|
|
@@ -80,11 +76,10 @@ function replayStage(value: unknown): CommerceStage | null {
|
|
|
80
76
|
return value === "preview" || value === "publication" ? value : null;
|
|
81
77
|
}
|
|
82
78
|
|
|
83
|
-
export function admitAdminReplay(input: {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}): { admitted: boolean; reason: "admitted" | "denied" | "refused" } {
|
|
79
|
+
export function admitAdminReplay(input: { tenantId: string; stage: CommerceStage; kind: "order" | "refund" }): {
|
|
80
|
+
admitted: boolean;
|
|
81
|
+
reason: "admitted" | "denied" | "refused";
|
|
82
|
+
} {
|
|
88
83
|
const actor = { role: "owner" as const, tenantId: input.tenantId, stage: input.stage };
|
|
89
84
|
const resource = { tenantId: input.tenantId, stage: input.stage };
|
|
90
85
|
if (!actorMayCommand(actor, resource)) return { admitted: false, reason: "denied" };
|
|
@@ -106,7 +101,20 @@ export function admitAdminReplay(input: {
|
|
|
106
101
|
export async function GET(context: APIContext): Promise<Response> {
|
|
107
102
|
const access = await requireAdminApiEntry(context, { requireOwner: true });
|
|
108
103
|
if (!access.ok) return json({ ok: false, reason: access.reason }, access.status);
|
|
109
|
-
|
|
104
|
+
const opened = await openFulfillment(context);
|
|
105
|
+
if (!opened.ok) return opened.response;
|
|
106
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
107
|
+
if (!d1) return json({ ok: false, reason: "unavailable" }, 503);
|
|
108
|
+
return json({
|
|
109
|
+
ok: true,
|
|
110
|
+
report: await readCommerceDurability(
|
|
111
|
+
fromD1(d1),
|
|
112
|
+
opened.actor.tenantId,
|
|
113
|
+
opened.actor.stage,
|
|
114
|
+
new Date().toISOString(),
|
|
115
|
+
workerCommit(),
|
|
116
|
+
),
|
|
117
|
+
});
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
export async function POST(context: APIContext): Promise<Response> {
|
|
@@ -120,7 +128,8 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
120
128
|
} catch {
|
|
121
129
|
return json({ ok: false, reason: "refused" }, 400);
|
|
122
130
|
}
|
|
123
|
-
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
131
|
+
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
132
|
+
return json({ ok: false, reason: "refused" }, 400);
|
|
124
133
|
const record = body as Record<string, unknown>;
|
|
125
134
|
if (Object.keys(record).some((key) => !REPLAY_KEYS.has(key))) return json({ ok: false, reason: "refused" }, 400);
|
|
126
135
|
const stage = replayStage(record.stage);
|
|
@@ -128,15 +137,36 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
128
137
|
if (!stage || !kind || typeof record.orderId !== "string" || typeof record.sourceEventId !== "string") {
|
|
129
138
|
return json({ ok: false, reason: "refused" }, 400);
|
|
130
139
|
}
|
|
131
|
-
if (!OPAQUE.test(record.orderId) || !OPAQUE.test(record.sourceEventId))
|
|
140
|
+
if (!OPAQUE.test(record.orderId) || !OPAQUE.test(record.sourceEventId))
|
|
141
|
+
return json({ ok: false, reason: "refused" }, 400);
|
|
132
142
|
const decision = admitAdminReplay({ tenantId: context.locals.tenant.tenant_id, stage, kind });
|
|
133
143
|
if (!decision.admitted) return json({ ok: false, reason: decision.reason }, 403);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
const opened = await openFulfillment(context);
|
|
145
|
+
if (!opened.ok) return opened.response;
|
|
146
|
+
if (stage !== opened.actor.stage) return json({ ok: false, reason: "denied" }, 403);
|
|
147
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
148
|
+
if (!d1) return json({ ok: false, reason: "unavailable" }, 503);
|
|
149
|
+
const db = fromD1(d1);
|
|
150
|
+
const row = await db.first<{ outbox_id: string }>(
|
|
151
|
+
`SELECT outbox_id FROM scrappy_commerce_outbox WHERE tenant_id=? AND stage=? AND order_id=? AND source_event_id=?`,
|
|
152
|
+
opened.actor.tenantId,
|
|
153
|
+
stage,
|
|
154
|
+
record.orderId,
|
|
155
|
+
record.sourceEventId,
|
|
156
|
+
);
|
|
157
|
+
if (!row) return json({ ok: false, reason: "not-found" }, 404);
|
|
158
|
+
const pipeline = await commercePipelineFor(opened.actor.tenantId);
|
|
159
|
+
if (!pipeline?.commerceReplay) return json({ ok: false, reason: "scheduler-unavailable" }, 503);
|
|
160
|
+
await pipeline.commerceReplay(opened.actor.tenantId, stage, row.outbox_id);
|
|
161
|
+
return json(
|
|
162
|
+
{
|
|
163
|
+
ok: true,
|
|
164
|
+
admitted: true,
|
|
165
|
+
durableWrite: true,
|
|
166
|
+
command: "reconcile",
|
|
167
|
+
reason: "queued",
|
|
168
|
+
report: await readCommerceDurability(db, opened.actor.tenantId, stage, new Date().toISOString(), workerCommit()),
|
|
169
|
+
},
|
|
170
|
+
202,
|
|
171
|
+
);
|
|
142
172
|
}
|
|
@@ -23,10 +23,14 @@ import {
|
|
|
23
23
|
foxyCartRedirectUrl,
|
|
24
24
|
parseConsentSnapshot,
|
|
25
25
|
defaultConsentSnapshot,
|
|
26
|
+
buildVariantCartPayload,
|
|
27
|
+
commerceTenantId,
|
|
28
|
+
MAX_CART_QUANTITY,
|
|
26
29
|
type CartDenialCode,
|
|
27
30
|
} from "@tot/public-runtime";
|
|
28
31
|
import { resolveCheckoutHandoffCorrelation } from "@/lib/checkout/commerce/checkoutHandoffCorrelation";
|
|
29
32
|
import { resolveCartSigning, shopperCartDenialMessage } from "@/lib/checkout/shared/checkoutCommerce";
|
|
33
|
+
import { readEnv } from "@/lib/publication/shared/runtimeEnv";
|
|
30
34
|
|
|
31
35
|
export const prerender = false;
|
|
32
36
|
|
|
@@ -97,13 +101,41 @@ async function cartFormPost(context: APIContext): Promise<Response> {
|
|
|
97
101
|
const form = await context.request.formData();
|
|
98
102
|
const entries: Array<[string, string]> = [];
|
|
99
103
|
for (const [name, value] of form.entries()) {
|
|
100
|
-
if (typeof value
|
|
104
|
+
if (typeof value !== "string") return cartDenial("signature", "Invalid cart.", 422);
|
|
105
|
+
entries.push([name, value]);
|
|
106
|
+
}
|
|
107
|
+
if (form.has("sample_product") || form.has("sample_variant")) {
|
|
108
|
+
const sampleTenant = await readEnv("COMMERCE_SAMPLE_TENANT_ID");
|
|
109
|
+
if (
|
|
110
|
+
!sampleTenant || sampleTenant !== commerceTenantId(tenant) ||
|
|
111
|
+
await readEnv("COMMERCE_DEMO_CHECKOUT_TIER") !== "test" || commerce.mode !== "signed"
|
|
112
|
+
) return cartDenial("tenant", "Invalid cart.", 422);
|
|
113
|
+
const allowed = new Set(["sample_product", "sample_variant", "quantity"]);
|
|
114
|
+
if (entries.length !== 3 || entries.some(([name]) => !allowed.has(name)) ||
|
|
115
|
+
new Set(entries.map(([name]) => name)).size !== 3) {
|
|
116
|
+
return cartDenial("signature", "Invalid cart.", 422);
|
|
117
|
+
}
|
|
118
|
+
const quantity = form.get("quantity") as string;
|
|
119
|
+
if (!/^[1-9][0-9]*$/.test(quantity) || !Number.isSafeInteger(Number(quantity)) || Number(quantity) > MAX_CART_QUANTITY) {
|
|
120
|
+
return cartDenial("quantity", "Invalid quantity.", 422);
|
|
121
|
+
}
|
|
122
|
+
const product = await context.locals.tot.getProductByHandle(form.get("sample_product") as string);
|
|
123
|
+
const variant = product?.variants.find((v) => v.id === form.get("sample_variant"));
|
|
124
|
+
if (!product || product.status !== "active" || product.visibility === "hidden" || product.visibility === "unlisted" ||
|
|
125
|
+
!variant || !variant.inventory.available || product.tenant_id !== tenant.tot_data_scope ||
|
|
126
|
+
variant.tenant_id !== tenant.tot_data_scope) {
|
|
127
|
+
return cartDenial("signature", "Product is unavailable.", 422);
|
|
128
|
+
}
|
|
129
|
+
const payload = await buildVariantCartPayload(product, variant, {
|
|
130
|
+
mode: "signed", secret: signing.secret, tenantId: commerceTenantId(tenant),
|
|
131
|
+
});
|
|
132
|
+
entries.splice(0, entries.length, ...payload.hidden.map((f): [string, string] => [f.name, f.value]), [payload.quantityName, quantity]);
|
|
101
133
|
}
|
|
102
134
|
const result = await enforceCartSubmission({
|
|
103
135
|
entries,
|
|
104
136
|
mode: commerce.mode,
|
|
105
137
|
secret: signing.secret,
|
|
106
|
-
trustedTenantId: tenant
|
|
138
|
+
trustedTenantId: commerceTenantId(tenant),
|
|
107
139
|
});
|
|
108
140
|
if (!result.ok) {
|
|
109
141
|
const status = result.code === "signing-unavailable" ? 503 : 422;
|