@labdigital/commercetools-mock 2.54.0 → 2.56.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 (53) hide show
  1. package/dist/index.d.ts +107 -77
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +455 -8
  4. package/dist/index.js.map +1 -1
  5. package/package.json +4 -3
  6. package/src/lib/predicateParser.test.ts +13 -37
  7. package/src/lib/predicateParser.ts +12 -1
  8. package/src/lib/productSearchFilter.test.ts +1 -0
  9. package/src/lib/projectionSearchFilter.test.ts +1 -0
  10. package/src/priceSelector.test.ts +1 -0
  11. package/src/product-projection-search.ts +2 -0
  12. package/src/product-search.ts +1 -0
  13. package/src/repositories/cart/actions.ts +178 -0
  14. package/src/repositories/cart/helpers.ts +170 -3
  15. package/src/repositories/cart/index.test.ts +86 -2
  16. package/src/repositories/cart/index.ts +19 -2
  17. package/src/repositories/cart-discount/index.ts +1 -1
  18. package/src/repositories/customer/index.ts +2 -0
  19. package/src/repositories/discount-group/actions.ts +50 -0
  20. package/src/repositories/discount-group/index.ts +29 -0
  21. package/src/repositories/index.ts +6 -0
  22. package/src/repositories/order/index.test.ts +126 -125
  23. package/src/repositories/payment/actions.ts +87 -0
  24. package/src/repositories/payment/index.ts +1 -1
  25. package/src/repositories/product/index.ts +1 -0
  26. package/src/repositories/product-type.ts +1 -0
  27. package/src/repositories/quote/index.ts +1 -0
  28. package/src/repositories/quote-request/index.test.ts +1 -0
  29. package/src/repositories/quote-request/index.ts +1 -0
  30. package/src/repositories/recurrence-policy/actions.ts +53 -0
  31. package/src/repositories/recurrence-policy/index.ts +36 -0
  32. package/src/repositories/recurring-order/actions.ts +157 -0
  33. package/src/repositories/recurring-order/index.ts +52 -0
  34. package/src/repositories/review.test.ts +2 -0
  35. package/src/repositories/shopping-list/actions.ts +1 -0
  36. package/src/repositories/shopping-list/index.ts +1 -0
  37. package/src/services/cart.test.ts +282 -0
  38. package/src/services/discount-group.test.ts +270 -0
  39. package/src/services/discount-group.ts +16 -0
  40. package/src/services/index.ts +12 -0
  41. package/src/services/my-cart.test.ts +1 -0
  42. package/src/services/my-payment.test.ts +1 -0
  43. package/src/services/payment.test.ts +1 -0
  44. package/src/services/product-projection.test.ts +4 -0
  45. package/src/services/product-type.test.ts +1 -0
  46. package/src/services/product.test.ts +1 -0
  47. package/src/services/recurrence-policy.test.ts +316 -0
  48. package/src/services/recurrence-policy.ts +16 -0
  49. package/src/services/recurring-order.test.ts +424 -0
  50. package/src/services/recurring-order.ts +16 -0
  51. package/src/services/shopping-list.test.ts +3 -0
  52. package/src/storage/in-memory.ts +6 -0
  53. package/src/types.ts +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@labdigital/commercetools-mock",
3
- "version": "2.54.0",
3
+ "version": "2.56.0",
4
4
  "license": "MIT",
5
5
  "author": "Michael van Tellingen",
6
6
  "type": "module",
@@ -31,7 +31,7 @@
31
31
  "@types/express": "^5.0.1",
32
32
  "@changesets/changelog-github": "0.5.1",
33
33
  "@changesets/cli": "2.28.1",
34
- "@commercetools/platform-sdk": "8.8.0",
34
+ "@commercetools/platform-sdk": "8.14.0",
35
35
  "@types/basic-auth": "1.1.8",
36
36
  "@types/body-parser": "1.19.5",
37
37
  "@types/express-serve-static-core": "^5.0.6",
@@ -66,6 +66,7 @@
66
66
  "publish:version": "pnpm changeset version && pnpm format",
67
67
  "start": "tsdown src/server.ts --watch --onSuccess 'node dist/server'",
68
68
  "test": "vitest run",
