@labdigital/commercetools-mock 2.55.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.
- package/dist/index.js +175 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/predicateParser.test.ts +13 -37
- package/src/lib/predicateParser.ts +12 -1
- package/src/repositories/cart/actions.ts +178 -0
- package/src/repositories/cart/helpers.ts +170 -3
- package/src/repositories/cart/index.test.ts +84 -2
- package/src/repositories/cart/index.ts +18 -2
- package/src/services/cart.test.ts +282 -0
package/package.json
CHANGED
|
@@ -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
|
|
155
|
-
expect(
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
})
|
|
@@ -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 {
|
|
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(
|
|
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 {
|
|
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";
|
|
@@ -83,6 +87,14 @@ describe("Cart repository", () => {
|
|
|
83
87
|
},
|
|
84
88
|
});
|
|
85
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
|
+
|
|
86
98
|
const cart: CartDraft = {
|
|
87
99
|
anonymousId: "1234567890",
|
|
88
100
|
billingAddress: {
|
|
@@ -97,7 +109,21 @@ describe("Cart repository", () => {
|
|
|
97
109
|
country: "NL",
|
|
98
110
|
currency: "EUR",
|
|
99
111
|
customerEmail: "john.doe@example.com",
|
|
100
|
-
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
|
+
],
|
|
101
127
|
inventoryMode: "None",
|
|
102
128
|
itemShippingAddresses: [],
|
|
103
129
|
lineItems: [
|
|
@@ -173,6 +199,11 @@ describe("Cart repository", () => {
|
|
|
173
199
|
expect(result.lineItems[0].custom?.fields.description as string).toEqual(
|
|
174
200
|
cart.lineItems![0].custom?.fields?.description,
|
|
175
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);
|
|
176
207
|
});
|
|
177
208
|
|
|
178
209
|
test("create cart with business unit", async () => {
|
|
@@ -212,4 +243,55 @@ describe("Cart repository", () => {
|
|
|
212
243
|
typeId: "business-unit",
|
|
213
244
|
});
|
|
214
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
|
+
});
|
|
215
297
|
});
|
|
@@ -5,6 +5,8 @@ import type {
|
|
|
5
5
|
import type {
|
|
6
6
|
Cart,
|
|
7
7
|
CartDraft,
|
|
8
|
+
CustomLineItem,
|
|
9
|
+
CustomLineItemDraft,
|
|
8
10
|
GeneralError,
|
|
9
11
|
LineItem,
|
|
10
12
|
LineItemDraft,
|
|
@@ -22,7 +24,11 @@ import {
|
|
|
22
24
|
} from "../abstract";
|
|
23
25
|
import { createAddress, createCustomFields } from "../helpers";
|
|
24
26
|
import { CartUpdateHandler } from "./actions";
|
|
25
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
calculateCartTotalPrice,
|
|
29
|
+
createCustomLineItemFromDraft,
|
|
30
|
+
selectPrice,
|
|
31
|
+
} from "./helpers";
|
|
26
32
|
|
|
27
33
|
export class CartRepository extends AbstractResourceRepository<"cart"> {
|
|
28
34
|
constructor(config: Config) {
|
|
@@ -69,6 +75,16 @@ export class CartRepository extends AbstractResourceRepository<"cart"> {
|
|
|
69
75
|
),
|
|
70
76
|
) ?? [];
|
|
71
77
|
|
|
78
|
+
const customLineItems =
|
|
79
|
+
draft.customLineItems?.map((draftCustomLineItem) =>
|
|
80
|
+
createCustomLineItemFromDraft(
|
|
81
|
+
context.projectKey,
|
|
82
|
+
draftCustomLineItem,
|
|
83
|
+
this._storage,
|
|
84
|
+
draft.country,
|
|
85
|
+
),
|
|
86
|
+
) ?? [];
|
|
87
|
+
|
|
72
88
|
const resource: Writable<Cart> = {
|
|
73
89
|
...getBaseResourceProperties(),
|
|
74
90
|
anonymousId: draft.anonymousId,
|
|
@@ -86,7 +102,7 @@ export class CartRepository extends AbstractResourceRepository<"cart"> {
|
|
|
86
102
|
country: draft.country,
|
|
87
103
|
customerId: draft.customerId,
|
|
88
104
|
customerEmail: draft.customerEmail,
|
|
89
|
-
customLineItems
|
|
105
|
+
customLineItems,
|
|
90
106
|
directDiscounts: [],
|
|
91
107
|
discountCodes: [],
|
|
92
108
|
inventoryMode: "None",
|