@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.
Files changed (114) hide show
  1. package/dist/ai-docs.d.ts +13 -0
  2. package/dist/browser-auth-CJDrpp5T.d.ts +11 -0
  3. package/dist/{chunk-UA7WNT2F.js → chunk-4LHYICUL.js} +1 -1
  4. package/dist/chunk-4LHYICUL.js.map +1 -0
  5. package/dist/{chunk-R4GGO33X.js → chunk-NJ4X7VNK.js} +1 -1
  6. package/dist/{chunk-R4GGO33X.js.map → chunk-NJ4X7VNK.js.map} +1 -1
  7. package/dist/chunk-STM4DKVZ.js +183 -0
  8. package/dist/chunk-STM4DKVZ.js.map +1 -0
  9. package/dist/{chunk-ENQSB4OF.js → chunk-WDWJ73KP.js} +40 -214
  10. package/dist/chunk-WDWJ73KP.js.map +1 -0
  11. package/dist/create-app-templates/ecommerce/AGENTS.md +88 -0
  12. package/dist/create-app-templates/ecommerce/CHANGELOG.md +48 -0
  13. package/dist/create-app-templates/ecommerce/CLAUDE.md +1 -0
  14. package/dist/create-app-templates/ecommerce/README.md +154 -0
  15. package/dist/create-app-templates/ecommerce/app/api/auth/login/route.ts +30 -0
  16. package/dist/create-app-templates/ecommerce/app/api/auth/logout/route.ts +18 -0
  17. package/dist/create-app-templates/ecommerce/app/api/auth/register/route.ts +41 -0
  18. package/dist/create-app-templates/ecommerce/app/api/cart/clear/route.ts +12 -0
  19. package/dist/create-app-templates/ecommerce/app/api/cart/items/route.ts +45 -0
  20. package/dist/create-app-templates/ecommerce/app/api/cart/route.ts +14 -0
  21. package/dist/create-app-templates/ecommerce/app/api/checkout/payment-return/route.ts +86 -0
  22. package/dist/create-app-templates/ecommerce/app/api/checkout/reconcile/route.ts +50 -0
  23. package/dist/create-app-templates/ecommerce/app/api/checkout/route.ts +41 -0
  24. package/dist/create-app-templates/ecommerce/app/cart/page.tsx +10 -0
  25. package/dist/create-app-templates/ecommerce/app/checkout/page.tsx +10 -0
  26. package/dist/create-app-templates/ecommerce/app/checkout/success/page.tsx +34 -0
  27. package/dist/create-app-templates/ecommerce/app/favicon.ico +0 -0
  28. package/dist/create-app-templates/ecommerce/app/globals.css +67 -0
  29. package/dist/create-app-templates/ecommerce/app/layout.tsx +23 -0
  30. package/dist/create-app-templates/ecommerce/app/login/page.tsx +11 -0
  31. package/dist/create-app-templates/ecommerce/app/page.tsx +5 -0
  32. package/dist/create-app-templates/ecommerce/app/products/[slug]/page.tsx +46 -0
  33. package/dist/create-app-templates/ecommerce/app/products/page.tsx +45 -0
  34. package/dist/create-app-templates/ecommerce/app/register/page.tsx +11 -0
  35. package/dist/create-app-templates/ecommerce/app/webhook/payment/route.ts +20 -0
  36. package/dist/create-app-templates/ecommerce/app-config.ts +54 -0
  37. package/dist/create-app-templates/ecommerce/components/auth/auth-form.tsx +109 -0
  38. package/dist/create-app-templates/ecommerce/components/cart/cart-content.tsx +129 -0
  39. package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +307 -0
  40. package/dist/create-app-templates/ecommerce/components/checkout/checkout-reconcile.tsx +78 -0
  41. package/dist/create-app-templates/ecommerce/components/layout/account-nav.tsx +48 -0
  42. package/dist/create-app-templates/ecommerce/components/layout/account-slot.tsx +12 -0
  43. package/dist/create-app-templates/ecommerce/components/layout/cart-link.tsx +13 -0
  44. package/dist/create-app-templates/ecommerce/components/layout/page-shell.tsx +11 -0
  45. package/dist/create-app-templates/ecommerce/components/layout/site-header.tsx +22 -0
  46. package/dist/create-app-templates/ecommerce/components/product/add-to-cart.tsx +116 -0
  47. package/dist/create-app-templates/ecommerce/components/product/product-card.tsx +50 -0
  48. package/dist/create-app-templates/ecommerce/components/product/product-gallery.tsx +39 -0
  49. package/dist/create-app-templates/ecommerce/data/mock-catalog.json +173 -0
  50. package/dist/create-app-templates/ecommerce/eslint.config.mjs +18 -0
  51. package/dist/create-app-templates/ecommerce/lib/cart/cookie.ts +40 -0
  52. package/dist/create-app-templates/ecommerce/lib/cart/normalize.ts +32 -0
  53. package/dist/create-app-templates/ecommerce/lib/cart/parse-cart-request.ts +56 -0
  54. package/dist/create-app-templates/ecommerce/lib/cart/route-helpers.ts +17 -0
  55. package/dist/create-app-templates/ecommerce/lib/cart/select-provider.ts +44 -0
  56. package/dist/create-app-templates/ecommerce/lib/cart/server-cart.ts +135 -0
  57. package/dist/create-app-templates/ecommerce/lib/cart/sync-on-login.server.ts +34 -0
  58. package/dist/create-app-templates/ecommerce/lib/cart/use-cart.tsx +151 -0
  59. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-errors.ts +22 -0
  60. package/dist/create-app-templates/ecommerce/lib/checkout/checkout-provider.ts +28 -0
  61. package/dist/create-app-templates/ecommerce/lib/checkout/parse-checkout-payload.ts +86 -0
  62. package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +73 -0
  63. package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +6 -0
  64. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +346 -0
  65. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software-mappers.ts +312 -0
  66. package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software.ts +930 -0
  67. package/dist/create-app-templates/ecommerce/lib/commerce/product-summary.ts +37 -0
  68. package/dist/create-app-templates/ecommerce/lib/commerce/provider.server.ts +60 -0
  69. package/dist/create-app-templates/ecommerce/lib/commerce/provider.ts +96 -0
  70. package/dist/create-app-templates/ecommerce/lib/commerce/stock.ts +37 -0
  71. package/dist/create-app-templates/ecommerce/lib/commerce/types.ts +208 -0
  72. package/dist/create-app-templates/ecommerce/lib/commerce/variant-selection.ts +23 -0
  73. package/dist/create-app-templates/ecommerce/lib/customer/auth-actions.ts +131 -0
  74. package/dist/create-app-templates/ecommerce/lib/customer/cart-sync.ts +44 -0
  75. package/dist/create-app-templates/ecommerce/lib/customer/client.server.ts +109 -0
  76. package/dist/create-app-templates/ecommerce/lib/customer/current-customer.ts +15 -0
  77. package/dist/create-app-templates/ecommerce/lib/customer/route-guard.ts +58 -0
  78. package/dist/create-app-templates/ecommerce/lib/customer/route-helpers.ts +75 -0
  79. package/dist/create-app-templates/ecommerce/lib/customer/session.ts +108 -0
  80. package/dist/create-app-templates/ecommerce/lib/format.ts +7 -0
  81. package/dist/create-app-templates/ecommerce/lib/payment/adapters/mock.ts +84 -0
  82. package/dist/create-app-templates/ecommerce/lib/payment/adapters/portone.ts +254 -0
  83. package/dist/create-app-templates/ecommerce/lib/payment/adapters/tosspayments.ts +287 -0
  84. package/dist/create-app-templates/ecommerce/lib/payment/amount-gate.ts +86 -0
  85. package/dist/create-app-templates/ecommerce/lib/payment/provider.server.ts +51 -0
  86. package/dist/create-app-templates/ecommerce/lib/payment/provider.ts +18 -0
  87. package/dist/create-app-templates/ecommerce/lib/payment/sync-order-payment.ts +96 -0
  88. package/dist/create-app-templates/ecommerce/lib/payment/types.ts +71 -0
  89. package/dist/create-app-templates/ecommerce/lib/server-only-guard.ts +20 -0
  90. package/dist/create-app-templates/ecommerce/next-env.d.ts +6 -0
  91. package/dist/create-app-templates/ecommerce/next.config.ts +16 -0
  92. package/dist/create-app-templates/ecommerce/package.json +33 -0
  93. package/dist/create-app-templates/ecommerce/postcss.config.mjs +7 -0
  94. package/dist/create-app-templates/ecommerce/tests/customer-auth.test.ts +263 -0
  95. package/dist/create-app-templates/ecommerce/tests/customer-cart.test.ts +401 -0
  96. package/dist/create-app-templates/ecommerce/tests/domain.test.ts +1632 -0
  97. package/dist/create-app-templates/ecommerce/tsconfig.json +35 -0
  98. package/dist/create-app-templates/registry.json +66 -0
  99. package/dist/create-app.d.ts +40 -0
  100. package/dist/create-app.js +652 -0
  101. package/dist/create-app.js.map +1 -0
  102. package/dist/detect-Bjxp9wcS.d.ts +13 -0
  103. package/dist/file-ops.d.ts +21 -0
  104. package/dist/file-ops.js +1 -1
  105. package/dist/index.d.ts +2 -0
  106. package/dist/index.js +4 -3
  107. package/dist/index.js.map +1 -1
  108. package/dist/init.d.ts +40 -0
  109. package/dist/init.js +4 -3
  110. package/dist/templates.d.ts +27 -0
  111. package/dist/templates.js +1 -1
  112. package/package.json +18 -3
  113. package/dist/chunk-ENQSB4OF.js.map +0 -1
  114. package/dist/chunk-UA7WNT2F.js.map +0 -1