69
- "test:ci": "vitest run --coverage"
69
+ "test:ci": "vitest run --coverage",
70
+ "test:watch": "vitest --watch"
70
71
  }
71
72
  }
@@ -151,43 +151,11 @@ describe("Predicate filter", () => {
151
151
  expect(match("numberProperty in (1234)")).toBeTruthy();
152
152
  });
153
153
 
154
- test("in operator with and clause", async () => {
155
- expect(
156
- match("numberProperty in (1234) and stringProperty=:val", {
157
- val: "foobar",
158
- }),
159
- ).toBeTruthy();
160
- expect(
161
- match("numberProperty in (1234) and stringProperty=:val", {
162
- val: "other",
163
- }),
164
- ).toBeFalsy();
165
- expect(
166
- match("numberProperty in (1235) and stringProperty=:val", {
167
- val: "foobar",
168
- }),
169
- ).toBeFalsy();
170
- });
171
-
172
- test("within operator with and clause", async () => {
173
- expect(
174
- match(
175
- "geoLocation within circle(5.121310867198959, 52.09068804569714, 2500) and stringProperty=:val",
176
- { val: "foobar" },
177
- ),
178
- ).toBeTruthy();
179
- expect(
180
- match(
181
- "geoLocation within circle(5.121310867198959, 52.09068804569714, 2500) and stringProperty=:val",
182
- { val: "other" },
183
- ),
184
- ).toBeFalsy();
185
- expect(
186
- match(
187
- "geoLocation within circle(5.121310867198959, 52.09068804569714, 1000) and stringProperty=:val",
188
- { val: "foobar" },
189
- ),
190
- ).toBeFalsy();
154
+ test("in operator works with with and without parentheses", async () => {
155
+ expect(match("numberProperty in :val", { val: 1234 })).toBeTruthy();
156
+ expect(match("numberProperty in :val", { val: 1235 })).toBeFalsy();
157
+ expect(match("numberProperty in :val", { val: [1234] })).toBeTruthy();
158
+ expect(match("numberProperty in :val", { val: [1235] })).toBeFalsy();
191
159
  });
192
160
 
193
161
  test("arrayProperty contains all (...)", async () => {
@@ -401,6 +369,14 @@ describe("Predicate filter", () => {
401
369
  ),
402
370
  ).toBeFalsy();
403
371
  });
372
+
373
+ test("in operator with array values", async () => {
374
+ expect(match(`arrayProperty in ("foo")`)).toBeTruthy();
375
+ expect(match(`arrayProperty in ("bar")`)).toBeTruthy();
376
+ expect(match(`arrayProperty in ("missing")`)).toBeFalsy();
377
+ expect(match(`arrayProperty in ("foo", "bar")`)).toBeTruthy();
378
+ expect(match(`arrayProperty in ("missing", "alsomissing")`)).toBeFalsy();
379
+ });
404
380
  });
405
381
 
406
382
  describe("Report parse errors", () => {
@@ -381,8 +381,14 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
381
381
  }
382
382
  })
