@labdigital/commercetools-mock 2.45.1 → 2.47.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.
Files changed (80) hide show
  1. package/dist/index.cjs +614 -250
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +788 -59
  4. package/dist/index.d.ts +788 -59
  5. package/dist/index.js +602 -238
  6. package/dist/index.js.map +1 -1
  7. package/package.json +41 -48
  8. package/src/ctMock.ts +11 -13
  9. package/src/index.test.ts +5 -5
  10. package/src/lib/predicateParser.test.ts +91 -60
  11. package/src/lib/predicateParser.ts +38 -42
  12. package/src/lib/productSearchFilter.test.ts +18 -0
  13. package/src/lib/productSearchFilter.ts +7 -0
  14. package/src/lib/projectionSearchFilter.test.ts +17 -17
  15. package/src/lib/projectionSearchFilter.ts +2 -3
  16. package/src/oauth/server.test.ts +1 -1
  17. package/src/oauth/server.ts +11 -11
  18. package/src/priceSelector.ts +1 -1
  19. package/src/product-projection-search.ts +18 -19
  20. package/src/product-search.ts +48 -8
  21. package/src/repositories/business-unit.ts +17 -16
  22. package/src/repositories/cart/actions.ts +32 -32
  23. package/src/repositories/cart/helpers.ts +1 -1
  24. package/src/repositories/cart/index.ts +25 -8
  25. package/src/repositories/cart-discount/actions.ts +1 -4
  26. package/src/repositories/category/actions.ts +2 -6
  27. package/src/repositories/custom-object.ts +20 -21
  28. package/src/repositories/customer/actions.ts +4 -4
  29. package/src/repositories/errors.ts +1 -1
  30. package/src/repositories/extension.ts +2 -1
  31. package/src/repositories/helpers.ts +27 -27
  32. package/src/repositories/index.ts +17 -17
  33. package/src/repositories/my-customer.ts +1 -1
  34. package/src/repositories/my-order.ts +2 -2
  35. package/src/repositories/order/index.ts +1 -1
  36. package/src/repositories/product/actions.ts +1 -1
  37. package/src/repositories/quote/actions.ts +83 -0
  38. package/src/repositories/quote/index.ts +54 -0
  39. package/src/repositories/quote-request/actions.ts +84 -0
  40. package/src/repositories/quote-request/index.test.ts +167 -0
  41. package/src/repositories/quote-request/index.ts +67 -0
  42. package/src/repositories/quote-staged/actions.ts +84 -0
  43. package/src/repositories/quote-staged/index.ts +47 -0
  44. package/src/repositories/review.ts +4 -4
  45. package/src/repositories/shipping-method/actions.ts +17 -17
  46. package/src/repositories/shipping-method/index.ts +6 -6
  47. package/src/repositories/shopping-list/actions.ts +1 -1
  48. package/src/repositories/shopping-list/index.ts +9 -1
  49. package/src/repositories/subscription.ts +2 -4
  50. package/src/server.ts +3 -2
  51. package/src/services/abstract.ts +7 -7
  52. package/src/services/as-associate-order.test.ts +1 -1
  53. package/src/services/cart-discount.test.ts +1 -1
  54. package/src/services/cart.test.ts +40 -15
  55. package/src/services/category.test.ts +1 -1
  56. package/src/services/customer.test.ts +16 -55
  57. package/src/services/customer.ts +1 -1
  58. package/src/services/index.ts +20 -14
  59. package/src/services/inventory-entry.test.ts +5 -5
  60. package/src/services/my-cart.test.ts +2 -2
  61. package/src/services/my-customer.test.ts +2 -2
  62. package/src/services/order.test.ts +8 -8
  63. package/src/services/product-projection.test.ts +5 -5
  64. package/src/services/product-projection.ts +12 -14
  65. package/src/services/product.test.ts +155 -71
  66. package/src/services/quote-request.test.ts +59 -0
  67. package/src/services/quote-request.ts +16 -0
  68. package/src/services/quote-staged.ts +16 -0
  69. package/src/services/quote.ts +16 -0
  70. package/src/services/standalone-price.test.ts +4 -4
  71. package/src/services/state.test.ts +1 -1
  72. package/src/services/store.test.ts +2 -2
  73. package/src/services/tax-category.test.ts +1 -1
  74. package/src/shipping.ts +3 -3
  75. package/src/storage/in-memory.ts +55 -63
  76. package/src/testing/customer.ts +40 -0
  77. package/src/types.ts +51 -31
  78. package/src/repositories/quote-request.ts +0 -17
  79. package/src/repositories/quote.ts +0 -14
  80. package/src/repositories/staged-quote.ts +0 -17
