@labdigital/commercetools-mock 2.8.0 → 2.10.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/README.md +9 -4
- package/dist/index.cjs +237 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -2
- package/dist/index.d.ts +33 -2
- package/dist/index.js +237 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/predicateParser.test.ts +96 -11
- package/src/lib/predicateParser.ts +43 -12
- package/src/oauth/server.ts +11 -1
- package/src/repositories/abstract.ts +5 -4
- package/src/repositories/cart.ts +86 -33
- package/src/repositories/custom-object.ts +18 -0
- package/src/repositories/shipping-method.ts +98 -22
- package/src/services/cart.test.ts +24 -0
- package/src/services/custom-object.ts +19 -0
- package/src/services/shipping-method.test.ts +120 -21
- package/src/services/shipping-method.ts +19 -2
- package/src/shippingCalculator.test.ts +280 -0
- package/src/shippingCalculator.ts +74 -0
- package/src/storage/abstract.ts +1 -1
- package/src/storage/in-memory.ts +82 -59
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
+
const value = resolveValue(obj, left)
|
|
387
|
+
return inValues.includes(value)
|
|
357
388
|
}
|
|
358
389
|
})
|
|
359
390
|
.led('MATCHES_IGNORE_CASE', 20, ({ left, bp }) => {
|
package/src/oauth/server.ts
CHANGED
|
@@ -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) {
|
|
@@ -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
|
-
|
|
122
|
-
where: params.where,
|
|
123
|
-
offset: params.offset,
|
|
124
|
-
limit: params.limit,
|
|
125
|
+
...params,
|
|
125
126
|
})
|
|
126
127
|
|
|
127
128
|
// @ts-ignore
|
package/src/repositories/cart.ts
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
|
-
import
|
|
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 {
|
|
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: [],
|
|
@@ -219,6 +230,15 @@ export class CartRepository extends AbstractResourceRepository<'cart'> {
|
|
|
219
230
|
// Update cart total price
|
|
220
231
|
resource.totalPrice.centAmount = calculateCartTotalPrice(resource)
|
|
221
232
|
},
|
|
233
|
+
recalculate: () => {
|
|
234
|
+
// Dummy action when triggering a recalculation of the cart
|
|
235
|
+
//
|
|
236
|
+
// From commercetools documentation:
|
|
237
|
+
// This update action does not set any Cart field in particular,
|
|
238
|
+
// but it triggers several Cart updates to bring prices and discounts to the latest state.
|
|
239
|
+
// Those can become stale over time when no Cart updates have been performed for a while
|
|
240
|
+
// and prices on related Products have changed in the meanwhile.
|
|
241
|
+
},
|
|
222
242
|
addItemShippingAddress: (
|
|
223
243
|
context: RepositoryContext,
|
|
224
244
|
resource: Writable<Cart>,
|
|
@@ -335,10 +355,6 @@ export class CartRepository extends AbstractResourceRepository<'cart'> {
|
|
|
335
355
|
shippingMethod
|
|
336
356
|
)
|
|
337
357
|
|
|
338
|
-
if (!method) {
|
|
339
|
-
throw new Error(`Type ${shippingMethod} not found`)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
358
|
// Based on the address we should select a shipping zone and
|
|
343
359
|
// use that to define the price.
|
|
344
360
|
// @ts-ignore
|
|
@@ -377,6 +393,43 @@ export class CartRepository extends AbstractResourceRepository<'cart'> {
|
|
|
377
393
|
}
|
|
378
394
|
resource.custom.fields[name] = value
|
|
379
395
|
},
|
|
396
|
+
setCustomShippingMethod: (
|
|
397
|
+
context: RepositoryContext,
|
|
398
|
+
resource: Writable<Cart>,
|
|
399
|
+
{
|
|
400
|
+
shippingMethodName,
|
|
401
|
+
shippingRate,
|
|
402
|
+
taxCategory,
|
|
403
|
+
externalTaxRate,
|
|
404
|
+
}: CartSetCustomShippingMethodAction
|
|
405
|
+
) => {
|
|
406
|
+
if (externalTaxRate) {
|
|
407
|
+
throw new Error('External tax rate is not supported')
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const tax = taxCategory
|
|
411
|
+
? this._storage.getByResourceIdentifier<'tax-category'>(
|
|
412
|
+
context.projectKey,
|
|
413
|
+
taxCategory
|
|
414
|
+
)
|
|
415
|
+
: undefined
|
|
416
|
+
|
|
417
|
+
resource.shippingInfo = {
|
|
418
|
+
shippingMethodName,
|
|
419
|
+
price: createCentPrecisionMoney(shippingRate.price),
|
|
420
|
+
shippingRate: {
|
|
421
|
+
price: createTypedMoney(shippingRate.price),
|
|
422
|
+
tiers: [],
|
|
423
|
+
},
|
|
424
|
+
taxCategory: tax
|
|
425
|
+
? {
|
|
426
|
+
typeId: 'tax-category',
|
|
427
|
+
id: tax?.id,
|
|
428
|
+
}
|
|
429
|
+
: undefined,
|
|
430
|
+
shippingMethodState: 'MatchesCart',
|
|
431
|
+
}
|
|
432
|
+
},
|
|
380
433
|
setCustomType: (
|
|
381
434
|
context: RepositoryContext,
|
|
382
435
|
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,
|
|
@@ -1,34 +1,41 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 {
|
|
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'],
|
|
@@ -303,6 +303,30 @@ describe('Cart Update Actions', () => {
|
|
|
303
303
|
expect(response.body.lineItems).toHaveLength(0)
|
|
304
304
|
})
|
|
305
305
|
|
|
306
|
+
test('recalculate', async () => {
|
|
307
|
+
const product = await supertest(ctMock.app)
|
|
308
|
+
.post(`/dummy/products`)
|
|
309
|
+
.send(productDraft)
|
|
310
|
+
.then((x) => x.body)
|
|
311
|
+
|
|
312
|
+
assert(cart, 'cart not created')
|
|
313
|
+
|
|
314
|
+
const response = await supertest(ctMock.app)
|
|
315
|
+
.post(`/dummy/carts/${cart.id}`)
|
|
316
|
+
.send({
|
|
317
|
+
version: 1,
|
|
318
|
+
actions: [
|
|
319
|
+
{
|
|
320
|
+
action: 'recalculate',
|
|
321
|
+
updateProductData: true,
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
expect(response.status).toBe(200)
|
|
327
|
+
expect(response.body.version).toBe(1)
|
|
328
|
+
})
|
|
329
|
+
|
|
306
330
|
test('removeLineItem', async () => {
|
|
307
331
|
const product = await supertest(ctMock.app)
|
|
308
332
|
.post(`/dummy/products`)
|
|
@@ -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),
|