@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
@@ -0,0 +1,38 @@
1
+ module.exports = {
2
+ "forbidden": [
3
+ {
4
+ "name": "renderer-no-controlplane",
5
+ "severity": "error",
6
+ "from": {},
7
+ "to": {
8
+ "path": "(^|/)packages/private-controlplane/|^@tot/private-controlplane(/|$)|^@tokenoftrust/private-apps-devkit(/|$)"
9
+ }
10
+ }
11
+ ],
12
+ "options": {
13
+ "tsConfig": {
14
+ "fileName": "tsconfig.json"
15
+ },
16
+ "tsPreCompilationDeps": true,
17
+ "doNotFollow": {
18
+ "path": "node_modules"
19
+ },
20
+ "enhancedResolveOptions": {
21
+ "exportsFields": [
22
+ "exports"
23
+ ],
24
+ "conditionNames": [
25
+ "import",
26
+ "types",
27
+ "node",
28
+ "default"
29
+ ],
30
+ "extensions": [
31
+ ".ts",
32
+ ".tsx",
33
+ ".js",
34
+ ".mjs"
35
+ ]
36
+ }
37
+ }
38
+ };
@@ -0,0 +1,28 @@
1
+ -- Append-only mirrored stock and trusted checkout admission receipts.
2
+ CREATE TABLE commerce_inventory_movements (
3
+ tenant_id TEXT NOT NULL, stage TEXT NOT NULL CHECK(stage IN ('preview','publication')),
4
+ source TEXT NOT NULL, source_event_id TEXT NOT NULL, sku TEXT NOT NULL,
5
+ location_id TEXT NOT NULL DEFAULT 'unspecified', quantity_delta INTEGER NOT NULL,
6
+ as_of TEXT NOT NULL, actor_id TEXT NOT NULL,
7
+ PRIMARY KEY(tenant_id,stage,source,source_event_id,sku,location_id)
8
+ );
9
+ CREATE INDEX commerce_inventory_scope ON commerce_inventory_movements(tenant_id,stage,sku,location_id,as_of);
10
+ CREATE TABLE commerce_admission_snapshots (
11
+ tenant_id TEXT NOT NULL, stage TEXT NOT NULL CHECK(stage IN ('preview','publication')),
12
+ authorization_id TEXT NOT NULL, snapshot_json TEXT NOT NULL,
13
+ provider_cart_digest TEXT NOT NULL, expires_at TEXT NOT NULL,
14
+ linked_order_id TEXT,
15
+ PRIMARY KEY(tenant_id,stage,authorization_id),
16
+ UNIQUE(tenant_id,stage,linked_order_id),
17
+ FOREIGN KEY(tenant_id,stage,authorization_id) REFERENCES scrappy_final_cart_authorizations(tenant_id,stage,authorization_id)
18
+ );
19
+ CREATE TABLE commerce_projection_checkpoints (
20
+ tenant_id TEXT NOT NULL, stage TEXT NOT NULL CHECK(stage IN ('preview','publication')),
21
+ order_id TEXT NOT NULL, source_version INTEGER NOT NULL, projected_at TEXT NOT NULL,
22
+ PRIMARY KEY(tenant_id,stage,order_id)
23
+ );
24
+ CREATE TABLE commerce_recovery_runs (
25
+ run_id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, stage TEXT NOT NULL,
26
+ started_at TEXT NOT NULL, completed_at TEXT, deployed_sha TEXT NOT NULL,
27
+ delivered INTEGER NOT NULL DEFAULT 0, retried INTEGER NOT NULL DEFAULT 0, dead INTEGER NOT NULL DEFAULT 0
28
+ );
@@ -0,0 +1,13 @@
1
+ -- A provider cart has one immutable admission until authoritative unpaid supersession exists.
2
+ CREATE TABLE commerce_provider_carts (
3
+ tenant_id TEXT NOT NULL, stage TEXT NOT NULL CHECK(stage IN ('preview','publication')),
4
+ provider TEXT NOT NULL CHECK(provider='foxycart'), provider_cart_id TEXT NOT NULL,
5
+ provider_cart_digest TEXT NOT NULL, authorization_id TEXT NOT NULL,
6
+ PRIMARY KEY(tenant_id,stage,provider,provider_cart_id),
7
+ UNIQUE(tenant_id,stage,authorization_id)
8
+ );
9
+ -- Conflicting persisted identities refuse migration rather than select an arbitrary receipt.
10
+ INSERT INTO commerce_provider_carts
11
+ (tenant_id,stage,provider,provider_cart_id,provider_cart_digest,authorization_id)
12
+ SELECT tenant_id,stage,json_extract(snapshot_json,'$.provider'),json_extract(snapshot_json,'$.providerCartId'),provider_cart_digest,authorization_id
13
+ FROM commerce_admission_snapshots;
@@ -14,7 +14,8 @@
14
14
  "preview": "wrangler dev",
