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