@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.
@@ -0,0 +1,280 @@
1
+ import {
2
+ Cart,
3
+ ShippingRate,
4
+ ShippingRatePriceTier,
5
+ } from '@commercetools/platform-sdk'
6
+ import { describe, expect, it } from 'vitest'
7
+ import {
8
+ markMatchingShippingRate,
9
+ markMatchingShippingRatePriceTiers,
10
+ } from './shippingCalculator'
11
+
12
+ // describe('markMatchingShippingMethods', () => {
13
+ // const zones: Record<string, Zone> = {
14
+ // NL: {
15
+ // id: '45b39469-4f3d-4d03-9e24-ed6bd5c038d9',
16
+ // createdAt: '2021-08-05T09:00:00.000Z',
17
+ // lastModifiedAt: '2021-08-05T09:00:00.000Z',
18
+ // version: 1,
19
+ // name: 'NL',
20
+ // description: '',
21
+ // locations: [
22
+ // {
23
+ // country: 'NL',
24
+ // },
25
+ // ],
26
+ // },
27
+ // DE: {
28
+ // id: '45b39469-4f3d-4d03-9e24-ed6bd5c038d9',
29
+ // createdAt: '2021-08-05T09:00:00.000Z',
30
+ // lastModifiedAt: '2021-08-05T09:00:00.000Z',
31
+ // version: 1,
32
+ // name: 'DE',
33
+ // description: '',
34
+ // locations: [
35
+ // {
36
+ // country: 'DE',
37
+ // },
38
+ // ],
39
+ // },
40
+ // }
41
+
42
+ // const shippingMethods: Record<string, Partial<ShippingMethod>> = {
43
+ // default: {
44
+ // id: '1c39b73f-186c-4711-8fd9-de60ec561ac0',
45
+ // key: 'default',
46
+ // zoneRates: [
47
+ // {
48
+ // zone: {
49
+ // typeId: 'zone',
50
+ // id: '45b39469-4f3d-4d03-9e24-ed6bd5c038d9',
51
+ // obj: zones['NL'],
52
+ // },
53
+ // shippingRates: [
54
+ // {
55
+ // price: {
56
+ // type: 'centPrecision',
57
+ // currencyCode: 'EUR',
58
+ // centAmount: 495,
59
+ // fractionDigits: 2,
60
+ // },
61
+ // freeAbove: {
62
+ // type: 'centPrecision',
63
+ // currencyCode: 'USD',
64
+ // centAmount: 5000,
65
+ // fractionDigits: 2,
66
+ // },
67
+ // tiers: [],
68
+ // },
69
+ // ],
70
+ // },
71
+ // ],
72
+ // isDefault: true,
73
+ // },
74
+ // tiered: {
75
+ // id: '2c39b73f-186c-4711-8fd9-de60ec561ac0',
76
+ // key: 'tiered',
77
+ // description: 'More expensive optional one',
78
+ // zoneRates: [
79
+ // {
80
+ // zone: {
81
+ // typeId: 'zone',
82
+ // id: '45b39469-4f3d-4d03-9e24-ed6bd5c038d9',
83
+ // obj: zones['NL'],
84
+ // },
85
+ // shippingRates: [
86
+ // {
87
+ // price: {
88
+ // type: 'centPrecision',
89
+ // currencyCode: 'EUR',
90
+ // centAmount: 495,
91
+ // fractionDigits: 2,
92
+ // },
93
+ // freeAbove: {
94
+ // type: 'centPrecision',
95
+ // currencyCode: 'USD',
96
+ // centAmount: 5000,
97
+ // fractionDigits: 2,
98
+ // },
99
+ // tiers: [],
100
+ // },
101
+ // ],
102
+ // },
103
+ // ],
104
+ // isDefault: false,
105
+ // },
106
+ // german: {
107
+ // id: '8c39b73f-186c-4711-8fd9-de60ec561ac0',
108
+ // key: 'tiered',
109
+ // description: 'More expensive optional one',
110
+ // zoneRates: [
111
+ // {
112
+ // zone: {
113
+ // typeId: 'zone',
114
+ // id: '45b39469-4f3d-4d03-9e24-ed6bd5c038d9',
115
+ // obj: zones['DE'],
116
+ // },
117
+ // shippingRates: [
118
+ // {
119
+ // price: {
120
+ // type: 'centPrecision',
121
+ // currencyCode: 'EUR',
122
+ // centAmount: 495,
123
+ // fractionDigits: 2,
124
+ // },
125
+ // freeAbove: {
126
+ // type: 'centPrecision',
127
+ // currencyCode: 'USD',
128
+ // centAmount: 5000,
129
+ // fractionDigits: 2,
130
+ // },
131
+ // tiers: [],
132
+ // },
133
+ // ],
134
+ // },
135
+ // ],
136
+ // isDefault: false,
137
+ // },
138
+ // }
139
+
140
+ // it('should mark the default shipping method', () => {
141
+ // const cart: Partial<Cart> = {
142
+ // id: '1',
143
+ // version: 1,
144
+ // totalPrice: {
145
+ // currencyCode: 'EUR',
146
+ // centAmount: 1000,
147
+ // fractionDigits: 2,
148
+ // type: 'centPrecision',
149
+ // },
150
+ // shippingAddress: {
151
+ // country: 'NL',
152
+ // },
153
+ // lineItems: [],
154
+ // customLineItems: [],
155
+ // }
156
+
157
+ // const data = markMatchingShippingMethods(
158
+ // cart as Cart,
159
+ // Object.values(shippingMethods) as ShippingMethod[]
160
+ // )
161
+ // })
162
+ // })
163
+
164
+ describe('markMatchingShippingRate', () => {
165
+ const rate: ShippingRate = {
166
+ price: {
167
+ type: 'centPrecision',
168
+ currencyCode: 'EUR',
169
+ centAmount: 495,
170
+ fractionDigits: 2,
171
+ },
172
+ freeAbove: {
173
+ type: 'centPrecision',
174
+ currencyCode: 'USD',
175
+ centAmount: 5000,
176
+ fractionDigits: 2,
177
+ },
178
+ tiers: [],
179
+ }
180
+
181
+ it('should mark the shipping rate as matching', () => {
182
+ const cart: Partial<Cart> = {
183
+ totalPrice: {
184
+ currencyCode: 'EUR',
185
+ centAmount: 1000,
186
+ fractionDigits: 2,
187
+ type: 'centPrecision',
188
+ },
189
+ }
190
+
191
+ const result = markMatchingShippingRate(cart as Cart, rate)
192
+ expect(result).toMatchObject({
193
+ ...rate,
194
+ isMatching: true,
195
+ })
196
+ })
197
+
198
+ it('should mark the shipping rate as not matching', () => {
199
+ const cart: Partial<Cart> = {
200
+ totalPrice: {
201
+ currencyCode: 'USD',
202
+ centAmount: 1000,
203
+ fractionDigits: 2,
204
+ type: 'centPrecision',
205
+ },
206
+ }
207
+
208
+ const result = markMatchingShippingRate(cart as Cart, rate)
209
+ expect(result).toMatchObject({
210
+ ...rate,
211
+ isMatching: false,
212
+ })
213
+ })
214
+ })
215
+
216
+ describe('markMatchingShippingRatePriceTiers', () => {
217
+ it('should handle CartValue types', () => {
218
+ const tiers: ShippingRatePriceTier[] = [
219
+ // Above 100 euro shipping is 4 euro
220
+ {
221
+ type: 'CartValue',
222
+ minimumCentAmount: 10000,
223
+ price: {
224
+ type: 'centPrecision',
225
+ currencyCode: 'EUR',
226
+ centAmount: 400,
227
+ fractionDigits: 2,
228
+ },
229
+ },
230
+ // Above 200 euro shipping is 3 euro
231
+ {
232
+ type: 'CartValue',
233
+ minimumCentAmount: 20000,
234
+ price: {
235
+ type: 'centPrecision',
236
+ currencyCode: 'EUR',
237
+ centAmount: 300,
238
+ fractionDigits: 2,
239
+ },
240
+ },
241
+ // Above 50 euro shipping is 5 euro
242
+ {
243
+ type: 'CartValue',
244
+ minimumCentAmount: 500,
245
+ price: {
246
+ type: 'centPrecision',
247
+ currencyCode: 'EUR',
248
+ centAmount: 700,
249
+ fractionDigits: 2,
250
+ },
251
+ },
252
+ ]
253
+
254
+ // Create a cart with a total price of 90 euro
255
+ const cart: Partial<Cart> = {
256
+ totalPrice: {
257
+ currencyCode: 'EUR',
258
+ centAmount: 9000,
259
+ fractionDigits: 2,
260
+ type: 'centPrecision',
261
+ },
262
+ }
263
+
264
+ const result = markMatchingShippingRatePriceTiers(cart as Cart, tiers)
265
+ expect(result).toMatchObject([
266
+ {
267
+ minimumCentAmount: 10000,
268
+ isMatching: false,
269
+ },
270
+ {
271
+ minimumCentAmount: 20000,
272
+ isMatching: false,
273
+ },
274
+ {
275
+ minimumCentAmount: 500,
276
+ isMatching: true,
277
+ },
278
+ ])
279
+ })
280
+ })
@@ -0,0 +1,74 @@
1
+ import {
2
+ Cart,
3
+ CartValueTier,
4
+ ShippingRate,
5
+ ShippingRatePriceTier,
6
+ } from '@commercetools/platform-sdk'
7
+
8
+ export const markMatchingShippingRate = (
9
+ cart: Cart,
10
+ shippingRate: ShippingRate
11
+ ): ShippingRate => {
12
+ const isMatching =
13
+ shippingRate.price.currencyCode === cart.totalPrice.currencyCode
14
+ return {
15
+ ...shippingRate,
16
+ tiers: markMatchingShippingRatePriceTiers(cart, shippingRate.tiers),
17
+ isMatching: isMatching,
18
+ }
19
+ }
20
+
21
+ export const markMatchingShippingRatePriceTiers = (
22
+ cart: Cart,
23
+ tiers: ShippingRatePriceTier[]
24
+ ): ShippingRatePriceTier[] => {
25
+ if (tiers.length === 0) {
26
+ return []
27
+ }
28
+
29
+ if (new Set(tiers.map((tier) => tier.type)).size > 1) {
30
+ throw new Error("Can't handle multiple types of tiers")
31
+ }
32
+
33
+ const tierType = tiers[0].type
34
+ switch (tierType) {
35
+ case 'CartValue':
36
+ return markMatchingCartValueTiers(cart, tiers as CartValueTier[])
37
+ // case 'CartClassification':
38
+ // return markMatchingCartClassificationTiers(cart, tiers)
39
+ // case 'CartScore':
40
+ // return markMatchingCartScoreTiers(cart, tiers)
41
+ default:
42
+ throw new Error(`Unsupported tier type: ${tierType}`)
43
+ }
44
+ }
45
+
46
+ const markMatchingCartValueTiers = (
47
+ cart: Cart,
48
+ tiers: readonly CartValueTier[]
49
+ ): ShippingRatePriceTier[] => {
50
+ // Sort tiers from high to low since we only want to match the highest tier
51
+ const sortedTiers = [...tiers].sort(
52
+ (a, b) => b.minimumCentAmount - a.minimumCentAmount
53
+ )
54
+
55
+ // Find the first tier that matches the cart and set the flag. We push
56
+ // the results into a map so that we can output the tiers in the same order as
57
+ // we received them.
58
+ const result: Record<number, ShippingRatePriceTier> = {}
59
+ let hasMatchingTier = false
60
+ for (const tier of sortedTiers) {
61
+ const isMatching =
62
+ !hasMatchingTier &&
63
+ cart.totalPrice.currencyCode === tier.price.currencyCode &&
64
+ cart.totalPrice.centAmount >= tier.minimumCentAmount
65
+
66
+ if (isMatching) hasMatchingTier = true
67
+ result[tier.minimumCentAmount] = {
68
+ ...tier,
69
+ isMatching: isMatching,
70
+ }
71
+ }
72
+
73
+ return tiers.map((tier) => result[tier.minimumCentAmount])
74
+ }
@@ -68,7 +68,7 @@ export abstract class AbstractStorage {
68
68
  abstract getByResourceIdentifier<RT extends ResourceType>(
69
69
  projectKey: string,
70
70
  identifier: ResourceIdentifier
71
- ): ResourceMap[RT] | null
71
+ ): ResourceMap[RT]
72
72
 