15
15
  "typecheck": "astro check && tsc --noEmit",
16
16
  "astro": "astro",
17
- "test": "NODE_OPTIONS=--experimental-sqlite vitest run --exclude src/lib/integrations/apps/v84CleanCloneReplay.test.ts --exclude src/lib/integrations/apps/v84NonportableRouteDogfood.test.ts --exclude src/lib/integrations/apps/v84CloneLifecycleEvidence.test.ts && NODE_OPTIONS=--experimental-sqlite vitest run src/lib/integrations/apps/v84CleanCloneReplay.test.ts src/lib/integrations/apps/v84NonportableRouteDogfood.test.ts src/lib/integrations/apps/v84CloneLifecycleEvidence.test.ts --fileParallelism=false && pnpm run test:pipeline && pnpm run test:pipeline-faults"
17
+ "test": "NODE_OPTIONS=--experimental-sqlite vitest run --exclude src/lib/integrations/apps/v84CleanCloneReplay.test.ts --exclude src/lib/integrations/apps/v84NonportableRouteDogfood.test.ts --exclude src/lib/integrations/apps/v84CloneLifecycleEvidence.test.ts && NODE_OPTIONS=--experimental-sqlite vitest run src/lib/integrations/apps/v84CleanCloneReplay.test.ts src/lib/integrations/apps/v84NonportableRouteDogfood.test.ts src/lib/integrations/apps/v84CloneLifecycleEvidence.test.ts --fileParallelism=false && pnpm run test:pipeline && pnpm run test:pipeline-faults",
18
+ "depcruise": "depcruise src --config .runner-dependency-cruiser.cjs"
18
19
  },
