@01.software/init 0.9.2 → 0.10.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 (116) hide show
  1. package/dist/ai-docs.d.ts +13 -0
  2. package/dist/ai-docs.js +0 -0
  3. package/dist/browser-auth-CJDrpp5T.d.ts +11 -0
  4. package/dist/{chunk-UA7WNT2F.js → chunk-4LHYICUL.js} +1 -1
  5. package/dist/chunk-4LHYICUL.js.map +1 -0
  6. package/dist/{chunk-R4GGO33X.js → chunk-NJ4X7VNK.js} +1 -1
  7. package/dist/{chunk-R4GGO33X.js.map → chunk-NJ4X7VNK.js.map} +1 -1
  8. package/dist/chunk-Q6MSORYN.js +0 -0
  9. package/dist/chunk-STM4DKVZ.js +183 -0
  10. package/dist/chunk-STM4DKVZ.js.map +1 -0
  11. package/dist/{chunk-ENQSB4OF.js → chunk-WDWJ73KP.js} +40 -214
  12. package/dist/chunk-WDWJ73KP.js.map +1 -0
  13. package/dist/create-app-templates/ecommerce/AGENTS.md +88 -0
  14. package/dist/create-app-templates/ecommerce/CHANGELOG.md +30 -0
  15. package/dist/create-app-templates/ecommerce/CLAUDE.md +1 -0
  16. package/dist/create-app-templates/ecommerce/README.md +139 -0
  17. package/dist/create-app-templates/ecommerce/app/api/auth/login/route.ts +30 -0
  18. package/dist/create-app-templates/ecommerce/app/api/auth/logout/route.ts +18 -0
  19. package/dist/create-app-templates/ecommerce/app/api/auth/register/route.ts +41 -0
  20. package/dist/create-app-templates/ecommerce/app/api/cart/clear/route.ts +12 -0
  21. package/dist/create-app-templates/ecommerce/app/api/cart/items/route.ts +45 -0
  22. package/dist/create-app-templates/ecommerce/app/api/cart/route.ts +14 -0
  23. package/dist/create-app-templates/ecommerce/app/api/checkout/payment-return/route.ts +86 -0
  24. package/dist/create-app-templates/ecommerce/app/api/checkout/reconcile/route.ts +50 -0
  25. package/dist/create-app-templates/ecommerce/app/api/checkout/route.ts +41 -0
  26. package/dist/create-app-templates/ecommerce/app/cart/page.tsx +10 -0
  27. package/dist/create-app-templates/ecommerce/app/checkout/page.tsx +10 -0
  28. package/dist/create-app-templates/ecommerce/app/checkout/success/page.tsx +34 -0
  29. package/dist/create-app-templates/ecommerce/app/favicon.ico +0 -0
  30. package/dist/create-app-templates/ecommerce/app/globals.css +67 -0
  31. package/dist/create-app-templates/ecommerce/app/layout.tsx +23 -0
  32. package/dist/create-app-templates/ecommerce/app/login/page.tsx +11 -0
  33. package/dist/create-app-templates/ecommerce/app/page.tsx +5 -0
  34. package/dist/create-app-templates/ecommerce/app/products/[slug]/page.tsx +46 -0
  35. package/dist/create-app-templates/ecommerce/app/products/page.tsx +45 -0
  36. package/dist/create-app-templates/ecommerce/app/register/page.tsx +11 -0
  37. package/dist/create-app-templates/ecommerce/app/webhook/payment/route.ts +20 -0
  38. package/dist/create-app-templates/ecommerce/app-config.ts +54 -0
  39. package/dist/create-app-templates/ecommerce/components/auth/auth-form.tsx +109 -0
  40. package/dist/create-app-templates/ecommerce/components/cart/cart-content.tsx +119 -0
  41. package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +267 -0
  42. package/dist/create-app-templates/ecommerce/components/checkout/checkout-reconcile.tsx +78 -0
  43. package/dist/create-app-templates/ecommerce/components/layout/account-nav.tsx +48 -0
  44. package/dist/create-app-templates/ecommerce/components/layout/account-slot.tsx +12 -0
  45. package/dist/create-app-templates/ecommerce/components/layout/cart-link.tsx +13 -0
  46. package/dist/create-app-templates/ecommerce/components/layout/page-shell.tsx +11 -0
  47. package/dist/create-app-templates/ecommerce/components/layout/site-header.tsx +22 -0
  48. package/dist/create-app-templates/ecommerce/components/product/add-to-cart.tsx +116 -0
  49. package/dist/create-app-templates/ecommerce/components/product/product-card.tsx +50 -0
  50. package/dist/create-app-templates/ecommerce/components/product/product-gallery.tsx +39 -0
  51. package/dist/create-app-templates/ecommerce/data/mock-catalog.json +173 -0
  52. package/dist/create-app-templates/ecommerce/eslint.config.mjs +18 -0
  53. package/dist/create-app-templates/ecommerce/lib/cart/cookie.ts +40 -0
  54. package/dist/create-app-templates/ecommerce/lib/cart/normalize.ts +32 -0
  55. package/dist/create-app-templates/ecommerce/lib/cart/parse-cart-request.ts +56 -0
  56. package/dist/create-app-templates/ecommerce/lib/cart/route-helpers.ts +17 -0
  57. package/dist/create-app-templates/ecommerce/lib/cart/select-provider.ts +44 -0
  58. package/dist/create-app-templates/ecommerce/lib/cart/server-cart.ts +96 -0
  59. package/dist/create-app-templates/ecommerce/lib/cart/sync-on-login.server.ts +34 -0
  60. package/dist/create-app-templates/ecommerce/lib/cart/use-cart.tsx +151 -0
  61. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-errors.ts +22 -0
  62. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-provider.ts +28 -0
  63. package/dist/create-app-templates/ecommerce/lib/checkout/parse-checkout-payload.ts +76 -0
  64. package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +63 -0
  65. package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +3 -0
  66. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +336 -0
  67. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software-mappers.ts +312 -0
  68. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software.ts +913 -0
  69. package/dist/create-app-templates/ecommerce/lib/commerce/product-summary.ts +37 -0
  70. package/dist/create-app-templates/ecommerce/lib/commerce/provider.server.ts +60 -0
  71. package/dist/create-app-templates/ecommerce/lib/commerce/provider.ts +96 -0
  72. package/dist/create-app-templates/ecommerce/lib/commerce/stock.ts +37 -0
  73. package/dist/create-app-templates/ecommerce/lib/commerce/types.ts +206 -0
  74. package/dist/create-app-templates/ecommerce/lib/commerce/variant-selection.ts +23 -0
  75. package/dist/create-app-templates/ecommerce/lib/customer/auth-actions.ts +131 -0
  76. package/dist/create-app-templates/ecommerce/lib/customer/cart-sync.ts +44 -0
  77. package/dist/create-app-templates/ecommerce/lib/customer/client.server.ts +109 -0
  78. package/dist/create-app-templates/ecommerce/lib/customer/current-customer.ts +15 -0
  79. package/dist/create-app-templates/ecommerce/lib/customer/route-guard.ts +58 -0
  80. package/dist/create-app-templates/ecommerce/lib/customer/route-helpers.ts +75 -0
  81. package/dist/create-app-templates/ecommerce/lib/customer/session.ts +108 -0
  82. package/dist/create-app-templates/ecommerce/lib/format.ts +7 -0
  83. package/dist/create-app-templates/ecommerce/lib/payment/adapters/mock.ts +84 -0
  84. package/dist/create-app-templates/ecommerce/lib/payment/adapters/portone.ts +254 -0
  85. package/dist/create-app-templates/ecommerce/lib/payment/adapters/tosspayments.ts +287 -0
  86. package/dist/create-app-templates/ecommerce/lib/payment/amount-gate.ts +86 -0
  87. package/dist/create-app-templates/ecommerce/lib/payment/provider.server.ts +51 -0
  88. package/dist/create-app-templates/ecommerce/lib/payment/provider.ts +18 -0
  89. package/dist/create-app-templates/ecommerce/lib/payment/sync-order-payment.ts +96 -0
  90. package/dist/create-app-templates/ecommerce/lib/payment/types.ts +71 -0
  91. package/dist/create-app-templates/ecommerce/lib/server-only-guard.ts +20 -0
  92. package/dist/create-app-templates/ecommerce/next-env.d.ts +6 -0
  93. package/dist/create-app-templates/ecommerce/next.config.ts +16 -0
  94. package/dist/create-app-templates/ecommerce/package.json +33 -0
  95. package/dist/create-app-templates/ecommerce/postcss.config.mjs +7 -0
  96. package/dist/create-app-templates/ecommerce/tests/customer-auth.test.ts +263 -0
  97. package/dist/create-app-templates/ecommerce/tests/customer-cart.test.ts +392 -0
  98. package/dist/create-app-templates/ecommerce/tests/domain.test.ts +1537 -0
  99. package/dist/create-app-templates/ecommerce/tsconfig.json +35 -0
  100. package/dist/create-app-templates/registry.json +66 -0
  101. package/dist/create-app.d.ts +40 -0
  102. package/dist/create-app.js +652 -0
  103. package/dist/create-app.js.map +1 -0
  104. package/dist/detect-Bjxp9wcS.d.ts +13 -0
  105. package/dist/file-ops.d.ts +21 -0
  106. package/dist/file-ops.js +1 -1
  107. package/dist/index.d.ts +2 -0
  108. package/dist/index.js +4 -3
  109. package/dist/index.js.map +1 -1
  110. package/dist/init.d.ts +40 -0
  111. package/dist/init.js +4 -3
  112. package/dist/templates.d.ts +27 -0
  113. package/dist/templates.js +1 -1
  114. package/package.json +31 -15
  115. package/dist/chunk-ENQSB4OF.js.map +0 -1
  116. package/dist/chunk-UA7WNT2F.js.map +0 -1
