@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,346 @@
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
+ requiresShipping?: boolean | null
30
+ }
31
+
32
+ type MockCart = {
33
+ id: string
34
+ cartToken: string
35
+ currency: string
36
+ items: MockCartItem[]
37
+ }
38
+
39
+ const carts = new Map<string, MockCart>()
40
+ const orders = new Map<string, Order>()
41
+
42
+ export function createMockCommerceProvider(): CommerceProvider {
43
+ const products = seed.products as ProductDetail[]
44
+ const shippingPolicy = seed.shippingPolicy as ShippingPolicy
45
+ const variants = allVariants(products)
46
+ const variantById = new Map(variants.map((variant) => [variant.id, variant]))
47
+
48
+ function viewCart(cart: MockCart): CartView {
49
+ const items: CartItemView[] = cart.items.map((item) => {
50
+ const variant = variantById.get(item.variantId)
51
+ const detail = products.find(
52
+ (entry) => entry.product.id === item.productId,
53
+ )
54
+ const image =
55
+ variant?.images[0] ??
56
+ detail?.product.thumbnail ??
57
+ detail?.product.images[0] ??
58
+ null
59
+ return {
60
+ cartItemId: item.cartItemId,
61
+ productId: item.productId,
62
+ variantId: item.variantId,
63
+ quantity: item.quantity,
64
+ unitAmount: item.unitPrice,
65
+ lineAmount: item.unitPrice * item.quantity,
66
+ requiresShipping: item.requiresShipping,
67
+ productTitle: detail?.product.title ?? 'Unknown product',
68
+ variantTitle: variant?.title,
69
+ image,
70
+ }
71
+ })
72
+
73
+ const subtotalAmount = items.reduce((sum, item) => sum + item.lineAmount, 0)
74
+ const hasShippableItems = items.some(
75
+ (item) => item.requiresShipping !== false,
76
+ )
77
+ const shippingAmount =
78
+ !hasShippableItems || subtotalAmount === 0
79
+ ? 0
80
+ : shippingPolicy.freeAboveAmount !== undefined &&
81
+ subtotalAmount >= shippingPolicy.freeAboveAmount
82
+ ? 0
83
+ : shippingPolicy.baseAmount
84
+
85
+ return {
86
+ id: cart.id,
87
+ currency: shippingPolicy.currency,
88
+ subtotalAmount,
89
+ shippingAmount,
90
+ discountAmount: 0,
91
+ totalAmount: subtotalAmount + shippingAmount,
92
+ items,
93
+ }
94
+ }
95
+
96
+ function requireCart(cartToken: string): MockCart {
97
+ const cart = carts.get(cartToken)
98
+ if (!cart) throw new Error('Cart not found')
99
+ return cart
100
+ }
101
+
102
+ return {
103
+ async listProducts(input) {
104
+ const limit = input?.limit ?? 24
105
+ const publishedProducts = products.filter(
106
+ (entry) => entry.product.status === 'published',
107
+ )
108
+ return {
109
+ products: publishedProducts.slice(0, limit),
110
+ total: publishedProducts.length,
111
+ }
112
+ },
113
+
114
+ async getProductBySlug(slug) {
115
+ return (
116
+ products.find(
117
+ (entry) =>
118
+ entry.product.slug === slug && entry.product.status === 'published',
119
+ ) ?? null
120
+ )
121
+ },
122
+
123
+ async getVariantsByIds(ids) {
124
+ const idSet = new Set(ids)
125
+ return variants.filter((variant) => idSet.has(variant.id))
126
+ },
127
+
128
+ async getShippingPolicy() {
129
+ return shippingPolicy
130
+ },
131
+
132
+ async checkStock(items) {
133
+ return checkVariantStock({
134
+ lines: normalizeCartLines(items),
135
+ variants,
136
+ })
137
+ },
138
+
139
+ async createCart() {
140
+ const cartToken = `mock_cart_${crypto.randomUUID()}`
141
+ carts.set(cartToken, {
142
+ id: `cart_${crypto.randomUUID()}`,
143
+ cartToken,
144
+ currency: shippingPolicy.currency,
145
+ items: [],
146
+ })
147
+ return { cartToken }
148
+ },
149
+
150
+ async getCart(cartToken) {
151
+ const cart = carts.get(cartToken)
152
+ return cart ? viewCart(cart) : null
153
+ },
154
+
155
+ async addCartItem({ cartToken, item }) {
156
+ const cart = requireCart(cartToken)
157
+ const variant = variantById.get(item.variantId)
158
+ if (!variant) throw new Error('Variant not found')
159
+
160
+ const existing = cart.items.find(
161
+ (line) => line.variantId === item.variantId,
162
+ )
163
+ const nextQuantity = (existing?.quantity ?? 0) + item.quantity
164
+ const stock = checkVariantStock({
165
+ lines: [{ variantId: item.variantId, quantity: nextQuantity }],
166
+ variants,
167
+ })
168
+ if (!stock.ok) throw new Error('Insufficient stock')
169
+
170
+ if (existing) {
171
+ existing.quantity = Math.min(99, nextQuantity)
172
+ existing.requiresShipping =
173
+ existing.requiresShipping !== false ||
174
+ variant.requiresShipping !== false
175
+ } else {
176
+ cart.items.push({
177
+ cartItemId: `item_${crypto.randomUUID()}`,
178
+ productId: item.productId || variant.productId,
179
+ variantId: item.variantId,
180
+ optionId: item.optionId,
181
+ quantity: Math.min(99, item.quantity),
182
+ unitPrice: variant.price,
183
+ requiresShipping: variant.requiresShipping !== false,
184
+ })
185
+ }
186
+ return viewCart(cart)
187
+ },
188
+
189
+ async updateCartItem({ cartToken, cartItemId, quantity }) {
190
+ const cart = requireCart(cartToken)
191
+ const line = cart.items.find((entry) => entry.cartItemId === cartItemId)
192
+ if (!line) throw new Error('Cart item not found')
193
+ if (quantity <= 0) {
194
+ cart.items = cart.items.filter(
195
+ (entry) => entry.cartItemId !== cartItemId,
196
+ )
197
+ } else {
198
+ line.quantity = Math.min(99, Math.floor(quantity))
199
+ }
200
+ return viewCart(cart)
201
+ },
202
+
203
+ async removeCartItem({ cartToken, cartItemId }) {
204
+ const cart = requireCart(cartToken)
205
+ cart.items = cart.items.filter((entry) => entry.cartItemId !== cartItemId)
206
+ return viewCart(cart)
207
+ },
208
+
209
+ async clearCart({ cartToken }) {
210
+ const cart = requireCart(cartToken)
211
+ cart.items = []
212
+ return viewCart(cart)
213
+ },
214
+
215
+ async checkoutCart(input) {
216
+ return checkoutMockCart({ input, products, viewCart, requireCart })
217
+ },
218
+
219
+ async getOrderByPaymentId({ paymentId }) {
220
+ return orders.get(paymentId) ?? null
221
+ },
222
+
223
+ async confirmOrderPayment(input) {
224
+ const order = orders.get(input.paymentId)
225
+ if (!order) throw new Error('Order not found')
226
+ const paidOrder: Order = {
227
+ ...order,
228
+ displayStatus: 'paid',
229
+ transactions: order.transactions.map((transaction) =>
230
+ transaction.paymentId === input.paymentId
231
+ ? { ...transaction, provider: input.provider, status: 'paid' }
232
+ : transaction,
233
+ ),
234
+ }
235
+ orders.set(input.paymentId, paidOrder)
236
+ return paidOrder
237
+ },
238
+
239
+ async markPaymentFailed(input) {
240
+ const order = orders.get(input.paymentId)
241
+ if (!order) return null
242
+ const failedOrder: Order = {
243
+ ...order,
244
+ transactions: order.transactions.map((transaction) =>
245
+ transaction.paymentId === input.paymentId
246
+ ? { ...transaction, provider: input.provider, status: 'failed' }
247
+ : transaction,
248
+ ),
249
+ }
250
+ orders.set(input.paymentId, failedOrder)
251
+ return failedOrder
252
+ },
253
+
254
+ async cancelPendingOrder(input) {
255
+ const order = orders.get(input.paymentId)
256
+ if (!order) return null
257
+ const canceledOrder: Order = {
258
+ ...order,
259
+ displayStatus: 'canceled',
260
+ transactions: order.transactions.map((transaction) =>
261
+ transaction.paymentId === input.paymentId
262
+ ? { ...transaction, provider: input.provider, status: 'canceled' }
263
+ : transaction,
264
+ ),
265
+ }
266
+ orders.set(input.paymentId, canceledOrder)
267
+ return canceledOrder
268
+ },
269
+ }
270
+ }
271
+
272
+ function checkoutMockCart(args: {
273
+ input: CheckoutCartInput
274
+ products: ProductDetail[]
275
+ viewCart: (cart: MockCart) => CartView
276
+ requireCart: (cartToken: string) => MockCart
277
+ }): CreatePendingOrderResult {
278
+ const cart = args.requireCart(args.input.cartToken)
279
+ const view = args.viewCart(cart)
280
+ if (view.items.length === 0) throw new Error('Cart is empty')
281
+
282
+ const paymentId = `mock_${Date.now()}_${Math.random().toString(16).slice(2)}`
283
+ const orderNumber = `ORD-${Date.now().toString().slice(-8)}`
284
+ const order: Order = {
285
+ id: `order_${paymentId}`,
286
+ orderNumber,
287
+ displayStatus: 'pending',
288
+ items: view.items.map((item) => ({
289
+ variantId: item.variantId,
290
+ productTitle: item.productTitle,
291
+ variantTitle: item.variantTitle,
292
+ quantity: item.quantity,
293
+ unitAmount: item.unitAmount,
294
+ lineAmount: item.lineAmount,
295
+ })),
296
+ customerSnapshot: args.input.customerSnapshot,
297
+ ...(args.input.shippingAddress
298
+ ? { shippingAddress: args.input.shippingAddress }
299
+ : {}),
300
+ subtotalAmount: view.subtotalAmount,
301
+ shippingAmount: view.shippingAmount,
302
+ totalAmount: view.totalAmount,
303
+ transactions: [
304
+ {
305
+ paymentId,
306
+ provider: 'mock',
307
+ status: 'pending',
308
+ amount: view.totalAmount,
309
+ },
310
+ ],
311
+ }
312
+
313
+ orders.set(paymentId, order)
314
+ // The cart is consumed by checkout (Shopify cart→checkout boundary); the
315
+ // browser cookie will resolve to no active cart and mint a fresh one.
316
+ carts.delete(args.input.cartToken)
317
+
318
+ return {
319
+ order,
320
+ paymentId,
321
+ paymentName: buildPaymentName(order.items.map((item) => item.productTitle)),
322
+ amount: view.totalAmount,
323
+ currency: view.currency,
324
+ }
325
+ }
326
+
327
+ function allVariants(products: ProductDetail[]): ProductVariant[] {
328
+ return products.flatMap((entry) => entry.variants)
329
+ }
330
+
331
+ function buildPaymentName(productTitles: string[]): string {
332
+ const [first, ...rest] = productTitles
333
+ if (!first) return 'Order'
334
+ return rest.length === 0 ? first : `${first} and ${rest.length} more`
335
+ }
336
+
337
+ /** Test helper: reset the in-memory stores between cases. */
338
+ export function resetMockCommerceState(): void {
339
+ carts.clear()
340
+ orders.clear()
341
+ }
342
+
343
+ /** Test helper: read a stored mock order by its mock payment id. */
344
+ export function getMockOrder(paymentId: string): Order | null {
345
+ return orders.get(paymentId) ?? null
346
+ }
@@ -0,0 +1,312 @@
1
+ import type {
2
+ CommerceImage,
3
+ Order,
4
+ PaymentTransaction,
5
+ Product,
6
+ ProductDetail,
7
+ ProductOption,
8
+ ProductVariant,
9
+ ProductVariantOptionValue,
10
+ } from "../types.ts";
11
+
12
+ type SoftwareMedia =
13
+ | string
14
+ | number
15
+ | {
16
+ url?: string | null;
17
+ alt?: string | null;
18
+ width?: number | null;
19
+ height?: number | null;
20
+ }
21
+ | null
22
+ | undefined;
23
+
24
+ type SoftwareProduct = {
25
+ id: string | number;
26
+ slug?: string | null;
27
+ title?: string | null;
28
+ description?: string | null;
29
+ status?: string | null;
30
+ };
31
+
32
+ type SoftwareOptionValue = {
33
+ id: string | number;
34
+ slug?: string | null;
35
+ value?: string | null;
36
+ label?: string | null;
37
+ };
38
+
39
+ type SoftwareOption = {
40
+ id: string | number;
41
+ slug?: string | null;
42
+ name?: string | null;
43
+ title?: string | null;
44
+ values?: SoftwareOptionValue[] | null;
45
+ };
46
+
47
+ type SoftwareVariantOptionValue = {
48
+ optionId?: string | number | null;
49
+ option?: string | number | { id?: string | number | null } | null;
50
+ valueId?: string | number | null;
51
+ value?: string | number | SoftwareOptionValue | null;
52
+ slug?: string | null;
53
+ valueSlug?: string | null;
54
+ label?: string | null;
55
+ };
56
+
57
+ type SoftwareVariant = {
58
+ id: string | number;
59
+ product?: string | number | SoftwareProduct | null;
60
+ title?: string | null;
61
+ displayName?: string | null;
62
+ sku?: string | null;
63
+ price?: number | null;
64
+ stock?: number | null;
65
+ reservedStock?: number | null;
66
+ isUnlimited?: boolean | null;
67
+ optionValues?: SoftwareVariantOptionValue[] | null;
68
+ images?: SoftwareMedia[] | null;
69
+ media?: SoftwareMedia[] | null;
70
+ };
71
+
72
+ export type SoftwareProductDetailLike = {
73
+ product: SoftwareProduct;
74
+ variants?: SoftwareVariant[] | null;
75
+ options?: SoftwareOption[] | null;
76
+ images?: SoftwareMedia[] | null;
77
+ };
78
+
79
+ export type SoftwareProductDetailResultLike =
80
+ | { found: true; product: SoftwareProductDetailLike }
81
+ | { found: false; reason?: string };
82
+
83
+ type SoftwareOrderItemLike = {
84
+ variant?: string | number | SoftwareVariant | null;
85
+ product?: string | number | SoftwareProduct | null;
86
+ quantity?: number | null;
87
+ unitPrice?: number | null;
88
+ totalPrice?: number | null;
89
+ unitAmount?: number | null;
90
+ lineAmount?: number | null;
91
+ };
92
+
93
+ type SoftwareTransactionLike = {
94
+ pgPaymentId?: string | null;
95
+ pgProvider?: string | null;
96
+ status?: PaymentTransaction["status"] | null;
97
+ amount?: number | null;
98
+ };
99
+
100
+ export type SoftwareOrderLike = {
101
+ id: string | number;
102
+ orderNumber?: string | null;
103
+ displayStatus?: Order["displayStatus"] | null;
104
+ status?: Order["displayStatus"] | null;
105
+ customerSnapshot?: {
106
+ name?: string | null;
107
+ email?: string | null;
108
+ phone?: string | null;
109
+ } | null;
110
+ shippingAddress?: {
111
+ recipientName?: string | null;
112
+ phone?: string | null;
113
+ postalCode?: string | null;
114
+ address?: string | null;
115
+ detailAddress?: string | null;
116
+ deliveryMessage?: string | null;
117
+ } | null;
118
+ subtotalAmount?: number | null;
119
+ shippingAmount?: number | null;
120
+ totalAmount?: number | null;
121
+ items?: { docs?: (string | SoftwareOrderItemLike)[] | null } | null;
122
+ transactions?: { docs?: (string | SoftwareTransactionLike)[] | null } | null;
123
+ };
124
+
125
+ export function mapSoftwareProductDetail(
126
+ detail: SoftwareProductDetailLike,
127
+ ): ProductDetail {
128
+ const images = (detail.images ?? []).flatMap(mapSoftwareImage);
129
+ const product = mapSoftwareProduct(detail.product, images);
130
+
131
+ return {
132
+ product,
133
+ variants: (detail.variants ?? []).map((variant) =>
134
+ mapSoftwareVariant(variant, product.id),
135
+ ),
136
+ options: (detail.options ?? []).map(mapSoftwareOption),
137
+ };
138
+ }
139
+
140
+ export function mapSoftwareProductDetailResult(
141
+ result: SoftwareProductDetailResultLike,
142
+ ): ProductDetail | null {
143
+ if (!result.found) return null;
144
+ return mapSoftwareProductDetail(result.product);
145
+ }
146
+
147
+ export function mapSoftwareProduct(
148
+ product: SoftwareProduct,
149
+ images: CommerceImage[] = [],
150
+ ): Product {
151
+ const status =
152
+ product.status === "draft" || product.status === "archived"
153
+ ? product.status
154
+ : "published";
155
+
156
+ return {
157
+ id: String(product.id),
158
+ slug: product.slug ?? String(product.id),
159
+ title: product.title ?? "Untitled product",
160
+ description: product.description ?? undefined,
161
+ thumbnail: images[0] ?? null,
162
+ images,
163
+ status,
164
+ };
165
+ }
166
+
167
+ export function mapSoftwareVariant(
168
+ variant: SoftwareVariant,
169
+ fallbackProductId: string,
170
+ ): ProductVariant {
171
+ const productId =
172
+ typeof variant.product === "object" && variant.product
173
+ ? String(variant.product.id)
174
+ : variant.product != null
175
+ ? String(variant.product)
176
+ : fallbackProductId;
177
+
178
+ return {
179
+ id: String(variant.id),
180
+ productId,
181
+ title: variant.displayName ?? variant.title ?? undefined,
182
+ sku: variant.sku ?? undefined,
183
+ price: variant.price ?? 0,
184
+ stock: variant.stock ?? 0,
185
+ reservedStock: variant.reservedStock ?? 0,
186
+ isUnlimited: Boolean(variant.isUnlimited),
187
+ optionValues: (variant.optionValues ?? []).map(mapSoftwareVariantOptionValue),
188
+ images: [...(variant.images ?? []), ...(variant.media ?? [])].flatMap(
189
+ mapSoftwareImage,
190
+ ),
191
+ };
192
+ }
193
+
194
+ export function mapSoftwareOrder(order: SoftwareOrderLike): Order {
195
+ const transactions = (order.transactions?.docs ?? []).flatMap((transaction) =>
196
+ typeof transaction === "string" ? [] : [mapSoftwareTransaction(transaction)],
197
+ );
198
+
199
+ return {
200
+ id: String(order.id),
201
+ orderNumber: order.orderNumber ?? String(order.id),
202
+ displayStatus: order.displayStatus ?? order.status ?? "pending",
203
+ items: (order.items?.docs ?? []).flatMap((item) =>
204
+ typeof item === "string" ? [] : [mapSoftwareOrderItem(item)],
205
+ ),
206
+ customerSnapshot: {
207
+ name: order.customerSnapshot?.name ?? "",
208
+ email: order.customerSnapshot?.email ?? "",
209
+ phone: order.customerSnapshot?.phone ?? "",
210
+ },
211
+ shippingAddress: {
212
+ recipientName: order.shippingAddress?.recipientName ?? "",
213
+ phone: order.shippingAddress?.phone ?? "",
214
+ postalCode: order.shippingAddress?.postalCode ?? "",
215
+ address: order.shippingAddress?.address ?? "",
216
+ detailAddress: order.shippingAddress?.detailAddress ?? "",
217
+ deliveryMessage: order.shippingAddress?.deliveryMessage ?? "",
218
+ },
219
+ subtotalAmount: order.subtotalAmount ?? 0,
220
+ shippingAmount: order.shippingAmount ?? 0,
221
+ totalAmount: order.totalAmount ?? 0,
222
+ transactions,
223
+ };
224
+ }
225
+
226
+ function mapSoftwareOption(option: SoftwareOption): ProductOption {
227
+ return {
228
+ id: String(option.id),
229
+ slug: option.slug ?? String(option.id),
230
+ title: option.title ?? option.name ?? "Option",
231
+ values: (option.values ?? []).map((value) => ({
232
+ id: String(value.id),
233
+ slug: value.slug ?? String(value.id),
234
+ value: value.value ?? value.label ?? String(value.id),
235
+ })),
236
+ };
237
+ }
238
+
239
+ function mapSoftwareVariantOptionValue(
240
+ value: SoftwareVariantOptionValue,
241
+ ): ProductVariantOptionValue {
242
+ const rawValue = value.value;
243
+ const valueObject =
244
+ typeof rawValue === "object" && rawValue !== null ? rawValue : null;
245
+ const option = value.option;
246
+ const optionId =
247
+ value.optionId ??
248
+ (typeof option === "object" && option !== null ? option.id : option);
249
+
250
+ return {
251
+ optionId: optionId != null ? String(optionId) : "",
252
+ valueId:
253
+ value.valueId != null
254
+ ? String(value.valueId)
255
+ : valueObject?.id != null
256
+ ? String(valueObject.id)
257
+ : rawValue != null
258
+ ? String(rawValue)
259
+ : "",
260
+ valueSlug: value.valueSlug ?? value.slug ?? valueObject?.slug ?? "",
261
+ value:
262
+ value.label ??
263
+ valueObject?.value ??
264
+ valueObject?.label ??
265
+ (rawValue != null ? String(rawValue) : ""),
266
+ };
267
+ }
268
+
269
+ function mapSoftwareOrderItem(item: SoftwareOrderItemLike): Order["items"][number] {
270
+ const product =
271
+ typeof item.product === "object" && item.product !== null ? item.product : null;
272
+ const variant =
273
+ typeof item.variant === "object" && item.variant !== null ? item.variant : null;
274
+
275
+ return {
276
+ variantId:
277
+ typeof item.variant === "object" && item.variant !== null
278
+ ? String(item.variant.id)
279
+ : item.variant != null
280
+ ? String(item.variant)
281
+ : "",
282
+ productTitle: product?.title ?? "Product",
283
+ variantTitle: variant?.displayName ?? variant?.title ?? undefined,
284
+ quantity: item.quantity ?? 0,
285
+ unitAmount: item.unitAmount ?? item.unitPrice ?? 0,
286
+ lineAmount: item.lineAmount ?? item.totalPrice ?? 0,
287
+ };
288
+ }
289
+
290
+ function mapSoftwareTransaction(
291
+ transaction: SoftwareTransactionLike,
292
+ ): PaymentTransaction {
293
+ return {
294
+ paymentId: transaction.pgPaymentId ?? "",
295
+ provider: transaction.pgProvider ?? "unknown",
296
+ status: transaction.status ?? "pending",
297
+ amount: transaction.amount ?? 0,
298
+ };
299
+ }
300
+
301
+ function mapSoftwareImage(media: SoftwareMedia): CommerceImage[] {
302
+ if (!media || typeof media !== "object" || !media.url) return [];
303
+
304
+ return [
305
+ {
306
+ url: media.url,
307
+ alt: media.alt ?? undefined,
308
+ width: media.width ?? undefined,
309
+ height: media.height ?? undefined,
310
+ },
311
+ ];
312
+ }