19
20
  "dependencies": {
20
21
  "@astrojs/cloudflare": "^14.0.1",
@@ -44,6 +44,11 @@ export function checkoutTotConfigs(): Map<string, Record<string, unknown>> {
44
44
  return map;
45
45
  }
46
46
 
47
+ /** Only the local renderer's scaffold declaration selects the hosted sample handoff. */
48
+ export function isLocalSampleCheckout(tenantId: string): boolean {
49
+ return import.meta.env.DEV && checkoutTotConfigs().get(tenantId)?.sample === true;
50
+ }
51
+
47
52
  /** The declaration in the checkout's own `.tot/config.json`, or `{}` when it has none. */
48
53
  export function checkoutDeclaredTenantConfig(tenantId: string): DeclaredTenantConfig {
49
54
  return declaredTenantConfig(checkoutConfigFiles[`../../../../tenants/${tenantId}/.tot/config.json`]
@@ -94,8 +94,9 @@ export function synthesizeDevTenant(
94
94
  ...(integrations.length > 0 ? { integrations } : {}),
95
95
  ...(euUkVisitors ? { euUkVisitors } : {}),
96
96
  };
97
- // Cart wires to the shared ToT demo checkout store (test mode, no secret), the
98
- // same as the bundled demo tenants — and the SAME constant the hosted QA/test
97
+ // Cart wires to the shared ToT demo checkout store (`tot-preview`, signed:
98
+ // HMAC cart validation required; FOXY_CART_SECRET must be set before carts
99
+ // are served), the same as the bundled demo tenants — and the SAME constant the hosted QA/test
99
100
  // tier's demo-checkout fallback reuses (checkoutCommerce.ts), so both surfaces
100
101
  // stay wired to one definition. A marketing tenant never resolves it (the
101
102
  // render chain gates on features.catalog); a commerce one gets its cart
@@ -0,0 +1,108 @@
1
+ /** Trusted admission is written in the reservation transaction, never reconstructed from a paid callback. */
2
+ import {
3
+ canonicalJson,
4
+ type CommerceAdmissionSnapshot,
5
+ type CommerceStage,
6
+ type MinorTotals,
7
+ } from "@tot/public-runtime";
8
+ import type { Queryable, QueryStatement } from "../../catalog/d1/catalog.js";
9
+ export type { CommerceAdmissionSnapshot } from "@tot/public-runtime";
10
+ export function admissionSnapshotStatement(
11
+ input: CommerceAdmissionSnapshot,
12
+ ): QueryStatement & { params: readonly unknown[] } {
13
+ if (
14
+ !/^[a-f0-9]{64}$/.test(input.providerCartDigest) ||
15
+ !/^[a-f0-9]{64}$/.test(input.destinationDigest) ||
16
+ Object.values(input.provenance).some(
17
+ (v) => !v || !v.source || !v.policyRevision || !Number.isFinite(Date.parse(v.expiresAt)),
18
+ ) ||
19
+ !Number.isFinite(Date.parse(input.expiresAt))
20
+ )
21
+ throw new Error("invalid-admission");
22
+ return {
23
+ sql: `INSERT INTO commerce_admission_snapshots
24
+ (tenant_id,stage,authorization_id,snapshot_json,provider_cart_digest,expires_at)
25
+ SELECT ?,?,?,?,?,? WHERE EXISTS(SELECT 1 FROM scrappy_final_cart_authorizations
26
+ WHERE tenant_id=? AND stage=? AND authorization_id=? AND cart_digest=? AND state='active')
27
+ ON CONFLICT DO NOTHING`,
28
+ params: [
29
+ input.tenantId,
30
+ input.stage,
31
+ input.authorizationId,
32
+ JSON.stringify(input),
33
+ input.providerCartDigest,
34
+ input.expiresAt,
35
+ input.tenantId,
36
+ input.stage,
37
+ input.authorizationId,
38
+ input.cartDigest,
39
+ ],
40
+ };
41
+ }
42
+ export async function readCommerceAdmission(
43
+ db: Queryable,
44
+ tenantId: string,
45
+ stage: CommerceStage,
46
+ authorizationId: string,
47
+ ): Promise<CommerceAdmissionSnapshot | null> {
48
+ const row = await db.first<{ snapshot_json: string }>(
49
+ `SELECT snapshot_json FROM commerce_admission_snapshots WHERE tenant_id=? AND stage=? AND authorization_id=?`,
50
+ tenantId,
51
+ stage,
52
+ authorizationId,
53
+ );
54
+ if (!row) return null;
55
+ try {
56
+ const snapshot = JSON.parse(row.snapshot_json) as CommerceAdmissionSnapshot;
57
+ if (
58
+ snapshot.tenantId !== tenantId ||
59
+ snapshot.stage !== stage ||
60
+ snapshot.authorizationId !== authorizationId ||
61
+ !Array.isArray(snapshot.lines) ||
62
+ !snapshot.lines.length ||
63
+ !snapshot.totals ||
64
+ !snapshot.evidence ||
65
+ !snapshot.provenance ||
66
+ !/^[a-f0-9]{64}$/.test(snapshot.providerCartDigest) ||
67
+ !/^[a-f0-9]{64}$/.test(snapshot.destinationDigest) ||
68
+ !Number.isFinite(Date.parse(snapshot.expiresAt))
69
+ )
70
+ return null;
71
+ for (const field of ["verification", "tax", "risk", "stock"] as const) {
72
+ const proof = snapshot.provenance[field];
73
+ if (
74
+ !proof ||
75
+ !proof.source ||
76
+ !proof.policyRevision ||
77
+ !Number.isFinite(Date.parse(proof.expiresAt)) ||
78
+ !["required", "not-required"].includes(proof.requirement) ||
79
+ !["satisfied", "not-required"].includes(proof.state)
80
+ )
81
+ return null;
82
+ }
83
+ return snapshot;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+ export function admissionMatches(
89
+ snapshot: CommerceAdmissionSnapshot,
90
+ event: {
91
+ currency: string;
92
+ totals: MinorTotals;
93
+ lines?: readonly { sku: string; quantity: number; unitMinor: number }[];
94
+ providerCartDigest?: string;
95
+ destinationDigest?: string;
96
+ },
97
+ ): boolean {
98
+ const sort = (lines: readonly { sku: string; quantity: number; unitMinor: number }[]) =>
99
+ [...lines].sort((a, b) => a.sku.localeCompare(b.sku));
100
+ return (
101
+ event.providerCartDigest === snapshot.providerCartDigest &&
102
+ event.destinationDigest === snapshot.destinationDigest &&
103
+ event.currency === snapshot.currency &&
104
+ canonicalJson(event.totals) === canonicalJson(snapshot.totals) &&
105
+ Array.isArray(event.lines) &&
106
+ canonicalJson(sort(event.lines)) === canonicalJson(sort(snapshot.lines))
107
+ );
108
+ }
@@ -92,7 +92,7 @@ function evidenceFinding(evidence: CommerceEvidence | null): string | null {
92
92
  if (!evidence) return "missing-evidence";
93
93
  for (const field of EVIDENCE_FIELDS) {
94
94
  const state: EvidenceState | string = evidence[field];
95
- if (state === "fresh") continue;
95
+ if (state === "fresh" || (field !== "stock" && state === "not-required")) continue;
96
96
  if (state === "UNKNOWN") return `evidence-${field}-unknown`;
97
97
  if (state === "stale" || state === "unreadable" || state === "absent") return `evidence-${field}-${state}`;
98
98
  return `evidence-${field}-unreadable`;
@@ -9,8 +9,8 @@
9
9
  * - `FOXY_STORE_SUBDOMAIN` overrides the tenant's default subdomain (one
10
10
  * env var points every commerce tenant at our shared demo store).
11
11
  * - `FOXY_CART_SECRET` is the cart-validation secret used only in `signed`
12
- * mode; the `unsigned-demo` mode never reads it (validation is OFF), which
13
- * is what lets a developer run the whole cart locally with zero secrets.
12
+ * mode; `unsigned-demo` never reads it. Local sample carts use the hosted
13
+ * identifier-only handoff, so developers receive no shared signing key.
14
14
  *
15
15
  * `COMMERCE_DEMO_CHECKOUT_TIER` gates a FALLBACK for a commerce tenant that has
16
16
  * no `commerce.checkout` of its own (e.g. a staff-provisioned store —
@@ -28,6 +28,7 @@ import {
28
28
  CART_SIGNING_OPERATOR_ERROR,
29
29
  resolveManagedCheckout,
30
30
  SHARED_DEMO_CHECKOUT,
31
+ isManagedCheckoutMode,
31
32
  } from "@tot/public-runtime";
32
33
  import { readEnv } from "@/lib/publication/shared/runtimeEnv";
33
34
 
@@ -40,7 +41,7 @@ export async function resolveTenantCommerce(
40
41
  tenant: TenantConfig,
41
42
  ): Promise<ResolvedManagedCheckout | null> {
42
43
  const checkout = tenant.commerce?.checkout ?? (await demoCheckoutFallback());
43
- if (!checkout) return null;
44
+ if (!checkout || !isManagedCheckoutMode(checkout.mode)) return null;
44
45
  // Per-environment overrides so the SAME tenant can point at different stores /
45
46
  // checkout hosts by env: FOXY_STORE_SUBDOMAIN picks the checkout store;
46
47
  // FOXY_STORE_DOMAIN sets a branded custom checkout domain (e.g. checkout.preview.
@@ -127,7 +128,8 @@ export interface CartSigningResolution {
127
128
  export async function resolveCartSigning(
128
129
  commerce: ResolvedManagedCheckout,
129
130
  ): Promise<CartSigningResolution> {
130
- if (commerce.mode !== "signed") return { enabled: true };
131
+ if (!isManagedCheckoutMode(commerce.mode)) return { enabled: false, operatorError: CART_SIGNING_OPERATOR_ERROR };
132
+ if (commerce.mode === "unsigned-demo") return { enabled: true };
131
133
  const secret = await readCartSecret(commerce);
132
134
  if (secret) return { enabled: true, secret };
133
135
  console.error(
@@ -0,0 +1,111 @@
1
+ /** Operational browser client. All authority and state changes remain in the authenticated APIs. */
2
+ import type { FulfillmentView } from "../fulfillmentService.js";
3
+ export function mountCommerceDesk(root: HTMLElement): void {
4
+ const status = root.querySelector<HTMLElement>("[data-desk-status]")!;
5
+ const rows = root.querySelector<HTMLElement>("[data-desk-orders]")!;
6
+ const next = root.querySelector<HTMLButtonElement>("[data-desk-next]")!;
7
+ let cursor: string | null = null;
8
+ async function request(url: string, body?: unknown): Promise<unknown> {
9
+ const response = await fetch(url, {
10
+ credentials: "same-origin",
11
+ cache: "no-store",
12
+ ...(body ? { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}),
13
+ });
14
+ const value = await response.json();
15
+ if (!response.ok) throw new Error("request-refused");
16
+ return value;
17
+ }
18
+ function text(tag: string, value: string, parent: HTMLElement): HTMLElement {
19
+ const node = document.createElement(tag);
20
+ node.textContent = value;
21
+ parent.append(node);
22
+ return node;
23
+ }
24
+ function render(order: FulfillmentView): void {
25
+ const card = document.createElement("section");
26
+ card.dataset.orderId = order.orderId;
27
+ rows.append(card);
28
+ text("h3", order.orderId, card);
29
+ text("p", `${order.payment} · ${order.fulfillment} · ${order.batch?.phase ?? "unclaimed"}`, card);
30
+ text("p", order.lines.map((line) => `${line.sku} × ${line.quantity}`).join(", ") || "No verified line items", card);
31
+ text("p", order.holds.length ? `Held: ${order.holds.join(", ")}` : "Ready", card);
32
+ const actions = order.batch
33
+ ? [
34
+ ["pick", "Confirm pick"],
35
+ ["pack", "Confirm pack"],
36
+ ]
37
+ : [["claim", "Build pick list"]];
38
+ actions.push(["hold", "Hold"], ["clear-hold", "Clear operator hold"]);
39
+ for (const [action, label] of actions) {
40
+ const button = text("button", label!, card) as HTMLButtonElement;
41
+ button.type = "button";
42
+ button.disabled = (action === "pick" || action === "pack" || action === "claim") && order.holds.length > 0;
43
+ button.onclick = async () => {
44
+ button.disabled = true;
45
+ try {
46
+ await request(
47
+ action === "claim"
48
+ ? "/api/admin/fulfillment/batches"
49
+ : `/api/admin/fulfillment/orders/${encodeURIComponent(order.orderId)}`,
50
+ action === "claim"
51
+ ? { orderIds: [order.orderId], expectedVersions: { [order.orderId]: order.eventVersion } }
52
+ : { action, expectedVersion: order.eventVersion, scope: "whole-order" },
53
+ );
54
+ await load(null);
55
+ } catch {
56
+ status.textContent = "The order could not be updated. Refresh and try again.";
57
+ button.disabled = false;
58
+ }
59
+ };
60
+ }
61
+ if (order.packingSlip) {
62
+ const slip = document.createElement("div");
63
+ slip.dataset.packingSlip = order.orderId;
64
+ card.append(slip);
65
+ text("h4", "Packing slip", slip);
66
+ for (const line of order.packingSlip.lines) text("p", `${line.sku} × ${line.quantity}`, slip);
67
+ const print = text("button", "Print packing slip", slip) as HTMLButtonElement;
68
+ print.type = "button";
69
+ print.onclick = () => {
70
+ const page = window.open("", "_blank");
71
+ if (!page) return;
72
+ page.opener = null;
73
+ const title = page.document.createElement("h1");
74
+ title.textContent = `Packing slip ${order.orderId}`;
75
+ page.document.body.append(title);
76
+ for (const line of order.packingSlip!.lines) {
77
+ const item = page.document.createElement("p");
78
+ item.textContent = `${line.sku} × ${line.quantity}`;
79
+ page.document.body.append(item);
80
+ }
81
+ page.print();
82
+ };
83
+ }
84
+ }
85
+ async function load(after: string | null): Promise<void> {
86
+ status.textContent = "Loading orders…";
87
+ try {
88
+ const page = (await request(
89
+ `/api/admin/fulfillment/orders?limit=25${after ? `&cursor=${encodeURIComponent(after)}` : ""}`,
90
+ )) as { tenantId: string; orders: FulfillmentView[]; nextCursor: string | null };
91
+ if (page.tenantId !== root.dataset.targetTenant) {
92
+ rows.replaceChildren();
93
+ next.hidden = true;
94
+ status.textContent = "Open the selected store to view its fulfillment orders.";
95
+ return;
96
+ }
97
+ rows.replaceChildren();
98
+ page.orders.forEach(render);
99
+ cursor = page.nextCursor;
100
+ next.hidden = !cursor;
101
+ status.textContent = page.orders.length ? `${page.orders.length} orders` : "No received orders.";
102
+ } catch {
103
+ rows.replaceChildren();
104
+ next.hidden = true;
105
+ status.textContent = "Orders are unavailable. Refresh and try again.";
106
+ }
107
+ }
108
+ root.querySelector<HTMLButtonElement>("[data-desk-refresh]")!.onclick = () => void load(null);
109
+ next.onclick = () => void load(cursor);
110
+ void load(null);
111
+ }
@@ -262,7 +262,7 @@ export function holdReasons(order: {
262
262
  if (order.fulfillment === "UNKNOWN") reasons.push("Fulfillment is unknown. Reconcile before shipping.");
263
263
  if (order.manualException) reasons.push(`Manual exception: ${order.manualException}.`);
264
264
  if (order.adultSignature === "refused") reasons.push("Adult signature was refused. The order does not ship.");
265
- const stale = (["verification", "tax", "risk", "stock"] as const).filter((key) => order.evidence[key] !== "fresh");
265
+ const stale = (["verification", "tax", "risk", "stock"] as const).filter((key) => order.evidence[key] !== "fresh" && order.evidence[key] !== "not-required");
266
266
  if (stale.length > 0) {
267
267
  reasons.push(`Evidence is not fresh: ${stale.map((key) => `${key} ${order.evidence[key]}`).join(", ")}.`);
268
268
  }
@@ -0,0 +1,146 @@
1
+ /** Durable receipt outbox is the recovery authority for the operational order projection. */
2
+ import { type CommerceEvidence, type CommerceStage } from "@tot/public-runtime";
3
+ import type { Queryable } from "../catalog/d1/catalog.js";
4
+ import { foldCommerceEvents, type StoredCommercePayload } from "../orders/data/orderMapper.js";
5
+ import { admissionMatches, readCommerceAdmission } from "../checkout/commerce/commerceAdmission.js";
6
+ import { FulfillmentService } from "./fulfillmentService.js";
7
+ import { fulfillmentDbFromQueryable } from "./store.js";
8
+ const UNKNOWN: CommerceEvidence = { verification: "UNKNOWN", tax: "UNKNOWN", risk: "UNKNOWN", stock: "UNKNOWN" };
9
+
10
+ export async function projectCommerceOrder(
11
+ db: Queryable,
12
+ input: { tenantId: string; stage: CommerceStage; orderId: string; now: string },
13
+ ): Promise<void> {
14
+ const rows = await db.all<{ payload: string }>(
15
+ `SELECT payload FROM scrappy_commerce_outbox WHERE tenant_id=? AND stage=? AND order_id=?`,
16
+ input.tenantId,
17
+ input.stage,
18
+ input.orderId,
19
+ );
20
+ const events = rows.map((row) => (JSON.parse(row.payload) as StoredCommercePayload).source);
21
+ if (!events.length) throw new Error("source-unavailable");
22
+ const snapshot = foldCommerceEvents(events);
23
+ const commandRank = { observe: 0, capture: 1, refund: 2, cancel: 3 };
24
+ const sorted = [...events].sort(
25
+ (a, b) =>
26
+ b.eventVersion - a.eventVersion ||
27
+ commandRank[b.command] - commandRank[a.command] ||
28
+ b.sourceEventId.localeCompare(a.sourceEventId),
29
+ );
30
+ // A paid/cart snapshot owns all of its binding fields together. A status-only
31
+ // update can retain those immutable facts, but cannot replace any of them.
32
+ const currentCart = sorted.find(
33
+ (event) =>
34
+ event.command === "capture" ||
35
+ event.lines !== undefined ||
36
+ event.admissionReference !== undefined ||
37
+ event.providerCartDigest !== undefined ||
38
+ event.destinationDigest !== undefined,
39
+ );
40
+ const lines = currentCart?.lines ?? [];
41
+ const admission = currentCart?.admissionReference
42
+ ? await readCommerceAdmission(db, input.tenantId, input.stage, currentCart.admissionReference)
43
+ : null;
44
+ const sameSourceOrder = events.every(
45
+ (event) => event.source === snapshot.source && event.sourceOrderId === snapshot.sourceOrderId,
46
+ );
47
+ let trusted = false;
48
+ if (
49
+ admission &&
50
+ currentCart &&
51
+ sameSourceOrder &&
52
+ admissionMatches(admission, {
53
+ currency: snapshot.currency,
54
+ totals: snapshot.totals,
55
+ lines,
56
+ providerCartDigest: currentCart.providerCartDigest,
57
+ destinationDigest: currentCart.destinationDigest,
58
+ }) &&
59
+ Date.parse(currentCart.placedAt) <= Date.parse(admission.expiresAt)
60
+ ) {
61
+ if (!db.batchResults) throw new Error("transaction-required");
62
+ const results = await db.batchResults([
63
+ {
64
+ sql: `UPDATE commerce_admission_snapshots SET linked_order_id=?
65
+ WHERE tenant_id=? AND stage=? AND authorization_id=? AND (linked_order_id IS NULL OR linked_order_id=?)
66
+ AND EXISTS(SELECT 1 FROM scrappy_final_cart_authorizations WHERE tenant_id=? AND stage=? AND authorization_id=? AND state IN ('active','consumed'))
67
+ AND NOT EXISTS(SELECT 1 FROM json_each(commerce_admission_snapshots.snapshot_json,'$.lines') line
68
+ WHERE NOT EXISTS(SELECT 1 FROM scrappy_allocations allocation WHERE allocation.tenant_id=commerce_admission_snapshots.tenant_id
69
+ AND allocation.stage=commerce_admission_snapshots.stage AND allocation.order_id IN (commerce_admission_snapshots.authorization_id,?)
70
+ AND allocation.sku=json_extract(line.value,'$.sku') AND allocation.quantity=json_extract(line.value,'$.quantity')
71
+ AND allocation.state IN ('reserved','confirmed')))
72
+ RETURNING authorization_id`,
73
+ params: [
74
+ input.orderId,
75
+ input.tenantId,
76
+ input.stage,
77
+ admission.authorizationId,
78
+ input.orderId,
79
+ input.tenantId,
80
+ input.stage,
81
+ admission.authorizationId,
82
+ input.orderId,
83
+ ],
84
+ },
85
+ {
86
+ sql: `UPDATE scrappy_allocations SET order_id=?,state='confirmed'
87
+ WHERE tenant_id=? AND stage=? AND order_id=? AND state='reserved'
88
+ AND EXISTS(SELECT 1 FROM commerce_admission_snapshots WHERE tenant_id=? AND stage=? AND authorization_id=? AND linked_order_id=?)`,
89
+ params: [
90
+ input.orderId,
91
+ input.tenantId,
92
+ input.stage,
93
+ admission.authorizationId,
94
+ input.tenantId,
95
+ input.stage,
96
+ admission.authorizationId,
97
+ input.orderId,
98
+ ],
99
+ },
100
+ {
101
+ sql: `UPDATE scrappy_final_cart_authorizations SET state='consumed'
102
+ WHERE tenant_id=? AND stage=? AND authorization_id=? AND state='active'
103
+ AND EXISTS(SELECT 1 FROM commerce_admission_snapshots WHERE tenant_id=? AND stage=? AND authorization_id=? AND linked_order_id=?)`,
104
+ params: [
105
+ input.tenantId,
106
+ input.stage,
107
+ admission.authorizationId,
108
+ input.tenantId,
109
+ input.stage,
110
+ admission.authorizationId,
111
+ input.orderId,
112
+ ],
113
+ },
114
+ ]);
115
+ trusted = Boolean(results[0]?.results.length);
116
+ }
117
+ const evidence = trusted && admission ? admission.evidence : UNKNOWN;
118
+ const service = new FulfillmentService(fulfillmentDbFromQueryable(db), {
119
+ policy: { adultSignatureRequired: false, prohibitedCarriers: [], carrierAccountId: null },
120
+ now: () => input.now,
121
+ });
122
+ const applied = await service.applySourceSnapshot(
123
+ { role: "owner", actorId: "commerce-source", tenantId: input.tenantId, stage: input.stage },
124
+ {
125
+ ...snapshot,
126
+ ...input,
127
+ sourceVersion: events.length,
128
+ amountMinor: snapshot.totals.totalMinor,
129
+ evidence,
130
+ ...(admission
131
+ ? {
132
+ evidenceExpiresAt: new Date(
133
+ Math.min(...Object.values(admission.provenance).map((proof) => Date.parse(proof.expiresAt))),
134
+ ).toISOString(),
135
+ }
136
+ : {}),
137
+ lines,
138
+ address: null,
139
+ compliance: trusted ? snapshot.compliance : "UNKNOWN",
140
+ stock: trusted && snapshot.payment === "captured" ? "confirmed" : "UNKNOWN",
141
+ fulfillment:
142
+ snapshot.fulfillment === "UNKNOWN" ? "UNKNOWN" : snapshot.fulfillment === "cancelled" ? "cancelled" : "held",
143
+ },
144
+ );
145
+ if (!applied.ok) throw new Error("projection-failed");
146
+ }
@@ -0,0 +1,60 @@
1
+ import type { Queryable } from "../catalog/d1/catalog.js";
2
+ import type { CommerceStage } from "@tot/public-runtime";
3
+ /** Readback reports projection health; it does not certify external payment reconciliation. */
4
+ export async function readCommerceDurability(
5
+ db: Queryable,
6
+ tenantId: string,
7
+ stage: CommerceStage,
8
+ now: string,
9
+ deployedSha: string,
10
+ ) {
11
+ const run = await db.first<{
12
+ run_id: string;
13
+ started_at: string;
14
+ completed_at: string | null;
15
+ deployed_sha: string;
16
+ delivered: number;
17
+ retried: number;
18
+ dead: number;
19
+ }>(
20
+ `SELECT * FROM commerce_recovery_runs WHERE tenant_id=? AND stage=? ORDER BY started_at DESC,run_id DESC LIMIT 1`,
21
+ tenantId,
22
+ stage,
23
+ );
24
+ const queue = await db.first<{ pending: number; dead: number; delivered: number }>(
25
+ `SELECT
26
+ SUM(CASE WHEN state='pending' THEN 1 ELSE 0 END) pending,
27
+ SUM(CASE WHEN state='dead' THEN 1 ELSE 0 END) dead,
28
+ SUM(CASE WHEN state='delivered' THEN 1 ELSE 0 END) delivered FROM scrappy_commerce_outbox WHERE tenant_id=? AND stage=?`,
29
+ tenantId,
30
+ stage,
31
+ );
32
+ const pending = Number(queue?.pending ?? 0),
33
+ dead = Number(queue?.dead ?? 0),
34
+ delivered = Number(queue?.delivered ?? 0);
35
+ const stale = Boolean(run?.completed_at && Date.parse(now) - Date.parse(run.completed_at) > 24 * 60 * 60 * 1000);
36
+ const readiness = !deployedSha ? "unreadable" : !run
37
+ ? "absent"
38
+ : !run.completed_at || pending || dead
39
+ ? "failed"
40
+ : stale || run.deployed_sha !== deployedSha
41
+ ? "stale"
42
+ : delivered > 0
43
+ ? "exercised"
44
+ : "configured-only";
45
+ return {
46
+ schema: "commerce-durability-v1",
47
+ readiness,
48
+ healthy: readiness === "exercised",
49
+ tenantId,
50
+ stage,
51
+ pending,
52
+ dead,
53
+ delivered,
54
+ run: run
55
+ ? { id: run.run_id, startedAt: run.started_at, completedAt: run.completed_at, deployedSha: run.deployed_sha }
56
+ : null,
57
+ paymentReconciliation: "not-measured",
58
+ labelAndPostageCompared: false,
59
+ };
60
+ }