@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.
Files changed (35) hide show
  1. package/apps/storefront/.runner-dependency-cruiser.cjs +38 -0
  2. package/apps/storefront/migrations-apps/0017_commerce_durable_projection.sql +28 -0
  3. package/apps/storefront/migrations-apps/0018_commerce_provider_cart_identity.sql +13 -0
  4. package/apps/storefront/package.json +2 -1
  5. package/apps/storefront/src/config/declaredTenantConfig.ts +5 -0
  6. package/apps/storefront/src/config/devTenantSeed.ts +3 -2
  7. package/apps/storefront/src/lib/checkout/commerce/commerceAdmission.ts +108 -0
  8. package/apps/storefront/src/lib/checkout/commerce/finalCartAuthorization.ts +1 -1
  9. package/apps/storefront/src/lib/checkout/shared/checkoutCommerce.ts +6 -4
  10. package/apps/storefront/src/lib/fulfillment/admin/commerceDesk.ts +111 -0
  11. package/apps/storefront/src/lib/fulfillment/admin/viewmodels/fulfillment.ts +1 -1
  12. package/apps/storefront/src/lib/fulfillment/commerceProjection.ts +146 -0
  13. package/apps/storefront/src/lib/fulfillment/commerceReadback.ts +60 -0
  14. package/apps/storefront/src/lib/fulfillment/commerceRecovery.ts +65 -0
  15. package/apps/storefront/src/lib/fulfillment/commerceRuntime.ts +13 -0
  16. package/apps/storefront/src/lib/fulfillment/fulfillmentService.ts +50 -4
  17. package/apps/storefront/src/lib/fulfillment/readiness.ts +1 -0
  18. package/apps/storefront/src/lib/fulfillment/store.ts +15 -0
  19. package/apps/storefront/src/lib/integrations/webhooks/deliveryEngine.ts +6 -3
  20. package/apps/storefront/src/lib/integrations/webhooks/deliveryStore.ts +6 -5
  21. package/apps/storefront/src/lib/inventory/allocations.ts +58 -9
  22. package/apps/storefront/src/lib/inventory/mirrorStock.ts +121 -0
  23. package/apps/storefront/src/lib/orders/admin/services/orders.ts +8 -8
  24. package/apps/storefront/src/lib/orders/data/orderMapper.ts +22 -1
  25. package/apps/storefront/src/lib/orders/data/ordersStore.ts +12 -4
  26. package/apps/storefront/src/pages/api/admin/commerce-readiness.ts +56 -26
  27. package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +34 -2
  28. package/apps/storefront/src/pages/api/commerce/final-cart-authorization.ts +158 -306
  29. package/apps/storefront/src/pages/products/[handle].astro +13 -8
  30. package/apps/storefront/test/pipeline/harness.ts +6 -0
  31. package/apps/storefront/test/pipeline/pipeline-host.ts +29 -0
  32. package/package.json +1 -1
  33. package/packages/public-runtime/src/checkout-orders.ts +45 -7
  34. package/packages/public-runtime/src/checkout.ts +37 -9
  35. 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 = excluded.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 = excluded.fulfillment_state,
342
- label_state = excluded.label_state,
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 = excluded.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
- * GET/POST /api/admin/commerce-readiness
3
- *
4
- * Phase 1: the client fulfills in their own carrier account. This route reports
5
- * whether order and payment reconcile has been exercised. It does not read an
6
- * order row, write a payment, or call a carrier. Postage is not a result.
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
- tenantId: string;
85
- stage: CommerceStage;
86
- kind: "order" | "refund";
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
- return json({ ok: true, report: projectAdminReadiness({ scheduler: COMMERCE_SCHEDULER, deployedSha: workerCommit() }) });
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)) return json({ ok: false, reason: "refused" }, 400);
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)) return json({ ok: false, reason: "refused" }, 400);
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
- return json({
135
- ok: true,
136
- admitted: true,
137
- durableWrite: false,
138
- command: "reconcile",
139
- reason: decision.reason,
140
- report: projectAdminReadiness({ scheduler: COMMERCE_SCHEDULER, deployedSha: workerCommit() }),
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 === "string") entries.push([name, 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.tenant_id,
138
+ trustedTenantId: commerceTenantId(tenant),
107
139
  });
108
140
  if (!result.ok) {
109
141
  const status = result.code === "signing-unavailable" ? 503 : 422;