@@ -5,7 +5,7 @@
5
5
  * See https://docs.commercetools.com/api/predicates/query
6
6
  */
7
7
  import { haversineDistance } from "./haversine";
8
- import { Lexer, Parser, type ITokenPosition } from "./parser";
8
+ import { type ITokenPosition, Lexer, Parser } from "./parser";
9
9
 
10
10
  export class PredicateError {
11
11
  message: string;
@@ -34,10 +34,9 @@ export const matchesPredicate = (
34
34
  const func = generateMatchFunc(item);
35
35
  return func(target, variables ?? {});
36
36
  });
37
- } else {
38
- const func = generateMatchFunc(predicate);
39
- return func(target, variables ?? {});
40
37
  }
38
+ const func = generateMatchFunc(predicate);
39
+ return func(target, variables ?? {});
41
40
  };
42
41
 
43
42
  export const parseQueryExpression = (
@@ -47,9 +46,8 @@ export const parseQueryExpression = (
47
46
  const callbacks = predicate.map((item) => generateMatchFunc(item));
48
47
  return (target: any, variables: VariableMap) =>
49
48
  callbacks.every((callback) => callback(target, variables));
50
- } else {
51
- return generateMatchFunc(predicate);
52
49
  }
50
+ return generateMatchFunc(predicate);
53
51
  };
54
52
 
55
53
  type TypeSymbol = {
@@ -124,6 +122,12 @@ const getLexer = (value: string) =>
124
122
  .token("IS", /is(?![-_a-z0-9]+)/i)
125
123
  .token("DEFINED", /defined(?![-_a-z0-9]+)/i)
126
124
 
125
+ // Special case for UUID identifiers,
126
+ // since they otherwise would get matched as INT, when starting with a digit
127
+ .token(
128
+ "IDENTIFIER",
129
+ /[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/,
130
+ )
127
131
  .token("FLOAT", /\d+\.\d+/)
128
132
  .token("INT", /\d+/)
129
133
  .token("VARIABLE", /:([-_A-Za-z0-9]+)/)
@@ -171,7 +175,7 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
171
175
  (t) =>
172
176
  ({
173
177
  type: "boolean",
174
- value: t.token.match === "true" ? true : false,
178
+ value: t.token.match === "true",
175
179
  pos: t.token.strpos(),
176
180
  }) as TypeSymbol,
177
181
  )
@@ -203,7 +207,7 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
203
207
  (t) =>
204
208
  ({
205
209
  type: "int",
206
- value: parseInt(t.token.match, 10),
210
+ value: Number.parseInt(t.token.match, 10),
207
211
  pos: t.token.strpos(),
208
212
  }) as TypeSymbol,
209
213
  )
@@ -213,7 +217,7 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
213
217
  (t) =>
214
218
  ({
215
219
  type: "float",
216
- value: parseFloat(t.token.match),
220
+ value: Number.parseFloat(t.token.match),
217
221
  pos: t.token.strpos(),
218
222
  }) as TypeSymbol,
219
223
  )
@@ -236,9 +240,8 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
236
240
  const expr: any = parser.parse({ terminals: [bp - 1] });
237
241
  if (Array.isArray(expr)) {
238
242
  return [left, ...expr];
239
- } else {
240
- return [left, expr];
241
243
  }
244
+ return [left, expr];
242
245
  })
243
246
  .nud("(", 100, (t) => {
244
247
  const expr: any = parser.parse({ terminals: [")"] });
@@ -256,17 +259,16 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
256
259
  }
257
260
  return false;
258
261
  });
259
- } else {
260
- const value = resolveValue(obj, left);
261
- if (value) {
262
- if (Array.isArray(value)) {
263
- return value.some((item) => expr(item, vars));
264
- }
265
-
266
- return expr(value, vars);
262
+ }
263
+ const value = resolveValue(obj, left);
264
+ if (value) {
265
+ if (Array.isArray(value)) {
266
+ return value.some((item) => expr(item, vars));
267
267
  }
268
- return false;
268
+
269
+ return expr(value, vars);
269
270
  }
271
+ return false;
270
272
  };
