@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,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
|
+
}
|