@01.software/init 0.9.2 → 0.10.1
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/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-R4GGO33X.js → chunk-NJ4X7VNK.js} +1 -1
- package/dist/{chunk-R4GGO33X.js.map → chunk-NJ4X7VNK.js.map} +1 -1
- package/dist/chunk-STM4DKVZ.js +183 -0
- package/dist/chunk-STM4DKVZ.js.map +1 -0
- package/dist/{chunk-ENQSB4OF.js → chunk-WDWJ73KP.js} +40 -214
- 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 +48 -0
- package/dist/create-app-templates/ecommerce/CLAUDE.md +1 -0
- package/dist/create-app-templates/ecommerce/README.md +154 -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 +129 -0
- package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +307 -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 +135 -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 +86 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +73 -0
- package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +6 -0
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +346 -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 +930 -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 +208 -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 +401 -0
- package/dist/create-app-templates/ecommerce/tests/domain.test.ts +1632 -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 +4 -3
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +40 -0
- package/dist/init.js +4 -3
- package/dist/templates.d.ts +27 -0
- package/dist/templates.js +1 -1
- package/package.json +18 -3
- package/dist/chunk-ENQSB4OF.js.map +0 -1
- package/dist/chunk-UA7WNT2F.js.map +0 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import type { CartItemRef, CartView } from "../commerce/types.ts";
|
|
13
|
+
|
|
14
|
+
type CartContextValue = {
|
|
15
|
+
cart: CartView | null;
|
|
16
|
+
loading: boolean;
|
|
17
|
+
error: string | null;
|
|
18
|
+
itemCount: number;
|
|
19
|
+
addItem: (item: CartItemRef) => Promise<boolean>;
|
|
20
|
+
updateItem: (input: {
|
|
21
|
+
cartItemId: string;
|
|
22
|
+
quantity: number;
|
|
23
|
+
}) => Promise<void>;
|
|
24
|
+
removeItem: (cartItemId: string) => Promise<void>;
|
|
25
|
+
clear: () => Promise<void>;
|
|
26
|
+
/** Drop the local cart view (e.g. after checkout consumed the server cart). */
|
|
27
|
+
reset: () => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const CartContext = createContext<CartContextValue | null>(null);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Client cart state backed by the server cart routes (`/api/cart*`). The cart
|
|
34
|
+
* lives in the Console (`commerce.cart.*`); this holds only the rendered view —
|
|
35
|
+
* the ownership `cartToken` stays in the HttpOnly cookie, never in JS.
|
|
36
|
+
*/
|
|
37
|
+
export function CartProvider({ children }: { children: ReactNode }) {
|
|
38
|
+
const [cart, setCart] = useState<CartView | null>(null);
|
|
39
|
+
const [loading, setLoading] = useState(true);
|
|
40
|
+
const [error, setError] = useState<string | null>(null);
|
|
41
|
+
|
|
42
|
+
// Hydrate the current cart on mount. setState lives in the async resolution
|
|
43
|
+
// callbacks (external-system → React sync), not the synchronous effect body.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let active = true;
|
|
46
|
+
fetch("/api/cart")
|
|
47
|
+
.then((response) => response.json() as Promise<{ cart?: CartView | null }>)
|
|
48
|
+
.then((data) => {
|
|
49
|
+
if (!active) return;
|
|
50
|
+
setCart(data.cart ?? null);
|
|
51
|
+
setLoading(false);
|
|
52
|
+
})
|
|
53
|
+
.catch(() => {
|
|
54
|
+
if (!active) return;
|
|
55
|
+
setCart(null);
|
|
56
|
+
setLoading(false);
|
|
57
|
+
});
|
|
58
|
+
return () => {
|
|
59
|
+
active = false;
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
const mutate = useCallback(
|
|
64
|
+
async (url: string, method: string, body?: unknown): Promise<boolean> => {
|
|
65
|
+
setError(null);
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(url, {
|
|
68
|
+
method,
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
71
|
+
});
|
|
72
|
+
const data = (await response.json().catch(() => ({}))) as {
|
|
73
|
+
cart?: CartView | null;
|
|
74
|
+
message?: string;
|
|
75
|
+
};
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
setError(data.message ?? "Cart update failed");
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
setCart(data.cart ?? null);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
setError("Cart update failed");
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const addItem = useCallback(
|
|
91
|
+
(item: CartItemRef) => mutate("/api/cart/items", "POST", item),
|
|
92
|
+
[mutate],
|
|
93
|
+
);
|
|
94
|
+
const updateItem = useCallback(
|
|
95
|
+
async (input: { cartItemId: string; quantity: number }) => {
|
|
96
|
+
await mutate("/api/cart/items", "PATCH", input);
|
|
97
|
+
},
|
|
98
|
+
[mutate],
|
|
99
|
+
);
|
|
100
|
+
const removeItem = useCallback(
|
|
101
|
+
async (cartItemId: string) => {
|
|
102
|
+
await mutate("/api/cart/items", "DELETE", { cartItemId });
|
|
103
|
+
},
|
|
104
|
+
[mutate],
|
|
105
|
+
);
|
|
106
|
+
const clear = useCallback(async () => {
|
|
107
|
+
await mutate("/api/cart/clear", "POST");
|
|
108
|
+
}, [mutate]);
|
|
109
|
+
const reset = useCallback(() => {
|
|
110
|
+
setCart(null);
|
|
111
|
+
setError(null);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const itemCount =
|
|
115
|
+
cart?.items.reduce((sum, item) => sum + item.quantity, 0) ?? 0;
|
|
116
|
+
|
|
117
|
+
const value = useMemo<CartContextValue>(
|
|
118
|
+
() => ({
|
|
119
|
+
cart,
|
|
120
|
+
loading,
|
|
121
|
+
error,
|
|
122
|
+
itemCount,
|
|
123
|
+
addItem,
|
|
124
|
+
updateItem,
|
|
125
|
+
removeItem,
|
|
126
|
+
clear,
|
|
127
|
+
reset,
|
|
128
|
+
}),
|
|
129
|
+
[
|
|
130
|
+
cart,
|
|
131
|
+
loading,
|
|
132
|
+
error,
|
|
133
|
+
itemCount,
|
|
134
|
+
addItem,
|
|
135
|
+
updateItem,
|
|
136
|
+
removeItem,
|
|
137
|
+
clear,
|
|
138
|
+
reset,
|
|
139
|
+
],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function useCart(): CartContextValue {
|
|
146
|
+
const context = useContext(CartContext);
|
|
147
|
+
if (!context) {
|
|
148
|
+
throw new Error("useCart must be used within a CartProvider");
|
|
149
|
+
}
|
|
150
|
+
return context;
|
|
151
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const CHECKOUT_VALIDATION_PATTERN = /required|must be|valid email|payload|empty/;
|
|
2
|
+
|
|
3
|
+
export function getCheckoutErrorMessage(error: unknown): string {
|
|
4
|
+
return error instanceof Error ? error.message : "Checkout failed";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getCheckoutErrorStatus(error: unknown): number {
|
|
8
|
+
const status = readHttpStatus(error);
|
|
9
|
+
if (status !== null) return status;
|
|
10
|
+
|
|
11
|
+
return CHECKOUT_VALIDATION_PATTERN.test(getCheckoutErrorMessage(error))
|
|
12
|
+
? 400
|
|
13
|
+
: 422;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readHttpStatus(error: unknown): number | null {
|
|
17
|
+
if (typeof error !== "object" || error === null) return null;
|
|
18
|
+
const status = (error as { status?: unknown }).status;
|
|
19
|
+
return typeof status === "number" && status >= 400 && status < 500
|
|
20
|
+
? status
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
@@ -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,86 @@
|
|
|
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(
|
|
17
|
+
input: unknown,
|
|
18
|
+
options: { requiresShipping?: boolean } = {},
|
|
19
|
+
): CheckoutPayload {
|
|
20
|
+
if (!input || typeof input !== 'object') {
|
|
21
|
+
throw new Error('Checkout payload must be an object')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const body = input as {
|
|
25
|
+
customerSnapshot?: unknown
|
|
26
|
+
shippingAddress?: unknown
|
|
27
|
+
}
|
|
28
|
+
const customer = parseRecord(body.customerSnapshot, 'customerSnapshot')
|
|
29
|
+
const email = requiredString(customer.email, 'email')
|
|
30
|
+
if (!EMAIL_RE.test(email)) {
|
|
31
|
+
throw new Error('email must be a valid email address')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const customerSnapshot = {
|
|
35
|
+
name: requiredString(customer.name, 'customer name'),
|
|
36
|
+
email,
|
|
37
|
+
phone: requiredString(customer.phone, 'customer phone'),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options.requiresShipping === false) {
|
|
41
|
+
return { customerSnapshot }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const shipping = parseRecord(body.shippingAddress, 'shippingAddress')
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
customerSnapshot,
|
|
48
|
+
shippingAddress: {
|
|
49
|
+
recipientName: requiredString(shipping.recipientName, 'recipient name'),
|
|
50
|
+
phone: requiredString(shipping.phone, 'shipping phone'),
|
|
51
|
+
postalCode: requiredString(shipping.postalCode, 'postal code'),
|
|
52
|
+
address: requiredString(shipping.address, 'address'),
|
|
53
|
+
detailAddress: requiredString(shipping.detailAddress, 'address detail'),
|
|
54
|
+
deliveryMessage: optionalString(
|
|
55
|
+
shipping.deliveryMessage,
|
|
56
|
+
'shipping message',
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseRecord(input: unknown, field: string): Record<string, unknown> {
|
|
63
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
64
|
+
throw new Error(`${field} must be an object`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return input as Record<string, unknown>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function requiredString(input: unknown, field: string): string {
|
|
71
|
+
const value = optionalString(input, field)
|
|
72
|
+
if (!value) throw new Error(`${field} is required`)
|
|
73
|
+
return value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function optionalString(input: unknown, field: string): string {
|
|
77
|
+
if (input === undefined || input === null) return ''
|
|
78
|
+
if (typeof input !== 'string') throw new Error(`${field} must be a string`)
|
|
79
|
+
|
|
80
|
+
const value = input.trim()
|
|
81
|
+
if (value.length > MAX_TEXT_LENGTH) {
|
|
82
|
+
throw new Error(`${field} must be ${MAX_TEXT_LENGTH} characters or fewer`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return value
|
|
86
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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 cart = await input.commerceProvider.getCart(input.cartToken)
|
|
32
|
+
if (!cart) throw new Error('Cart not found')
|
|
33
|
+
|
|
34
|
+
const requiresShipping = cart.items.some(
|
|
35
|
+
(item) => item.requiresShipping !== false,
|
|
36
|
+
)
|
|
37
|
+
const payload: CheckoutPayload = parseCheckoutPayload(input.payload, {
|
|
38
|
+
requiresShipping,
|
|
39
|
+
})
|
|
40
|
+
const pending = await input.commerceProvider.checkoutCart({
|
|
41
|
+
cartToken: input.cartToken,
|
|
42
|
+
customerSnapshot: payload.customerSnapshot,
|
|
43
|
+
...(payload.shippingAddress
|
|
44
|
+
? { shippingAddress: payload.shippingAddress }
|
|
45
|
+
: {}),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const payment = await input.paymentProvider.requestPayment({
|
|
49
|
+
paymentId: pending.paymentId,
|
|
50
|
+
orderNumber: pending.order.orderNumber,
|
|
51
|
+
orderName: pending.paymentName,
|
|
52
|
+
amount: pending.amount,
|
|
53
|
+
currency: pending.currency,
|
|
54
|
+
customer: pending.order.customerSnapshot,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (!payment.ok) {
|
|
58
|
+
// Checkout created an OPEN Checkout but no Order/Transaction yet. A failed
|
|
59
|
+
// PG handoff leaves nothing durable to cancel; the abandoned Checkout (and
|
|
60
|
+
// its inventory hold) is released by the Console `expire-checkouts` cron.
|
|
61
|
+
throw new Error(payment.reason ?? 'payment_request_failed')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
orderNumber: pending.order.orderNumber,
|
|
66
|
+
paymentId: pending.paymentId,
|
|
67
|
+
paymentName: pending.paymentName,
|
|
68
|
+
amount: pending.amount,
|
|
69
|
+
currency: pending.currency,
|
|
70
|
+
redirectUrl: payment.redirectUrl,
|
|
71
|
+
clientPayment: payment.clientPayment,
|
|
72
|
+
}
|
|
73
|
+
}
|