@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.
@@ -36,6 +36,7 @@ import type {
36
36
  ProductAddToCategoryAction,
37
37
  ProductRemoveFromCategoryAction,
38
38
  ProductTransitionStateAction,
39
+ ChannelReference,
39
40
  } from '@commercetools/platform-sdk'
40
41
  import { v4 as uuidv4 } from 'uuid'
41
42
  import type { Writable } from '../types.js'
@@ -126,10 +127,10 @@ export class ProductRepository extends AbstractResourceRepository<'product'> {
126
127
  slug: draft.slug,
127
128
  description: draft.description,
128
129
  categories: categoryReferences,
129
- masterVariant: variantFromDraft(1, draft.masterVariant),
130
+ masterVariant: this.variantFromDraft(context, 1, draft.masterVariant),
130
131
  variants:
131
132
  draft.variants?.map((variant, index) =>
132
- variantFromDraft(index + 2, variant)
133
+ this.variantFromDraft(context, index + 2, variant)
133
134
  ) ?? [],
134
135
  metaTitle: draft.metaTitle,
135
136
  metaDescription: draft.metaDescription,
@@ -156,6 +157,38 @@ export class ProductRepository extends AbstractResourceRepository<'product'> {
156
157
  return resource
157
158
  }
158
159
 
160
+ private variantFromDraft(
161
+ context: RepositoryContext,
162
+ variantId: number,
163
+ variant: ProductVariantDraft
164
+ ): ProductVariant {
165
+ return {
166
+ id: variantId,
167
+ sku: variant?.sku,
168
+ key: variant?.key,
169
+ attributes: variant?.attributes ?? [],
170
+ prices: variant?.prices?.map((p) => this.priceFromDraft(context, p)),
171
+ assets: [],
172
+ images: [],
173
+ }
174
+ }
175
+
176
+ private priceFromDraft(context: RepositoryContext, draft: PriceDraft): Price {
177
+ return {
178
+ id: uuidv4(),
179
+ key: draft.key,
180
+ country: draft.country,
181
+ value: createTypedMoney(draft.value),
182
+ channel: draft.channel
183
+ ? getReferenceFromResourceIdentifier<ChannelReference>(
184
+ draft.channel,
185
+ context.projectKey,
186
+ this._storage
187
+ )
188
+ : undefined,
189
+ }
190
+ }
191
+
159
192
  actions: Partial<
160
193
  Record<
161
194
  ProductUpdateAction['action'],
@@ -528,7 +561,7 @@ export class ProductRepository extends AbstractResourceRepository<'product'> {
528
561
  }
529
562
 
530
563
  // Pre-creating the price object ensures consistency between staged and current versions
531
- const priceToAdd = priceFromDraft(price)
564
+ const priceToAdd = this.priceFromDraft(context, price)
532
565
 
533
566
  // If true, only the staged Attribute is set. If false, both current and
534
567
  // staged Attribute is set. Default is true
@@ -754,7 +787,7 @@ export class ProductRepository extends AbstractResourceRepository<'product'> {
754
787
  (max, element) => (element.id > max ? element.id : max),
755
788
  0
756
789
  )
757
- const variant = variantFromDraft(maxId + 1, variantDraft)
790
+ const variant = this.variantFromDraft(context, maxId + 1, variantDraft)
758
791
  dataStaged.variants.push(variant)
759
792
 
760
793
  const onlyStaged = staged !== undefined ? staged : true
@@ -1071,23 +1104,3 @@ const getVariant = (
1071
1104
  : -1,
1072
1105
  }
1073
1106
  }
