@labdigital/commercetools-mock 2.17.1 → 2.18.0
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/index.cjs +4186 -3974
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +266 -413
- package/dist/index.d.ts +266 -413
- package/dist/index.js +4186 -3974
- package/dist/index.js.map +1 -1
- package/package.json +44 -46
- package/src/constants.ts +2 -2
- package/src/ctMock.test.ts +11 -11
- package/src/ctMock.ts +141 -127
- package/src/deprecation.ts +8 -0
- package/src/exceptions.ts +17 -15
- package/src/helpers.ts +32 -32
- package/src/index.test.ts +128 -128
- package/src/index.ts +3 -3
- package/src/lib/expandParser.ts +13 -13
- package/src/lib/haversine.test.ts +9 -9
- package/src/lib/haversine.ts +11 -11
- package/src/lib/masking.ts +11 -11
- package/src/lib/parser.ts +2 -2
- package/src/lib/password.ts +23 -3
- package/src/lib/predicateParser.test.ts +185 -183
- package/src/lib/predicateParser.ts +234 -234
- package/src/lib/projectionSearchFilter.test.ts +103 -101
- package/src/lib/projectionSearchFilter.ts +152 -150
- package/src/lib/proxy.ts +5 -5
- package/src/oauth/errors.ts +4 -4
- package/src/oauth/helpers.ts +6 -6
- package/src/oauth/server.test.ts +86 -86
- package/src/oauth/server.ts +158 -144
- package/src/oauth/store.ts +44 -43
- package/src/priceSelector.test.ts +35 -35
- package/src/priceSelector.ts +30 -30
- package/src/product-projection-search.ts +136 -134
- package/src/projectAPI.test.ts +7 -7
- package/src/projectAPI.ts +24 -22
- package/src/repositories/abstract.ts +168 -116
- package/src/repositories/associate-role.ts +90 -77
- package/src/repositories/attribute-group.ts +51 -40
- package/src/repositories/business-unit.ts +168 -148
- package/src/repositories/cart/actions.ts +489 -0
- package/src/repositories/cart/helpers.ts +30 -0
- package/src/repositories/cart/index.ts +180 -0
- package/src/repositories/cart-discount/actions.ts +148 -0
- package/src/repositories/cart-discount/index.ts +86 -0
- package/src/repositories/category/actions.ts +231 -0
- package/src/repositories/category/index.ts +52 -0
- package/src/repositories/channel.ts +88 -90
- package/src/repositories/custom-object.ts +46 -45
- package/src/repositories/customer/actions.ts +165 -0
- package/src/repositories/customer/index.ts +79 -0
- package/src/repositories/customer-group.ts +66 -55
- package/src/repositories/discount-code/actions.ts +149 -0
- package/src/repositories/discount-code/index.ts +50 -0
- package/src/repositories/errors.ts +10 -10
- package/src/repositories/extension.ts +64 -62
- package/src/repositories/helpers.ts +117 -118
- package/src/repositories/index.ts +80 -79
- package/src/repositories/inventory-entry/actions.ts +84 -0
- package/src/repositories/inventory-entry/index.ts +44 -0
- package/src/repositories/my-customer.ts +114 -0
- package/src/repositories/my-order.ts +8 -8
- package/src/repositories/order/actions.ts +281 -0
- package/src/repositories/{order.test.ts → order/index.test.ts} +77 -77
- package/src/repositories/order/index.ts +260 -0
- package/src/repositories/order-edit.ts +10 -23
- package/src/repositories/payment/actions.ts +305 -0
- package/src/repositories/payment/helpers.ts +17 -0
- package/src/repositories/payment/index.ts +56 -0
- package/src/repositories/product/actions.ts +943 -0
- package/src/repositories/product/helpers.ts +98 -0
- package/src/repositories/product/index.ts +130 -0
- package/src/repositories/product-discount.ts +127 -117
- package/src/repositories/product-projection.ts +56 -62
- package/src/repositories/product-selection.ts +31 -28
- package/src/repositories/product-type.ts +136 -134
- package/src/repositories/project.ts +133 -118
- package/src/repositories/quote-request.ts +7 -19
- package/src/repositories/quote.ts +7 -22
- package/src/repositories/review.ts +13 -26
- package/src/repositories/shipping-method/actions.ts +198 -0
- package/src/repositories/shipping-method/helpers.ts +10 -0
- package/src/repositories/shipping-method/index.ts +138 -0
- package/src/repositories/shopping-list/actions.ts +295 -0
- package/src/repositories/shopping-list/index.ts +122 -0
- package/src/repositories/staged-quote.ts +7 -20
- package/src/repositories/standalone-price.ts +57 -44
- package/src/repositories/state.ts +113 -68
- package/src/repositories/store.ts +106 -94
- package/src/repositories/subscription.ts +46 -22
- package/src/repositories/tax-category/actions.ts +94 -0
- package/src/repositories/tax-category/helpers.ts +8 -0
- package/src/repositories/tax-category/index.ts +25 -0
- package/src/repositories/type/actions.ts +162 -0
- package/src/repositories/type/index.ts +24 -0
- package/src/repositories/zone.ts +62 -58
- package/src/server.ts +9 -9
- package/src/services/abstract.ts +75 -72
- package/src/services/associate-roles.test.ts +27 -27
- package/src/services/associate-roles.ts +7 -7
- package/src/services/attribute-group.ts +7 -7
- package/src/services/business-units.test.ts +28 -28
- package/src/services/business-units.ts +7 -7
- package/src/services/cart-discount.test.ts +199 -199
- package/src/services/cart-discount.ts +7 -7
- package/src/services/cart.test.ts +261 -261
- package/src/services/cart.ts +22 -21
- package/src/services/category.test.ts +121 -121
- package/src/services/category.ts +7 -7
- package/src/services/channel.ts +7 -7
- package/src/services/custom-object.test.ts +130 -130
- package/src/services/custom-object.ts +34 -31
- package/src/services/customer-group.ts +7 -7
- package/src/services/customer.test.ts +205 -205
- package/src/services/customer.ts +23 -36
- package/src/services/discount-code.ts +7 -7
- package/src/services/extension.ts +7 -7
- package/src/services/index.ts +85 -81
- package/src/services/inventory-entry.test.ts +106 -106
- package/src/services/inventory-entry.ts +7 -7
- package/src/services/my-cart.test.ts +56 -56
- package/src/services/my-cart.ts +20 -20
- package/src/services/my-customer.test.ts +155 -104
- package/src/services/my-customer.ts +61 -75
- package/src/services/my-order.ts +16 -16
- package/src/services/my-payment.test.ts +40 -40
- package/src/services/my-payment.ts +7 -7
- package/src/services/my-shopping-list.ts +7 -7
- package/src/services/order.test.ts +243 -243
- package/src/services/order.ts +23 -18
- package/src/services/payment.test.ts +40 -40
- package/src/services/payment.ts +7 -7
- package/src/services/product-discount.ts +7 -7
- package/src/services/product-projection.test.ts +190 -190
- package/src/services/product-projection.ts +34 -32
- package/src/services/product-selection.test.ts +19 -19
- package/src/services/product-selection.ts +7 -7
- package/src/services/product-type.test.ts +38 -38
- package/src/services/product-type.ts +7 -7
- package/src/services/product.test.ts +658 -656
- package/src/services/product.ts +7 -7
- package/src/services/project.test.ts +24 -24
- package/src/services/project.ts +17 -17
- package/src/services/reviews.ts +7 -7
- package/src/services/shipping-method.test.ts +78 -78
- package/src/services/shipping-method.ts +16 -16
- package/src/services/shopping-list.test.ts +170 -170
- package/src/services/shopping-list.ts +7 -7
- package/src/services/standalone-price.test.ts +112 -112
- package/src/services/standalone-price.ts +7 -7
- package/src/services/state.test.ts +30 -30
- package/src/services/state.ts +7 -7
- package/src/services/store.test.ts +40 -40
- package/src/services/store.ts +7 -7
- package/src/services/subscription.ts +7 -7
- package/src/services/tax-category.test.ts +43 -43
- package/src/services/tax-category.ts +7 -7
- package/src/services/type.ts +7 -7
- package/src/services/zone.ts +7 -7
- package/src/shippingCalculator.test.ts +43 -43
- package/src/shippingCalculator.ts +23 -23
- package/src/storage/abstract.ts +36 -34
- package/src/storage/in-memory.ts +237 -233
- package/src/storage/index.ts +2 -2
- package/src/types.ts +91 -91
- package/src/repositories/cart-discount.ts +0 -219
- package/src/repositories/cart.ts +0 -659
- package/src/repositories/category.ts +0 -256
- package/src/repositories/customer.ts +0 -228
- package/src/repositories/discount-code.ts +0 -181
- package/src/repositories/inventory-entry.ts +0 -109
- package/src/repositories/order.ts +0 -514
- package/src/repositories/payment.ts +0 -342
- package/src/repositories/product.ts +0 -1106
- package/src/repositories/shipping-method.ts +0 -312
- package/src/repositories/shopping-list.ts +0 -392
- package/src/repositories/tax-category.ts +0 -111
- package/src/repositories/type.ts +0 -172
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CartUpdateAction,
|
|
3
|
+
type Address,
|
|
4
|
+
type AddressDraft,
|
|
5
|
+
type Cart,
|
|
6
|
+
type CartAddItemShippingAddressAction,
|
|
7
|
+
type CartAddLineItemAction,
|
|
8
|
+
type CartChangeLineItemQuantityAction,
|
|
9
|
+
type CartRemoveDiscountCodeAction,
|
|
10
|
+
type CartRemoveLineItemAction,
|
|
11
|
+
type CartSetBillingAddressAction,
|
|
12
|
+
type CartSetCountryAction,
|
|
13
|
+
type CartSetCustomFieldAction,
|
|
14
|
+
type CartSetCustomShippingMethodAction,
|
|
15
|
+
type CartSetCustomTypeAction,
|
|
16
|
+
type CartSetCustomerEmailAction,
|
|
17
|
+
type CartSetDirectDiscountsAction,
|
|
18
|
+
type CartSetLineItemShippingDetailsAction,
|
|
19
|
+
type CartSetLocaleAction,
|
|
20
|
+
type CartSetShippingAddressAction,
|
|
21
|
+
type CartSetShippingMethodAction,
|
|
22
|
+
type CustomFields,
|
|
23
|
+
type GeneralError,
|
|
24
|
+
type ItemShippingDetails,
|
|
25
|
+
type LineItem,
|
|
26
|
+
type Product,
|
|
27
|
+
type ProductPagedQueryResponse,
|
|
28
|
+
type ProductVariant,
|
|
29
|
+
} from "@commercetools/platform-sdk";
|
|
30
|
+
import { v4 as uuidv4 } from "uuid";
|
|
31
|
+
import { CommercetoolsError } from "~src/exceptions";
|
|
32
|
+
import type { Writable } from "~src/types";
|
|
33
|
+
import {
|
|
34
|
+
AbstractUpdateHandler,
|
|
35
|
+
UpdateHandlerInterface,
|
|
36
|
+
type RepositoryContext,
|
|
37
|
+
} from "../abstract";
|
|
38
|
+
import {
|
|
39
|
+
createAddress,
|
|
40
|
+
createCentPrecisionMoney,
|
|
41
|
+
createCustomFields,
|
|
42
|
+
createTypedMoney,
|
|
43
|
+
} from "../helpers";
|
|
44
|
+
import {
|
|
45
|
+
calculateCartTotalPrice,
|
|
46
|
+
calculateLineItemTotalPrice,
|
|
47
|
+
selectPrice,
|
|
48
|
+
} from "./helpers";
|
|
49
|
+
|
|
50
|
+
export class CartUpdateHandler
|
|
51
|
+
extends AbstractUpdateHandler
|
|
52
|
+
implements Partial<UpdateHandlerInterface<Cart, CartUpdateAction>>
|
|
53
|
+
{
|
|
54
|
+
addItemShippingAddress(
|
|
55
|
+
context: RepositoryContext,
|
|
56
|
+
resource: Writable<Cart>,
|
|
57
|
+
{ action, address }: CartAddItemShippingAddressAction,
|
|
58
|
+
) {
|
|
59
|
+
const newAddress = createAddress(
|
|
60
|
+
address,
|
|
61
|
+
context.projectKey,
|
|
62
|
+
this._storage,
|
|
63
|
+
);
|
|
64
|
+
if (newAddress) {
|
|
65
|
+
resource.itemShippingAddresses.push(newAddress);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
addLineItem(
|
|
70
|
+
context: RepositoryContext,
|
|
71
|
+
resource: Writable<Cart>,
|
|
72
|
+
{ productId, variantId, sku, quantity = 1 }: CartAddLineItemAction,
|
|
73
|
+
) {
|
|
74
|
+
let product: Product | null = null;
|
|
75
|
+
|
|
76
|
+
if (productId && variantId) {
|
|
77
|
+
// Fetch product and variant by ID
|
|
78
|
+
product = this._storage.get(context.projectKey, "product", productId, {});
|
|
79
|
+
} else if (sku) {
|
|
80
|
+
// Fetch product and variant by SKU
|
|
81
|
+
const items = this._storage.query(context.projectKey, "product", {
|
|
82
|
+
where: [
|
|
83
|
+
`masterData(current(masterVariant(sku="${sku}"))) or masterData(current(variants(sku="${sku}")))`,
|
|
84
|
+
],
|
|
85
|
+
}) as ProductPagedQueryResponse;
|
|
86
|
+
|
|
87
|
+
if (items.count === 1) {
|
|
88
|
+
product = items.results[0];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!product) {
|
|
93
|
+
// Check if product is found
|
|
94
|
+
throw new CommercetoolsError<GeneralError>({
|
|
95
|
+
code: "General",
|
|
96
|
+
message: sku
|
|
97
|
+
? `A product containing a variant with SKU '${sku}' not found.`
|
|
98
|
+
: `A product with ID '${productId}' not found.`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Find matching variant
|
|
103
|
+
const variant: ProductVariant | undefined = [
|
|
104
|
+
product.masterData.current.masterVariant,
|
|
105
|
+
...product.masterData.current.variants,
|
|
106
|
+
].find((x) => {
|
|
107
|
+
if (sku) return x.sku === sku;
|
|
108
|
+
if (variantId) return x.id === variantId;
|
|
109
|
+
return false;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!variant) {
|
|
113
|
+
// Check if variant is found
|
|
114
|
+
throw new CommercetoolsError<GeneralError>({
|
|
115
|
+
code: "General",
|
|
116
|
+
message: sku
|
|
117
|
+
? `A variant with SKU '${sku}' for product '${product.id}' not found.`
|
|
118
|
+
: `A variant with ID '${variantId}' for product '${product.id}' not found.`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const alreadyAdded = resource.lineItems.some(
|
|
123
|
+
(x) => x.productId === product?.id && x.variant.id === variant?.id,
|
|
124
|
+
);
|
|
125
|
+
if (alreadyAdded) {
|
|
126
|
+
// increase quantity and update total price
|
|
127
|
+
resource.lineItems.forEach((x) => {
|
|
128
|
+
if (x.productId === product?.id && x.variant.id === variant?.id) {
|
|
129
|
+
x.quantity += quantity;
|
|
130
|
+
x.totalPrice.centAmount = calculateLineItemTotalPrice(x);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
// add line item
|
|
135
|
+
if (!variant.prices?.length) {
|
|
136
|
+
throw new CommercetoolsError<GeneralError>({
|
|
137
|
+
code: "General",
|
|
138
|
+
message: `A product with ID '${productId}' doesn't have any prices.`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const currency = resource.totalPrice.currencyCode;
|
|
143
|
+
|
|
144
|
+
const price = selectPrice({
|
|
145
|
+
prices: variant.prices,
|
|
146
|
+
currency,
|
|
147
|
+
country: resource.country,
|
|
148
|
+
});
|
|
149
|
+
if (!price) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`No valid price found for ${productId} for country ${resource.country} and currency ${currency}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
resource.lineItems.push({
|
|
155
|
+
id: uuidv4(),
|
|
156
|
+
productId: product.id,
|
|
157
|
+
productKey: product.key,
|
|
158
|
+
productSlug: product.masterData.current.slug,
|
|
159
|
+
productType: product.productType,
|
|
160
|
+
name: product.masterData.current.name,
|
|
161
|
+
variant,
|
|
162
|
+
price: price,
|
|
163
|
+
taxedPricePortions: [],
|
|
164
|
+
perMethodTaxRate: [],
|
|
165
|
+
totalPrice: {
|
|
166
|
+
...price.value,
|
|
167
|
+
type: "centPrecision",
|
|
168
|
+
centAmount: price.value.centAmount * quantity,
|
|
169
|
+
},
|
|
170
|
+
quantity,
|
|
171
|
+
discountedPricePerQuantity: [],
|
|
172
|
+
lineItemMode: "Standard",
|
|
173
|
+
priceMode: "Platform",
|
|
174
|
+
state: [],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Update cart total price
|
|
179
|
+
resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
changeLineItemQuantity(
|
|
183
|
+
context: RepositoryContext,
|
|
184
|
+
resource: Writable<Cart>,
|
|
185
|
+
{ lineItemId, lineItemKey, quantity }: CartChangeLineItemQuantityAction,
|
|
186
|
+
) {
|
|
187
|
+
let lineItem: Writable<LineItem> | undefined;
|
|
188
|
+
|
|
189
|
+
if (lineItemId) {
|
|
190
|
+
lineItem = resource.lineItems.find((x) => x.id === lineItemId);
|
|
191
|
+
if (!lineItem) {
|
|
192
|
+
throw new CommercetoolsError<GeneralError>({
|
|
193
|
+
code: "General",
|
|
194
|
+
message: `A line item with ID '${lineItemId}' not found.`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} else if (lineItemKey) {
|
|
198
|
+
lineItem = resource.lineItems.find((x) => x.id === lineItemId);
|
|
199
|
+
if (!lineItem) {
|
|
200
|
+
throw new CommercetoolsError<GeneralError>({
|
|
201
|
+
code: "General",
|
|
202
|
+
message: `A line item with Key '${lineItemKey}' not found.`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
throw new CommercetoolsError<GeneralError>({
|
|
207
|
+
code: "General",
|
|
208
|
+
message: `Either lineItemid or lineItemKey needs to be provided.`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (quantity === 0) {
|
|
213
|
+
// delete line item
|
|
214
|
+
resource.lineItems = resource.lineItems.filter(
|
|
215
|
+
(x) => x.id !== lineItemId,
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
resource.lineItems.forEach((x) => {
|
|
219
|
+
if (x.id === lineItemId && quantity) {
|
|
220
|
+
x.quantity = quantity;
|
|
221
|
+
x.totalPrice.centAmount = calculateLineItemTotalPrice(x);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Update cart total price
|
|
227
|
+
resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
recalculate() {
|
|
231
|
+
// Dummy action when triggering a recalculation of the cart
|
|
232
|
+
//
|
|
233
|
+
// From commercetools documentation:
|
|
234
|
+
// This update action does not set any Cart field in particular,
|
|
235
|
+
// but it triggers several Cart updates to bring prices and discounts to the latest state.
|
|
236
|
+
// Those can become stale over time when no Cart updates have been performed for a while
|
|
237
|
+
// and prices on related Products have changed in the meanwhile.
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
removeDiscountCode(
|
|
241
|
+
context: RepositoryContext,
|
|
242
|
+
resource: Writable<Cart>,
|
|
243
|
+
{ discountCode }: CartRemoveDiscountCodeAction,
|
|
244
|
+
) {
|
|
245
|
+
resource.discountCodes = resource.discountCodes.filter(
|
|
246
|
+
(code) => code.discountCode.id !== discountCode.id,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
removeLineItem(
|
|
251
|
+
context: RepositoryContext,
|
|
252
|
+
resource: Writable<Cart>,
|
|
253
|
+
{ lineItemId, quantity }: CartRemoveLineItemAction,
|
|
254
|
+
) {
|
|
255
|
+
const lineItem = resource.lineItems.find((x) => x.id === lineItemId);
|
|
256
|
+
if (!lineItem) {
|
|
257
|
+
// Check if product is found
|
|
258
|
+
throw new CommercetoolsError<GeneralError>({
|
|
259
|
+
code: "General",
|
|
260
|
+
message: `A line item with ID '${lineItemId}' not found.`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const shouldDelete = !quantity || quantity >= lineItem.quantity;
|
|
265
|
+
if (shouldDelete) {
|
|
266
|
+
// delete line item
|
|
267
|
+
resource.lineItems = resource.lineItems.filter(
|
|
268
|
+
(x) => x.id !== lineItemId,
|
|
269
|
+
);
|
|
270
|
+
} else {
|
|
271
|
+
// decrease quantity and update total price
|
|
272
|
+
resource.lineItems.forEach((x) => {
|
|
273
|
+
if (x.id === lineItemId && quantity) {
|
|
274
|
+
x.quantity -= quantity;
|
|
275
|
+
x.totalPrice.centAmount = calculateLineItemTotalPrice(x);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Update cart total price
|
|
281
|
+
resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
setBillingAddress(
|
|
285
|
+
context: RepositoryContext,
|
|
286
|
+
resource: Writable<Cart>,
|
|
287
|
+
{ address }: CartSetBillingAddressAction,
|
|
288
|
+
) {
|
|
289
|
+
resource.billingAddress = createAddress(
|
|
290
|
+
address,
|
|
291
|
+
context.projectKey,
|
|
292
|
+
this._storage,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
setCountry(
|
|
297
|
+
context: RepositoryContext,
|
|
298
|
+
resource: Writable<Cart>,
|
|
299
|
+
{ country }: CartSetCountryAction,
|
|
300
|
+
) {
|
|
301
|
+
resource.country = country;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
setCustomerEmail(
|
|
305
|
+
context: RepositoryContext,
|
|
306
|
+
resource: Writable<Cart>,
|
|
307
|
+
{ email }: CartSetCustomerEmailAction,
|
|
308
|
+
) {
|
|
309
|
+
resource.customerEmail = email;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
setCustomField(
|
|
313
|
+
context: RepositoryContext,
|
|
314
|
+
resource: Cart,
|
|
315
|
+
{ name, value }: CartSetCustomFieldAction,
|
|
316
|
+
) {
|
|
317
|
+
if (!resource.custom) {
|
|
318
|
+
throw new Error("Resource has no custom field");
|
|
319
|
+
}
|
|
320
|
+
resource.custom.fields[name] = value;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
setCustomShippingMethod(
|
|
324
|
+
context: RepositoryContext,
|
|
325
|
+
resource: Writable<Cart>,
|
|
326
|
+
{
|
|
327
|
+
shippingMethodName,
|
|
328
|
+
shippingRate,
|
|
329
|
+
taxCategory,
|
|
330
|
+
externalTaxRate,
|
|
331
|
+
}: CartSetCustomShippingMethodAction,
|
|
332
|
+
) {
|
|
333
|
+
if (externalTaxRate) {
|
|
334
|
+
throw new Error("External tax rate is not supported");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const tax = taxCategory
|
|
338
|
+
? this._storage.getByResourceIdentifier<"tax-category">(
|
|
339
|
+
context.projectKey,
|
|
340
|
+
taxCategory,
|
|
341
|
+
)
|
|
342
|
+
: undefined;
|
|
343
|
+
|
|
344
|
+
resource.shippingInfo = {
|
|
345
|
+
shippingMethodName,
|
|
346
|
+
price: createCentPrecisionMoney(shippingRate.price),
|
|
347
|
+
shippingRate: {
|
|
348
|
+
price: createTypedMoney(shippingRate.price),
|
|
349
|
+
tiers: [],
|
|
350
|
+
},
|
|
351
|
+
taxCategory: tax
|
|
352
|
+
? {
|
|
353
|
+
typeId: "tax-category",
|
|
354
|
+
id: tax?.id,
|
|
355
|
+
}
|
|
356
|
+
: undefined,
|
|
357
|
+
shippingMethodState: "MatchesCart",
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
setCustomType(
|
|
362
|
+
context: RepositoryContext,
|
|
363
|
+
resource: Writable<Cart>,
|
|
364
|
+
{ type, fields }: CartSetCustomTypeAction,
|
|
365
|
+
) {
|
|
366
|
+
if (!type) {
|
|
367
|
+
resource.custom = undefined;
|
|
368
|
+
} else {
|
|
369
|
+
const resolvedType = this._storage.getByResourceIdentifier(
|
|
370
|
+
context.projectKey,
|
|
371
|
+
type,
|
|
372
|
+
);
|
|
373
|
+
if (!resolvedType) {
|
|
374
|
+
throw new Error(`Type ${type} not found`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
resource.custom = {
|
|
378
|
+
type: {
|
|
379
|
+
typeId: "type",
|
|
380
|
+
id: resolvedType.id,
|
|
381
|
+
},
|
|
382
|
+
fields: fields || {},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
setDirectDiscounts(
|
|
388
|
+
context: RepositoryContext,
|
|
389
|
+
resource: Writable<Cart>,
|
|
390
|
+
{ discounts }: CartSetDirectDiscountsAction,
|
|
391
|
+
) {
|
|
392
|
+
// Doesn't apply any discounts logic, just sets the directDiscounts field
|
|
393
|
+
resource.directDiscounts = discounts.map((discount) => ({
|
|
394
|
+
...discount,
|
|
395
|
+
id: uuidv4(),
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
setLineItemShippingDetails(
|
|
400
|
+
context: RepositoryContext,
|
|
401
|
+
resource: Writable<Cart>,
|
|
402
|
+
{
|
|
403
|
+
action,
|
|
404
|
+
shippingDetails,
|
|
405
|
+
lineItemId,
|
|
406
|
+
lineItemKey,
|
|
407
|
+
}: CartSetLineItemShippingDetailsAction,
|
|
408
|
+
) {
|
|
409
|
+
const lineItem = resource.lineItems.find(
|
|
410
|
+
(x) =>
|
|
411
|
+
(lineItemId && x.id === lineItemId) ||
|
|
412
|
+
(lineItemKey && x.key === lineItemKey),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
if (!lineItem) {
|
|
416
|
+
// Check if line item is found
|
|
417
|
+
throw new CommercetoolsError<GeneralError>({
|
|
418
|
+
code: "General",
|
|
419
|
+
message: lineItemKey
|
|
420
|
+
? `A line item with key '${lineItemKey}' not found.`
|
|
421
|
+
: `A line item with ID '${lineItemId}' not found.`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
lineItem.shippingDetails = {
|
|
426
|
+
...shippingDetails,
|
|
427
|
+
valid: true,
|
|
428
|
+
} as ItemShippingDetails;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
setLocale(
|
|
432
|
+
context: RepositoryContext,
|
|
433
|
+
resource: Writable<Cart>,
|
|
434
|
+
{ locale }: CartSetLocaleAction,
|
|
435
|
+
) {
|
|
436
|
+
resource.locale = locale;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
setShippingAddress(
|
|
440
|
+
context: RepositoryContext,
|
|
441
|
+
resource: Writable<Cart>,
|
|
442
|
+
{ address }: CartSetShippingAddressAction,
|
|
443
|
+
) {
|
|
444
|
+
if (!address) {
|
|
445
|
+
resource.shippingAddress = undefined;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let custom: CustomFields | undefined = undefined;
|
|
450
|
+
if ((address as Address & AddressDraft).custom) {
|
|
451
|
+
custom = createCustomFields(
|
|
452
|
+
(address as Address & AddressDraft).custom,
|
|
453
|
+
context.projectKey,
|
|
454
|
+
this._storage,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
resource.shippingAddress = {
|
|
459
|
+
...address,
|
|
460
|
+
custom: custom,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
setShippingMethod(
|
|
465
|
+
context: RepositoryContext,
|
|
466
|
+
resource: Writable<Cart>,
|
|
467
|
+
{ shippingMethod }: CartSetShippingMethodAction,
|
|
468
|
+
) {
|
|
469
|
+
if (shippingMethod) {
|
|
470
|
+
const method = this._storage.getByResourceIdentifier<"shipping-method">(
|
|
471
|
+
context.projectKey,
|
|
472
|
+
shippingMethod,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// Based on the address we should select a shipping zone and
|
|
476
|
+
// use that to define the price.
|
|
477
|
+
// @ts-ignore
|
|
478
|
+
resource.shippingInfo = {
|
|
479
|
+
shippingMethod: {
|
|
480
|
+
typeId: "shipping-method",
|
|
481
|
+
id: method.id,
|
|
482
|
+
},
|
|
483
|
+
shippingMethodName: method.name,
|
|
484
|
+
};
|
|
485
|
+
} else {
|
|
486
|
+
resource.shippingInfo = undefined;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Cart, LineItem, Price } from "@commercetools/platform-sdk";
|
|
2
|
+
|
|
3
|
+
export const selectPrice = ({
|
|
4
|
+
prices,
|
|
5
|
+
currency,
|
|
6
|
+
country,
|
|
7
|
+
}: {
|
|
8
|
+
prices: Price[] | undefined;
|
|
9
|
+
currency: string;
|
|
10
|
+
country: string | undefined;
|
|
11
|
+
}): Price | undefined => {
|
|
12
|
+
if (!prices) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Quick-and-dirty way of selecting price based on the given currency and country.
|
|
17
|
+
// Can be improved later to give more priority to exact matches over
|
|
18
|
+
// 'all country' matches, and include customer groups in the mix as well
|
|
19
|
+
return prices.find((price) => {
|
|
20
|
+
const countryMatch = !price.country || price.country === country;
|
|
21
|
+
const currencyMatch = price.value.currencyCode === currency;
|
|
22
|
+
return countryMatch && currencyMatch;
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const calculateLineItemTotalPrice = (lineItem: LineItem): number =>
|
|
27
|
+
lineItem.price!.value.centAmount * lineItem.quantity;
|
|
28
|
+
|
|
29
|
+
export const calculateCartTotalPrice = (cart: Cart): number =>
|
|
30
|
+
cart.lineItems.reduce((cur, item) => cur + item.totalPrice.centAmount, 0);
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Cart,
|
|
3
|
+
type CartDraft,
|
|
4
|
+
type GeneralError,
|
|
5
|
+
type LineItem,
|
|
6
|
+
type LineItemDraft,
|
|
7
|
+
type Product,
|
|
8
|
+
type ProductPagedQueryResponse,
|
|
9
|
+
} from "@commercetools/platform-sdk";
|
|
10
|
+
import { v4 as uuidv4 } from "uuid";
|
|
11
|
+
import { CommercetoolsError } from "~src/exceptions";
|
|
12
|
+
import { getBaseResourceProperties } from "~src/helpers";
|
|
13
|
+
import { AbstractStorage } from "~src/storage/abstract";
|
|
14
|
+
import type { Writable } from "~src/types";
|
|
15
|
+
import {
|
|
16
|
+
AbstractResourceRepository,
|
|
17
|
+
type RepositoryContext,
|
|
18
|
+
} from "../abstract";
|
|
19
|
+
import { createAddress, createCustomFields } from "../helpers";
|
|
20
|
+
import { CartUpdateHandler } from "./actions";
|
|
21
|
+
import { calculateCartTotalPrice, selectPrice } from "./helpers";
|
|
22
|
+
|
|
23
|
+
export class CartRepository extends AbstractResourceRepository<"cart"> {
|
|
24
|
+
constructor(storage: AbstractStorage) {
|
|
25
|
+
super("cart", storage);
|
|
26
|
+
this.actions = new CartUpdateHandler(this._storage);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
create(context: RepositoryContext, draft: CartDraft): Cart {
|
|
30
|
+
const lineItems =
|
|
31
|
+
draft.lineItems?.map((draftLineItem) =>
|
|
32
|
+
this.draftLineItemtoLineItem(
|
|
33
|
+
context.projectKey,
|
|
34
|
+
draftLineItem,
|
|
35
|
+
draft.currency,
|
|
36
|
+
draft.country,
|
|
37
|
+
),
|
|
38
|
+
) ?? [];
|
|
39
|
+
|
|
40
|
+
const resource: Writable<Cart> = {
|
|
41
|
+
...getBaseResourceProperties(),
|
|
42
|
+
cartState: "Active",
|
|
43
|
+
country: draft.country,
|
|
44
|
+
customLineItems: [],
|
|
45
|
+
directDiscounts: [],
|
|
46
|
+
discountCodes: [],
|
|
47
|
+
inventoryMode: "None",
|
|
48
|
+
itemShippingAddresses: [],
|
|
49
|
+
lineItems,
|
|
50
|
+
locale: draft.locale,
|
|
51
|
+
taxCalculationMode: draft.taxCalculationMode ?? "LineItemLevel",
|
|
52
|
+
taxMode: draft.taxMode ?? "Platform",
|
|
53
|
+
taxRoundingMode: draft.taxRoundingMode ?? "HalfEven",
|
|
54
|
+
totalPrice: {
|
|
55
|
+
type: "centPrecision",
|
|
56
|
+
centAmount: 0,
|
|
57
|
+
currencyCode: draft.currency,
|
|
58
|
+
fractionDigits: 0,
|
|
59
|
+
},
|
|
60
|
+
shippingMode: "Single",
|
|
61
|
+
shippingAddress: createAddress(
|
|
62
|
+
draft.shippingAddress,
|
|
63
|
+
context.projectKey,
|
|
64
|
+
this._storage,
|
|
65
|
+
),
|
|
66
|
+
shipping: [],
|
|
67
|
+
origin: draft.origin ?? "Customer",
|
|
68
|
+
refusedGifts: [],
|
|
69
|
+
custom: createCustomFields(
|
|
70
|
+
draft.custom,
|
|
71
|
+
context.projectKey,
|
|
72
|
+
this._storage,
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
|
|
76
|
+
|
|
77
|
+
return this.saveNew(context, resource);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getActiveCart(projectKey: string): Cart | undefined {
|
|
81
|
+
// Get first active cart
|
|
82
|
+
const results = this._storage.query(projectKey, this.getTypeId(), {
|
|
83
|
+
where: [`cartState="Active"`],
|
|
84
|
+
});
|
|
85
|
+
if (results.count > 0) {
|
|
86
|
+
return results.results[0] as Cart;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
draftLineItemtoLineItem = (
|
|
93
|
+
projectKey: string,
|
|
94
|
+
draftLineItem: LineItemDraft,
|
|
95
|
+
currency: string,
|
|
96
|
+
country: string | undefined,
|
|
97
|
+
): LineItem => {
|
|
98
|
+
const { productId, quantity, variantId, sku } = draftLineItem;
|
|
99
|
+
|
|
100
|
+
let product: Product | null = null;
|
|
101
|
+
|
|
102
|
+
if (productId && variantId) {
|
|
103
|
+
// Fetch product and variant by ID
|
|
104
|
+
product = this._storage.get(projectKey, "product", productId, {});
|
|
105
|
+
} else if (sku) {
|
|
106
|
+
// Fetch product and variant by SKU
|
|
107
|
+
const items = this._storage.query(projectKey, "product", {
|
|
108
|
+
where: [
|
|
109
|
+
`masterData(current(masterVariant(sku="${sku}"))) or masterData(current(variants(sku="${sku}")))`,
|
|
110
|
+
],
|
|
111
|
+
}) as ProductPagedQueryResponse;
|
|
112
|
+
|
|
113
|
+
if (items.count === 1) {
|
|
114
|
+
product = items.results[0];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!product) {
|
|
119
|
+
// Check if product is found
|
|
120
|
+
throw new CommercetoolsError<GeneralError>({
|
|
121
|
+
code: "General",
|
|
122
|
+
message: sku
|
|
123
|
+
? `A product containing a variant with SKU '${sku}' not found.`
|
|
124
|
+
: `A product with ID '${productId}' not found.`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Find matching variant
|
|
129
|
+
const variant = [
|
|
130
|
+
product.masterData.current.masterVariant,
|
|
131
|
+
...product.masterData.current.variants,
|
|
132
|
+
].find((x) => {
|
|
133
|
+
if (sku) return x.sku === sku;
|
|
134
|
+
if (variantId) return x.id === variantId;
|
|
135
|
+
return false;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!variant) {
|
|
139
|
+
// Check if variant is found
|
|
140
|
+
throw new Error(
|
|
141
|
+
sku
|
|
142
|
+
? `A variant with SKU '${sku}' for product '${product.id}' not found.`
|
|
143
|
+
: `A variant with ID '${variantId}' for product '${product.id}' not found.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const quant = quantity ?? 1;
|
|
148
|
+
|
|
149
|
+
const price = selectPrice({ prices: variant.prices, currency, country });
|
|
150
|
+
if (!price) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`No valid price found for ${productId} for country ${country} and currency ${currency}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
id: uuidv4(),
|
|
158
|
+
productId: product.id,
|
|
159
|
+
productKey: product.key,
|
|
160
|
+
productSlug: product.masterData.current.slug,
|
|
161
|
+
productType: product.productType,
|
|
162
|
+
name: product.masterData.current.name,
|
|
163
|
+
variant,
|
|
164
|
+
price: price,
|
|
165
|
+
totalPrice: {
|
|
166
|
+
type: "centPrecision",
|
|
167
|
+
currencyCode: price.value.currencyCode,
|
|
168
|
+
fractionDigits: price.value.fractionDigits,
|
|
169
|
+
centAmount: price.value.centAmount * quant,
|
|
170
|
+
},
|
|
171
|
+
taxedPricePortions: [],
|
|
172
|
+
perMethodTaxRate: [],
|
|
173
|
+
quantity: quant,
|
|
174
|
+
discountedPricePerQuantity: [],
|
|
175
|
+
lineItemMode: "Standard",
|
|
176
|
+
priceMode: "Platform",
|
|
177
|
+
state: [],
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
}
|