@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.
- 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/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
|
@@ -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;
|