@01.software/init 0.9.1 → 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.
- package/dist/ai-docs.d.ts +13 -0
- package/dist/ai-docs.js +1 -1
- package/dist/browser-auth-CJDrpp5T.d.ts +11 -0
- package/dist/{chunk-UA7WNT2F.js → chunk-4LHYICUL.js} +1 -1
- package/dist/chunk-4LHYICUL.js.map +1 -0
- package/dist/{chunk-TBGKXE3Q.js → chunk-NJ4X7VNK.js} +5 -5
- package/dist/chunk-NJ4X7VNK.js.map +1 -0
- package/dist/{chunk-5K2CB2Y5.js → chunk-Q6MSORYN.js} +14 -37
- package/dist/chunk-Q6MSORYN.js.map +1 -0
- package/dist/chunk-STM4DKVZ.js +183 -0
- package/dist/chunk-STM4DKVZ.js.map +1 -0
- package/dist/{chunk-2IGKOSK7.js → chunk-WDWJ73KP.js} +41 -215
- package/dist/chunk-WDWJ73KP.js.map +1 -0
- package/dist/create-app-templates/ecommerce/AGENTS.md +88 -0
- package/dist/create-app-templates/ecommerce/CHANGELOG.md +30 -0
- package/dist/create-app-templates/ecommerce/CLAUDE.md +1 -0
- package/dist/create-app-templates/ecommerce/README.md +139 -0
- package/dist/create-app-templates/ecommerce/app/api/auth/login/route.ts +30 -0
- package/dist/create-app-templates/ecommerce/app/api/auth/logout/route.ts +18 -0
- package/dist/create-app-templates/ecommerce/app/api/auth/register/route.ts +41 -0
- package/dist/create-app-templates/ecommerce/app/api/cart/clear/route.ts +12 -0
- package/dist/create-app-templates/ecommerce/app/api/cart/items/route.ts +45 -0
- package/dist/create-app-templates/ecommerce/app/api/cart/route.ts +14 -0
- package/dist/create-app-templates/ecommerce/app/api/checkout/payment-return/route.ts +86 -0
- package/dist/create-app-templates/ecommerce/app/api/checkout/reconcile/route.ts +50 -0
- package/dist/create-app-templates/ecommerce/app/api/checkout/route.ts +41 -0
- package/dist/create-app-templates/ecommerce/app/cart/page.tsx +10 -0
- package/dist/create-app-templates/ecommerce/app/checkout/page.tsx +10 -0
- package/dist/create-app-templates/ecommerce/app/checkout/success/page.tsx +34 -0
- package/dist/create-app-templates/ecommerce/app/favicon.ico +0 -0
- package/dist/create-app-templates/ecommerce/app/globals.css +67 -0
- package/dist/create-app-templates/ecommerce/app/layout.tsx +23 -0
- package/dist/create-app-templates/ecommerce/app/login/page.tsx +11 -0
- package/dist/create-app-templates/ecommerce/app/page.tsx +5 -0
- package/dist/create-app-templates/ecommerce/app/products/[slug]/page.tsx +46 -0
- package/dist/create-app-templates/ecommerce/app/products/page.tsx +45 -0
- package/dist/create-app-templates/ecommerce/app/register/page.tsx +11 -0
- package/dist/create-app-templates/ecommerce/app/webhook/payment/route.ts +20 -0
- package/dist/create-app-templates/ecommerce/app-config.ts +54 -0
- package/dist/create-app-templates/ecommerce/components/auth/auth-form.tsx +109 -0
- package/dist/create-app-templates/ecommerce/components/cart/cart-content.tsx +119 -0
- package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +267 -0
- package/dist/create-app-templates/ecommerce/components/checkout/checkout-reconcile.tsx +78 -0
- package/dist/create-app-templates/ecommerce/components/layout/account-nav.tsx +48 -0
- package/dist/create-app-templates/ecommerce/components/layout/account-slot.tsx +12 -0
- package/dist/create-app-templates/ecommerce/components/layout/cart-link.tsx +13 -0
- package/dist/create-app-templates/ecommerce/components/layout/page-shell.tsx +11 -0
- package/dist/create-app-templates/ecommerce/components/layout/site-header.tsx +22 -0
- package/dist/create-app-templates/ecommerce/components/product/add-to-cart.tsx +116 -0
- package/dist/create-app-templates/ecommerce/components/product/product-card.tsx +50 -0
- package/dist/create-app-templates/ecommerce/components/product/product-gallery.tsx +39 -0
- package/dist/create-app-templates/ecommerce/data/mock-catalog.json +173 -0
- package/dist/create-app-templates/ecommerce/eslint.config.mjs +18 -0
- package/dist/create-app-templates/ecommerce/lib/cart/cookie.ts +40 -0
- package/dist/create-app-templates/ecommerce/lib/cart/normalize.ts +32 -0
- package/dist/create-app-templates/ecommerce/lib/cart/parse-cart-request.ts +56 -0
- package/dist/create-app-templates/ecommerce/lib/cart/route-helpers.ts +17 -0
- package/dist/create-app-templates/ecommerce/lib/cart/select-provider.ts +44 -0
- package/dist/create-app-templates/ecommerce/lib/cart/server-cart.ts +96 -0
- package/dist/create-app-templates/ecommerce/lib/cart/sync-on-login.server.ts +34 -0
- package/dist/create-app-templates/ecommerce/lib/cart/use-cart.tsx +151 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/checkout-errors.ts +22 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/checkout-provider.ts +28 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/parse-checkout-payload.ts +76 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +63 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +3 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +336 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software-mappers.ts +312 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software.ts +913 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/product-summary.ts +37 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/provider.server.ts +60 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/provider.ts +96 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/stock.ts +37 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/types.ts +206 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/variant-selection.ts +23 -0
- package/dist/create-app-templates/ecommerce/lib/customer/auth-actions.ts +131 -0
- package/dist/create-app-templates/ecommerce/lib/customer/cart-sync.ts +44 -0
- package/dist/create-app-templates/ecommerce/lib/customer/client.server.ts +109 -0
- package/dist/create-app-templates/ecommerce/lib/customer/current-customer.ts +15 -0
- package/dist/create-app-templates/ecommerce/lib/customer/route-guard.ts +58 -0
- package/dist/create-app-templates/ecommerce/lib/customer/route-helpers.ts +75 -0
- package/dist/create-app-templates/ecommerce/lib/customer/session.ts +108 -0
- package/dist/create-app-templates/ecommerce/lib/format.ts +7 -0
- package/dist/create-app-templates/ecommerce/lib/payment/adapters/mock.ts +84 -0
- package/dist/create-app-templates/ecommerce/lib/payment/adapters/portone.ts +254 -0
- package/dist/create-app-templates/ecommerce/lib/payment/adapters/tosspayments.ts +287 -0
- package/dist/create-app-templates/ecommerce/lib/payment/amount-gate.ts +86 -0
- package/dist/create-app-templates/ecommerce/lib/payment/provider.server.ts +51 -0
- package/dist/create-app-templates/ecommerce/lib/payment/provider.ts +18 -0
- package/dist/create-app-templates/ecommerce/lib/payment/sync-order-payment.ts +96 -0
- package/dist/create-app-templates/ecommerce/lib/payment/types.ts +71 -0
- package/dist/create-app-templates/ecommerce/lib/server-only-guard.ts +20 -0
- package/dist/create-app-templates/ecommerce/next-env.d.ts +6 -0
- package/dist/create-app-templates/ecommerce/next.config.ts +16 -0
- package/dist/create-app-templates/ecommerce/package.json +33 -0
- package/dist/create-app-templates/ecommerce/postcss.config.mjs +7 -0
- package/dist/create-app-templates/ecommerce/tests/customer-auth.test.ts +263 -0
- package/dist/create-app-templates/ecommerce/tests/customer-cart.test.ts +392 -0
- package/dist/create-app-templates/ecommerce/tests/domain.test.ts +1537 -0
- package/dist/create-app-templates/ecommerce/tsconfig.json +35 -0
- package/dist/create-app-templates/registry.json +66 -0
- package/dist/create-app.d.ts +40 -0
- package/dist/create-app.js +652 -0
- package/dist/create-app.js.map +1 -0
- package/dist/detect-Bjxp9wcS.d.ts +13 -0
- package/dist/file-ops.d.ts +21 -0
- package/dist/file-ops.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +40 -0
- package/dist/init.js +5 -4
- package/dist/templates.d.ts +27 -0
- package/dist/templates.js +1 -1
- package/package.json +31 -15
- package/dist/chunk-2IGKOSK7.js.map +0 -1
- package/dist/chunk-5K2CB2Y5.js.map +0 -1
- package/dist/chunk-TBGKXE3Q.js.map +0 -1
- 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,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
|
+
}
|