@01.software/init 0.10.0 → 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.js +0 -0
- package/dist/chunk-4LHYICUL.js +0 -0
- package/dist/chunk-NJ4X7VNK.js +0 -0
- package/dist/chunk-Q6MSORYN.js +0 -0
- package/dist/chunk-STM4DKVZ.js +0 -0
- package/dist/chunk-WDWJ73KP.js +0 -0
- package/dist/create-app-templates/ecommerce/CHANGELOG.md +18 -0
- package/dist/create-app-templates/ecommerce/README.md +31 -16
- package/dist/create-app-templates/ecommerce/components/cart/cart-content.tsx +23 -13
- package/dist/create-app-templates/ecommerce/components/checkout/checkout-form.tsx +145 -105
- package/dist/create-app-templates/ecommerce/lib/cart/server-cart.ts +45 -6
- package/dist/create-app-templates/ecommerce/lib/checkout/parse-checkout-payload.ts +49 -39
- package/dist/create-app-templates/ecommerce/lib/checkout/start-checkout.ts +12 -2
- package/dist/create-app-templates/ecommerce/lib/checkout/types.ts +5 -2
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/mock.ts +134 -124
- package/dist/create-app-templates/ecommerce/lib/commerce/adapters/software.ts +69 -52
- package/dist/create-app-templates/ecommerce/lib/commerce/types.ts +139 -137
- package/dist/create-app-templates/ecommerce/package.json +2 -2
- package/dist/create-app-templates/ecommerce/tests/customer-cart.test.ts +18 -9
- package/dist/create-app-templates/ecommerce/tests/domain.test.ts +107 -12
- package/dist/file-ops.js +0 -0
- package/dist/init.js +0 -0
- package/dist/templates.js +0 -0
- package/package.json +15 -16
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { CustomerSnapshot, ShippingAddress } from
|
|
1
|
+
import type { CustomerSnapshot, ShippingAddress } from '../commerce/types.ts'
|
|
2
2
|
|
|
3
|
-
const MAX_TEXT_LENGTH = 160
|
|
4
|
-
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]
|
|
3
|
+
const MAX_TEXT_LENGTH = 160
|
|
4
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Checkout collects only the buyer + shipping snapshot. The cart contents live
|
|
@@ -9,68 +9,78 @@ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
9
9
|
* client-supplied lines or totals — the cart is the quote.
|
|
10
10
|
*/
|
|
11
11
|
export type CheckoutPayload = {
|
|
12
|
-
customerSnapshot: CustomerSnapshot
|
|
13
|
-
shippingAddress
|
|
14
|
-
}
|
|
12
|
+
customerSnapshot: CustomerSnapshot
|
|
13
|
+
shippingAddress?: ShippingAddress
|
|
14
|
+
}
|
|
15
15
|
|
|
16
|
-
export function parseCheckoutPayload(
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
export function parseCheckoutPayload(
|
|
17
|
+
input: unknown,
|
|
18
|
+
options: { requiresShipping?: boolean } = {},
|
|
19
|
+
): CheckoutPayload {
|
|
20
|
+
if (!input || typeof input !== 'object') {
|
|
21
|
+
throw new Error('Checkout payload must be an object')
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
const body = input as {
|
|
22
|
-
customerSnapshot?: unknown
|
|
23
|
-
shippingAddress?: unknown
|
|
24
|
-
}
|
|
25
|
-
const customer = parseRecord(body.customerSnapshot,
|
|
26
|
-
const
|
|
27
|
-
const email = requiredString(customer.email, "email");
|
|
25
|
+
customerSnapshot?: unknown
|
|
26
|
+
shippingAddress?: unknown
|
|
27
|
+
}
|
|
28
|
+
const customer = parseRecord(body.customerSnapshot, 'customerSnapshot')
|
|
29
|
+
const email = requiredString(customer.email, 'email')
|
|
28
30
|
if (!EMAIL_RE.test(email)) {
|
|
29
|
-
throw new Error(
|
|
31
|
+
throw new Error('email must be a valid email address')
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
const customerSnapshot = {
|
|
35
|
+
name: requiredString(customer.name, 'customer name'),
|
|
36
|
+
email,
|
|
37
|
+
phone: requiredString(customer.phone, 'customer phone'),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options.requiresShipping === false) {
|
|
41
|
+
return { customerSnapshot }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const shipping = parseRecord(body.shippingAddress, 'shippingAddress')
|
|
45
|
+
|
|
32
46
|
return {
|
|
33
|
-
customerSnapshot
|
|
34
|
-
name: requiredString(customer.name, "customer name"),
|
|
35
|
-
email,
|
|
36
|
-
phone: requiredString(customer.phone, "customer phone"),
|
|
37
|
-
},
|
|
47
|
+
customerSnapshot,
|
|
38
48
|
shippingAddress: {
|
|
39
|
-
recipientName: requiredString(shipping.recipientName,
|
|
40
|
-
phone: requiredString(shipping.phone,
|
|
41
|
-
postalCode: requiredString(shipping.postalCode,
|
|
42
|
-
address: requiredString(shipping.address,
|
|
43
|
-
detailAddress: requiredString(shipping.detailAddress,
|
|
49
|
+
recipientName: requiredString(shipping.recipientName, 'recipient name'),
|
|
50
|
+
phone: requiredString(shipping.phone, 'shipping phone'),
|
|
51
|
+
postalCode: requiredString(shipping.postalCode, 'postal code'),
|
|
52
|
+
address: requiredString(shipping.address, 'address'),
|
|
53
|
+
detailAddress: requiredString(shipping.detailAddress, 'address detail'),
|
|
44
54
|
deliveryMessage: optionalString(
|
|
45
55
|
shipping.deliveryMessage,
|
|
46
|
-
|
|
56
|
+
'shipping message',
|
|
47
57
|
),
|
|
48
58
|
},
|
|
49
|
-
}
|
|
59
|
+
}
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
function parseRecord(input: unknown, field: string): Record<string, unknown> {
|
|
53
|
-
if (!input || typeof input !==
|
|
54
|
-
throw new Error(`${field} must be an object`)
|
|
63
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
64
|
+
throw new Error(`${field} must be an object`)
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
return input as Record<string, unknown
|
|
67
|
+
return input as Record<string, unknown>
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
function requiredString(input: unknown, field: string): string {
|
|
61
|
-
const value = optionalString(input, field)
|
|
62
|
-
if (!value) throw new Error(`${field} is required`)
|
|
63
|
-
return value
|
|
71
|
+
const value = optionalString(input, field)
|
|
72
|
+
if (!value) throw new Error(`${field} is required`)
|
|
73
|
+
return value
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
function optionalString(input: unknown, field: string): string {
|
|
67
|
-
if (input === undefined || input === null) return
|
|
68
|
-
if (typeof input !==
|
|
77
|
+
if (input === undefined || input === null) return ''
|
|
78
|
+
if (typeof input !== 'string') throw new Error(`${field} must be a string`)
|
|
69
79
|
|
|
70
|
-
const value = input.trim()
|
|
80
|
+
const value = input.trim()
|
|
71
81
|
if (value.length > MAX_TEXT_LENGTH) {
|
|
72
|
-
throw new Error(`${field} must be ${MAX_TEXT_LENGTH} characters or fewer`)
|
|
82
|
+
throw new Error(`${field} must be ${MAX_TEXT_LENGTH} characters or fewer`)
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
return value
|
|
85
|
+
return value
|
|
76
86
|
}
|
|
@@ -28,11 +28,21 @@ export async function startCheckout(input: {
|
|
|
28
28
|
commerceProvider: CheckoutCommerceProvider
|
|
29
29
|
paymentProvider: PaymentProvider
|
|
30
30
|
}): Promise<StartCheckoutResult> {
|
|
31
|
-
const
|
|
31
|
+
const cart = await input.commerceProvider.getCart(input.cartToken)
|
|
32
|
+
if (!cart) throw new Error('Cart not found')
|
|
33
|
+
|
|
34
|
+
const requiresShipping = cart.items.some(
|
|
35
|
+
(item) => item.requiresShipping !== false,
|
|
36
|
+
)
|
|
37
|
+
const payload: CheckoutPayload = parseCheckoutPayload(input.payload, {
|
|
38
|
+
requiresShipping,
|
|
39
|
+
})
|
|
32
40
|
const pending = await input.commerceProvider.checkoutCart({
|
|
33
41
|
cartToken: input.cartToken,
|
|
34
42
|
customerSnapshot: payload.customerSnapshot,
|
|
35
|
-
|
|
43
|
+
...(payload.shippingAddress
|
|
44
|
+
? { shippingAddress: payload.shippingAddress }
|
|
45
|
+
: {}),
|
|
36
46
|
})
|
|
37
47
|
|
|
38
48
|
const payment = await input.paymentProvider.requestPayment({
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
import type { CommerceProvider } from
|
|
1
|
+
import type { CommerceProvider } from '../commerce/provider.ts'
|
|
2
2
|
|
|
3
|
-
export type CheckoutCommerceProvider = Pick<
|
|
3
|
+
export type CheckoutCommerceProvider = Pick<
|
|
4
|
+
CommerceProvider,
|
|
5
|
+
'checkoutCart' | 'getCart'
|
|
6
|
+
>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import seed from
|
|
2
|
-
import { normalizeCartLines } from
|
|
3
|
-
import type { CommerceProvider } from
|
|
4
|
-
import { checkVariantStock } from
|
|
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
5
|
import type {
|
|
6
6
|
CartItemView,
|
|
7
7
|
CartView,
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
ProductDetail,
|
|
12
12
|
ProductVariant,
|
|
13
13
|
ShippingPolicy,
|
|
14
|
-
} from
|
|
14
|
+
} from '../types.ts'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* In-memory server-cart emulation for the zero-backend demo. The mock stands in
|
|
@@ -20,41 +20,42 @@ import type {
|
|
|
20
20
|
* lifetime of the dev server process — there is no file-based index.
|
|
21
21
|
*/
|
|
22
22
|
type MockCartItem = {
|
|
23
|
-
cartItemId: string
|
|
24
|
-
productId: string
|
|
25
|
-
variantId: string
|
|
26
|
-
optionId?: string
|
|
27
|
-
quantity: number
|
|
28
|
-
unitPrice: number
|
|
29
|
-
|
|
23
|
+
cartItemId: string
|
|
24
|
+
productId: string
|
|
25
|
+
variantId: string
|
|
26
|
+
optionId?: string
|
|
27
|
+
quantity: number
|
|
28
|
+
unitPrice: number
|
|
29
|
+
requiresShipping?: boolean | null
|
|
30
|
+
}
|
|
30
31
|
|
|
31
32
|
type MockCart = {
|
|
32
|
-
id: string
|
|
33
|
-
cartToken: string
|
|
34
|
-
currency: string
|
|
35
|
-
items: MockCartItem[]
|
|
36
|
-
}
|
|
33
|
+
id: string
|
|
34
|
+
cartToken: string
|
|
35
|
+
currency: string
|
|
36
|
+
items: MockCartItem[]
|
|
37
|
+
}
|
|
37
38
|
|
|
38
|
-
const carts = new Map<string, MockCart>()
|
|
39
|
-
const orders = new Map<string, Order>()
|
|
39
|
+
const carts = new Map<string, MockCart>()
|
|
40
|
+
const orders = new Map<string, Order>()
|
|
40
41
|
|
|
41
42
|
export function createMockCommerceProvider(): CommerceProvider {
|
|
42
|
-
const products = seed.products as ProductDetail[]
|
|
43
|
-
const shippingPolicy = seed.shippingPolicy as ShippingPolicy
|
|
44
|
-
const variants = allVariants(products)
|
|
45
|
-
const variantById = new Map(variants.map((variant) => [variant.id, variant]))
|
|
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]))
|
|
46
47
|
|
|
47
48
|
function viewCart(cart: MockCart): CartView {
|
|
48
49
|
const items: CartItemView[] = cart.items.map((item) => {
|
|
49
|
-
const variant = variantById.get(item.variantId)
|
|
50
|
+
const variant = variantById.get(item.variantId)
|
|
50
51
|
const detail = products.find(
|
|
51
52
|
(entry) => entry.product.id === item.productId,
|
|
52
|
-
)
|
|
53
|
+
)
|
|
53
54
|
const image =
|
|
54
55
|
variant?.images[0] ??
|
|
55
56
|
detail?.product.thumbnail ??
|
|
56
57
|
detail?.product.images[0] ??
|
|
57
|
-
null
|
|
58
|
+
null
|
|
58
59
|
return {
|
|
59
60
|
cartItemId: item.cartItemId,
|
|
60
61
|
productId: item.productId,
|
|
@@ -62,20 +63,24 @@ export function createMockCommerceProvider(): CommerceProvider {
|
|
|
62
63
|
quantity: item.quantity,
|
|
63
64
|
unitAmount: item.unitPrice,
|
|
64
65
|
lineAmount: item.unitPrice * item.quantity,
|
|
65
|
-
|
|
66
|
+
requiresShipping: item.requiresShipping,
|
|
67
|
+
productTitle: detail?.product.title ?? 'Unknown product',
|
|
66
68
|
variantTitle: variant?.title,
|
|
67
69
|
image,
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
+
}
|
|
71
|
+
})
|
|
70
72
|
|
|
71
|
-
const subtotalAmount = items.reduce((sum, item) => sum + item.lineAmount, 0)
|
|
73
|
+
const subtotalAmount = items.reduce((sum, item) => sum + item.lineAmount, 0)
|
|
74
|
+
const hasShippableItems = items.some(
|
|
75
|
+
(item) => item.requiresShipping !== false,
|
|
76
|
+
)
|
|
72
77
|
const shippingAmount =
|
|
73
|
-
|
|
74
|
-
subtotalAmount >= shippingPolicy.freeAboveAmount
|
|
78
|
+
!hasShippableItems || subtotalAmount === 0
|
|
75
79
|
? 0
|
|
76
|
-
:
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
: shippingPolicy.freeAboveAmount !== undefined &&
|
|
81
|
+
subtotalAmount >= shippingPolicy.freeAboveAmount
|
|
82
|
+
? 0
|
|
83
|
+
: shippingPolicy.baseAmount
|
|
79
84
|
|
|
80
85
|
return {
|
|
81
86
|
id: cart.id,
|
|
@@ -85,86 +90,88 @@ export function createMockCommerceProvider(): CommerceProvider {
|
|
|
85
90
|
discountAmount: 0,
|
|
86
91
|
totalAmount: subtotalAmount + shippingAmount,
|
|
87
92
|
items,
|
|
88
|
-
}
|
|
93
|
+
}
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
function requireCart(cartToken: string): MockCart {
|
|
92
|
-
const cart = carts.get(cartToken)
|
|
93
|
-
if (!cart) throw new Error(
|
|
94
|
-
return cart
|
|
97
|
+
const cart = carts.get(cartToken)
|
|
98
|
+
if (!cart) throw new Error('Cart not found')
|
|
99
|
+
return cart
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
return {
|
|
98
103
|
async listProducts(input) {
|
|
99
|
-
const limit = input?.limit ?? 24
|
|
104
|
+
const limit = input?.limit ?? 24
|
|
100
105
|
const publishedProducts = products.filter(
|
|
101
|
-
(entry) => entry.product.status ===
|
|
102
|
-
)
|
|
106
|
+
(entry) => entry.product.status === 'published',
|
|
107
|
+
)
|
|
103
108
|
return {
|
|
104
109
|
products: publishedProducts.slice(0, limit),
|
|
105
110
|
total: publishedProducts.length,
|
|
106
|
-
}
|
|
111
|
+
}
|
|
107
112
|
},
|
|
108
113
|
|
|
109
114
|
async getProductBySlug(slug) {
|
|
110
115
|
return (
|
|
111
116
|
products.find(
|
|
112
117
|
(entry) =>
|
|
113
|
-
entry.product.slug === slug &&
|
|
114
|
-
entry.product.status === "published",
|
|
118
|
+
entry.product.slug === slug && entry.product.status === 'published',
|
|
115
119
|
) ?? null
|
|
116
|
-
)
|
|
120
|
+
)
|
|
117
121
|
},
|
|
118
122
|
|
|
119
123
|
async getVariantsByIds(ids) {
|
|
120
|
-
const idSet = new Set(ids)
|
|
121
|
-
return variants.filter((variant) => idSet.has(variant.id))
|
|
124
|
+
const idSet = new Set(ids)
|
|
125
|
+
return variants.filter((variant) => idSet.has(variant.id))
|
|
122
126
|
},
|
|
123
127
|
|
|
124
128
|
async getShippingPolicy() {
|
|
125
|
-
return shippingPolicy
|
|
129
|
+
return shippingPolicy
|
|
126
130
|
},
|
|
127
131
|
|
|
128
132
|
async checkStock(items) {
|
|
129
133
|
return checkVariantStock({
|
|
130
134
|
lines: normalizeCartLines(items),
|
|
131
135
|
variants,
|
|
132
|
-
})
|
|
136
|
+
})
|
|
133
137
|
},
|
|
134
138
|
|
|
135
139
|
async createCart() {
|
|
136
|
-
const cartToken = `mock_cart_${crypto.randomUUID()}
|
|
140
|
+
const cartToken = `mock_cart_${crypto.randomUUID()}`
|
|
137
141
|
carts.set(cartToken, {
|
|
138
142
|
id: `cart_${crypto.randomUUID()}`,
|
|
139
143
|
cartToken,
|
|
140
144
|
currency: shippingPolicy.currency,
|
|
141
145
|
items: [],
|
|
142
|
-
})
|
|
143
|
-
return { cartToken }
|
|
146
|
+
})
|
|
147
|
+
return { cartToken }
|
|
144
148
|
},
|
|
145
149
|
|
|
146
150
|
async getCart(cartToken) {
|
|
147
|
-
const cart = carts.get(cartToken)
|
|
148
|
-
return cart ? viewCart(cart) : null
|
|
151
|
+
const cart = carts.get(cartToken)
|
|
152
|
+
return cart ? viewCart(cart) : null
|
|
149
153
|
},
|
|
150
154
|
|
|
151
155
|
async addCartItem({ cartToken, item }) {
|
|
152
|
-
const cart = requireCart(cartToken)
|
|
153
|
-
const variant = variantById.get(item.variantId)
|
|
154
|
-
if (!variant) throw new Error(
|
|
156
|
+
const cart = requireCart(cartToken)
|
|
157
|
+
const variant = variantById.get(item.variantId)
|
|
158
|
+
if (!variant) throw new Error('Variant not found')
|
|
155
159
|
|
|
156
160
|
const existing = cart.items.find(
|
|
157
161
|
(line) => line.variantId === item.variantId,
|
|
158
|
-
)
|
|
159
|
-
const nextQuantity = (existing?.quantity ?? 0) + item.quantity
|
|
162
|
+
)
|
|
163
|
+
const nextQuantity = (existing?.quantity ?? 0) + item.quantity
|
|
160
164
|
const stock = checkVariantStock({
|
|
161
165
|
lines: [{ variantId: item.variantId, quantity: nextQuantity }],
|
|
162
166
|
variants,
|
|
163
|
-
})
|
|
164
|
-
if (!stock.ok) throw new Error(
|
|
167
|
+
})
|
|
168
|
+
if (!stock.ok) throw new Error('Insufficient stock')
|
|
165
169
|
|
|
166
170
|
if (existing) {
|
|
167
|
-
existing.quantity = Math.min(99, nextQuantity)
|
|
171
|
+
existing.quantity = Math.min(99, nextQuantity)
|
|
172
|
+
existing.requiresShipping =
|
|
173
|
+
existing.requiresShipping !== false ||
|
|
174
|
+
variant.requiresShipping !== false
|
|
168
175
|
} else {
|
|
169
176
|
cart.items.push({
|
|
170
177
|
cartItemId: `item_${crypto.randomUUID()}`,
|
|
@@ -173,110 +180,111 @@ export function createMockCommerceProvider(): CommerceProvider {
|
|
|
173
180
|
optionId: item.optionId,
|
|
174
181
|
quantity: Math.min(99, item.quantity),
|
|
175
182
|
unitPrice: variant.price,
|
|
176
|
-
|
|
183
|
+
requiresShipping: variant.requiresShipping !== false,
|
|
184
|
+
})
|
|
177
185
|
}
|
|
178
|
-
return viewCart(cart)
|
|
186
|
+
return viewCart(cart)
|
|
179
187
|
},
|
|
180
188
|
|
|
181
189
|
async updateCartItem({ cartToken, cartItemId, quantity }) {
|
|
182
|
-
const cart = requireCart(cartToken)
|
|
183
|
-
const line = cart.items.find((entry) => entry.cartItemId === cartItemId)
|
|
184
|
-
if (!line) throw new Error(
|
|
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')
|
|
185
193
|
if (quantity <= 0) {
|
|
186
194
|
cart.items = cart.items.filter(
|
|
187
195
|
(entry) => entry.cartItemId !== cartItemId,
|
|
188
|
-
)
|
|
196
|
+
)
|
|
189
197
|
} else {
|
|
190
|
-
line.quantity = Math.min(99, Math.floor(quantity))
|
|
198
|
+
line.quantity = Math.min(99, Math.floor(quantity))
|
|
191
199
|
}
|
|
192
|
-
return viewCart(cart)
|
|
200
|
+
return viewCart(cart)
|
|
193
201
|
},
|
|
194
202
|
|
|
195
203
|
async removeCartItem({ cartToken, cartItemId }) {
|
|
196
|
-
const cart = requireCart(cartToken)
|
|
197
|
-
cart.items = cart.items.filter((entry) => entry.cartItemId !== cartItemId)
|
|
198
|
-
return viewCart(cart)
|
|
204
|
+
const cart = requireCart(cartToken)
|
|
205
|
+
cart.items = cart.items.filter((entry) => entry.cartItemId !== cartItemId)
|
|
206
|
+
return viewCart(cart)
|
|
199
207
|
},
|
|
200
208
|
|
|
201
209
|
async clearCart({ cartToken }) {
|
|
202
|
-
const cart = requireCart(cartToken)
|
|
203
|
-
cart.items = []
|
|
204
|
-
return viewCart(cart)
|
|
210
|
+
const cart = requireCart(cartToken)
|
|
211
|
+
cart.items = []
|
|
212
|
+
return viewCart(cart)
|
|
205
213
|
},
|
|
206
214
|
|
|
207
215
|
async checkoutCart(input) {
|
|
208
|
-
return checkoutMockCart({ input, products, viewCart, requireCart })
|
|
216
|
+
return checkoutMockCart({ input, products, viewCart, requireCart })
|
|
209
217
|
},
|
|
210
218
|
|
|
211
219
|
async getOrderByPaymentId({ paymentId }) {
|
|
212
|
-
return orders.get(paymentId) ?? null
|
|
220
|
+
return orders.get(paymentId) ?? null
|
|
213
221
|
},
|
|
214
222
|
|
|
215
223
|
async confirmOrderPayment(input) {
|
|
216
|
-
const order = orders.get(input.paymentId)
|
|
217
|
-
if (!order) throw new Error(
|
|
224
|
+
const order = orders.get(input.paymentId)
|
|
225
|
+
if (!order) throw new Error('Order not found')
|
|
218
226
|
const paidOrder: Order = {
|
|
219
227
|
...order,
|
|
220
|
-
displayStatus:
|
|
228
|
+
displayStatus: 'paid',
|
|
221
229
|
transactions: order.transactions.map((transaction) =>
|
|
222
230
|
transaction.paymentId === input.paymentId
|
|
223
|
-
? { ...transaction, provider: input.provider, status:
|
|
231
|
+
? { ...transaction, provider: input.provider, status: 'paid' }
|
|
224
232
|
: transaction,
|
|
225
233
|
),
|
|
226
|
-
}
|
|
227
|
-
orders.set(input.paymentId, paidOrder)
|
|
228
|
-
return paidOrder
|
|
234
|
+
}
|
|
235
|
+
orders.set(input.paymentId, paidOrder)
|
|
236
|
+
return paidOrder
|
|
229
237
|
},
|
|
230
238
|
|
|
231
239
|
async markPaymentFailed(input) {
|
|
232
|
-
const order = orders.get(input.paymentId)
|
|
233
|
-
if (!order) return null
|
|
240
|
+
const order = orders.get(input.paymentId)
|
|
241
|
+
if (!order) return null
|
|
234
242
|
const failedOrder: Order = {
|
|
235
243
|
...order,
|
|
236
244
|
transactions: order.transactions.map((transaction) =>
|
|
237
245
|
transaction.paymentId === input.paymentId
|
|
238
|
-
? { ...transaction, provider: input.provider, status:
|
|
246
|
+
? { ...transaction, provider: input.provider, status: 'failed' }
|
|
239
247
|
: transaction,
|
|
240
248
|
),
|
|
241
|
-
}
|
|
242
|
-
orders.set(input.paymentId, failedOrder)
|
|
243
|
-
return failedOrder
|
|
249
|
+
}
|
|
250
|
+
orders.set(input.paymentId, failedOrder)
|
|
251
|
+
return failedOrder
|
|
244
252
|
},
|
|
245
253
|
|
|
246
254
|
async cancelPendingOrder(input) {
|
|
247
|
-
const order = orders.get(input.paymentId)
|
|
248
|
-
if (!order) return null
|
|
255
|
+
const order = orders.get(input.paymentId)
|
|
256
|
+
if (!order) return null
|
|
249
257
|
const canceledOrder: Order = {
|
|
250
258
|
...order,
|
|
251
|
-
displayStatus:
|
|
259
|
+
displayStatus: 'canceled',
|
|
252
260
|
transactions: order.transactions.map((transaction) =>
|
|
253
261
|
transaction.paymentId === input.paymentId
|
|
254
|
-
? { ...transaction, provider: input.provider, status:
|
|
262
|
+
? { ...transaction, provider: input.provider, status: 'canceled' }
|
|
255
263
|
: transaction,
|
|
256
264
|
),
|
|
257
|
-
}
|
|
258
|
-
orders.set(input.paymentId, canceledOrder)
|
|
259
|
-
return canceledOrder
|
|
265
|
+
}
|
|
266
|
+
orders.set(input.paymentId, canceledOrder)
|
|
267
|
+
return canceledOrder
|
|
260
268
|
},
|
|
261
|
-
}
|
|
269
|
+
}
|
|
262
270
|
}
|
|
263
271
|
|
|
264
272
|
function checkoutMockCart(args: {
|
|
265
|
-
input: CheckoutCartInput
|
|
266
|
-
products: ProductDetail[]
|
|
267
|
-
viewCart: (cart: MockCart) => CartView
|
|
268
|
-
requireCart: (cartToken: string) => MockCart
|
|
273
|
+
input: CheckoutCartInput
|
|
274
|
+
products: ProductDetail[]
|
|
275
|
+
viewCart: (cart: MockCart) => CartView
|
|
276
|
+
requireCart: (cartToken: string) => MockCart
|
|
269
277
|
}): CreatePendingOrderResult {
|
|
270
|
-
const cart = args.requireCart(args.input.cartToken)
|
|
271
|
-
const view = args.viewCart(cart)
|
|
272
|
-
if (view.items.length === 0) throw new Error(
|
|
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')
|
|
273
281
|
|
|
274
|
-
const paymentId = `mock_${Date.now()}_${Math.random().toString(16).slice(2)}
|
|
275
|
-
const orderNumber = `ORD-${Date.now().toString().slice(-8)}
|
|
282
|
+
const paymentId = `mock_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
|
283
|
+
const orderNumber = `ORD-${Date.now().toString().slice(-8)}`
|
|
276
284
|
const order: Order = {
|
|
277
285
|
id: `order_${paymentId}`,
|
|
278
286
|
orderNumber,
|
|
279
|
-
displayStatus:
|
|
287
|
+
displayStatus: 'pending',
|
|
280
288
|
items: view.items.map((item) => ({
|
|
281
289
|
variantId: item.variantId,
|
|
282
290
|
productTitle: item.productTitle,
|
|
@@ -286,24 +294,26 @@ function checkoutMockCart(args: {
|
|
|
286
294
|
lineAmount: item.lineAmount,
|
|
287
295
|
})),
|
|
288
296
|
customerSnapshot: args.input.customerSnapshot,
|
|
289
|
-
|
|
297
|
+
...(args.input.shippingAddress
|
|
298
|
+
? { shippingAddress: args.input.shippingAddress }
|
|
299
|
+
: {}),
|
|
290
300
|
subtotalAmount: view.subtotalAmount,
|
|
291
301
|
shippingAmount: view.shippingAmount,
|
|
292
302
|
totalAmount: view.totalAmount,
|
|
293
303
|
transactions: [
|
|
294
304
|
{
|
|
295
305
|
paymentId,
|
|
296
|
-
provider:
|
|
297
|
-
status:
|
|
306
|
+
provider: 'mock',
|
|
307
|
+
status: 'pending',
|
|
298
308
|
amount: view.totalAmount,
|
|
299
309
|
},
|
|
300
310
|
],
|
|
301
|
-
}
|
|
311
|
+
}
|
|
302
312
|
|
|
303
|
-
orders.set(paymentId, order)
|
|
313
|
+
orders.set(paymentId, order)
|
|
304
314
|
// The cart is consumed by checkout (Shopify cart→checkout boundary); the
|
|
305
315
|
// browser cookie will resolve to no active cart and mint a fresh one.
|
|
306
|
-
carts.delete(args.input.cartToken)
|
|
316
|
+
carts.delete(args.input.cartToken)
|
|
307
317
|
|
|
308
318
|
return {
|
|
309
319
|
order,
|
|
@@ -311,26 +321,26 @@ function checkoutMockCart(args: {
|
|
|
311
321
|
paymentName: buildPaymentName(order.items.map((item) => item.productTitle)),
|
|
312
322
|
amount: view.totalAmount,
|
|
313
323
|
currency: view.currency,
|
|
314
|
-
}
|
|
324
|
+
}
|
|
315
325
|
}
|
|
316
326
|
|
|
317
327
|
function allVariants(products: ProductDetail[]): ProductVariant[] {
|
|
318
|
-
return products.flatMap((entry) => entry.variants)
|
|
328
|
+
return products.flatMap((entry) => entry.variants)
|
|
319
329
|
}
|
|
320
330
|
|
|
321
331
|
function buildPaymentName(productTitles: string[]): string {
|
|
322
|
-
const [first, ...rest] = productTitles
|
|
323
|
-
if (!first) return
|
|
324
|
-
return rest.length === 0 ? first : `${first} and ${rest.length} more
|
|
332
|
+
const [first, ...rest] = productTitles
|
|
333
|
+
if (!first) return 'Order'
|
|
334
|
+
return rest.length === 0 ? first : `${first} and ${rest.length} more`
|
|
325
335
|
}
|
|
326
336
|
|
|
327
337
|
/** Test helper: reset the in-memory stores between cases. */
|
|
328
338
|
export function resetMockCommerceState(): void {
|
|
329
|
-
carts.clear()
|
|
330
|
-
orders.clear()
|
|
339
|
+
carts.clear()
|
|
340
|
+
orders.clear()
|
|
331
341
|
}
|
|
332
342
|
|
|
333
343
|
/** Test helper: read a stored mock order by its mock payment id. */
|
|
334
344
|
export function getMockOrder(paymentId: string): Order | null {
|
|
335
|
-
return orders.get(paymentId) ?? null
|
|
345
|
+
return orders.get(paymentId) ?? null
|
|
336
346
|
}
|