@tokenoftrust/storefront-runner 2.4.91 → 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 (33) hide show
  1. package/apps/storefront/migrations-apps/0017_commerce_durable_projection.sql +28 -0
  2. package/apps/storefront/migrations-apps/0018_commerce_provider_cart_identity.sql +13 -0
  3. package/apps/storefront/src/config/declaredTenantConfig.ts +5 -0
  4. package/apps/storefront/src/config/devTenantSeed.ts +3 -2
  5. package/apps/storefront/src/lib/checkout/commerce/commerceAdmission.ts +108 -0
  6. package/apps/storefront/src/lib/checkout/commerce/finalCartAuthorization.ts +1 -1
  7. package/apps/storefront/src/lib/checkout/shared/checkoutCommerce.ts +6 -4
  8. package/apps/storefront/src/lib/fulfillment/admin/commerceDesk.ts +111 -0
  9. package/apps/storefront/src/lib/fulfillment/admin/viewmodels/fulfillment.ts +1 -1
  10. package/apps/storefront/src/lib/fulfillment/commerceProjection.ts +146 -0
  11. package/apps/storefront/src/lib/fulfillment/commerceReadback.ts +60 -0
  12. package/apps/storefront/src/lib/fulfillment/commerceRecovery.ts +65 -0
  13. package/apps/storefront/src/lib/fulfillment/commerceRuntime.ts +13 -0
  14. package/apps/storefront/src/lib/fulfillment/fulfillmentService.ts +50 -4
  15. package/apps/storefront/src/lib/fulfillment/readiness.ts +1 -0
  16. package/apps/storefront/src/lib/fulfillment/store.ts +15 -0
  17. package/apps/storefront/src/lib/integrations/webhooks/deliveryEngine.ts +6 -3
  18. package/apps/storefront/src/lib/integrations/webhooks/deliveryStore.ts +6 -5
  19. package/apps/storefront/src/lib/inventory/allocations.ts +58 -9
  20. package/apps/storefront/src/lib/inventory/mirrorStock.ts +121 -0
  21. package/apps/storefront/src/lib/orders/admin/services/orders.ts +8 -8
  22. package/apps/storefront/src/lib/orders/data/orderMapper.ts +22 -1
  23. package/apps/storefront/src/lib/orders/data/ordersStore.ts +12 -4
  24. package/apps/storefront/src/pages/api/admin/commerce-readiness.ts +56 -26
  25. package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +34 -2
  26. package/apps/storefront/src/pages/api/commerce/final-cart-authorization.ts +158 -306
  27. package/apps/storefront/src/pages/products/[handle].astro +13 -8
  28. package/apps/storefront/test/pipeline/harness.ts +6 -0
  29. package/apps/storefront/test/pipeline/pipeline-host.ts +29 -0
  30. package/package.json +1 -1
  31. package/packages/public-runtime/src/checkout-orders.ts +45 -7
  32. package/packages/public-runtime/src/checkout.ts +37 -9
  33. package/packages/public-runtime/src/tenant.ts +3 -0
@@ -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;