73
73
  abstract expand<T>(
74
74
  projectKey: string,
@@ -1,39 +1,41 @@
1
- import type {
2
- AssociateRole,
3
- AttributeGroup,
4
- BusinessUnit,
5
- Cart,
6
- CartDiscount,
7
- Category,
8
- Channel,
9
- Customer,
10
- CustomerGroup,
11
- CustomObject,
12
- DiscountCode,
13
- Extension,
14
- InvalidInputError,
15
- InventoryEntry,
16
- Order,
17
- PagedQueryResponse,
18
- Payment,
19
- Product,
20
- ProductDiscount,
21
- ProductProjection,
22
- ProductType,
23
- Project,
24
- Quote,
25
- QuoteRequest,
26
- Reference,
27
- ResourceIdentifier,
28
- ShippingMethod,
29
- ShoppingList,
30
- StagedQuote,
31
- State,
32
- Store,
33
- Subscription,
34
- TaxCategory,
35
- Type,
36
- Zone,
1
+ import {
2
+ ReferencedResourceNotFoundError,
3
+ type AssociateRole,
4
+ type AttributeGroup,
5
+ type BusinessUnit,
6
+ type Cart,
7
+ type CartDiscount,
8
+ type Category,
9
+ type Channel,
10
+ type Customer,
11
+ type CustomerGroup,
12
+ type CustomObject,
13
+ type DiscountCode,
14
+ type Extension,
15
+ type InvalidInputError,
16
+ type InventoryEntry,
17
+ type Order,
18
+ type PagedQueryResponse,
19
+ type Payment,
20
+ type Product,
21
+ type ProductDiscount,
22
+ type ProductProjection,
23
+ type ProductType,
24
+ type Project,
25
+ type Quote,
26
+ type QuoteRequest,
27
+ type Reference,
28
+ type ResourceIdentifier,
29
+ type ShippingMethod,
30
+ type ShoppingList,
31
+ type StagedQuote,
32
+ type State,
33
+ type Store,
34
+ type Subscription,
35
+ type TaxCategory,
36
+ type Type,
37
+ type Zone,
38
+ InvalidJsonInputError,
37
39
  } from '@commercetools/platform-sdk'
38
40
  import assert from 'assert'
39
41
  import { CommercetoolsError } from '../exceptions.js'
@@ -204,9 +206,17 @@ export class InMemoryStorage extends AbstractStorage {
204
206
 
205
207
  // Apply predicates
206
208
  if (params.where) {
209
+ // Get all key-value pairs starting with 'var.' to pass as variables, removing
210
+ // the 'var.' prefix.
211
+ const vars = Object.fromEntries(
212
+ Object.entries(params)
213
+ .filter(([key]) => key.startsWith('var.'))
214
+ .map(([key, value]) => [key.slice(4), value])
215
+ )
216
+
207
217
  try {
208
218
  const filterFunc = parseQueryExpression(params.where)
209
- resources = resources.filter((resource) => filterFunc(resource, {}))
219
+ resources = resources.filter((resource) => filterFunc(resource, vars))
210
220
  } catch (err) {
211
221
  throw new CommercetoolsError<InvalidInputError>(
212
222
  {
@@ -292,38 +302,51 @@ export class InMemoryStorage extends AbstractStorage {
292
302
  getByResourceIdentifier<RT extends ResourceType>(
293
303
  projectKey: string,
294
304
  identifier: ResourceIdentifier
295
- ): ResourceMap[RT] | null {
305
+ ): ResourceMap[RT] {
296
306
  if (identifier.id) {
297
307
  const resource = this.get(projectKey, identifier.typeId, identifier.id)
298
308
  if (resource) {
299
309
  return resource as ResourceMap[RT]
300
310
  }
301
- console.error(
302
- `No resource found with typeId=${identifier.typeId}, id=${identifier.id}`
303
- )
304
- return null
311
+
312
+ throw new CommercetoolsError<ReferencedResourceNotFoundError>({
313
+ code: 'ReferencedResourceNotFound',
314
+ message:
315
+ `The referenced object of type '${identifier.typeId}' with id ` +
316
+ `'${identifier.id}' was not found. It either doesn't exist, or it ` +
317
+ `can't be accessed from this endpoint (e.g., if the endpoint ` +
318
+ `filters by store or customer account).`,
319
+ typeId: identifier.typeId,
320
+ id: identifier.id,
321
+ })
305
322
  }
306
323
 
307
324
  if (identifier.key) {
308
- const store = this.forProjectKey(projectKey)[identifier.typeId]
309
-
310
- if (store) {
311
- // TODO: BaseResource has no key attribute, but the subclasses should
312
- // have them all.
313
- const resource = Array.from(store.values()).find(
314
- // @ts-ignore
315
- (r) => r.key === identifier.key
316
- )
317
- if (resource) {
318
- return resource as ResourceMap[RT]
319
- }
320
- } else {
321
- throw new Error(
322
- `No storage found for resource type: ${identifier.typeId}`
323
- )
325
+ const resource = this.getByKey(
326
+ projectKey,
327
+ identifier.typeId,
328
+ identifier.key
329
+ )
330
+ if (resource) {
331
+ return resource as ResourceMap[RT]
324
332
  }
333
+
334
+ throw new CommercetoolsError<ReferencedResourceNotFoundError>({
335
+ code: 'ReferencedResourceNotFound',
336
+ message:
337
+ `The referenced object of type '${identifier.typeId}' with key ` +
338
+ `'${identifier.key}' was not found. It either doesn't exist, or it ` +
339
+ `can't be accessed from this endpoint (e.g., if the endpoint ` +
340
+ `filters by store or customer account).`,
341
+ typeId: identifier.typeId,
342
+ key: identifier.key,
343
+ })
325
344
  }
326
- return null
345
+ throw new CommercetoolsError<InvalidJsonInputError>({
346
+ code: 'InvalidJsonInput',
347
+ message: 'Request body does not contain valid JSON.',
348
+ detailedErrorMessage: "ResourceIdentifier requires an 'id' xor a 'key'",
349
+ })
327
350
  }
328
351
 
329
352
  addProject = (projectKey: string): Project => {
@@ -415,12 +438,15 @@ export class InMemoryStorage extends AbstractStorage {
415
438
  reference.typeId !== undefined &&
416
439
  (reference.id !== undefined || reference.key !== undefined)
417
440
  ) {
418
- // @ts-ignore
419
- reference.obj = this.getByResourceIdentifier(projectKey, {
420
- typeId: reference.typeId,
421
- id: reference.id,
422
- key: reference.key,
423
- } as ResourceIdentifier)
441
+ // First check if the object is already resolved. This is the case when
442
+ // the complete resource is pushed via the .add() method.
443
+ if (!reference.obj) {
444
+ reference.obj = this.getByResourceIdentifier(projectKey, {
445
+ typeId: reference.typeId,
446
+ id: reference.id,
447
+ key: reference.key,
448
+ } as ResourceIdentifier)
449
+ }
424
450
  if (expand) {
425
451
  this._resolveResource(projectKey, reference.obj, expand)
426
452
  }
package/src/types.ts CHANGED
@@ -2,6 +2,8 @@ import type * as ctp from '@commercetools/platform-sdk'
2
2
  import { RepositoryMap } from './repositories/index.js'
3
3
  import AbstractService from './services/abstract.js'
4
4
 
5
+ export const isType = <T>(x: T) => x
6
+
5
7
  export type Writable<T> = { -readonly [P in keyof T]: Writable<T[P]> }
6
8
  export type ShallowWritable<T> = { -readonly [P in keyof T]: T[P] }
7
9