1074
-
1075
- const variantFromDraft = (
1076
- variantId: number,
1077
- variant: ProductVariantDraft
1078
- ): ProductVariant => ({
1079
- id: variantId,
1080
- sku: variant?.sku,
1081
- key: variant?.key,
1082
- attributes: variant?.attributes ?? [],
1083
- prices: variant?.prices?.map(priceFromDraft),
1084
- assets: [],
1085
- images: [],
1086
- })
1087
-
1088
- const priceFromDraft = (draft: PriceDraft): Price => ({
1089
- id: uuidv4(),
1090
- key: draft.key,
1091
- country: draft.country,
1092
- value: createTypedMoney(draft.value),
1093
- })
@@ -1,11 +1,18 @@
1
- import type {
2
- Review,
3
- ReviewDraft,
4
- ReviewUpdateAction,
1
+ import {
2
+ ChannelReference,
3
+ ProductReference,
4
+ type Review,
5
+ type ReviewDraft,
6
+ type ReviewUpdateAction,
7
+ type StateReference,
5
8
  } from '@commercetools/platform-sdk'
6
9
  import { getBaseResourceProperties } from '../helpers.js'
7
10
  import type { Writable } from '../types.js'
8
11
  import { AbstractResourceRepository, RepositoryContext } from './abstract.js'
12
+ import {
13
+ createCustomFields,
14
+ getReferenceFromResourceIdentifier,
15
+ } from './helpers.js'
9
16
 
10
17
  export class ReviewRepository extends AbstractResourceRepository<'review'> {
11
18
  getTypeId() {
@@ -13,9 +20,34 @@ export class ReviewRepository extends AbstractResourceRepository<'review'> {
13
20
  }
14
21
 
15
22
  create(context: RepositoryContext, draft: ReviewDraft): Review {
23
+ if (!draft.target) throw new Error('Missing target')
16
24
  const resource: Review = {
17
25
  ...getBaseResourceProperties(),
26
+
27
+ locale: draft.locale,
28
+ authorName: draft.authorName,
29
+ title: draft.title,
30
+ text: draft.text,
31
+ rating: draft.rating,
32
+ uniquenessValue: draft.uniquenessValue,
33
+ state: draft.state
34
+ ? getReferenceFromResourceIdentifier<StateReference>(
35
+ draft.state,
36
+ context.projectKey,
37
+ this._storage
38
+ )
39
+ : undefined,
40
+ target: draft.target
41
+ ? getReferenceFromResourceIdentifier<
42
+ ProductReference | ChannelReference
43
+ >(draft.target, context.projectKey, this._storage)
44
+ : undefined,
18
45
  includedInStatistics: false,
46
+ custom: createCustomFields(
47
+ draft.custom,
48
+ context.projectKey,
49
+ this._storage
50
+ ),
19
51
  }
20
52
  this.saveNew(context, resource)
21
53
  return resource
@@ -1,34 +1,41 @@
1
- import type {
2
- ShippingMethod,
3
- ShippingMethodAddShippingRateAction,
4
- ShippingMethodAddZoneAction,
5
- ShippingMethodChangeIsDefaultAction,
6
- ShippingMethodChangeNameAction,
7
- ShippingMethodDraft,
8
- ShippingMethodRemoveZoneAction,
9
- ShippingMethodSetCustomFieldAction,
10
- ShippingMethodSetCustomTypeAction,
11
- ShippingMethodSetDescriptionAction,
12
- ShippingMethodSetKeyAction,
13
- ShippingMethodSetLocalizedDescriptionAction,
14
- ShippingMethodSetLocalizedNameAction,
15
- ShippingMethodSetPredicateAction,
16
- ShippingMethodUpdateAction,
17
- ShippingRate,
18
- ShippingRateDraft,
19
- ZoneRate,
20
- ZoneRateDraft,
21
- ZoneReference,
1
+ import {
2
+ InvalidOperationError,
3
+ type ShippingMethod,
4
+ type ShippingMethodAddShippingRateAction,
5
+ type ShippingMethodAddZoneAction,
6
+ type ShippingMethodChangeIsDefaultAction,
7
+ type ShippingMethodChangeNameAction,
8
+ type ShippingMethodDraft,
9
+ type ShippingMethodRemoveZoneAction,
10
+ type ShippingMethodSetCustomFieldAction,
11
+ type ShippingMethodSetCustomTypeAction,
12
+ type ShippingMethodSetDescriptionAction,
13
+ type ShippingMethodSetKeyAction,
14
+ type ShippingMethodSetLocalizedDescriptionAction,
15
+ type ShippingMethodSetLocalizedNameAction,
16
+ type ShippingMethodSetPredicateAction,
17
+ type ShippingMethodUpdateAction,
18
+ type ShippingRate,
19
+ type ShippingRateDraft,
20
+ type ZoneRate,
21
+ type ZoneRateDraft,
22
+ type ZoneReference,
22
23
  } from '@commercetools/platform-sdk'
23
24
  import deepEqual from 'deep-equal'
24
25
  import { getBaseResourceProperties } from '../helpers.js'
25
26
  import type { Writable } from '../types.js'
26
- import { AbstractResourceRepository, RepositoryContext } from './abstract.js'
27
+ import {
28
+ AbstractResourceRepository,
29
+ GetParams,
30
+ RepositoryContext,
31
+ } from './abstract.js'
27
32
  import {
28
33
  createCustomFields,
29
34
  createTypedMoney,
30
35
  getReferenceFromResourceIdentifier,
31
36
  } from './helpers.js'
37
+ import { CommercetoolsError } from '../exceptions.js'
38
+ import { markMatchingShippingRate } from '../shippingCalculator.js'
32
39
 
33
40
  export class ShippingMethodRepository extends AbstractResourceRepository<'shipping-method'> {
34
41
  getTypeId() {
@@ -79,6 +86,75 @@ export class ShippingMethodRepository extends AbstractResourceRepository<'shippi
79
86
  tiers: rate.tiers || [],
80
87
  })
81
88
 
89
+ /*
90
+ * Retrieves all the ShippingMethods that can ship to the shipping address of
91
+ * the given Cart. Each ShippingMethod contains exactly one ShippingRate with
92
+ * the flag isMatching set to true. This ShippingRate is used when the
93
+ * ShippingMethod is added to the Cart.
94
+ */
95
+ public matchingCart(
96
+ context: RepositoryContext,
97
+ cartId: string,
98
+ params: GetParams = {}
99
+ ) {
100
+ const cart = this._storage.get(context.projectKey, 'cart', cartId)
101
+ if (!cart) {
102
+ return undefined
103
+ }
104
+
105
+ if (!cart.shippingAddress?.country) {
106
+ throw new CommercetoolsError<InvalidOperationError>({
107
+ code: 'InvalidOperation',
108
+ message: `The cart with ID '${cart.id}' does not have a shipping address set.`,
109
+ })
110
+ }
111
+
112
+ // Get all shipping methods that have a zone that matches the shipping address
113
+ const zones = this._storage.query<'zone'>(context.projectKey, 'zone', {
114
+ where: [`locations(country="${cart.shippingAddress.country}"))`],
115
+ limit: 100,
116
+ })
117
+ const zoneIds = zones.results.map((zone) => zone.id)
118
+ const shippingMethods = this.query(context, {
119
+ where: [
120
+ `zoneRates(zone(id in (:zoneIds)))`,
121
+ `zoneRates(shippingRates(price(currencyCode="${cart.totalPrice.currencyCode}")))`,
122
+ ],
123
+ 'var.zoneIds': zoneIds,
124
+ expand: params.expand,
125
+ })
126
+
127
+ // Make sure that each shipping method has exactly one shipping rate and
128
+ // that the shipping rate is marked as matching
129
+ const results = shippingMethods.results
130
+ .map((shippingMethod) => {
131
+ // Iterate through the zoneRates, process the shipping rates and filter
132
+ // out all zoneRates which have no matching shipping rates left
133
+ const rates = shippingMethod.zoneRates
134
+ .map((zoneRate) => ({
135
+ zone: zoneRate.zone,
136
+
137
+ // Iterate through the shippingRates and mark the matching ones
138
+ // then we filter out the non-matching ones
139
+ shippingRates: zoneRate.shippingRates
140
+ .map((rate) => markMatchingShippingRate(cart, rate))
141
+ .filter((rate) => rate.isMatching),
142
+ }))
143
+ .filter((zoneRate) => zoneRate.shippingRates.length > 0)
144
+
145
+ return {
146
+ ...shippingMethod,
147
+ zoneRates: rates,
148
+ }
149
+ })
150
+ .filter((shippingMethod) => shippingMethod.zoneRates.length > 0)
151
+
152
+ return {
153
+ ...shippingMethods,
154
+ results: results,
155
+ }
156
+ }
157
+
82
158
  actions: Partial<
83
159
  Record<
84
160
  ShippingMethodUpdateAction['action'],
@@ -17,11 +17,30 @@ export class CustomObjectService extends AbstractService {
17
17
  }
18
18
 
19
19
  extraRoutes(router: Router) {
20
+ router.get('/:container', this.getWithContainer.bind(this))
20
21
  router.get('/:container/:key', this.getWithContainerAndKey.bind(this))
21
22
  router.post('/:container/:key', this.createWithContainerAndKey.bind(this))
22
23
  router.delete('/:container/:key', this.deleteWithContainerAndKey.bind(this))
23
24
  }
24
25
 
26
+ getWithContainer(request: Request, response: Response) {
27
+ const limit = this._parseParam(request.query.limit)
28
+ const offset = this._parseParam(request.query.offset)
29
+
30
+ const result = this.repository.queryWithContainer(
31
+ getRepositoryContext(request),
32
+ request.params.container,
33
+ {
34
+ expand: this._parseParam(request.query.expand),
35
+ where: this._parseParam(request.query.where),
36
+ limit: limit !== undefined ? Number(limit) : undefined,
37
+ offset: offset !== undefined ? Number(offset) : undefined,
38
+ }
39
+ )
40
+
41
+ return response.status(200).send(result)
42
+ }
43
+
25
44
  getWithContainerAndKey(request: Request, response: Response) {
26
45
  const result = this.repository.getWithContainerAndKey(
27
46
  getRepositoryContext(request),
@@ -23,6 +23,7 @@ import { ProductProjectionService } from './product-projection.js'
23
23
  import { ProductSelectionService } from './product-selection.js'
24
24
  import { ProductTypeService } from './product-type.js'
25
25
  import { ProductService } from './product.js'
26
+ import { ReviewService } from './reviews.js'
26
27
  import { ShippingMethodService } from './shipping-method.js'
27
28
  import { ShoppingListService } from './shopping-list.js'
28
29
  import { StandAlonePriceService } from './standalone-price.js'
@@ -84,6 +85,7 @@ export const createServices = (router: any, repos: any) => ({
84
85
  router,
85
86
  repos['product-selection']
86
87
  ),
88
+ reviews: new ReviewService(router, repos['review']),
87
89
  'shopping-list': new ShoppingListService(router, repos['shopping-list']),
88
90
  state: new StateService(router, repos['state']),
89
91
  store: new StoreService(router, repos['store']),
@@ -29,7 +29,6 @@ describe('product-selection', () => {
29
29
  key: 'foo',
30
30
  version: 1,
31
31
  productCount: 0,
32
- type: 'individual',
33
32
  mode: 'Individual',
34
33
  })
35
34
  })
@@ -0,0 +1,16 @@
1
+ import { Router } from 'express'
2
+ import AbstractService from './abstract.js'
3
+ import { ReviewRepository } from '../repositories/review.js'
4
+
5
+ export class ReviewService extends AbstractService {
6
+ public repository: ReviewRepository
7
+
8
+ constructor(parent: Router, repository: ReviewRepository) {
9
+ super(parent)
10
+ this.repository = repository
11
+ }
12
+
13
+ getBasePath() {
14
+ return 'reviews'
15
+ }
16
+ }
@@ -1,24 +1,48 @@
1
1
  import type {
2
+ Cart,
3
+ CartDraft,
2
4
  ShippingMethodDraft,
3
5
  TaxCategoryDraft,
6
+ ZoneDraft,
4
7
  } from '@commercetools/platform-sdk'
5
8
  import supertest from 'supertest'
6
9
  import { afterEach, beforeEach, describe, expect, test } from 'vitest'
7
10
  import { CommercetoolsMock } from '../index.js'
11
+ import { isType } from '../types.js'
8
12
 
9
13
  const ctMock = new CommercetoolsMock()
10
14
 
11
15
  describe('Shipping method', () => {
12
16
  beforeEach(async () => {
13
- const draft: TaxCategoryDraft = {
14
- name: 'foo',
15
- key: 'standard',
16
- rates: [],
17
- }
18
- const createResponse = await supertest(ctMock.app)
17
+ await supertest(ctMock.app)
19
18
  .post('/dummy/tax-categories')
20
- .send(draft)
21
- expect(createResponse.status).toEqual(201)
19
+ .send(
20
+ isType<TaxCategoryDraft>({
21
+ name: 'foo',
22
+ key: 'standard',
23
+ rates: [],
24
+ })
25
+ )
26
+ .then((res) => {
27
+ expect(res.status).toEqual(201)
28
+ })
29
+
30
+ await supertest(ctMock.app)
31
+ .post('/dummy/zones')
32
+ .send(
33
+ isType<ZoneDraft>({
34
+ name: 'The Netherlands',
35
+ key: 'NL',
36
+ locations: [
37
+ {
38
+ country: 'NL',
39
+ },
40
+ ],
41
+ })
42
+ )
43
+ .then((res) => {
44
+ expect(res.status).toEqual(201)
45
+ })
22
46
  })
23
47
 
24
48
  afterEach(async () => {
@@ -75,28 +99,103 @@ describe('Shipping method', () => {
75
99
  })
76
100
 
77
101
  test('Get shipping methods matching cart', async () => {
78
- const draft: ShippingMethodDraft = {
79
- name: 'foo',
80
- taxCategory: { typeId: 'tax-category', key: 'standard' },
81
- isDefault: true,
82
- zoneRates: [],
83
- }
84
- const createResponse = await supertest(ctMock.app)
102
+ const cart = await supertest(ctMock.app)
103
+ .post('/dummy/carts')
104
+ .send(
105
+ isType<CartDraft>({
106
+ currency: 'EUR',
107
+ shippingAddress: {
108
+ country: 'NL',
109
+ },
110
+ })
111
+ )
112
+ .then((res) => res.body as Cart)
113
+
114
+ await supertest(ctMock.app)
85
115
  .post('/dummy/shipping-methods')
86
- .send(draft)
116
+ .send(
117
+ isType<ShippingMethodDraft>({
118
+ name: 'NL',
119
+ taxCategory: { typeId: 'tax-category', key: 'standard' },
120
+ isDefault: true,
121
+ zoneRates: [
122
+ {
123
+ zone: {
124
+ typeId: 'zone',
125
+ key: 'NL',
126
+ },
127
+ shippingRates: [
128
+ {
129
+ price: {
130
+ currencyCode: 'EUR',
131
+ centAmount: 495,
132
+ },
133
+ },
134
+ ],
135
+ },
136
+ ],
137
+ })
138
+ )
139
+ .then((res) => {
140
+ expect(res.status).toEqual(201)
141
+ })
87
142
 
88
- expect(createResponse.status).toBe(201)
143
+ await supertest(ctMock.app)
144
+ .post('/dummy/shipping-methods')
145
+ .send(
146
+ isType<ShippingMethodDraft>({
147
+ name: 'NL/GBP',
148
+ taxCategory: { typeId: 'tax-category', key: 'standard' },
149
+ isDefault: true,
150
+ zoneRates: [
151
+ {
152
+ zone: {
153
+ typeId: 'zone',
154
+ key: 'NL',
155
+ },
156
+ shippingRates: [
157
+ {
158
+ price: {
159
+ currencyCode: 'GBP',
160
+ centAmount: 495,
161
+ },
162
+ },
163
+ ],
164
+ },
165
+ ],
166
+ })
167
+ )
168
+ .then((res) => {
169
+ expect(res.status).toEqual(201)
170
+ })
89
171
 
90
172
  const response = await supertest(ctMock.app).get(
91
- `/dummy/shipping-methods/matching-cart?cartId=fake-cart-id`
173
+ `/dummy/shipping-methods/matching-cart?cartId=${cart.id}`
92
174
  )
93
175
 
94
- expect(response.status).toBe(200)
95
- expect(response.body).toEqual({
176
+ expect(response.status, JSON.stringify(response.body)).toBe(200)
177
+ expect(response.body).toMatchObject({
96
178
  count: 1,
97
179
  limit: 20,
98
180
  offset: 0,
99
- results: [createResponse.body],
181
+ results: [
182
+ {
183
+ name: 'NL',
184
+ zoneRates: [
185
+ {
186
+ shippingRates: [
187
+ {
188
+ isMatching: true,
189
+ price: {
190
+ currencyCode: 'EUR',
191
+ centAmount: 495,
192
+ },
193
+ },
194
+ ],
195
+ },
196
+ ],
197
+ },
198
+ ],
100
199
  total: 1,
101
200
  })
102
201
  })
@@ -1,6 +1,8 @@
1
- import { Router } from 'express'
1
+ import { Request, Response, Router } from 'express'
2
2
  import { ShippingMethodRepository } from '../repositories/shipping-method.js'
3
3
  import AbstractService from './abstract.js'
4
+ import { getRepositoryContext } from '../repositories/helpers.js'
5
+ import { queryParamsValue } from '../helpers.js'
4
6
 
5
7
  export class ShippingMethodService extends AbstractService {
6
8
  public repository: ShippingMethodRepository
@@ -16,6 +18,21 @@ export class ShippingMethodService extends AbstractService {
16
18
  }
17
19
 
18
20
  extraRoutes(parent: Router) {
19
- parent.get('/matching-cart', this.get.bind(this))
21
+ parent.get('/matching-cart', this.matchingCart.bind(this))
22
+ }
23
+
24
+ matchingCart(request: Request, response: Response) {
25
+ const cartId = queryParamsValue(request.query.cartId)
26
+ if (!cartId) {
27
+ return response.status(400).send()
28
+ }
29
+ const result = this.repository.matchingCart(
30
+ getRepositoryContext(request),
31
+ cartId,
32
+ {
33
+ expand: this._parseParam(request.query.expand),
34
+ }
35
+ )
36
+ return response.status(200).send(result)
20
37
  }
21
38
  }