@labdigital/commercetools-mock 0.5.22 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { GetParams } from 'repositories/abstract';
1
2
  import { AbstractStorage } from './storage';
2
3
  import { RepositoryMap, ResourceMap, Services } from './types';
3
4
  export declare class ProjectAPI {
@@ -6,6 +7,6 @@ export declare class ProjectAPI {
6
7
  private _services;
7
8
  constructor(projectKey: string, services: Services, storage: AbstractStorage);
8
9
  add<ReferenceTypeId extends keyof ResourceMap>(typeId: ReferenceTypeId | 'custom-object', resource: ResourceMap[ReferenceTypeId]): void;
9
- get<ReferenceTypeId extends keyof ResourceMap>(typeId: ReferenceTypeId, id: string): ResourceMap[ReferenceTypeId];
10
+ get<ReferenceTypeId extends keyof ResourceMap>(typeId: ReferenceTypeId, id: string, params: GetParams): ResourceMap[ReferenceTypeId];
10
11
  getRepository<ReferenceTypeId extends keyof RepositoryMap>(typeId: ReferenceTypeId): RepositoryMap[ReferenceTypeId];
11
12
  }
@@ -1,4 +1,4 @@
1
- import { Cart, CartAddLineItemAction, CartDraft, CartRemoveLineItemAction, CartSetBillingAddressAction, CartSetCountryAction, CartSetCustomerEmailAction, CartSetCustomFieldAction, CartSetCustomTypeAction, CartSetLocaleAction, CartSetShippingAddressAction, ReferenceTypeId } from '@commercetools/platform-sdk';
1
+ import { Cart, CartAddLineItemAction, CartDraft, CartRemoveLineItemAction, CartSetBillingAddressAction, CartSetCountryAction, CartSetCustomerEmailAction, CartSetCustomFieldAction, CartSetCustomTypeAction, CartSetLocaleAction, CartSetShippingAddressAction, CartSetShippingMethodAction, LineItem, LineItemDraft, ReferenceTypeId } from '@commercetools/platform-sdk';
2
2
  import { AbstractResourceRepository } from './abstract';
3
3
  import { Writable } from '../types';
4
4
  export declare class CartRepository extends AbstractResourceRepository {
@@ -9,6 +9,7 @@ export declare class CartRepository extends AbstractResourceRepository {
9
9
  addLineItem: (projectKey: string, resource: Writable<Cart>, { productId, variantId, sku, quantity }: CartAddLineItemAction) => void;
10
10
  removeLineItem: (projectKey: string, resource: Writable<Cart>, { lineItemId, quantity }: CartRemoveLineItemAction) => void;
11
11
  setBillingAddress: (projectKey: string, resource: Writable<Cart>, { address }: CartSetBillingAddressAction) => void;
12
+ setShippingMethod: (projectKey: string, resource: Writable<Cart>, { shippingMethod }: CartSetShippingMethodAction) => void;
12
13
  setCountry: (projectKey: string, resource: Writable<Cart>, { country }: CartSetCountryAction) => void;
13
14
  setCustomerEmail: (projectKey: string, resource: Writable<Cart>, { email }: CartSetCustomerEmailAction) => void;
14
15
  setCustomField: (projectKey: string, resource: Cart, { name, value }: CartSetCustomFieldAction) => void;
@@ -16,4 +17,5 @@ export declare class CartRepository extends AbstractResourceRepository {
16
17
  setLocale: (projectKey: string, resource: Writable<Cart>, { locale }: CartSetLocaleAction) => void;
17
18
  setShippingAddress: (projectKey: string, resource: Writable<Cart>, { address }: CartSetShippingAddressAction) => void;
18
19
  };
20
+ draftLineItemtoLineItem: (projectKey: string, draftLineItem: LineItemDraft, currency: string, country: string | undefined) => LineItem;
19
21
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.5.22",
2
+ "version": "0.6.1",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -30,6 +30,7 @@
30
30
  "module": "dist/commercetools--mock.esm.js",
31
31
  "devDependencies": {
32
32
  "@babel/preset-typescript": "^7.16.7",
33
+ "@commercetools/platform-sdk": "^2.4.1",
33
34
  "@types/basic-auth": "^1.1.3",
34
35
  "@types/deep-equal": "^1.0.1",
35
36
  "@types/express": "^4.17.13",
@@ -46,7 +47,6 @@
46
47
  "typescript": "^4.5.4"
47
48
  },
48
49
  "dependencies": {
49
- "@commercetools/platform-sdk": "2.4.1",
50
50
  "ajv": "^8.8.2",
51
51
  "ajv-formats": "^2.1.1",
52
52
  "basic-auth": "^2.0.1",
@@ -57,5 +57,8 @@
57
57
  "perplex": "^0.11.0",
58
58
  "pratt": "^0.7.0",
59
59
  "supertest": "^6.1.6"
60
+ },
61
+ "peerDependencies": {
62
+ "@commercetools/platform-sdk": "^2.4.1"
60
63
  }
61
64
  }
package/src/projectAPI.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { GetParams } from 'repositories/abstract'
1
2
  import { getBaseResourceProperties } from './helpers'
2
3
  import { AbstractStorage } from './storage'
3
4
  import { RepositoryMap, ResourceMap, Services } from './types'
@@ -39,13 +40,14 @@ export class ProjectAPI {
39
40
 
40
41
  get<ReferenceTypeId extends keyof ResourceMap>(
41
42
  typeId: ReferenceTypeId,
42
- id: string
43
+ id: string,
44
+ params: GetParams
43
45
  ): ResourceMap[ReferenceTypeId] {
44
46
  return this._storage.get(
45
47
  this.projectKey,
46
48
  typeId,
47
49
  id,
48
- {}
50
+ params
49
51
  ) as ResourceMap[ReferenceTypeId]
50
52
  }
51
53
 
@@ -10,8 +10,11 @@ import {
10
10
  CartSetCustomTypeAction,
11
11
  CartSetLocaleAction,
12
12
  CartSetShippingAddressAction,
13
+ CartSetShippingMethodAction,
13
14
  GeneralError,
14
15
  LineItem,
16
+ LineItemDraft,
17
+ Price,
15
18
  Product,
16
19
  ProductPagedQueryResponse,
17
20
  ProductVariant,
@@ -30,10 +33,20 @@ export class CartRepository extends AbstractResourceRepository {
30
33
  }
31
34
 
32
35
  create(projectKey: string, draft: CartDraft): Cart {
36
+ const lineItems =
37
+ draft.lineItems?.map(draftLineItem =>
38
+ this.draftLineItemtoLineItem(
39
+ projectKey,
40
+ draftLineItem,
41
+ draft.currency,
42
+ draft.country
43
+ )
44
+ ) ?? []
45
+
33
46
  const resource: Cart = {
34
47
  ...getBaseResourceProperties(),
35
48
  cartState: 'Active',
36
- lineItems: [],
49
+ lineItems,
37
50
  customLineItems: [],
38
51
  totalPrice: {
39
52
  type: 'centPrecision',
@@ -41,13 +54,19 @@ export class CartRepository extends AbstractResourceRepository {
41
54
  currencyCode: draft.currency,
42
55
  fractionDigits: 0,
43
56
  },
44
- taxMode: 'Platform',
45
- taxRoundingMode: 'HalfEven',
46
- taxCalculationMode: 'LineItemLevel',
57
+ taxMode: draft.taxMode ?? 'Platform',
58
+ taxRoundingMode: draft.taxRoundingMode ?? 'HalfEven',
59
+ taxCalculationMode: draft.taxCalculationMode ?? 'LineItemLevel',
47
60
  refusedGifts: [],
48
- origin: 'Customer',
61
+ locale: draft.locale,
62
+ country: draft.country,
63
+ origin: draft.origin ?? 'Customer',
49
64
  custom: createCustomFields(draft.custom, projectKey, this._storage),
50
65
  }
66
+
67
+ // @ts-ignore
68
+ resource.totalPrice.centAmount = calculateCartTotalPrice(resource)
69
+
51
70
  this.save(projectKey, resource)
52
71
  return resource
53
72
  }
@@ -140,7 +159,18 @@ export class CartRepository extends AbstractResourceRepository {
140
159
  })
141
160
  }
142
161
 
143
- const price = variant.prices[0]
162
+ const currency = resource.totalPrice.currencyCode
163
+
164
+ const price = selectPrice({
165
+ prices: variant.prices,
166
+ currency,
167
+ country: resource.country,
168
+ })
169
+ if (!price) {
170
+ throw new Error(
171
+ `No valid price found for ${productId} for country ${resource.country} and currency ${currency}`
172
+ )
173
+ }
144
174
  resource.lineItems.push({
145
175
  id: uuidv4(),
146
176
  productId: product.id,
@@ -204,6 +234,28 @@ export class CartRepository extends AbstractResourceRepository {
204
234
  ) => {
205
235
  resource.billingAddress = address
206
236
  },
237
+ setShippingMethod: (
238
+ projectKey: string,
239
+ resource: Writable<Cart>,
240
+ { shippingMethod }: CartSetShippingMethodAction
241
+ ) => {
242
+ const resolvedType = this._storage.getByResourceIdentifier(
243
+ projectKey,
244
+ //@ts-ignore
245
+ shippingMethod
246
+ )
247
+
248
+ if (!resolvedType) {
249
+ throw new Error(`Type ${shippingMethod} not found`)
250
+ }
251
+ //@ts-ignore
252
+ resource.shippingInfo = {
253
+ shippingMethod: {
254
+ typeId: 'shipping-method',
255
+ id: resolvedType.id,
256
+ },
257
+ }
258
+ },
207
259
  setCountry: (
208
260
  projectKey: string,
209
261
  resource: Writable<Cart>,
@@ -268,6 +320,114 @@ export class CartRepository extends AbstractResourceRepository {
268
320
  resource.shippingAddress = address
269
321
  },
270
322
  }
323
+ draftLineItemtoLineItem = (
324
+ projectKey: string,
325
+ draftLineItem: LineItemDraft,
326
+ currency: string,
327
+ country: string | undefined
328
+ ): LineItem => {
329
+ const { productId, quantity, variantId, sku } = draftLineItem
330
+
331
+ let product: Product | null = null
332
+ let variant: ProductVariant | undefined
333
+
334
+ if (productId && variantId) {
335
+ // Fetch product and variant by ID
336
+ product = this._storage.get(projectKey, 'product', productId, {})
337
+ } else if (sku) {
338
+ // Fetch product and variant by SKU
339
+ const items = this._storage.query(projectKey, 'product', {
340
+ where: [
341
+ `masterData(current(masterVariant(sku="${sku}"))) or masterData(current(variants(sku="${sku}")))`,
342
+ ],
343
+ }) as ProductPagedQueryResponse
344
+
345
+ if (items.count === 1) {
346
+ product = items.results[0]
347
+ }
348
+ }
349
+
350
+ if (!product) {
351
+ // Check if product is found
352
+ throw new CommercetoolsError<GeneralError>({
353
+ code: 'General',
354
+ message: sku
355
+ ? `A product containing a variant with SKU '${sku}' not found.`
356
+ : `A product with ID '${productId}' not found.`,
357
+ })
358
+ }
359
+
360
+ // Find matching variant
361
+ variant = [
362
+ product.masterData.current.masterVariant,
363
+ ...product.masterData.current.variants,
364
+ ].find(x => {
365
+ if (sku) return x.sku === sku
366
+ if (variantId) return x.id === variantId
367
+ return false
368
+ })
369
+
370
+ if (!variant) {
371
+ // Check if variant is found
372
+ throw new Error(
373
+ sku
374
+ ? `A variant with SKU '${sku}' for product '${product.id}' not found.`
375
+ : `A variant with ID '${variantId}' for product '${product.id}' not found.`
376
+ )
377
+ }
378
+
379
+ const quant = quantity ?? 1
380
+
381
+ const price = selectPrice({ prices: variant.prices, currency, country })
382
+ if (!price) {
383
+ throw new Error(
384
+ `No valid price found for ${productId} for country ${country} and currency ${currency}`
385
+ )
386
+ }
387
+
388
+ return {
389
+ id: uuidv4(),
390
+ productId: product.id,
391
+ productKey: product.key,
392
+ name: product.masterData.current.name,
393
+ productSlug: product.masterData.current.slug,
394
+ productType: product.productType,
395
+ variant,
396
+ price: price,
397
+ totalPrice: {
398
+ ...price.value,
399
+ centAmount: price.value.centAmount * quant,
400
+ },
401
+ quantity: quant,
402
+ discountedPricePerQuantity: [],
403
+ lineItemMode: 'Standard',
404
+ priceMode: 'Platform',
405
+ state: [],
406
+ }
407
+ }
408
+ }
409
+
410
+ const selectPrice = ({
411
+ prices,
412
+ currency,
413
+ country,
414
+ }: {
415
+ prices: Price[] | undefined
416
+ currency: string
417
+ country: string | undefined
418
+ }) => {
419
+ if (!prices) {
420
+ return undefined
421
+ }
422
+
423
+ // Quick-and-dirty way of selecting price based on the given currency and country.
424
+ // Can be improved later to give more priority to exact matches over
425
+ // 'all country' matches, and include customer groups in the mix as well
426
+ return prices.find(price => {
427
+ const countryMatch = !price.country || price.country === country
428
+ const currencyMatch = price.value.currencyCode === currency
429
+ return countryMatch && currencyMatch
430
+ })
271
431
  }
272
432
 
273
433
  const calculateLineItemTotalPrice = (lineItem: LineItem): number =>
@@ -17,7 +17,9 @@ export class CustomerRepository extends AbstractResourceRepository {
17
17
  const resource: Customer = {
18
18
  ...getBaseResourceProperties(),
19
19
  email: draft.email,
20
- password: Buffer.from(draft.password).toString('base64'),
20
+ password: draft.password
21
+ ? Buffer.from(draft.password).toString('base64')
22
+ : undefined,
21
23
  isEmailVerified: draft.isEmailVerified || false,
22
24
  addresses: [],
23
25
  }
@@ -76,9 +76,20 @@ describe('Carts Query', () => {
76
76
  })
77
77
  })
78
78
 
79
- describe('Order Update Actions', () => {
79
+ describe('Cart Update Actions', () => {
80
80
  const ctMock = new CommercetoolsMock()
81
81
  let cart: Cart | undefined
82
+
83
+ const createCart = async (currency: string) => {
84
+ let response = await supertest(ctMock.app)
85
+ .post('/dummy/carts')
86
+ .send({
87
+ currency,
88
+ })
89
+ expect(response.status).toBe(201)
90
+ cart = response.body
91
+ }
92
+
82
93
  const productDraft: ProductDraft = {
83
94
  name: {
84
95
  'nl-NL': 'test product',
@@ -98,6 +109,14 @@ describe('Order Update Actions', () => {
98
109
  fractionDigits: 2,
99
110
  } as CentPrecisionMoney,
100
111
  },
112
+ {
113
+ value: {
114
+ type: 'centPrecision',
115
+ currencyCode: 'GBP',
116
+ centAmount: 18900,
117
+ fractionDigits: 2,
118
+ } as CentPrecisionMoney,
119
+ },
101
120
  ],
102
121
 
103
122
  attributes: [
@@ -135,13 +154,7 @@ describe('Order Update Actions', () => {
135
154
  }
136
155
 
137
156
  beforeEach(async () => {
138
- let response = await supertest(ctMock.app)
139
- .post('/dummy/carts')
140
- .send({
141
- currency: 'EUR',
142
- })
143
- expect(response.status).toBe(201)
144
- cart = response.body
157
+ await createCart('EUR')
145
158
  })
146
159
 
147
160
  afterEach(() => {
@@ -220,6 +233,33 @@ describe('Order Update Actions', () => {
220
233
  expect(response.body.totalPrice.centAmount).toEqual(29800)
221
234
  })
222
235
 
236
+ test.each([
237
+ ['EUR', 29800],
238
+ ['GBP', 37800],
239
+ ])('addLineItem with price selection', async (currency, total) => {
240
+ await createCart(currency)
241
+
242
+ const product = await supertest(ctMock.app)
243
+ .post(`/dummy/products`)
244
+ .send(productDraft)
245
+ .then(x => x.body)
246
+
247
+ assert(cart, 'cart not created')
248
+ assert(product, 'product not created')
249
+
250
+ const response = await supertest(ctMock.app)
251
+ .post(`/dummy/carts/${cart.id}`)
252
+ .send({
253
+ version: 1,
254
+ actions: [{ action: 'addLineItem', sku: '1337', quantity: 2 }],
255
+ })
256
+ expect(response.status).toBe(200)
257
+ expect(response.body.version).toBe(2)
258
+ expect(response.body.lineItems).toHaveLength(1)
259
+ expect(response.body.lineItems[0].price.value.currencyCode).toBe(currency)
260
+ expect(response.body.totalPrice.centAmount).toEqual(total)
261
+ })
262
+
223
263
  test('addLineItem unknown product', async () => {
224
264
  assert(cart, 'cart not created')
225
265
 
@@ -2,7 +2,7 @@ import AbstractService from './abstract'
2
2
  import { Router } from 'express'
3
3
  import { CartRepository } from '../repositories/cart'
4
4
  import { AbstractStorage } from '../storage'
5
- import { Cart, Order } from '@commercetools/platform-sdk'
5
+ import { Cart, CartDraft, Order } from '@commercetools/platform-sdk'
6
6
  import { OrderRepository } from '../repositories/order'
7
7
 
8
8
  export class CartService extends AbstractService {
@@ -37,11 +37,23 @@ export class CartService extends AbstractService {
37
37
  return response.status(400).send()
38
38
  }
39
39
 
40
- const newCart = this.repository.create(request.params.projectKey, {
40
+ const cartDraft: CartDraft = {
41
41
  ...cartOrOrder,
42
42
  currency: cartOrOrder.totalPrice.currencyCode,
43
43
  discountCodes: [],
44
- })
44
+ lineItems: cartOrOrder.lineItems.map(lineItem => {
45
+ return {
46
+ ...lineItem,
47
+ variantId: lineItem.variant.id,
48
+ sku: lineItem.variant.sku,
49
+ }
50
+ }),
51
+ }
52
+
53
+ const newCart = this.repository.create(
54
+ request.params.projectKey,
55
+ cartDraft
56
+ )
45
57
 
46
58
  return response.status(200).send(newCart)
47
59
  })