@@ -0,0 +1,37 @@
1
+ import { formatMoney } from "../format.ts";
2
+ import type { ProductDetail } from "./types.ts";
3
+
4
+ export type ProductAvailabilitySummary = {
5
+ hasVariants: boolean;
6
+ available: boolean;
7
+ priceLabel: string;
8
+ };
9
+
10
+ export function summarizeProductAvailability(
11
+ detail: ProductDetail,
12
+ ): ProductAvailabilitySummary {
13
+ const prices = detail.variants.map((variant) => variant.price);
14
+ const available = detail.variants.some(
15
+ (variant) => variant.isUnlimited || variant.stock - variant.reservedStock > 0,
16
+ );
17
+
18
+ if (prices.length === 0) {
19
+ return {
20
+ hasVariants: false,
21
+ available: false,
22
+ priceLabel: "Unavailable",
23
+ };
24
+ }
25
+
26
+ const minPrice = Math.min(...prices);
27
+ const maxPrice = Math.max(...prices);
28
+
29
+ return {
30
+ hasVariants: true,
31
+ available,
32
+ priceLabel:
33
+ minPrice === maxPrice
34
+ ? formatMoney(minPrice)
35
+ : `${formatMoney(minPrice)} - ${formatMoney(maxPrice)}`,
36
+ };
37
+ }
@@ -0,0 +1,60 @@
1
+ import "../server-only-guard.ts";
2
+ import { createMockCommerceProvider } from "./adapters/mock.ts";
3
+ import {
4
+ createSoftwareCommerceProvider,
5
+ createSoftwareCustomerCartProvider,
6
+ createSoftwareCustomerCheckoutProvider,
7
+ } from "./adapters/software.ts";
8
+ import type { CartProvider, CommerceProvider } from "./provider.ts";
9
+ import type { CheckoutCommerceProvider } from "../checkout/types.ts";
10
+
11
+ export function getCommerceProvider(): CommerceProvider {
12
+ if (hasSoftwareCredentials(process.env)) {
13
+ return createSoftwareCommerceProvider();
14
+ }
15
+
16
+ return createMockCommerceProvider();
17
+ }
18
+
19
+ /**
20
+ * Resolve the cart provider for a logged-in customer: cart reads/writes go
21
+ * through a customer-JWT-scoped client so they operate the customer's own,
22
+ * device-portable cart.
23
+ *
24
+ * Gated on the same full software credentials as the guest provider (publishable
25
+ * **and** secret): `createSoftwareCustomerCartProvider` reuses
26
+ * `getSoftwareCommerceConfig`, which needs the secret key for the shipping policy
27
+ * even though the customer client itself uses only the publishable key + JWT. The
28
+ * supported software config sets both keys; in the unsupported half-config
29
+ * (publishable only) this degrades to the in-memory mock just like the guest
30
+ * provider, rather than throwing.
31
+ */
32
+ export function getCustomerCartProvider(token: string): CartProvider {
33
+ if (hasSoftwareCredentials(process.env)) {
34
+ return createSoftwareCustomerCartProvider(token);
35
+ }
36
+
37
+ return createMockCommerceProvider();
38
+ }
39
+
40
+ /**
41
+ * Resolve checkout for a logged-in customer through the customer-JWT client
42
+ * (publishable key + JWT, no secret key) so Console checkout runs under the
43
+ * customer identity and enforces cart ownership.
44
+ */
45
+ export function getCustomerCheckoutProvider(
46
+ token: string,
47
+ ): CheckoutCommerceProvider {
48
+ if (hasSoftwareCredentials(process.env)) {
49
+ return createSoftwareCustomerCheckoutProvider(token);
50
+ }
51
+
52
+ return createMockCommerceProvider();
53
+ }
54
+
55
+ function hasSoftwareCredentials(env: NodeJS.ProcessEnv): boolean {
56
+ return Boolean(
57
+ env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY?.trim() &&
58
+ env.SOFTWARE_SECRET_KEY?.trim(),
59
+ );
60
+ }
@@ -0,0 +1,96 @@
1
+ import type {
2
+ CartItemRef,
3
+ CartLine,
4
+ CartView,
5
+ CheckoutCartInput,
6
+ CreatePendingOrderResult,
7
+ Order,
8
+ ProductDetail,
9
+ ProductListResult,
10
+ ProductVariant,
11
+ ShippingPolicy,
12
+ StockCheckResult,
13
+ } from "./types.ts";
14
+
15
+ export type ConfirmOrderPaymentInput = {
16
+ paymentId: string;
17
+ /**
18
+ * Resolves the open checkout that placement promotes to a paid order. The
19
+ * Console resolves the checkout by `pgPaymentId` or by this `orderNumber`
20
+ * (→ `checkoutToken`); the synchronous return path always has it, the webhook
21
+ * backstop may not.
22
+ */
23
+ orderNumber?: string;
24
+ provider: string;
25
+ /** Provider-verified amount (server re-fetch), not a client-supplied value. */
26
+ amount: number;
27
+ providerEventId?: string;
28
+ };
29
+
30
+ export type PaymentStateInput = {
31
+ paymentId: string;
32
+ orderNumber?: string;
33
+ provider: string;
34
+ providerEventId?: string;
35
+ };
36
+
37
+ export type OrderLookupInput = {
38
+ paymentId: string;
39
+ provider: string;
40
+ };
41
+
42
+ /**
43
+ * The storefront commerce port. The cart is server-authoritative
44
+ * (`commerce.cart.*`): the browser never holds cart contents, only the
45
+ * unguessable `cartToken` in an HttpOnly cookie. Ordering goes through
46
+ * `orders.checkout({ cartId })`; the populated order is resolved by PG payment
47
+ * id (`getByPaymentId`) — there is no local order index.
48
+ */
49
+ export type CommerceProvider = {
50
+ // Catalog
51
+ listProducts(input?: { limit?: number }): Promise<ProductListResult>;
52
+ getProductBySlug(slug: string): Promise<ProductDetail | null>;
53
+ getVariantsByIds(ids: string[]): Promise<ProductVariant[]>;
54
+ getShippingPolicy(): Promise<ShippingPolicy>;
55
+ checkStock(items: CartLine[]): Promise<StockCheckResult>;
56
+
57
+ // Server cart
58
+ createCart(): Promise<{ cartToken: string }>;
59
+ getCart(cartToken: string): Promise<CartView | null>;
60
+ addCartItem(input: { cartToken: string; item: CartItemRef }): Promise<CartView>;
61
+ updateCartItem(input: {
62
+ cartToken: string;
63
+ cartItemId: string;
64
+ quantity: number;
65
+ }): Promise<CartView>;
66
+ removeCartItem(input: {
67
+ cartToken: string;
68
+ cartItemId: string;
69
+ }): Promise<CartView>;
70
+ clearCart(input: { cartToken: string }): Promise<CartView>;
71
+
72
+ // Checkout + payment lifecycle
73
+ checkoutCart(input: CheckoutCartInput): Promise<CreatePendingOrderResult>;
74
+ getOrderByPaymentId(input: OrderLookupInput): Promise<Order | null>;
75
+ confirmOrderPayment(input: ConfirmOrderPaymentInput): Promise<Order>;
76
+ markPaymentFailed(input: PaymentStateInput): Promise<Order | null>;
77
+ cancelPendingOrder(input: PaymentStateInput): Promise<Order | null>;
78
+ };
79
+
80
+ /**
81
+ * The cart read/write subset of {@link CommerceProvider}. A logged-in customer
82
+ * operates the cart through a **customer-JWT-scoped** client (the JWT binds the
83
+ * customer; a publishable key alone cannot read/write a customer-bound cart), so
84
+ * `server-cart.ts` resolves one of these per request. Narrowing to the cart ops
85
+ * keeps the customer client — which cannot perform secret-key-only checkout /
86
+ * payment / collection-write operations — from being handed those calls.
87
+ */
88
+ export type CartProvider = Pick<
89
+ CommerceProvider,
90
+ | "createCart"
91
+ | "getCart"
92
+ | "addCartItem"
93
+ | "updateCartItem"
94
+ | "removeCartItem"
95
+ | "clearCart"
96
+ >;
@@ -0,0 +1,37 @@
1
+ import type { CartLine, ProductVariant, StockCheckResult } from "./types.ts";
2
+
3
+ export function checkVariantStock(input: {
4
+ lines: CartLine[];
5
+ variants: ProductVariant[];
6
+ }): StockCheckResult {
7
+ const variantById = new Map(input.variants.map((variant) => [variant.id, variant]));
8
+
9
+ const lines = input.lines.map((line) => {
10
+ const variant = variantById.get(line.variantId);
11
+ if (!variant) {
12
+ return {
13
+ ...line,
14
+ availableQuantity: 0,
15
+ ok: false,
16
+ reason: "variant_not_found" as const,
17
+ };
18
+ }
19
+
20
+ const availableQuantity = variant.isUnlimited
21
+ ? null
22
+ : Math.max(0, variant.stock - variant.reservedStock);
23
+ const ok = availableQuantity === null || availableQuantity >= line.quantity;
24
+
25
+ return {
26
+ ...line,
27
+ availableQuantity,
28
+ ok,
29
+ reason: ok ? undefined : ("insufficient_stock" as const),
30
+ };
31
+ });
32
+
33
+ return {
34
+ ok: lines.every((line) => line.ok),
35
+ lines,
36
+ };
37
+ }
@@ -0,0 +1,208 @@
1
+ export type CommerceImage = {
2
+ url: string
3
+ alt?: string
4
+ width?: number
5
+ height?: number
6
+ }
7
+
8
+ export type Product = {
9
+ id: string
10
+ slug: string
11
+ title: string
12
+ description?: string
13
+ thumbnail?: CommerceImage | null
14
+ images: CommerceImage[]
15
+ status: 'draft' | 'published' | 'archived'
16
+ }
17
+
18
+ export type ProductDetail = {
19
+ product: Product
20
+ variants: ProductVariant[]
21
+ options: ProductOption[]
22
+ }
23
+
24
+ export type ProductOption = {
25
+ id: string
26
+ slug: string
27
+ title: string
28
+ values: ProductOptionValue[]
29
+ }
30
+
31
+ export type ProductOptionValue = {
32
+ id: string
33
+ slug: string
34
+ value: string
35
+ }
36
+
37
+ export type ProductVariantOptionValue = {
38
+ optionId: string
39
+ valueId: string
40
+ valueSlug: string
41
+ value: string
42
+ }
43
+
44
+ export type ProductVariant = {
45
+ id: string
46
+ productId: string
47
+ title?: string
48
+ sku?: string
49
+ price: number
50
+ stock: number
51
+ reservedStock: number
52
+ isUnlimited: boolean
53
+ requiresShipping?: boolean | null
54
+ optionValues: ProductVariantOptionValue[]
55
+ images: CommerceImage[]
56
+ }
57
+
58
+ export type CartLine = {
59
+ variantId: string
60
+ quantity: number
61
+ }
62
+
63
+ /**
64
+ * Reference used to add a line to the server cart. The catalog identity
65
+ * (`product`/`variant`/`option`) is what `commerce.cart.addItem` expects — the
66
+ * cart, not the browser, owns price and stock.
67
+ */
68
+ export type CartItemRef = {
69
+ productId: string
70
+ variantId: string
71
+ optionId?: string
72
+ quantity: number
73
+ }
74
+
75
+ /**
76
+ * A single line of the server cart, enriched with catalog display fields so the
77
+ * storefront can render it without a second round-trip. `cartItemId` is the
78
+ * handle used by update/remove.
79
+ */
80
+ export type CartItemView = {
81
+ cartItemId: string
82
+ productId: string
83
+ variantId: string
84
+ quantity: number
85
+ unitAmount: number
86
+ lineAmount: number
87
+ requiresShipping?: boolean | null
88
+ productTitle: string
89
+ variantTitle?: string
90
+ image?: CommerceImage | null
91
+ }
92
+
93
+ /**
94
+ * The server cart projected for the storefront. `id` is the cart's DB id used
95
+ * by `orders.checkout({ cartId })`; the unguessable `cartToken` capability is
96
+ * never part of this view — it lives only in the HttpOnly cookie.
97
+ * `totalAmount` is the single payable quote (subtotal − discount + shipping,
98
+ * computed and stored by the Console cart).
99
+ */
100
+ export type CartView = {
101
+ id: string
102
+ currency: string
103
+ subtotalAmount: number
104
+ shippingAmount: number
105
+ discountAmount: number
106
+ totalAmount: number
107
+ discountCode?: string
108
+ items: CartItemView[]
109
+ }
110
+
111
+ export type ShippingPolicy = {
112
+ currency: string
113
+ baseAmount: number
114
+ freeAboveAmount?: number
115
+ }
116
+
117
+ export type PricedOrderLine = CartLine & {
118
+ unitAmount: number
119
+ lineAmount: number
120
+ variant: ProductVariant
121
+ }
122
+
123
+ export type PricedOrder = {
124
+ currency: string
125
+ lines: PricedOrderLine[]
126
+ subtotalAmount: number
127
+ shippingAmount: number
128
+ totalAmount: number
129
+ }
130
+
131
+ export type StockCheckLine = CartLine & {
132
+ availableQuantity: number | null
133
+ ok: boolean
134
+ reason?: 'variant_not_found' | 'insufficient_stock'
135
+ }
136
+
137
+ export type StockCheckResult = {
138
+ ok: boolean
139
+ lines: StockCheckLine[]
140
+ }
141
+
142
+ export type CustomerSnapshot = {
143
+ name: string
144
+ email: string
145
+ phone: string
146
+ }
147
+
148
+ export type ShippingAddress = {
149
+ recipientName: string
150
+ phone: string
151
+ postalCode: string
152
+ address: string
153
+ detailAddress: string
154
+ deliveryMessage?: string
155
+ }
156
+
157
+ export type OrderItem = {
158
+ variantId: string
159
+ productTitle: string
160
+ variantTitle?: string
161
+ quantity: number
162
+ unitAmount: number
163
+ lineAmount: number
164
+ }
165
+
166
+ export type PaymentTransaction = {
167
+ paymentId: string
168
+ provider: string
169
+ status: 'pending' | 'paid' | 'failed' | 'canceled'
170
+ amount: number
171
+ }
172
+
173
+ export type Order = {
174
+ id: string
175
+ orderNumber: string
176
+ displayStatus: 'pending' | 'paid' | 'canceled'
177
+ items: OrderItem[]
178
+ customerSnapshot: CustomerSnapshot
179
+ shippingAddress?: ShippingAddress
180
+ subtotalAmount: number
181
+ shippingAmount: number
182
+ totalAmount: number
183
+ transactions: PaymentTransaction[]
184
+ }
185
+
186
+ export type ProductListResult = {
187
+ products: ProductDetail[]
188
+ total: number
189
+ }
190
+
191
+ /**
192
+ * Checkout input. The cart contents live server-side (addressed by `cartToken`
193
+ * from the HttpOnly cookie); the storefront supplies only the buyer + shipping
194
+ * snapshot. No client-supplied lines or totals — the cart is the quote.
195
+ */
196
+ export type CheckoutCartInput = {
197
+ cartToken: string
198
+ customerSnapshot: CustomerSnapshot
199
+ shippingAddress?: ShippingAddress
200
+ }
201
+
202
+ export type CreatePendingOrderResult = {
203
+ order: Order
204
+ paymentId: string
205
+ paymentName: string
206
+ amount: number
207
+ currency: string
208
+ }
@@ -0,0 +1,23 @@
1
+ import type { ProductDetail, ProductVariant } from "./types.ts";
2
+
3
+ export function resolveVariantFromSelection(
4
+ detail: ProductDetail,
5
+ selectedValueIdsByOptionId: Record<string, string | undefined>,
6
+ ): ProductVariant | null {
7
+ const requiredOptionIds = detail.options.map((option) => option.id);
8
+ if (requiredOptionIds.some((optionId) => !selectedValueIdsByOptionId[optionId])) {
9
+ return null;
10
+ }
11
+
12
+ return (
13
+ detail.variants.find((variant) =>
14
+ requiredOptionIds.every((optionId) =>
15
+ variant.optionValues.some(
16
+ (value) =>
17
+ value.optionId === optionId &&
18
+ value.valueId === selectedValueIdsByOptionId[optionId],
19
+ ),
20
+ ),
21
+ ) ?? null
22
+ );
23
+ }
@@ -0,0 +1,131 @@
1
+ import "../server-only-guard.ts";
2
+ import type { CustomerAuthClient } from "./client.server.ts";
3
+ import { createCustomerClient } from "./client.server.ts";
4
+
5
+ /**
6
+ * Customer auth core logic, isolated from `next/headers` and the SDK transport so
7
+ * it unit-tests with a fake client. Route handlers call these and then apply the
8
+ * returned cookie effect; render paths use {@link loadCustomer} read-only.
9
+ */
10
+
11
+ export interface CustomerSummary {
12
+ id: string;
13
+ name: string;
14
+ email?: string | null;
15
+ }
16
+
17
+ export interface LoginInput {
18
+ email: string;
19
+ password: string;
20
+ }
21
+
22
+ export interface RegisterInput {
23
+ name: string;
24
+ email: string;
25
+ password: string;
26
+ phone?: string;
27
+ }
28
+
29
+ /** Result of a login/register attempt the route handler turns into a response. */
30
+ export type AuthResult =
31
+ | { ok: true; token: string | null; customer: CustomerSummary | null }
32
+ | { ok: false; status: number; message: string };
33
+
34
+ /** A client factory so tests can inject a fake SDK client. */
35
+ export type CustomerClientFactory = (
36
+ token: string | null,
37
+ ) => Promise<CustomerAuthClient | null>;
38
+
39
+ const UNCONFIGURED: AuthResult = {
40
+ ok: false,
41
+ status: 503,
42
+ message: "Customer accounts are not configured for this storefront.",
43
+ };
44
+
45
+ function toSummary(customer: unknown): CustomerSummary | null {
46
+ if (!customer || typeof customer !== "object") return null;
47
+ const c = customer as Record<string, unknown>;
48
+ if (typeof c.id !== "string" || typeof c.name !== "string") return null;
49
+ return {
50
+ id: c.id,
51
+ name: c.name,
52
+ email: typeof c.email === "string" ? c.email : null,
53
+ };
54
+ }
55
+
56
+ function statusOf(error: unknown): number {
57
+ const status = (error as { status?: unknown })?.status;
58
+ return typeof status === "number" ? status : 502;
59
+ }
60
+
61
+ function messageOf(error: unknown, fallback: string): string {
62
+ if (error instanceof Error && error.message) return error.message;
63
+ return fallback;
64
+ }
65
+
66
+ /** Log in and return the JWT for the route to persist in the session cookie. */
67
+ export async function login(
68
+ input: LoginInput,
69
+ factory: CustomerClientFactory = (token) => createCustomerClient(token),
70
+ ): Promise<AuthResult> {
71
+ const client = await factory(null);
72
+ if (!client) return UNCONFIGURED;
73
+ try {
74
+ const { token, customer } = await client.customer.auth.login(input);
75
+ return { ok: true, token, customer: toSummary(customer) };
76
+ } catch (error) {
77
+ return {
78
+ ok: false,
79
+ status: statusOf(error),
80
+ message: messageOf(error, "Login failed"),
81
+ };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Register an account. The register endpoint returns the customer (no token), so
87
+ * the caller logs in afterward to mint the session; we surface only the customer
88
+ * here and let the route chain the login.
89
+ */
90
+ export async function register(
91
+ input: RegisterInput,
92
+ factory: CustomerClientFactory = (token) => createCustomerClient(token),
93
+ ): Promise<AuthResult> {
94
+ const client = await factory(null);
95
+ if (!client) return UNCONFIGURED;
96
+ try {
97
+ const { customer } = await client.customer.auth.register(input);
98
+ return { ok: true, token: null, customer: toSummary(customer) };
99
+ } catch (error) {
100
+ return {
101
+ ok: false,
102
+ status: statusOf(error),
103
+ message: messageOf(error, "Registration failed"),
104
+ };
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Resolve the current customer from a session token, for render paths. Returns
110
+ * `null` (guest) when there is no token, auth is unconfigured, the token is
111
+ * expired/invalid (the SDK's `me()` returns `null` on a 401), **or** the backend
112
+ * call fails — a render path must degrade to guest, never throw. We never
113
+ * refresh here: there is no infinite loop against a cookie a render path cannot
114
+ * rewrite. The stale (HttpOnly, harmless) cookie is overwritten on the next
115
+ * login or dropped on logout.
116
+ */
117
+ export async function loadCustomer(
118
+ token: string | null,
119
+ factory: CustomerClientFactory = (t) => createCustomerClient(t),
120
+ ): Promise<CustomerSummary | null> {
121
+ if (!token) return null;
122
+ try {
123
+ const client = await factory(token);
124
+ if (!client) return null;
125
+ return toSummary(await client.customer.auth.me());
126
+ } catch {
127
+ // 5xx / timeout / network error — fall back to guest rather than crash the
128
+ // page. `me()` already maps 401 to `null` without throwing.
129
+ return null;
130
+ }
131
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Login cart-sync core, isolated from `next/headers` and the SDK transport so it
3
+ * unit-tests with a fake commerce client. The route wrapper
4
+ * (`lib/cart/sync-on-login.server.ts`) reads the guest cart cookie, builds a
5
+ * customer-JWT commerce client, calls this, and applies the cookie effect.
6
+ */
7
+
8
+ /** The merge/mine surface the login cart-sync consumes (customer-JWT scoped). */
9
+ export interface CartSyncCommerce {
10
+ cart: {
11
+ merge(params: {
12
+ guestCartToken: string;
13
+ }): Promise<{ cartToken: string | null }>;
14
+ mine(): Promise<{ cartToken: string | null }>;
15
+ };
16
+ }
17
+
18
+ export interface CartSyncResult {
19
+ /** The resolved customer cart token to persist, or `null` to clear the cookie. */
20
+ cartToken: string | null;
21
+ }
22
+
23
+ /**
24
+ * After login, resolve the customer's active-cart token. If a pre-login guest
25
+ * cart exists, union/claim it into the customer cart (`merge`); otherwise load
26
+ * the existing customer cart (`mine`). The returned token is what the caller
27
+ * persists in the cart cookie (or clears the cookie when `null`).
28
+ *
29
+ * `merge` returns the resolved customer cart's token on **both** the `claimed`
30
+ * (guest cart taken over) and `merged` (lines unioned into an existing customer
31
+ * cart) branches, so the guest cookie always ends up pointing at the customer
32
+ * cart — never the abandoned guest cart.
33
+ */
34
+ export async function resolveCustomerCartToken(
35
+ commerce: CartSyncCommerce,
36
+ guestCartToken: string | null,
37
+ ): Promise<CartSyncResult> {
38
+ if (guestCartToken) {
39
+ const { cartToken } = await commerce.cart.merge({ guestCartToken });
40
+ return { cartToken };
41
+ }
42
+ const { cartToken } = await commerce.cart.mine();
43
+ return { cartToken };
44
+ }