@labdigital/commercetools-mock 2.7.0 → 2.9.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@labdigital/commercetools-mock",
3
3
  "author": "Michael van Tellingen",
4
- "version": "2.7.0",
4
+ "version": "2.9.0",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.js",
@@ -46,7 +46,7 @@
46
46
  "devDependencies": {
47
47
  "@changesets/changelog-github": "^0.4.8",
48
48
  "@changesets/cli": "^2.26.2",
49
- "@commercetools/platform-sdk": "4.11.0",
49
+ "@commercetools/platform-sdk": "6.0.0",
50
50
  "@types/basic-auth": "^1.1.3",
51
51
  "@types/body-parser": "^1.19.2",
52
52
  "@types/deep-equal": "^1.0.1",
package/src/ctMock.ts CHANGED
@@ -264,6 +264,10 @@ export class CommercetoolsMock {
264
264
  )
265
265
  }
266
266
 
267
+ public mswServer() {
268
+ return this._mswServer
269
+ }
270
+
267
271
  private startServer() {
268
272
  // Check if there are any other servers running
269
273
  if (_globalListeners.length > 0) {
@@ -16,7 +16,67 @@ describe('Predicate filter', () => {
16
16
  stringProperty: 'foobar',
17
17
  booleanProperty: true,
18
18
  },
19
+ array: [
20
+ {
21
+ numberProperty: 1234,
22
+ stringProperty: 'foo',
23
+ objectProperty: {
24
+ stringProperty: 'foo',
25
+ booleanProperty: true,
26
+ },
27
+ },
28
+ {
29
+ numberProperty: 2345,
30
+ stringProperty: 'bar',
31
+ objectProperty: {
32
+ stringProperty: 'bar',
33
+ booleanProperty: false,
34
+ },
35
+ },
36
+ ],
19
37
  },
38
+ array: [
39
+ {
40
+ nestedArray: [
41
+ {
42
+ stringProperty: 'foo',
43
+ nested: [
44
+ {
45
+ stringProperty: 'foo',
46
+ },
47
+ ],
48
+ },
49
+ {
50
+ stringProperty: 'bar',
51
+ nested: [
52
+ {
53
+ stringProperty: 'bar',
54
+ },
55
+ ],
56
+ },
57
+ ],
58
+ },
59
+ {
60
+ nestedArray: [
61
+ {
62
+ stringProperty: 'foo-2',
63
+ nested: [
64
+ {
65
+ stringProperty: 'foo-2',
66
+ },
67
+ ],
68
+ },
69
+ {
70
+ stringProperty: 'bar-2',
71
+ nested: [
72
+ {
73
+ stringProperty: 'bar-2',
74
+ },
75
+ ],
76
+ },
77
+ ],
78
+ },
79
+ ],
20
80
 
21
81
  // Longitude, latitude
22
82
  geoLocation: [5.110230209615395, 52.06969591642097],
@@ -115,6 +175,30 @@ describe('Predicate filter', () => {
115
175
  ).toBeTruthy()
116
176
  })
117
177
 
178
+ test('nestedArray filters on property', async () => {
179
+ expect(match(`nested(array(stringProperty="foo"))`)).toBeTruthy()
180
+ expect(match(`nested(array(stringProperty="bar"))`)).toBeTruthy()
181
+ expect(match(`nested(array(stringProperty="foobar"))`)).toBeFalsy()
182
+
183
+ // One level deeper
184
+ expect(
185
+ match(`nested(array(objectProperty(stringProperty="foo")))`)
186
+ ).toBeTruthy()
187
+ expect(
188
+ match(`nested(array(objectProperty(stringProperty="bar")))`)
189
+ ).toBeTruthy()
190
+ expect(
191
+ match(`nested(array(objectProperty(stringProperty="foobar")))`)
192
+ ).toBeFalsy()
193
+ })
194
+
195
+ test('array filters on property', async () => {
196
+ expect(match(`array(nestedArray(stringProperty="foo")))`)).toBeTruthy()
197
+ expect(
198
+ match(`array(nestedArray(nested(stringProperty="foo"))))`)
199
+ ).toBeTruthy()
200
+ })
201
+
118
202
  test('geolocation within circle (...)', async () => {
119
203
  expect(
120
204
  match(
@@ -185,17 +269,18 @@ describe('Predicate filter', () => {
185
269
  ).toBeTruthy()
186
270
  })
187
271
 
188
- test('lexer confusion', async () => {
189
- expect(() => match(`orSomething="foobar"`)).toThrow(PredicateError)
190
- expect(() => match(`orSomething="foobar"`)).toThrow(
191
- "The field 'orSomething' does not exist."
192
- )
193
-
194
- expect(() => match(`andSomething="foobar"`)).toThrow(PredicateError)
195
- expect(() => match(`andSomething="foobar"`)).toThrow(
196
- "The field 'andSomething' does not exist."
197
- )
198
- })
272
+ // TODO: disabled for now, see remark in predicateParser.ts in resolveValue
273
+ // test('lexer confusion', async () => {
274
+ // expect(() => match(`orSomething="foobar"`)).toThrow(PredicateError)
275
+ // expect(() => match(`orSomething="foobar"`)).toThrow(
276
+ // "The field 'orSomething' does not exist."
277
+ // )
278
+
279
+ // expect(() => match(`andSomething="foobar"`)).toThrow(PredicateError)
280
+ // expect(() => match(`andSomething="foobar"`)).toThrow(
281
+ // "The field 'andSomething' does not exist."
282
+ // )
283
+ // })
199
284
 
200
285
  test('invalid predicate', async () => {
201
286
  expect(() => match(`stringProperty=nomatch`)).toThrow(PredicateError)
@@ -96,7 +96,12 @@ const resolveValue = (obj: any, val: TypeSymbol): any => {
96
96
  .filter((v) => val.value in v)
97
97
  .map((v) => v[val.value])
98
98
  }
99
- throw new PredicateError(`The field '${val.value}' does not exist.`)
99
+
100
+ // TODO: We don't really validate the shape of the object here. To actually
101
+ // match commercetools behaviour we should throw an error if the requested
102
+ // field doesn't exist (unless it's a map)
103
+ // throw new PredicateError(`The field '${val.value}' does not exist.`)
104
+ return undefined
100
105
  }
101
106
 
102
107
  return obj[val.value]
@@ -243,11 +248,21 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
243
248
  const expr = parser.parse()
244
249
  lexer.expect(')')
245
250
  return (obj: any, vars: object) => {
246
- const value = resolveValue(obj, left)
247
- if (value) {
248
- return expr(value, vars)
251
+ if (Array.isArray(obj)) {
252
+ return obj.some((item) => {
253
+ const value = resolveValue(item, left)
254
+ if (value) {
255
+ return expr(value, vars)
256
+ }
257
+ return false
258
+ })
259
+ } else {
260
+ const value = resolveValue(obj, left)
261
+ if (value) {
262
+ return expr(value, vars)
263
+ }
264
+ return false
249
265
  }
250
- return false
251
266
  }
252
267
  })
253
268
  .bp(')', 0)
@@ -256,12 +271,23 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
256
271
  validateSymbol(expr)
257
272
 
258
273
  return (obj: any, vars: VariableMap) => {
259
- const resolvedValue = resolveValue(obj, left)
260
- const resolvedSymbol = resolveSymbol(expr, vars)
261
- if (Array.isArray(resolvedValue)) {
262
- return !!resolvedValue.some((elem) => elem === resolvedSymbol)
274
+ if (Array.isArray(obj)) {
275
+ return obj.some((item) => {
276
+ const value = resolveValue(item, left)
277
+ const other = resolveSymbol(expr, vars)
278
+ if (Array.isArray(value)) {
279
+ return !!value.some((elem) => elem === other)
280
+ }
281
+ return value === other
282
+ })
283
+ } else {
284
+ const resolvedValue = resolveValue(obj, left)
285
+ const resolvedSymbol = resolveSymbol(expr, vars)
286
+ if (Array.isArray(resolvedValue)) {
287
+ return !!resolvedValue.some((elem) => elem === resolvedSymbol)
288
+ }
289
+ return resolvedValue === resolvedSymbol
263
290
  }
264
- return resolvedValue === resolvedSymbol
265
291
  }
266
292
  })
267
293
  .led('!=', 20, ({ left, bp }) => {
@@ -350,10 +376,15 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
350
376
  symbols = [expr]
351
377
  }
352
378
 
353
- const inValues = symbols.map((item: TypeSymbol) =>
379
+ // The expression can be a list of variables, like
380
+ // :value_1, :value_2, but it can also be one variable
381
+ // containing a list, like :values.
382
+ // So to support both we just flatten the list.
383
+ const inValues = symbols.flatMap((item: TypeSymbol) =>
354
384
  resolveSymbol(item, vars)
355
385
  )
356
- return inValues.includes(resolveValue(obj, left))
386
+ const value = resolveValue(obj, left)
387
+ return inValues.includes(value)
357
388
  }
358
389
  })
359
390
  .led('MATCHES_IGNORE_CASE', 20, ({ left, bp }) => {
@@ -24,7 +24,7 @@ export class OAuth2Server {
24
24
  store: OAuth2Store
25
25
  private customerRepository: CustomerRepository
26
26
 
27
- constructor(options: { enabled: boolean; validate: boolean }) {
27
+ constructor(private options: { enabled: boolean; validate: boolean }) {
28
28
  this.store = new OAuth2Store(options.validate)
29
29
  }
30
30
 
@@ -53,6 +53,16 @@ export class OAuth2Server {
53
53
  }
54
54
 
55
55
  createMiddleware() {
56
+ if (!this.options.validate) {
57
+ return async (
58
+ request: Request,
59
+ response: Response,
60
+ next: NextFunction
61
+ ) => {
62
+ next()
63
+ }
64
+ }
65
+
56
66
  return async (request: Request, response: Response, next: NextFunction) => {
57
67
  const token = getBearerToken(request)
58
68
  if (!token) {
@@ -21,11 +21,13 @@ export type PriceSelector = {
21
21
  */
22
22
  export const applyPriceSelector = (
23
23
  products: ProductProjection[],
24
- selector: PriceSelector
24
+ selector: PriceSelector,
25
+ noScopedPrice: boolean = false
25
26
  ) => {
26
27
  validatePriceSelector(selector)
27
28
 
28
29
  for (const product of products) {
30
+ // Get list of all variants (master + variants)
29
31
  const variants: Writable<ProductVariant>[] = [
30
32
  product.masterVariant,
31
33
  ...(product.variants ?? []),
@@ -38,10 +40,13 @@ export const applyPriceSelector = (
38
40
  if (scopedPrices.length > 0) {
39
41
  const price = scopedPrices[0]
40
42
 
41
- variant.scopedPriceDiscounted = false
42
- variant.scopedPrice = {
43
- ...price,
44
- currentValue: price.value,
43
+ variant.price = scopedPrices[0]
44
+ if (!noScopedPrice) {
45
+ variant.scopedPriceDiscounted = false
46
+ variant.scopedPrice = {
47
+ ...price,
48
+ currentValue: price.value,
49
+ }
45
50
  }
46
51
  }
47
52
  }
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  BaseResource,
3
3
  Project,
4
+ QueryParam,
4
5
  ResourceNotFoundError,
5
6
  UpdateAction,
6
7
  } from '@commercetools/platform-sdk'
@@ -16,6 +17,9 @@ export type QueryParams = {
16
17
  where?: string[]
17
18
  offset?: number
18
19
  limit?: number
20
+
21
+ // Predicate var values. Should always start with `var.`
22
+ [key: string]: QueryParam
19
23
  }
20
24
 
21
25
  export type GetParams = {
@@ -118,10 +122,7 @@ export abstract class AbstractResourceRepository<
118
122
 
119
123
  query(context: RepositoryContext, params: QueryParams = {}) {
120
124
  const result = this._storage.query(context.projectKey, this.getTypeId(), {
121
- expand: params.expand,
122
- where: params.where,
123
- offset: params.offset,
124
- limit: params.limit,
125
+ ...params,
125
126
  })
126
127
 
127
128
  // @ts-ignore
@@ -1,31 +1,32 @@
1
- import type {
2
- Address,
3
- AddressDraft,
4
- Cart,
5
- CartAddLineItemAction,
6
- CartChangeLineItemQuantityAction,
7
- CartAddItemShippingAddressAction,
8
- CartSetLineItemShippingDetailsAction,
9
- CartDraft,
10
- CartRemoveLineItemAction,
11
- CartSetBillingAddressAction,
12
- CartSetCountryAction,
13
- CartSetCustomerEmailAction,
14
- CartSetCustomFieldAction,
15
- CartSetCustomTypeAction,
16
- CartSetLocaleAction,
17
- CartSetShippingAddressAction,
18
- CartSetShippingMethodAction,
19
- CustomFields,
20
- GeneralError,
21
- LineItem,
22
- LineItemDraft,
23
- ItemShippingDetails,
24
- Price,
25
- Product,
26
- ProductPagedQueryResponse,
27
- CartRemoveDiscountCodeAction,
28
- ProductVariant,
1
+ import {
2
+ type Address,
3
+ type AddressDraft,
4
+ type Cart,
5
+ type CartAddLineItemAction,
6
+ type CartChangeLineItemQuantityAction,
7
+ type CartAddItemShippingAddressAction,
8
+ type CartSetLineItemShippingDetailsAction,
9
+ type CartDraft,
10
+ type CartRemoveLineItemAction,
11
+ type CartSetBillingAddressAction,
12
+ type CartSetCountryAction,
13
+ type CartSetCustomerEmailAction,
14
+ type CartSetCustomFieldAction,
15
+ type CartSetCustomTypeAction,
16
+ type CartSetLocaleAction,
17
+ type CartSetShippingAddressAction,
18
+ type CartSetShippingMethodAction,
19
+ type CustomFields,
20
+ type GeneralError,
21
+ type LineItem,
22
+ type LineItemDraft,
23
+ type ItemShippingDetails,
24
+ type Price,
25
+ type Product,
26
+ type ProductPagedQueryResponse,
27
+ type CartRemoveDiscountCodeAction,
28
+ type ProductVariant,
29
+ type CartSetCustomShippingMethodAction,
29
30
  } from '@commercetools/platform-sdk'
30
31
  import { v4 as uuidv4 } from 'uuid'
31
32
  import { CommercetoolsError } from '../exceptions.js'
@@ -35,7 +36,12 @@ import {
35
36
  AbstractResourceRepository,
36
37
  type RepositoryContext,
37
38
  } from './abstract.js'
38
- import { createAddress, createCustomFields } from './helpers.js'
39
+ import {
40
+ createAddress,
41
+ createCentPrecisionMoney,
42
+ createCustomFields,
43
+ createTypedMoney,
44
+ } from './helpers.js'
39
45
 
40
46
  export class CartRepository extends AbstractResourceRepository<'cart'> {
41
47
  getTypeId() {
@@ -74,6 +80,11 @@ export class CartRepository extends AbstractResourceRepository<'cart'> {
74
80
  fractionDigits: 0,
75
81
  },
76
82
  shippingMode: 'Single',
83
+ shippingAddress: createAddress(
84
+ draft.shippingAddress,
85
+ context.projectKey,
86
+ this._storage
87
+ ),
77
88
  shipping: [],
78
89
  origin: draft.origin ?? 'Customer',
79
90
  refusedGifts: [],
@@ -335,10 +346,6 @@ export class CartRepository extends AbstractResourceRepository<'cart'> {
335
346
  shippingMethod
336
347
  )
337
348
 
338
- if (!method) {
339
- throw new Error(`Type ${shippingMethod} not found`)
340
- }
341
-
342
349
  // Based on the address we should select a shipping zone and
343
350
  // use that to define the price.
344
351
  // @ts-ignore
@@ -377,6 +384,43 @@ export class CartRepository extends AbstractResourceRepository<'cart'> {
377
384
  }
378
385
  resource.custom.fields[name] = value
379
386
  },
387
+ setCustomShippingMethod: (
388
+ context: RepositoryContext,
389
+ resource: Writable<Cart>,
390
+ {
391
+ shippingMethodName,
392
+ shippingRate,
393
+ taxCategory,
394
+ externalTaxRate,
395
+ }: CartSetCustomShippingMethodAction
396
+ ) => {
397
+ if (externalTaxRate) {
398
+ throw new Error('External tax rate is not supported')
399
+ }
400
+
401
+ const tax = taxCategory
402
+ ? this._storage.getByResourceIdentifier<'tax-category'>(
403
+ context.projectKey,
404
+ taxCategory
405
+ )
406
+ : undefined
407
+
408
+ resource.shippingInfo = {
409
+ shippingMethodName,
410
+ price: createCentPrecisionMoney(shippingRate.price),
411
+ shippingRate: {
412
+ price: createTypedMoney(shippingRate.price),
413
+ tiers: [],
414
+ },
415
+ taxCategory: tax
416
+ ? {
417
+ typeId: 'tax-category',
418
+ id: tax?.id,
419
+ }
420
+ : undefined,
421
+ shippingMethodState: 'MatchesCart',
422
+ }
423
+ },
380
424
  setCustomType: (
381
425
  context: RepositoryContext,
382
426
  resource: Writable<Cart>,
@@ -8,6 +8,7 @@ import { cloneObject, getBaseResourceProperties } from '../helpers.js'
8
8
  import type { Writable } from '../types.js'
9
9
  import {
10
10
  AbstractResourceRepository,
11
+ QueryParams,
11
12
  type RepositoryContext,
12
13
  } from './abstract.js'
13
14
  import { checkConcurrentModification } from './errors.js'
@@ -67,6 +68,23 @@ export class CustomObjectRepository extends AbstractResourceRepository<'key-valu
67
68
  }
68
69
  }
69
70
 
71
+ queryWithContainer(
72
+ context: RepositoryContext,
73
+ container: string,
74
+ params: QueryParams = {}
75
+ ) {
76
+ const whereClause = params.where || []
77
+ whereClause.push(`container="${container}"`)
78
+ const result = this._storage.query(context.projectKey, this.getTypeId(), {
79
+ ...params,
80
+ where: whereClause,
81
+ })
82
+
83
+ // @ts-ignore
84
+ result.results = result.results.map(this.postProcessResource)
85
+ return result
86
+ }
87
+
70
88
  getWithContainerAndKey(
71
89
  context: RepositoryContext,
72
90
  container: string,
@@ -51,6 +51,7 @@ export class CustomerRepository extends AbstractResourceRepository<'customer'> {
51
51
  password: draft.password ? hashPassword(draft.password) : undefined,
52
52
  isEmailVerified: draft.isEmailVerified || false,
53
53
  addresses: [],
54
+ customerNumber: draft.customerNumber,
54
55
  }
55
56
  this.saveNew(context, resource)
56
57
  return resource
@@ -180,7 +180,7 @@ export const getReferenceFromResourceIdentifier = <T extends Reference>(
180
180
  if (!resource) {
181
181
  const errIdentifier = resourceIdentifier.key
182
182
  ? `key '${resourceIdentifier.key}'`
183
- : `identifier '${resourceIdentifier.key}'`
183
+ : `identifier '${resourceIdentifier.id}'`
184
184
 
185
185
  throw new CommercetoolsError<ReferencedResourceNotFoundError>(
186
186
  {
@@ -2,12 +2,15 @@ import type {
2
2
  Cart,
3
3
  CartReference,
4
4
  CustomLineItem,
5
- CustomLineItemDraft,
5
+ CustomLineItemImportDraft,
6
+ CustomLineItemReturnItem,
6
7
  GeneralError,
7
8
  LineItem,
8
9
  LineItemImportDraft,
10
+ LineItemReturnItem,
9
11
  Order,
10
12
  OrderAddPaymentAction,
13
+ OrderAddReturnInfoAction,
11
14
  OrderChangeOrderStateAction,
12
15
  OrderChangePaymentStateAction,
13
16
  OrderFromCartDraft,
@@ -24,6 +27,7 @@ import type {
24
27
  Product,
25
28
  ProductPagedQueryResponse,
26
29
  ProductVariant,
30
+ ReturnInfo,
27
31
  State,
28
32
  Store,
29
33
  } from '@commercetools/platform-sdk'
@@ -227,7 +231,7 @@ export class OrderRepository extends AbstractResourceRepository<'order'> {
227
231
 
228
232
  private customLineItemFromImportDraft(
229
233
  context: RepositoryContext,
230
- draft: CustomLineItemDraft
234
+ draft: CustomLineItemImportDraft
231
235
  ): CustomLineItem {
232
236
  const lineItem: CustomLineItem = {
233
237
  ...getBaseResourceProperties(),
@@ -241,10 +245,11 @@ export class OrderRepository extends AbstractResourceRepository<'order'> {
241
245
  name: draft.name,
242
246
  quantity: draft.quantity ?? 0,
243
247
  perMethodTaxRate: [],
244
- priceMode: draft.priceMode,
248
+ priceMode: draft.priceMode ?? 'Standard',
245
249
  slug: draft.slug,
246
250
  state: [],
247
251
  totalPrice: createCentPrecisionMoney(draft.money),
252
+ taxedPricePortions: [],
248
253
  }
249
254
 
250
255
  return lineItem
@@ -296,6 +301,43 @@ export class OrderRepository extends AbstractResourceRepository<'order'> {
296
301
  id: payment.id!,
297
302
  })
298
303
  },
304
+ addReturnInfo: (
305
+ context: RepositoryContext,
306
+ resource: Writable<Order>,
307
+ info: OrderAddReturnInfoAction
308
+ ) => {
309
+ if (!resource.returnInfo) {
310
+ resource.returnInfo = []
311
+ }
312
+
313
+ const resolved: ReturnInfo = {
314
+ items: info.items.map((item) => {
315
+ const common = {
316
+ ...getBaseResourceProperties(),
317
+ quantity: item.quantity,
318
+ paymentState: 'Initial',
319
+ shipmentState: 'Initial',
320
+ comment: item.comment,
321
+ }
322
+ if (item.customLineItemId) {
323
+ return {
324
+ ...common,
325
+ type: 'CustomLineItemReturnItem',
326
+ customLineItemId: item.customLineItemId,
327
+ } as CustomLineItemReturnItem
328
+ }
329
+ return {
330
+ ...common,
331
+ type: 'LineItemReturnItem',
332
+ lineItemId: item.customLineItemId || item.lineItemId,
333
+ } as LineItemReturnItem
334
+ }),
335
+ returnTrackingId: info.returnTrackingId,
336
+ returnDate: info.returnDate,
337
+ }
338
+
339
+ resource.returnInfo.push(resolved)
340
+ },
299
341
  changeOrderState: (
300
342
  context: RepositoryContext,
301
343
  resource: Writable<Order>,
@@ -13,6 +13,7 @@ import {
13
13
  GetParams,
14
14
  RepositoryContext,
15
15
  } from './abstract.js'
16
+ import { applyPriceSelector } from '../priceSelector.js'
16
17
 
17
18
  export type ProductProjectionQueryParams = {
18
19
  staged?: boolean
@@ -100,6 +101,20 @@ export class ProductProjectionRepository extends AbstractResourceRepository<'pro
100
101
  }
101
102
  }
102
103
 
104
+ // We do this after the filtering, since the docs mention:
105
+ // Only available when Price selection is used. Cannot be used in a Query
106
+ // Predicate.
107
+ applyPriceSelector(
108
+ resources,
109
+ {
110
+ country: params.priceCountry,
111
+ channel: params.priceChannel,
112
+ customerGroup: params.priceCustomerGroup,
113
+ currency: params.priceCurrency,
114
+ },
115
+ true
116
+ )
117
+
103
118
  // Expand the resources
104
119
  if (params.expand !== undefined) {
105
120
  resources = resources.map((resource) =>
@@ -22,7 +22,6 @@ export class ProductSelectionRepository extends AbstractResourceRepository<'prod
22
22
  productCount: 0,
23
23
  key: draft.key,
24
24
  name: draft.name,
25
- type: 'individual',
26
25
  mode: 'Individual',
27
26
  }
28
27
  this.saveNew(context, resource)