271
273
  })
272
274
  .bp(")", 0)
@@ -284,14 +286,13 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
284
286
  }
285
287
  return value === other;
286
288
  });
287
- } else {
288
- const resolvedValue = resolveValue(obj, left);
289
- const resolvedSymbol = resolveSymbol(expr, vars);
290
- if (Array.isArray(resolvedValue)) {
291
- return !!resolvedValue.some((elem) => elem === resolvedSymbol);
292
- }
293
- return resolvedValue === resolvedSymbol;
294
289
  }
290
+ const resolvedValue = resolveValue(obj, left);
291
+ const resolvedSymbol = resolveSymbol(expr, vars);
292
+ if (Array.isArray(resolvedValue)) {
293
+ return !!resolvedValue.some((elem) => elem === resolvedSymbol);
294
+ }
295
+ return resolvedValue === resolvedSymbol;
295
296
  };
296
297
  })
297
298
  .led("!=", 20, ({ left, bp }) => {
@@ -347,12 +348,11 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
347
348
  const val = resolveValue(obj, left);
348
349
  return val.length === 0;
349
350
  };
350
- } else {
351
- return (obj: any, vars: VariableMap) => {
352
- const val = resolveValue(obj, left);
353
- return val.length !== 0;
354
- };
355
351
  }
352
+ return (obj: any, vars: VariableMap) => {
353
+ const val = resolveValue(obj, left);
354
+ return val.length !== 0;
355
+ };
356
356
  }
357
357
  case "defined": {
358
358
  if (!invert) {
@@ -360,12 +360,11 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
360
360
  const val = resolveValue(obj, left);
361
361
  return val !== undefined;
362
362
  };
363
- } else {
364
- return (obj: any, vars: VariableMap) => {
365
- const val = resolveValue(obj, left);
366
- return val === undefined;
367
- };
368
363
  }
364
+ return (obj: any, vars: VariableMap) => {
365
+ const val = resolveValue(obj, left);
366
+ return val === undefined;
367
+ };
369
368
  }
370
369
  default: {
371
370
  throw new Error("Unexpected");
@@ -457,9 +456,8 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
457
456
  const array = expr.map((item: TypeSymbol) => resolveSymbol(item, vars));
458
457
  if (keyword.type === "ALL") {
459
458
  return array.every((item: any) => value.includes(item));
460
- } else {
461
- return array.some((item: any) => value.includes(item));
462
459
  }
460
+ return array.some((item: any) => value.includes(item));
463
461
  };
464
462
  })
465
463
 
@@ -472,9 +470,7 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
472
470
  const column = lines[lines.length - 1].length;
473
471
 
474
472
  throw new PredicateError(
475
- `Unexpected end of input, expected SphereIdentifierChar, comparison ` +
476
- `operator, not, in, contains, is, within or matches` +
477
- ` (line ${lines.length}, column ${column})`,
473
+ `Unexpected end of input, expected SphereIdentifierChar, comparison operator, not, in, contains, is, within or matches (line ${lines.length}, column ${column})`,
478
474
  );
479
475
  }
480
476
  return result;
@@ -138,6 +138,24 @@ describe("Product search filter", () => {
138
138
  },
139
139
  }).isMatch,
140
140
  ).toBeTruthy();
141
+
142
+ expect(
143
+ match({
144
+ exact: {
145
+ field: "variants.sku",
146
+ values: ["MYSKU", "OTHER"],
147
+ },
148
+ }).isMatch,
149
+ ).toBeTruthy();
150
+
151
+ expect(
152
+ match({
153
+ exact: {
154
+ field: "variants.sku",
155
+ values: ["OTHER"],
156
+ },
157
+ }).isMatch,
158
+ ).toBeFalsy();
141
159
  });
142
160
 