383
383
  .led("IN", 20, ({ left, bp }) => {
384
+ const firstToken = lexer.peek();
384
385
  const expr = parser.parse({ terminals: [bp - 1] });
385
- lexer.expect(")");
386
+
387
+ // IN can be a single value or a list of values
388
+ if (firstToken.match === "(") {
389
+ lexer.expect(")");
390
+ }
391
+
386
392
  return (obj: any, vars: object) => {
387
393
  let symbols = expr;
388
394
  if (!Array.isArray(symbols)) {
@@ -397,6 +403,11 @@ const generateMatchFunc = (predicate: string): MatchFunc => {
397
403
  resolveSymbol(item, vars),
398
404
  );
399
405
  const value = resolveValue(obj, left);
406
+
407
+ if (Array.isArray(value)) {
408
+ return inValues.some((inValue: any) => value.includes(inValue));
409
+ }
410
+
400
411
  return inValues.includes(value);
401
412
  };
402
413
  })
@@ -25,6 +25,7 @@ describe("Product search filter", () => {
25
25
  "nl-NL": "test",
26
26
  "en-US": "test",
27
27
  },
28
+ attributes: [],
28
29
  variants: [],
29
30
  searchKeywords: {},
30
31
  categories: [],
@@ -24,6 +24,7 @@ describe("Search filter", () => {
24
24
  variants: [],
25
25
  searchKeywords: {},
26
26
  categories: [],
27
+ attributes: [],
27
28
  masterVariant: {
28
29
  id: 1,
29
30
  sku: "MYSKU",
@@ -24,6 +24,7 @@ describe("priceSelector", () => {
24
24
  variants: [],
25
25
  searchKeywords: {},
26
26
  categories: [],
27
+ attributes: [],
27
28
  masterVariant: {
28
29
  id: 1,
29
30
  sku: "MYSKU",
@@ -3,6 +3,7 @@ import type {
3
3
  FilteredFacetResult,
4
4
  InvalidInputError,
5
5
  Product,
6
+ ProductData,
6
7
  ProductProjection,
7
8
  ProductProjectionPagedSearchResponse,
8
9
  QueryParam,
@@ -169,6 +170,7 @@ export class ProductProjectionSearch {
169
170
  return {
170
171
  id: product.id,
171
172
  createdAt: product.createdAt,
173
+ attributes: obj.attributes,
172
174
  lastModifiedAt: product.lastModifiedAt,
173
175
  version: product.version,
174
176
  name: obj.name,
@@ -176,6 +176,7 @@ export class ProductSearch {
176
176
  metaDescription: obj.metaDescription,
177
177
  slug: obj.slug,
178
178
  categories: obj.categories,
179
+ attributes: obj.attributes,
179
180
  masterVariant: {
180
181
  ...obj.masterVariant,
181
182
  availability: getVariantAvailability(obj.masterVariant.sku),
@@ -11,10 +11,14 @@ import type {
11
11
  Address,
12
12
  AddressDraft,
13
13
  Cart,
14
+ CartAddCustomLineItemAction,
14
15
  CartAddItemShippingAddressAction,
15
16
  CartAddLineItemAction,
17
+ CartChangeCustomLineItemMoneyAction,
18
+ CartChangeCustomLineItemQuantityAction,
16
19
  CartChangeLineItemQuantityAction,
17
20
  CartChangeTaxRoundingModeAction,
21
+ CartRemoveCustomLineItemAction,
18
22
  CartRemoveDiscountCodeAction,
19
23
  CartRemoveLineItemAction,
20
24
  CartRemoveShippingMethodAction,
@@ -39,8 +43,10 @@ import type {
39
43
  Product,
40
44
  ProductPagedQueryResponse,
41
45
  ProductVariant,
46
+ TaxCategoryReference,
42
47
  } from "@commercetools/platform-sdk";
43
48
  import type {
49
+ CustomLineItem,
44
50
  DirectDiscount,
45
51
  TaxPortion,
46
52
  TaxedItemPrice,
@@ -58,11 +64,14 @@ import {
58
64
  createCentPrecisionMoney,
59
65
  createCustomFields,
60
66
  createTypedMoney,
67
+ getReferenceFromResourceIdentifier,
61
68
  roundDecimal,
62
69
  } from "../helpers";
63
70
  import {
64
71
  calculateCartTotalPrice,
65
72
  calculateLineItemTotalPrice,
73
+ calculateTaxedPrice,
74
+ createCustomLineItemFromDraft,
66
75
  selectPrice,
67
76
  } from "./helpers";
68
77
 
@@ -319,6 +328,175 @@ export class CartUpdateHandler
319
328
  resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
320
329
  }
321
330
 
331
+ addCustomLineItem(
332
+ context: RepositoryContext,
333
+ resource: Writable<Cart>,
334
+ {
335
+ money,
336
+ name,
337
+ slug,
338
+ quantity = 1,
339
+ taxCategory,
340
+ custom,
341
+ priceMode = "Standard",
342
+ key,
343
+ }: CartAddCustomLineItemAction,
344
+ ) {
345
+ const customLineItem = createCustomLineItemFromDraft(
346
+ context.projectKey,
347
+ { money, name, slug, quantity, taxCategory, custom, priceMode, key },
348
+ this._storage,
349
+ resource.country,
350
+ );
351
+
352
+ resource.customLineItems.push(customLineItem);
353
+ resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
354
+ }
355
+
356
+ removeCustomLineItem(
357
+ context: RepositoryContext,
358
+ resource: Writable<Cart>,
359
+ { customLineItemId, customLineItemKey }: CartRemoveCustomLineItemAction,
360
+ ) {
361
+ let customLineItem;
362
+
363
+ if (!customLineItemId && !customLineItemKey) {
364
+ throw new CommercetoolsError<GeneralError>({
365
+ code: "General",
366
+ message:
367
+ "Either customLineItemId or customLineItemKey needs to be provided.",
368
+ });
369
+ }
370
+
371
+ if (customLineItemId) {
372
+ customLineItem = resource.customLineItems.find(
373
+ (x) => x.id === customLineItemId,
374
+ );
375
+ if (!customLineItem) {
376
+ throw new CommercetoolsError<GeneralError>({
377
+ code: "General",
378
+ message: `A custom line item with ID '${customLineItemId}' not found.`,
379
+ });
380
+ }
381
+ resource.customLineItems = resource.customLineItems.filter(
382
+ (x) => x.id !== customLineItemId,
383
+ );
384
+ }
385
+
386
+ if (customLineItemKey) {
387
+ customLineItem = resource.customLineItems.find(
388
+ (x) => x.key === customLineItemKey,
389
+ );
390
+ if (!customLineItem) {
391
+ throw new CommercetoolsError<GeneralError>({
392
+ code: "General",
393
+ message: `A custom line item with key '${customLineItemKey}' not found.`,
394
+ });
395
+ }
396
+ resource.customLineItems = resource.customLineItems.filter(
397
+ (x) => x.key !== customLineItemKey,
398
+ );
399
+ }
400
+
401
+ resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
402
+ }
403
+
404
+ changeCustomLineItemQuantity(
405
+ context: RepositoryContext,
406
+ resource: Writable<Cart>,
407
+ {
408
+ customLineItemId,
409
+ customLineItemKey,
410
+ quantity,
411
+ }: CartChangeCustomLineItemQuantityAction,
412
+ ) {
413
+ let customLineItem;
414
+
415
+ if (!customLineItemId && !customLineItemKey) {
416
+ throw new CommercetoolsError<GeneralError>({
417
+ code: "General",
418
+ message:
419
+ "Either customLineItemId or customLineItemKey needs to be provided.",
420
+ });
421
+ }
422
+
423
+ const setQuantity = (
424
+ customLineItem: Writable<CustomLineItem> | undefined,
425
+ ) => {
426
+ if (!customLineItem) {
427
+ throw new CommercetoolsError<GeneralError>({
428
+ code: "General",
429
+ message: `A custom line item with ${customLineItemId ? `ID '${customLineItemId}'` : `key '${customLineItemKey}'`} not found.`,
430
+ });
431
+ }
432
+ customLineItem.quantity = quantity;
433
+ customLineItem.totalPrice = createCentPrecisionMoney({
434
+ ...customLineItem.money,
435
+ centAmount: (customLineItem.money.centAmount ?? 0) * quantity,
436
+ });
437
+ };
438
+
439
+ if (customLineItemId) {
440
+ customLineItem = resource.customLineItems.find(
441
+ (x) => x.id === customLineItemId,
442
+ );
443
+ setQuantity(customLineItem);
444
+ }
445
+
446
+ if (customLineItemKey) {
447
+ customLineItem = resource.customLineItems.find(
448
+ (x) => x.key === customLineItemKey,
449
+ );
450
+ setQuantity(customLineItem);
451
+ }
452
+
453
+ // Update cart total price
454
+ resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
455
+ }
456
+
457
+ changeCustomLineItemMoney(
458
+ context: RepositoryContext,
459
+ resource: Writable<Cart>,
460
+ {
461
+ customLineItemId,
462
+ customLineItemKey,
463
+ money,
464
+ }: CartChangeCustomLineItemMoneyAction,
465
+ ) {
466
+ let customLineItem;
467
+
468
+ const setMoney = (customLineItem: Writable<CustomLineItem> | undefined) => {
469
+ if (!customLineItem) {
470
+ throw new CommercetoolsError<GeneralError>({
471
+ code: "General",
472
+ message: `A custom line item with ${customLineItemId ? `ID '${customLineItemId}'` : `key '${customLineItemKey}'`} not found.`,
473
+ });
474
+ }
475
+ customLineItem.money = createTypedMoney(money);
476
+ customLineItem.totalPrice = createCentPrecisionMoney({
477
+ ...money,
478
+ centAmount: (money.centAmount ?? 0) * customLineItem.quantity,
479
+ });
480
+ };
481
+
482
+ if (customLineItemId) {
483
+ customLineItem = resource.customLineItems.find(
484
+ (x) => x.id === customLineItemId,
485
+ );
486
+ setMoney(customLineItem);
487
+ }
488
+
489
+ if (customLineItemKey) {
490
+ customLineItem = resource.customLineItems.find(
491
+ (x) => x.key === customLineItemKey,
492
+ );
493
+ setMoney(customLineItem);
494
+ }
495
+
496
+ // Update cart total price
497
+ resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
498
+ }
499
+
322
500
  setAnonymousId(
323
501
  _context: RepositoryContext,
324
502
  resource: Writable<Cart>,
@@ -1,4 +1,22 @@
1
- import type { Cart, LineItem, Price } from "@commercetools/platform-sdk";
1
+ import type {
2
+ Cart,
3
+ CentPrecisionMoney,
4
+ CustomLineItem,
5
+ CustomLineItemDraft,
6
+ LineItem,
7
+ Price,
8
+ TaxCategory,
9
+ TaxCategoryReference,
10
+ TaxedPrice,
11
+ } from "@commercetools/platform-sdk";
12
+ import { v4 as uuidv4 } from "uuid";
13
+ import type { AbstractStorage } from "~src/storage/abstract";
14
+ import {
15
+ createCentPrecisionMoney,
16
+ createCustomFields,
17
+ createTypedMoney,
18
+ getReferenceFromResourceIdentifier,
19
+ } from "../helpers";
2
20
 
3
21
  export const selectPrice = ({
4
22
  prices,
@@ -26,5 +44,154 @@ export const selectPrice = ({
26
44
  export const calculateLineItemTotalPrice = (lineItem: LineItem): number =>
27
45
  lineItem.price?.value.centAmount * lineItem.quantity;
28
46
 
29
- export const calculateCartTotalPrice = (cart: Cart): number =>
30
- cart.lineItems.reduce((cur, item) => cur + item.totalPrice.centAmount, 0);
47
+ export const calculateCartTotalPrice = (cart: Cart): number => {
48
+ const lineItemsTotal = cart.lineItems.reduce(
49
+ (cur, item) => cur + item.totalPrice.centAmount,
50
+ 0,
51
+ );
52
+ const customLineItemsTotal = cart.customLineItems.reduce(
53
+ (cur, item) => cur + item.totalPrice.centAmount,
54
+ 0,
55
+ );
56
+ return lineItemsTotal + customLineItemsTotal;
57
+ };
58
+
59
+ export const calculateTaxedPrice = (
60
+ amount: number,
61
+ taxCategory: TaxCategory | undefined,
62
+ currency: string,
63
+ country: string | undefined,
64
+ ): TaxedPrice | undefined => {
65
+ if (!taxCategory || !taxCategory.rates.length) {
66
+ return undefined;
67
+ }
68
+
69
+ // Find the appropriate tax rate for the country
70
+ const taxRate =
71
+ taxCategory.rates.find(
72
+ (rate) => !rate.country || rate.country === country,
73
+ ) || taxCategory.rates[0]; // Fallback to first rate if no country-specific rate found
74
+
75
+ if (!taxRate) {
76
+ return undefined;
77
+ }
78
+
79
+ let netAmount: number;
80
+ let grossAmount: number;
81
+ let taxAmount: number;
82
+
83
+ if (taxRate.includedInPrice) {
84
+ // Amount is gross, calculate net
85
+ grossAmount = amount;
86
+ taxAmount = Math.round(
87
+ (grossAmount * taxRate.amount) / (1 + taxRate.amount),
88
+ );
89
+ netAmount = grossAmount - taxAmount;
90
+ } else {
91
+ // Amount is net, calculate gross
92
+ netAmount = amount;
93
+ taxAmount = Math.round(netAmount * taxRate.amount);
94
+ grossAmount = netAmount + taxAmount;
95
+ }
96
+
97
+ return {
98
+ totalNet: {
99
+ type: "centPrecision",
100
+ currencyCode: currency,
101
+ centAmount: netAmount,
102
+ fractionDigits: 2,
103
+ },
104
+ totalGross: {
105
+ type: "centPrecision",
106
+ currencyCode: currency,
107
+ centAmount: grossAmount,
108
+ fractionDigits: 2,
109
+ },
110
+ taxPortions:
111
+ taxAmount > 0
112
+ ? [
113
+ {
114
+ rate: taxRate.amount,
115
+ amount: {
116
+ type: "centPrecision",
117
+ currencyCode: currency,
118
+ centAmount: taxAmount,
119
+ fractionDigits: 2,
120
+ },
121
+ name: taxRate.name,
122
+ },
123
+ ]
124
+ : [],
125
+ totalTax:
126
+ taxAmount > 0
127
+ ? {
128
+ type: "centPrecision",
129
+ currencyCode: currency,
130
+ centAmount: taxAmount,
131
+ fractionDigits: 2,
132
+ }
133
+ : undefined,
134
+ };
135
+ };
136
+
137
+ export const createCustomLineItemFromDraft = (
138
+ projectKey: string,
139
+ draft: CustomLineItemDraft,
140
+ storage: AbstractStorage,
141
+ country?: string,
142
+ ): CustomLineItem => {
143
+ const quantity = draft.quantity ?? 1;
144
+
145
+ const taxCategoryRef = draft.taxCategory
146
+ ? getReferenceFromResourceIdentifier<TaxCategoryReference>(
147
+ draft.taxCategory,
148
+ projectKey,
149
+ storage,
150
+ )
151
+ : undefined;
152
+
153
+ // Get the tax category to calculate taxed price
154
+ let taxCategory: TaxCategory | undefined = undefined;
155
+ if (taxCategoryRef) {
156
+ try {
157
+ taxCategory =
158
+ storage.get(projectKey, "tax-category", taxCategoryRef.id, {}) ||
159
+ undefined;
160
+ } catch (error) {
161
+ // Tax category not found, continue without tax calculation
162
+ }
163
+ }
164
+
165
+ const totalPrice = createCentPrecisionMoney({
166
+ ...draft.money,
167
+ centAmount: (draft.money.centAmount ?? 0) * quantity,
168
+ });
169
+
170
+ // Calculate taxed price if tax category is available
171
+ const taxedPrice = taxCategory
172
+ ? calculateTaxedPrice(
173
+ totalPrice.centAmount,
174
+ taxCategory,
175
+ totalPrice.currencyCode,
176
+ country,
177
+ )
178
+ : undefined;
179
+
180
+ return {
181
+ id: uuidv4(),
182
+ key: draft.key,
183
+ name: draft.name,
184
+ money: createTypedMoney(draft.money),
185
+ slug: draft.slug,
186
+ quantity: draft.quantity ?? 1,
187
+ state: [],
188
+ taxCategory: taxCategoryRef,
189
+ taxedPrice,
190
+ custom: createCustomFields(draft.custom, projectKey, storage),
191
+ discountedPricePerQuantity: [],
192
+ perMethodTaxRate: [],
193
+ priceMode: draft.priceMode ?? "Standard",
194
+ totalPrice,
195
+ taxedPricePortions: [],
196
+ };
197
+ };
@@ -1,4 +1,8 @@
1
- import type { CartDraft, LineItem } from "@commercetools/platform-sdk";
1
+ import type {
2
+ CartDraft,
3
+ CustomLineItemDraft,
4
+ LineItem,
5
+ } from "@commercetools/platform-sdk";
2
6
  import { describe, expect, test } from "vitest";
3
7
  import type { Config } from "~src/config";
4
8
  import { getBaseResourceProperties } from "~src/helpers";
@@ -33,6 +37,7 @@ describe("Cart repository", () => {
33
37
  current: {
34
38
  name: { "nl-NL": "Dummy" },
35
39
  slug: { "nl-NL": "Dummy" },
40
+ attributes: [],
36
41
  categories: [],
37
42
  masterVariant: {
38
43
  sku: "MYSKU",
@@ -57,6 +62,7 @@ describe("Cart repository", () => {
57
62
  staged: {
58
63
  name: { "nl-NL": "Dummy" },
59
64
  slug: { "nl-NL": "Dummy" },
65
+ attributes: [],
60
66
  categories: [],
61
67
  masterVariant: {
62
68
  sku: "MYSKU",
@@ -81,6 +87,14 @@ describe("Cart repository", () => {
81
87
  },
82
88
  });
83
89
 
90
+ storage.add("dummy", "tax-category", {
91
+ ...getBaseResourceProperties(),
92
+ id: "tax-category-id",
93
+ key: "standard-tax",
94
+ name: "Standard Tax",
95
+ rates: [],
96
+ });
97
+
84
98
  const cart: CartDraft = {
85
99
  anonymousId: "1234567890",
86
100
  billingAddress: {
@@ -95,7 +109,21 @@ describe("Cart repository", () => {
95
109
  country: "NL",
96
110
  currency: "EUR",
97
111
  customerEmail: "john.doe@example.com",
98
- customLineItems: [],
112
+ customLineItems: [
113
+ {
114
+ name: { "nl-NL": "Douane kosten" },
115
+ slug: "customs-fee",
116
+ money: {
117
+ currencyCode: "EUR",
118
+ centAmount: 2500,
119
+ },
120
+ quantity: 1,
121
+ taxCategory: {
122
+ typeId: "tax-category" as const,
123
+ id: "tax-category-id",
124
+ },
125
+ },
126
+ ],
99
127
  inventoryMode: "None",
100
128
  itemShippingAddresses: [],
101
129
  lineItems: [
@@ -171,6 +199,11 @@ describe("Cart repository", () => {
171
199
  expect(result.lineItems[0].custom?.fields.description as string).toEqual(
172
200
  cart.lineItems![0].custom?.fields?.description,
173
201
  );
202
+ expect(result.customLineItems).toHaveLength(1);
203
+ expect(result.customLineItems[0].name).toEqual(
204
+ cart.customLineItems?.[0].name,
205
+ );
206
+ expect(result.totalPrice.centAmount).toBe(3500);
174
207
  });
175
208
 
176
209
  test("create cart with business unit", async () => {
@@ -210,4 +243,55 @@ describe("Cart repository", () => {
210
243
  typeId: "business-unit",
211
244
  });
212
245
  });
246
+
247
+ test("should calculate taxed price for custom line items with tax category", () => {
248
+ storage.add("dummy", "tax-category", {
249
+ ...getBaseResourceProperties(),
250
+ id: "tax-category-with-rate",
251
+ key: "vat-tax",
252
+ name: "VAT Tax",
253
+ rates: [
254
+ {
255
+ id: "rate-1",
256
+ name: "Standard VAT",
257
+ amount: 0.21,
258
+ includedInPrice: false,
259
+ country: "NL",
260
+ },
261
+ ],
262
+ });
263
+
264
+ const cart: CartDraft = {
265
+ currency: "EUR",
266
+ country: "NL",
267
+ customLineItems: [
268
+ {
269
+ name: { en: "Service Fee" },
270
+ slug: "service-fee",
271
+ money: {
272
+ currencyCode: "EUR",
273
+ centAmount: 1000,
274
+ },
275
+ quantity: 1,
276
+ taxCategory: {
277
+ typeId: "tax-category" as const,
278
+ id: "tax-category-with-rate",
279
+ },
280
+ },
281
+ ],
282
+ };
283
+
284
+ const ctx = { projectKey: "dummy", storeKey: "dummyStore" };
285
+ const result = repository.create(ctx, cart);
286
+
287
+ expect(result.customLineItems).toHaveLength(1);
288
+ const customLineItem = result.customLineItems[0];
289
+
290
+ expect(customLineItem.taxedPrice).toBeDefined();
291
+ expect(customLineItem.taxedPrice?.totalGross.centAmount).toBe(1210);
292
+ expect(customLineItem.taxedPrice?.totalNet.centAmount).toBe(1000);
293
+ expect(customLineItem.taxedPrice?.totalTax?.centAmount).toBe(210);
294
+ expect(customLineItem.taxedPrice?.taxPortions).toHaveLength(1);
295
+ expect(customLineItem.taxedPrice?.taxPortions[0].rate).toBe(0.21);
296
+ });
213
297
  });