@@ -0,0 +1,28 @@
1
+ import "../server-only-guard.ts";
2
+ import {
3
+ getCommerceProvider,
4
+ getCustomerCheckoutProvider,
5
+ } from "../commerce/provider.server.ts";
6
+ import { getSessionToken } from "../customer/session.ts";
7
+ import type { CheckoutCommerceProvider } from "./types.ts";
8
+
9
+ export type CheckoutProviderFactories = {
10
+ guest: () => CheckoutCommerceProvider;
11
+ customer: (token: string) => CheckoutCommerceProvider;
12
+ };
13
+
14
+ export function resolveCheckoutCommerceProvider(
15
+ sessionToken: string | null,
16
+ factories: CheckoutProviderFactories,
17
+ ): CheckoutCommerceProvider {
18
+ return sessionToken
19
+ ? factories.customer(sessionToken)
20
+ : factories.guest();
21
+ }
22
+
23
+ export async function getCheckoutCommerceProvider(): Promise<CheckoutCommerceProvider> {
24
+ return resolveCheckoutCommerceProvider(await getSessionToken(), {
25
+ guest: getCommerceProvider,
26
+ customer: getCustomerCheckoutProvider,
27
+ });
28
+ }
@@ -0,0 +1,76 @@
1
+ import type { CustomerSnapshot, ShippingAddress } from "../commerce/types.ts";
2
+
3
+ const MAX_TEXT_LENGTH = 160;
4
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5
+
6
+ /**
7
+ * Checkout collects only the buyer + shipping snapshot. The cart contents live
8
+ * server-side (addressed by the HttpOnly `cartToken`); there are no
9
+ * client-supplied lines or totals — the cart is the quote.
10
+ */
11
+ export type CheckoutPayload = {
12
+ customerSnapshot: CustomerSnapshot;
13
+ shippingAddress: ShippingAddress;
14
+ };
15
+
16
+ export function parseCheckoutPayload(input: unknown): CheckoutPayload {
17
+ if (!input || typeof input !== "object") {
18
+ throw new Error("Checkout payload must be an object");
19
+ }
20
+
21
+ const body = input as {
22
+ customerSnapshot?: unknown;
23
+ shippingAddress?: unknown;
24
+ };
25
+ const customer = parseRecord(body.customerSnapshot, "customerSnapshot");
26
+ const shipping = parseRecord(body.shippingAddress, "shippingAddress");
27
+ const email = requiredString(customer.email, "email");
28
+ if (!EMAIL_RE.test(email)) {
29
+ throw new Error("email must be a valid email address");
30
+ }
31
+
32
+ return {
33
+ customerSnapshot: {
34
+ name: requiredString(customer.name, "customer name"),
35
+ email,
36
+ phone: requiredString(customer.phone, "customer phone"),
37
+ },
38
+ shippingAddress: {
39
+ recipientName: requiredString(shipping.recipientName, "recipient name"),
40
+ phone: requiredString(shipping.phone, "shipping phone"),
41
+ postalCode: requiredString(shipping.postalCode, "postal code"),
42
+ address: requiredString(shipping.address, "address"),
43
+ detailAddress: requiredString(shipping.detailAddress, "address detail"),
44
+ deliveryMessage: optionalString(
45
+ shipping.deliveryMessage,
46
+ "shipping message",
47
+ ),
48
+ },
49
+ };
50
+ }
51
+
52
+ function parseRecord(input: unknown, field: string): Record<string, unknown> {
53
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
54
+ throw new Error(`${field} must be an object`);
55
+ }
56
+
57
+ return input as Record<string, unknown>;
58
+ }
59
+
60
+ function requiredString(input: unknown, field: string): string {
61
+ const value = optionalString(input, field);
62
+ if (!value) throw new Error(`${field} is required`);
63
+ return value;
64
+ }
65
+
66
+ function optionalString(input: unknown, field: string): string {
67
+ if (input === undefined || input === null) return "";
68
+ if (typeof input !== "string") throw new Error(`${field} must be a string`);
69
+
70
+ const value = input.trim();
71
+ if (value.length > MAX_TEXT_LENGTH) {
72
+ throw new Error(`${field} must be ${MAX_TEXT_LENGTH} characters or fewer`);
73
+ }
74
+
75
+ return value;
76
+ }
@@ -0,0 +1,63 @@
1
+ import '../server-only-guard.ts'
2
+ import type { CheckoutCommerceProvider } from './types.ts'
3
+ import type { ClientPaymentRequest } from '../payment/types.ts'
4
+ import type { PaymentProvider } from '../payment/provider.ts'
5
+ import {
6
+ parseCheckoutPayload,
7
+ type CheckoutPayload,
8
+ } from './parse-checkout-payload.ts'
9
+
10
+ export type StartCheckoutResult = {
11
+ orderNumber: string
12
+ paymentId: string
13
+ paymentName: string
14
+ amount: number
15
+ currency: string
16
+ redirectUrl?: string
17
+ clientPayment?: ClientPaymentRequest
18
+ }
19
+
20
+ /**
21
+ * Convert the server cart (addressed by `cartToken`) into an order via
22
+ * `orders.checkout({ cartId })` and open the PG payment window. The cart is the
23
+ * single payable quote — no client-supplied lines or totals.
24
+ */
25
+ export async function startCheckout(input: {
26
+ cartToken: string
27
+ payload: unknown
28
+ commerceProvider: CheckoutCommerceProvider
29
+ paymentProvider: PaymentProvider
30
+ }): Promise<StartCheckoutResult> {
31
+ const payload: CheckoutPayload = parseCheckoutPayload(input.payload)
32
+ const pending = await input.commerceProvider.checkoutCart({
33
+ cartToken: input.cartToken,
34
+ customerSnapshot: payload.customerSnapshot,
35
+ shippingAddress: payload.shippingAddress,
36
+ })
37
+
38
+ const payment = await input.paymentProvider.requestPayment({
39
+ paymentId: pending.paymentId,
40
+ orderNumber: pending.order.orderNumber,
41
+ orderName: pending.paymentName,
42
+ amount: pending.amount,
43
+ currency: pending.currency,
44
+ customer: pending.order.customerSnapshot,
45
+ })
46
+
47
+ if (!payment.ok) {
48
+ // Checkout created an OPEN Checkout but no Order/Transaction yet. A failed
49
+ // PG handoff leaves nothing durable to cancel; the abandoned Checkout (and
50
+ // its inventory hold) is released by the Console `expire-checkouts` cron.
51
+ throw new Error(payment.reason ?? 'payment_request_failed')
52
+ }
53
+
54
+ return {
55
+ orderNumber: pending.order.orderNumber,
56
+ paymentId: pending.paymentId,
57
+ paymentName: pending.paymentName,
58
+ amount: pending.amount,
59
+ currency: pending.currency,
60
+ redirectUrl: payment.redirectUrl,
61
+ clientPayment: payment.clientPayment,
62
+ }
63
+ }
@@ -0,0 +1,3 @@
1
+ import type { CommerceProvider } from "../commerce/provider.ts";
2
+
3
+ export type CheckoutCommerceProvider = Pick<CommerceProvider, "checkoutCart">;
@@ -0,0 +1,336 @@
1
+ import seed from "../../../data/mock-catalog.json" with { type: "json" };
2
+ import { normalizeCartLines } from "../../cart/normalize.ts";
3
+ import type { CommerceProvider } from "../provider.ts";
4
+ import { checkVariantStock } from "../stock.ts";
5
+ import type {
6
+ CartItemView,
7
+ CartView,
8
+ CheckoutCartInput,
9
+ CreatePendingOrderResult,
10
+ Order,
11
+ ProductDetail,
12
+ ProductVariant,
13
+ ShippingPolicy,
14
+ } from "../types.ts";
15
+
16
+ /**
17
+ * In-memory server-cart emulation for the zero-backend demo. The mock stands in
18
+ * for the Console `commerce.cart.*` + `orders.checkout` surface so the template
19
+ * runs without credentials. Carts and orders live in module-level maps for the
20
+ * lifetime of the dev server process — there is no file-based index.
21
+ */
22
+ type MockCartItem = {
23
+ cartItemId: string;
24
+ productId: string;
25
+ variantId: string;
26
+ optionId?: string;
27
+ quantity: number;
28
+ unitPrice: number;
29
+ };
30
+
31
+ type MockCart = {
32
+ id: string;
33
+ cartToken: string;
34
+ currency: string;
35
+ items: MockCartItem[];
36
+ };
37
+
38
+ const carts = new Map<string, MockCart>();
39
+ const orders = new Map<string, Order>();
40
+
41
+ export function createMockCommerceProvider(): CommerceProvider {
42
+ const products = seed.products as ProductDetail[];
43
+ const shippingPolicy = seed.shippingPolicy as ShippingPolicy;
44
+ const variants = allVariants(products);
45
+ const variantById = new Map(variants.map((variant) => [variant.id, variant]));
46
+
47
+ function viewCart(cart: MockCart): CartView {
48
+ const items: CartItemView[] = cart.items.map((item) => {
49
+ const variant = variantById.get(item.variantId);
50
+ const detail = products.find(
51
+ (entry) => entry.product.id === item.productId,
52
+ );
53
+ const image =
54
+ variant?.images[0] ??
55
+ detail?.product.thumbnail ??
56
+ detail?.product.images[0] ??
57
+ null;
58
+ return {
59
+ cartItemId: item.cartItemId,
60
+ productId: item.productId,
61
+ variantId: item.variantId,
62
+ quantity: item.quantity,
63
+ unitAmount: item.unitPrice,
64
+ lineAmount: item.unitPrice * item.quantity,
65
+ productTitle: detail?.product.title ?? "Unknown product",
66
+ variantTitle: variant?.title,
67
+ image,
68
+ };
69
+ });
70
+
71
+ const subtotalAmount = items.reduce((sum, item) => sum + item.lineAmount, 0);
72
+ const shippingAmount =
73
+ shippingPolicy.freeAboveAmount !== undefined &&
74
+ subtotalAmount >= shippingPolicy.freeAboveAmount
75
+ ? 0
76
+ : subtotalAmount > 0
77
+ ? shippingPolicy.baseAmount
78
+ : 0;
79
+
80
+ return {
81
+ id: cart.id,
82
+ currency: shippingPolicy.currency,
83
+ subtotalAmount,
84
+ shippingAmount,
85
+ discountAmount: 0,
86
+ totalAmount: subtotalAmount + shippingAmount,
87
+ items,
88
+ };
89
+ }
90
+
91
+ function requireCart(cartToken: string): MockCart {
92
+ const cart = carts.get(cartToken);
93
+ if (!cart) throw new Error("Cart not found");
94
+ return cart;
95
+ }
96
+
97
+ return {
98
+ async listProducts(input) {
99
+ const limit = input?.limit ?? 24;
100
+ const publishedProducts = products.filter(
101
+ (entry) => entry.product.status === "published",
102
+ );
103
+ return {
104
+ products: publishedProducts.slice(0, limit),
105
+ total: publishedProducts.length,
106
+ };
107
+ },
108
+
109
+ async getProductBySlug(slug) {
110
+ return (
111
+ products.find(
112
+ (entry) =>
113
+ entry.product.slug === slug &&
114
+ entry.product.status === "published",
115
+ ) ?? null
116
+ );
117
+ },
118
+
119
+ async getVariantsByIds(ids) {
120
+ const idSet = new Set(ids);
121
+ return variants.filter((variant) => idSet.has(variant.id));
122
+ },
123
+
124
+ async getShippingPolicy() {
125
+ return shippingPolicy;
126
+ },
127
+
128
+ async checkStock(items) {
129
+ return checkVariantStock({
130
+ lines: normalizeCartLines(items),
131
+ variants,
132
+ });
133
+ },
134
+
135
+ async createCart() {
136
+ const cartToken = `mock_cart_${crypto.randomUUID()}`;
137
+ carts.set(cartToken, {
138
+ id: `cart_${crypto.randomUUID()}`,
139
+ cartToken,
140
+ currency: shippingPolicy.currency,
141
+ items: [],
142
+ });
143
+ return { cartToken };
144
+ },
145
+
146
+ async getCart(cartToken) {
147
+ const cart = carts.get(cartToken);
148
+ return cart ? viewCart(cart) : null;
149
+ },
150
+
151
+ async addCartItem({ cartToken, item }) {
152
+ const cart = requireCart(cartToken);
153
+ const variant = variantById.get(item.variantId);
154
+ if (!variant) throw new Error("Variant not found");
155
+
156
+ const existing = cart.items.find(
157
+ (line) => line.variantId === item.variantId,
158
+ );
159
+ const nextQuantity = (existing?.quantity ?? 0) + item.quantity;
160
+ const stock = checkVariantStock({
161
+ lines: [{ variantId: item.variantId, quantity: nextQuantity }],
162
+ variants,
163
+ });
164
+ if (!stock.ok) throw new Error("Insufficient stock");
165
+
166
+ if (existing) {
167
+ existing.quantity = Math.min(99, nextQuantity);
168
+ } else {
169
+ cart.items.push({
170
+ cartItemId: `item_${crypto.randomUUID()}`,
171
+ productId: item.productId || variant.productId,
172
+ variantId: item.variantId,
173
+ optionId: item.optionId,
174
+ quantity: Math.min(99, item.quantity),
175
+ unitPrice: variant.price,
176
+ });
177
+ }
178
+ return viewCart(cart);
179
+ },
180
+
181
+ async updateCartItem({ cartToken, cartItemId, quantity }) {
182
+ const cart = requireCart(cartToken);
183
+ const line = cart.items.find((entry) => entry.cartItemId === cartItemId);
184
+ if (!line) throw new Error("Cart item not found");
185
+ if (quantity <= 0) {
186
+ cart.items = cart.items.filter(
187
+ (entry) => entry.cartItemId !== cartItemId,
188
+ );
189
+ } else {
190
+ line.quantity = Math.min(99, Math.floor(quantity));
191
+ }
192
+ return viewCart(cart);
193
+ },
194
+
195
+ async removeCartItem({ cartToken, cartItemId }) {
196
+ const cart = requireCart(cartToken);
197
+ cart.items = cart.items.filter((entry) => entry.cartItemId !== cartItemId);
198
+ return viewCart(cart);
199
+ },
200
+
201
+ async clearCart({ cartToken }) {
202
+ const cart = requireCart(cartToken);
203
+ cart.items = [];
204
+ return viewCart(cart);
205
+ },
206
+
207
+ async checkoutCart(input) {
208
+ return checkoutMockCart({ input, products, viewCart, requireCart });
209
+ },
210
+
211
+ async getOrderByPaymentId({ paymentId }) {
212
+ return orders.get(paymentId) ?? null;
213
+ },
214
+
215
+ async confirmOrderPayment(input) {
216
+ const order = orders.get(input.paymentId);
217
+ if (!order) throw new Error("Order not found");
218
+ const paidOrder: Order = {
219
+ ...order,
220
+ displayStatus: "paid",
221
+ transactions: order.transactions.map((transaction) =>
222
+ transaction.paymentId === input.paymentId
223
+ ? { ...transaction, provider: input.provider, status: "paid" }
224
+ : transaction,
225
+ ),
226
+ };
227
+ orders.set(input.paymentId, paidOrder);
228
+ return paidOrder;
229
+ },
230
+
231
+ async markPaymentFailed(input) {
232
+ const order = orders.get(input.paymentId);
233
+ if (!order) return null;
234
+ const failedOrder: Order = {
235
+ ...order,
236
+ transactions: order.transactions.map((transaction) =>
237
+ transaction.paymentId === input.paymentId
238
+ ? { ...transaction, provider: input.provider, status: "failed" }
239
+ : transaction,
240
+ ),
241
+ };
242
+ orders.set(input.paymentId, failedOrder);
243
+ return failedOrder;
244
+ },
245
+
246
+ async cancelPendingOrder(input) {
247
+ const order = orders.get(input.paymentId);
248
+ if (!order) return null;
249
+ const canceledOrder: Order = {
250
+ ...order,
251
+ displayStatus: "canceled",
252
+ transactions: order.transactions.map((transaction) =>
253
+ transaction.paymentId === input.paymentId
254
+ ? { ...transaction, provider: input.provider, status: "canceled" }
255
+ : transaction,
256
+ ),
257
+ };
258
+ orders.set(input.paymentId, canceledOrder);
259
+ return canceledOrder;
260
+ },
261
+ };
262
+ }
263
+
264
+ function checkoutMockCart(args: {
265
+ input: CheckoutCartInput;
266
+ products: ProductDetail[];
267
+ viewCart: (cart: MockCart) => CartView;
268
+ requireCart: (cartToken: string) => MockCart;
269
+ }): CreatePendingOrderResult {
270
+ const cart = args.requireCart(args.input.cartToken);
271
+ const view = args.viewCart(cart);
272
+ if (view.items.length === 0) throw new Error("Cart is empty");
273
+
274
+ const paymentId = `mock_${Date.now()}_${Math.random().toString(16).slice(2)}`;
275
+ const orderNumber = `ORD-${Date.now().toString().slice(-8)}`;
276
+ const order: Order = {
277
+ id: `order_${paymentId}`,
278
+ orderNumber,
279
+ displayStatus: "pending",
280
+ items: view.items.map((item) => ({
281
+ variantId: item.variantId,
282
+ productTitle: item.productTitle,
283
+ variantTitle: item.variantTitle,
284
+ quantity: item.quantity,
285
+ unitAmount: item.unitAmount,
286
+ lineAmount: item.lineAmount,
287
+ })),
288
+ customerSnapshot: args.input.customerSnapshot,
289
+ shippingAddress: args.input.shippingAddress,
290
+ subtotalAmount: view.subtotalAmount,
291
+ shippingAmount: view.shippingAmount,
292
+ totalAmount: view.totalAmount,
293
+ transactions: [
294
+ {
295
+ paymentId,
296
+ provider: "mock",
297
+ status: "pending",
298
+ amount: view.totalAmount,
299
+ },
300
+ ],
301
+ };
302
+
303
+ orders.set(paymentId, order);
304
+ // The cart is consumed by checkout (Shopify cart→checkout boundary); the
305
+ // browser cookie will resolve to no active cart and mint a fresh one.
306
+ carts.delete(args.input.cartToken);
307
+
308
+ return {
309
+ order,
310
+ paymentId,
311
+ paymentName: buildPaymentName(order.items.map((item) => item.productTitle)),
312
+ amount: view.totalAmount,
313
+ currency: view.currency,
314
+ };
315
+ }
316
+
317
+ function allVariants(products: ProductDetail[]): ProductVariant[] {
318
+ return products.flatMap((entry) => entry.variants);
319
+ }
320
+
321
+ function buildPaymentName(productTitles: string[]): string {
322
+ const [first, ...rest] = productTitles;
323
+ if (!first) return "Order";
324
+ return rest.length === 0 ? first : `${first} and ${rest.length} more`;
325
+ }
326
+
327
+ /** Test helper: reset the in-memory stores between cases. */
328
+ export function resetMockCommerceState(): void {
329
+ carts.clear();
330
+ orders.clear();
331
+ }
332
+
333
+ /** Test helper: read a stored mock order by its mock payment id. */
334
+ export function getMockOrder(paymentId: string): Order | null {
335
+ return orders.get(paymentId) ?? null;
336
+ }