143
161
  test("by attribute value", async () => {
@@ -98,6 +98,13 @@ export const parseSearchQuery = (
98
98
  }
99
99
 
100
100
  if (isSearchExactExpression(searchQuery)) {
101
+ if (Array.isArray(searchQuery.exact.values)) {
102
+ return generateFieldMatchFunc(
103
+ (value: any) => (searchQuery.exact.values ?? []).includes(value),
104
+ searchQuery.exact,
105
+ );
106
+ }
107
+
101
108
  return generateFieldMatchFunc(
102
109
  (value: any) => value === searchQuery.exact.value,
103
110
  searchQuery.exact,
@@ -68,8 +68,8 @@ describe("Search filter", () => {
68
68
  };
69
69
 
70
70
  test("by product key", async () => {
71
- expect(match(`key:exists`).isMatch).toBeTruthy();
72
- expect(match(`key:missing`).isMatch).toBeFalsy();
71
+ expect(match("key:exists").isMatch).toBeTruthy();
72
+ expect(match("key:missing").isMatch).toBeFalsy();
73
73
  expect(match(`key:"test-product"`).isMatch).toBeTruthy();
74
74
  });
75
75
 
@@ -80,29 +80,29 @@ describe("Search filter", () => {
80
80
  });
81
81
 
82
82
  test("by SKU", async () => {
83
- expect(match(`variants.sku:exists`).isMatch).toBeTruthy();
84
- expect(match(`variants.sku:missing`).isMatch).toBeFalsy();
83
+ expect(match("variants.sku:exists").isMatch).toBeTruthy();
84
+ expect(match("variants.sku:missing").isMatch).toBeFalsy();
85
85
  expect(match(`variants.sku:"MYSKU"`).isMatch).toBeTruthy();
86
86
  });
87
87
 
88
88
  test("by attribute value", async () => {
89
- expect(match(`variants.attributes.number:4`).isMatch).toBeTruthy();
90
- expect(match(`variants.attributes.number:3,4`).isMatch).toBeTruthy();
91
- expect(match(`variants.attributes.number:3,4,5`).isMatch).toBeTruthy();
92
- expect(match(`variants.attributes.number:1,2,3,5`).isMatch).toBeFalsy();
89
+ expect(match("variants.attributes.number:4").isMatch).toBeTruthy();
90
+ expect(match("variants.attributes.number:3,4").isMatch).toBeTruthy();
91
+ expect(match("variants.attributes.number:3,4,5").isMatch).toBeTruthy();
92
+ expect(match("variants.attributes.number:1,2,3,5").isMatch).toBeFalsy();
93
93
  });
94
94
 
95
95
  test("by attribute range", async () => {
96
96
  expect(
97
- match(`variants.attributes.number:range (0 TO 5)`).isMatch,
97
+ match("variants.attributes.number:range (0 TO 5)").isMatch,
98
98
  ).toBeTruthy();
99
99
 
100
100
  expect(
101
- match(`variants.attributes.number:range (* TO 5)`).isMatch,
101
+ match("variants.attributes.number:range (* TO 5)").isMatch,
102
102
  ).toBeTruthy();
103
103
 
104
104
  expect(
105
- match(`variants.attributes.number:range (* TO *)`).isMatch,
105
+ match("variants.attributes.number:range (* TO *)").isMatch,
106
106
  ).toBeTruthy();
107
107
  });
108
108
 
@@ -118,14 +118,14 @@ describe("Search filter", () => {
118
118
 
119
119
  test("by price range", async () => {
120
120
  expect(
121
- match(`variants.price.centAmount:range (1500 TO 2000)`).isMatch,
121
+ match("variants.price.centAmount:range (1500 TO 2000)").isMatch,
122
122
  ).toBeTruthy();
123
123
  });
124
124
 
125
125
  test("by price range - or", async () => {
126
126
  expect(
127
127
  match(
128
- `variants.price.centAmount:range (2 TO 1500 ), (1500 TO 3000), (3000 TO 6000)`,
128
+ "variants.price.centAmount:range (2 TO 1500 ), (1500 TO 3000), (3000 TO 6000)",
129
129
  ).isMatch,
130
130
  ).toBeTruthy();
131
131
  });
@@ -136,7 +136,7 @@ describe("Search filter", () => {
136
136
 
137
137
  // No currency given
138
138
  result = match(
139
- `variants.scopedPrice.value.centAmount:range (1500 TO 2000)`,
139
+ "variants.scopedPrice.value.centAmount:range (1500 TO 2000)",
140
140
  );
141
141
  expect(result.isMatch).toBeFalsy();
142
142
 
@@ -145,7 +145,7 @@ describe("Search filter", () => {
145
145
  applyPriceSelector(products, { currency: "EUR" });
146
146
 
147
147
  result = match(
148
- `variants.scopedPrice.value.centAmount:range (1500 TO 2000)`,
148
+ "variants.scopedPrice.value.centAmount:range (1500 TO 2000)",
149
149
  products[0],
150
150
  );
151
151
  expect(result.isMatch).toBeTruthy();
@@ -161,7 +161,7 @@ describe("Search filter", () => {
161
161
  applyPriceSelector(products, { currency: "USD" });
162
162
 
163
163
  result = match(
164
- `variants.scopedPrice.value.centAmount:range (1500 TO 2000)`,
164
+ "variants.scopedPrice.value.centAmount:range (1500 TO 2000)",
165
165
  products[0],
166
166
  );
167
167
  expect(result.isMatch).toBeFalsy();
@@ -170,7 +170,7 @@ describe("Search filter", () => {
170
170
  products = [cloneObject(exampleProduct)];
171
171
  applyPriceSelector(products, { currency: "EUR", country: "NL" });
172
172
  result = match(
173
- `variants.scopedPrice.value.centAmount:range (1500 TO 2000)`,
173
+ "variants.scopedPrice.value.centAmount:range (1500 TO 2000)",
174
174
  products[0],
175
175
  );
176
176
  expect(result.isMatch).toBeFalsy();
@@ -154,7 +154,7 @@ const parseFilter = (filter: string): ExpressionSet => {
154
154
  ({
155
155
  type: "Symbol",
156
156
  kind: "int",
157
- value: parseInt(t.token.match, 10),
157
+ value: Number.parseInt(t.token.match, 10),
158
158
  }) as TypeSymbol,
159
159
  )
160
160
  .nud("STAR", 5, (_) => ({
@@ -184,9 +184,8 @@ const parseFilter = (filter: string): ExpressionSet => {
184
184
  const expr: any = parser.parse({ terminals: [bp - 1] });
185
185
  if (Array.isArray(expr)) {
186
186
  return [left, ...expr];
187
- } else {
188
- return [left, expr];
189
187
  }
188
+ return [left, expr];
190
189
  })
191
190
  .nud("(", 100, (t) => {
192
191
  const expr: any = parser.parse({ terminals: [")"] });
@@ -55,7 +55,7 @@ describe("OAuth2Server", () => {
55
55
 
56
56
  it("should refresh a token", async () => {
57
57
  const createResponse = await supertest(app)
58
- .post(`/my-project/anonymous/token`)
58
+ .post("/my-project/anonymous/token")
59
59
  .auth("validClientId", "validClientSecret")
60
60
  .query({ grant_type: "client_credentials" })
61
61
  .send();
@@ -180,7 +180,8 @@ export class OAuth2Server {
180
180
  request.query.scope?.toString(),
181
181
  );
182
182
  return response.status(200).send(token);
183
- } else if (grantType === "refresh_token") {
183
+ }
184
+ if (grantType === "refresh_token") {
184
185
  const refreshToken =
185
186
  request.query.refresh_token?.toString() || request.body.refresh_token;
186
187
  if (!refreshToken) {
@@ -214,17 +215,16 @@ export class OAuth2Server {
214
215
  );
215
216
  }
216
217
  return response.status(200).send(token);
217
- } else {
218
- return next(
219
- new CommercetoolsError<UnsupportedGrantType>(
220
- {
221
- code: "unsupported_grant_type",
222
- message: `Invalid parameter: grant_type: Invalid grant type: ${grantType}`,
223
- },
224
- 400,
225
- ),
226
- );
227
218
  }
219
+ return next(
220
+ new CommercetoolsError<UnsupportedGrantType>(
221
+ {
222
+ code: "unsupported_grant_type",
223
+ message: `Invalid parameter: grant_type: Invalid grant type: ${grantType}`,
224
+ },
225
+ 400,
226
+ ),
227
+ );
228
228
  }
229
229
 
230
230
  async customerTokenHandler(
@@ -22,7 +22,7 @@ export type PriceSelector = {
22
22
  export const applyPriceSelector = (
23
23
  products: ProductProjection[],
24
24
  selector: PriceSelector,
25
- noScopedPrice: boolean = false,
25
+ noScopedPrice = false,
26
26
  ) => {
27
27
  validatePriceSelector(selector);
28
28
 
@@ -27,25 +27,25 @@ import type { AbstractStorage } from "./storage";
27
27
  import type { Writable } from "./types";
28
28
 
29
29
  export type ProductProjectionSearchParams = {
30
- "fuzzy"?: boolean;
31
- "fuzzyLevel"?: number;
32
- "markMatchingVariants"?: boolean;
33
- "staged"?: boolean;
34
- "filter"?: string[];
30
+ fuzzy?: boolean;
31
+ fuzzyLevel?: number;
32
+ markMatchingVariants?: boolean;
33
+ staged?: boolean;
34
+ filter?: string[];
35
35
  "filter.facets"?: string[];
36
36
  "filter.query"?: string[];
37
- "facet"?: string | string[];
38
- "sort"?: string | string[];
39
- "limit"?: number;
40
- "offset"?: number;
41
- "withTotal"?: boolean;
42
- "priceCurrency"?: string;
43
- "priceCountry"?: string;
44
- "priceCustomerGroup"?: string;
45
- "priceChannel"?: string;
46
- "localeProjection"?: string;
47
- "storeProjection"?: string;
48
- "expand"?: string | string[];
37
+ facet?: string | string[];
38
+ sort?: string | string[];
39
+ limit?: number;
40
+ offset?: number;
41
+ withTotal?: boolean;
42
+ priceCurrency?: string;
43
+ priceCountry?: string;
44
+ priceCustomerGroup?: string;
45
+ priceChannel?: string;
46
+ localeProjection?: string;
47
+ storeProjection?: string;
48
+ expand?: string | string[];
49
49
  [key: string]: QueryParam;
50
50
  };
51
51
 
@@ -342,9 +342,8 @@ export class ProductProjectionSearch {
342
342
  max: numValues > 0 ? Math.max(...values) : 0,
343
343
  mean: numValues > 0 ? mean(values) : 0,
344
344
  };
345
- } else {
346
- throw new Error("not supported");
347
345
  }
346
+ throw new Error("not supported");
348
347
  }) || [];
349
348
  const data: RangeFacetResult = {
350
349
  type: "range",
@@ -13,6 +13,12 @@ import { validateSearchQuery } from "./lib/searchQueryTypeChecker";
13
13
  import { applyPriceSelector } from "./priceSelector";
14
14
  import type { AbstractStorage } from "./storage";
15
15
 
16
+ interface ProductSearchVariantAvailability {
17
+ isOnStock: boolean;
18
+ availableQuantity: number;
19
+ isOnStockForChannel: string | undefined;
20
+ }
21
+
16
22
  export class ProductSearch {
17
23
  protected _storage: AbstractStorage;
18
24
 
@@ -24,10 +30,32 @@ export class ProductSearch {
24
30
  projectKey: string,
25
31
  params: ProductSearchRequest,
26
32
  ): ProductPagedSearchResponse {
27
- let resources = this._storage
33
+ const availabilityBySku = this._storage
34
+ .all(projectKey, "inventory-entry")
35
+ .reduce((acc, entry) => {
36
+ const existingEntry = acc.get(entry.sku);
37
+
38
+ acc.set(entry.sku, {
39
+ isOnStock: existingEntry?.isOnStock || entry.quantityOnStock > 0,
40
+ availableQuantity:
41
+ existingEntry?.availableQuantity ?? 0 + entry.quantityOnStock,
42
+ // NOTE: This doesn't handle inventory entries for multiple channels,
43
+ // so it doesn't exactly replicate the behavior of the commercetools api.
44
+ isOnStockForChannel:
45
+ existingEntry?.isOnStockForChannel ?? entry.supplyChannel?.id,
46
+ });
47
+
48
+ return acc;
49
+ }, new Map<string, ProductSearchVariantAvailability>());
50
+
51
+ let productResources = this._storage
28
52
  .all(projectKey, "product")
29
53
  .map((r) =>
30
- this.transform(r, params.productProjectionParameters?.staged ?? false),
54
+ this.transformProduct(
55
+ r,
56
+ params.productProjectionParameters?.staged ?? false,
57
+ availabilityBySku,
58
+ ),
31
59
  )
32
60
  .filter((p) => {
33
61
  if (!(params.productProjectionParameters?.staged ?? false)) {
@@ -46,7 +74,7 @@ export class ProductSearch {
46
74
  const matchFunc = parseSearchQuery(params.query);
47
75
 
48
76
  // Filters can modify the output. So clone the resources first.
49
- resources = resources.filter((resource) =>
77
+ productResources = productResources.filter((resource) =>
50
78
  matchFunc(resource, markMatchingVariant),
51
79
  );
52
80
  } catch (err) {
@@ -63,7 +91,7 @@ export class ProductSearch {
63
91
 
64
92
  // Apply the priceSelector
65
93
  if (params.productProjectionParameters) {
66
- applyPriceSelector(resources, {
94
+ applyPriceSelector(productResources, {
67
95
  country: params.productProjectionParameters.priceCountry,
68
96
  channel: params.productProjectionParameters.priceChannel,
69
97
  customerGroup: params.productProjectionParameters.priceCustomerGroup,
@@ -76,7 +104,10 @@ export class ProductSearch {
76
104
 
77
105
  const offset = params.offset || 0;
78
106
  const limit = params.limit || 20;
79
- const productProjectionsResult = resources.slice(offset, offset + limit);
107
+ const productProjectionsResult = productResources.slice(
108
+ offset,
109
+ offset + limit,
110
+ );
80
111
 
81
112
  /**
82
113
  * Do not supply productProjection if productProjectionParameters are not given
@@ -100,7 +131,7 @@ export class ProductSearch {
100
131
  );
101
132
 
102
133
  return {
103
- total: resources.length,
134
+ total: productResources.length,
104
135
  offset: offset,
105
136
  limit: limit,
106
137
  results: results,
@@ -108,7 +139,11 @@ export class ProductSearch {
108
139
  };
109
140
  }
110
141
 
111
- transform(product: Product, staged: boolean): ProductProjection {
142
+ transformProduct(
143
+ product: Product,
144
+ staged: boolean,
145
+ availabilityBySku: Map<string, ProductSearchVariantAvailability>,
146
+ ): ProductProjection {
112
147
  const obj = !staged
113
148
  ? product.masterData.current
114
149
  : product.masterData.staged;
@@ -125,7 +160,12 @@ export class ProductSearch {
125
160
  slug: obj.slug,
126
161
  categories: obj.categories,
127
162
  masterVariant: obj.masterVariant,
128
- variants: obj.variants,
163
+ variants: obj.variants.map((variant) => ({
164
+ ...variant,
165
+ availability: variant.sku
166
+ ? availabilityBySku.get(variant.sku)
167
+ : { isOnStock: false, availableQuantity: 0, isOnStockForChannel: [] },
168
+ })),
129
169
  productType: product.productType,
130
170
  hasStagedChanges: product.masterData.hasStagedChanges,
131
171
  published: product.masterData.published,
@@ -7,21 +7,21 @@ import type {
7
7
  CompanyDraft,
8
8
  DivisionDraft,
9
9
  } from "@commercetools/platform-sdk";
10
- import {
11
- type Associate,
12
- type BusinessUnit,
13
- type BusinessUnitAddAddressAction,
14
- type BusinessUnitAddAssociateAction,
15
- type BusinessUnitAddStoreAction,
16
- type BusinessUnitChangeAddressAction,
17
- type BusinessUnitChangeNameAction,
18
- type BusinessUnitChangeParentUnitAction,
19
- type BusinessUnitDraft,
20
- type BusinessUnitSetAssociatesAction,
21
- type BusinessUnitSetContactEmailAction,
22
- type BusinessUnitSetStoreModeAction,
23
- type Company,
24
- type Division,
10
+ import type {
11
+ Associate,
12
+ BusinessUnit,
13
+ BusinessUnitAddAddressAction,
14
+ BusinessUnitAddAssociateAction,
15
+ BusinessUnitAddStoreAction,
16
+ BusinessUnitChangeAddressAction,
17
+ BusinessUnitChangeNameAction,
18
+ BusinessUnitChangeParentUnitAction,
19
+ BusinessUnitDraft,
20
+ BusinessUnitSetAssociatesAction,
21
+ BusinessUnitSetContactEmailAction,
22
+ BusinessUnitSetStoreModeAction,
23
+ Company,
24
+ Division,
25
25
  } from "@commercetools/platform-sdk";
26
26
  import type { Config } from "~src/config";
27
27
  import { generateRandomString, getBaseResourceProperties } from "../helpers";
@@ -110,7 +110,8 @@ export class BusinessUnitRepository extends AbstractResourceRepository<"business
110
110
 
111
111
  this.saveNew(context, division);
112
112
  return division;
113
- } else if (this._isCompanyDraft(draft)) {
113
+ }
114
+ if (this._isCompanyDraft(draft)) {
114
115
  const company = resource as Company;
115
116
 
116
117
  this.saveNew(context, company);