@labdigital/commercetools-mock 2.17.0 → 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.
Files changed (178) hide show
  1. package/dist/index.cjs +4219 -3989
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +268 -415
  4. package/dist/index.d.ts +268 -415
  5. package/dist/index.js +4219 -3989
  6. package/dist/index.js.map +1 -1
  7. package/package.json +44 -46
  8. package/src/constants.ts +2 -2
  9. package/src/ctMock.test.ts +11 -11
  10. package/src/ctMock.ts +141 -127
  11. package/src/deprecation.ts +8 -0
  12. package/src/exceptions.ts +17 -15
  13. package/src/helpers.ts +32 -32
  14. package/src/index.test.ts +128 -128
  15. package/src/index.ts +3 -3
  16. package/src/lib/expandParser.ts +13 -13
  17. package/src/lib/haversine.test.ts +9 -9
  18. package/src/lib/haversine.ts +11 -11
  19. package/src/lib/masking.ts +11 -11
  20. package/src/lib/parser.ts +2 -2
  21. package/src/lib/password.ts +23 -3
  22. package/src/lib/predicateParser.test.ts +185 -183
  23. package/src/lib/predicateParser.ts +234 -234
  24. package/src/lib/projectionSearchFilter.test.ts +103 -101
  25. package/src/lib/projectionSearchFilter.ts +152 -150
  26. package/src/lib/proxy.ts +5 -5
  27. package/src/oauth/errors.ts +4 -4
  28. package/src/oauth/helpers.ts +6 -6
  29. package/src/oauth/server.test.ts +110 -67
  30. package/src/oauth/server.ts +161 -141
  31. package/src/oauth/store.ts +49 -44
  32. package/src/priceSelector.test.ts +35 -35
  33. package/src/priceSelector.ts +30 -30
  34. package/src/product-projection-search.ts +136 -134
  35. package/src/projectAPI.test.ts +7 -7
  36. package/src/projectAPI.ts +24 -22
  37. package/src/repositories/abstract.ts +168 -116
  38. package/src/repositories/associate-role.ts +90 -77
  39. package/src/repositories/attribute-group.ts +51 -40
  40. package/src/repositories/business-unit.ts +168 -148
  41. package/src/repositories/cart/actions.ts +489 -0
  42. package/src/repositories/cart/helpers.ts +30 -0
  43. package/src/repositories/cart/index.ts +180 -0
  44. package/src/repositories/cart-discount/actions.ts +148 -0
  45. package/src/repositories/cart-discount/index.ts +86 -0
  46. package/src/repositories/category/actions.ts +231 -0
  47. package/src/repositories/category/index.ts +52 -0
  48. package/src/repositories/channel.ts +88 -90
  49. package/src/repositories/custom-object.ts +46 -45
  50. package/src/repositories/customer/actions.ts +165 -0
  51. package/src/repositories/customer/index.ts +79 -0
  52. package/src/repositories/customer-group.ts +66 -55
  53. package/src/repositories/discount-code/actions.ts +149 -0
  54. package/src/repositories/discount-code/index.ts +50 -0
  55. package/src/repositories/errors.ts +10 -10
  56. package/src/repositories/extension.ts +64 -62
  57. package/src/repositories/helpers.ts +117 -118
  58. package/src/repositories/index.ts +80 -79
  59. package/src/repositories/inventory-entry/actions.ts +84 -0
  60. package/src/repositories/inventory-entry/index.ts +44 -0
  61. package/src/repositories/my-customer.ts +114 -0
  62. package/src/repositories/my-order.ts +8 -8
  63. package/src/repositories/order/actions.ts +281 -0
  64. package/src/repositories/{order.test.ts → order/index.test.ts} +77 -77
  65. package/src/repositories/order/index.ts +260 -0
  66. package/src/repositories/order-edit.ts +10 -23
  67. package/src/repositories/payment/actions.ts +305 -0
  68. package/src/repositories/payment/helpers.ts +17 -0
  69. package/src/repositories/payment/index.ts +56 -0
  70. package/src/repositories/product/actions.ts +943 -0
  71. package/src/repositories/product/helpers.ts +98 -0
  72. package/src/repositories/product/index.ts +130 -0
  73. package/src/repositories/product-discount.ts +127 -117
  74. package/src/repositories/product-projection.ts +56 -62
  75. package/src/repositories/product-selection.ts +31 -28
  76. package/src/repositories/product-type.ts +136 -134
  77. package/src/repositories/project.ts +133 -118
  78. package/src/repositories/quote-request.ts +7 -19
  79. package/src/repositories/quote.ts +7 -22
  80. package/src/repositories/review.ts +13 -26
  81. package/src/repositories/shipping-method/actions.ts +198 -0
  82. package/src/repositories/shipping-method/helpers.ts +10 -0
  83. package/src/repositories/shipping-method/index.ts +138 -0
  84. package/src/repositories/shopping-list/actions.ts +295 -0
  85. package/src/repositories/shopping-list/index.ts +122 -0
  86. package/src/repositories/staged-quote.ts +7 -20
  87. package/src/repositories/standalone-price.ts +57 -44
  88. package/src/repositories/state.ts +113 -68
  89. package/src/repositories/store.ts +106 -94
  90. package/src/repositories/subscription.ts +46 -22
  91. package/src/repositories/tax-category/actions.ts +94 -0
  92. package/src/repositories/tax-category/helpers.ts +8 -0
  93. package/src/repositories/tax-category/index.ts +25 -0
  94. package/src/repositories/type/actions.ts +162 -0
  95. package/src/repositories/type/index.ts +24 -0
  96. package/src/repositories/zone.ts +62 -58
  97. package/src/server.ts +9 -9
  98. package/src/services/abstract.ts +75 -72
  99. package/src/services/associate-roles.test.ts +27 -27
  100. package/src/services/associate-roles.ts +7 -7
  101. package/src/services/attribute-group.ts +7 -7
  102. package/src/services/business-units.test.ts +28 -28
  103. package/src/services/business-units.ts +7 -7
  104. package/src/services/cart-discount.test.ts +199 -199
  105. package/src/services/cart-discount.ts +7 -7
  106. package/src/services/cart.test.ts +261 -261
  107. package/src/services/cart.ts +22 -21
  108. package/src/services/category.test.ts +121 -121
  109. package/src/services/category.ts +7 -7
  110. package/src/services/channel.ts +7 -7
  111. package/src/services/custom-object.test.ts +130 -130
  112. package/src/services/custom-object.ts +34 -31
  113. package/src/services/customer-group.ts +7 -7
  114. package/src/services/customer.test.ts +205 -205
  115. package/src/services/customer.ts +31 -29
  116. package/src/services/discount-code.ts +7 -7
  117. package/src/services/extension.ts +7 -7
  118. package/src/services/index.ts +85 -81
  119. package/src/services/inventory-entry.test.ts +106 -106
  120. package/src/services/inventory-entry.ts +7 -7
  121. package/src/services/my-cart.test.ts +56 -56
  122. package/src/services/my-cart.ts +20 -20
  123. package/src/services/my-customer.test.ts +155 -104
  124. package/src/services/my-customer.ts +61 -75
  125. package/src/services/my-order.ts +16 -16
  126. package/src/services/my-payment.test.ts +40 -40
  127. package/src/services/my-payment.ts +7 -7
  128. package/src/services/my-shopping-list.ts +7 -7
  129. package/src/services/order.test.ts +243 -243
  130. package/src/services/order.ts +23 -18
  131. package/src/services/payment.test.ts +40 -40
  132. package/src/services/payment.ts +7 -7
  133. package/src/services/product-discount.ts +7 -7
  134. package/src/services/product-projection.test.ts +190 -190
  135. package/src/services/product-projection.ts +34 -32
  136. package/src/services/product-selection.test.ts +19 -19
  137. package/src/services/product-selection.ts +7 -7
  138. package/src/services/product-type.test.ts +38 -38
  139. package/src/services/product-type.ts +7 -7
  140. package/src/services/product.test.ts +658 -656
  141. package/src/services/product.ts +7 -7
  142. package/src/services/project.test.ts +24 -24
  143. package/src/services/project.ts +17 -17
  144. package/src/services/reviews.ts +7 -7
  145. package/src/services/shipping-method.test.ts +78 -78
  146. package/src/services/shipping-method.ts +16 -16
  147. package/src/services/shopping-list.test.ts +170 -170
  148. package/src/services/shopping-list.ts +7 -7
  149. package/src/services/standalone-price.test.ts +112 -112
  150. package/src/services/standalone-price.ts +7 -7
  151. package/src/services/state.test.ts +30 -30
  152. package/src/services/state.ts +7 -7
  153. package/src/services/store.test.ts +40 -40
  154. package/src/services/store.ts +7 -7
  155. package/src/services/subscription.ts +7 -7
  156. package/src/services/tax-category.test.ts +43 -43
  157. package/src/services/tax-category.ts +7 -7
  158. package/src/services/type.ts +7 -7
  159. package/src/services/zone.ts +7 -7
  160. package/src/shippingCalculator.test.ts +43 -43
  161. package/src/shippingCalculator.ts +23 -23
  162. package/src/storage/abstract.ts +36 -34
  163. package/src/storage/in-memory.ts +237 -233
  164. package/src/storage/index.ts +2 -2
  165. package/src/types.ts +91 -91
  166. package/src/repositories/cart-discount.ts +0 -219
  167. package/src/repositories/cart.ts +0 -659
  168. package/src/repositories/category.ts +0 -256
  169. package/src/repositories/customer.ts +0 -228
  170. package/src/repositories/discount-code.ts +0 -181
  171. package/src/repositories/inventory-entry.ts +0 -109
  172. package/src/repositories/order.ts +0 -514
  173. package/src/repositories/payment.ts +0 -342
  174. package/src/repositories/product.ts +0 -1106
  175. package/src/repositories/shipping-method.ts +0 -312
  176. package/src/repositories/shopping-list.ts +0 -392
  177. package/src/repositories/tax-category.ts +0 -111
  178. 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
+ }