@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.
- package/dist/index.d.ts +107 -77
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +455 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/lib/predicateParser.test.ts +13 -37
- package/src/lib/predicateParser.ts +12 -1
- package/src/lib/productSearchFilter.test.ts +1 -0
- package/src/lib/projectionSearchFilter.test.ts +1 -0
- package/src/priceSelector.test.ts +1 -0
- package/src/product-projection-search.ts +2 -0
- package/src/product-search.ts +1 -0
- package/src/repositories/cart/actions.ts +178 -0
- package/src/repositories/cart/helpers.ts +170 -3
- package/src/repositories/cart/index.test.ts +86 -2
- package/src/repositories/cart/index.ts +19 -2
- package/src/repositories/cart-discount/index.ts +1 -1
- package/src/repositories/customer/index.ts +2 -0
- package/src/repositories/discount-group/actions.ts +50 -0
- package/src/repositories/discount-group/index.ts +29 -0
- package/src/repositories/index.ts +6 -0
- package/src/repositories/order/index.test.ts +126 -125
- package/src/repositories/payment/actions.ts +87 -0
- package/src/repositories/payment/index.ts +1 -1
- package/src/repositories/product/index.ts +1 -0
- package/src/repositories/product-type.ts +1 -0
- package/src/repositories/quote/index.ts +1 -0
- package/src/repositories/quote-request/index.test.ts +1 -0
- package/src/repositories/quote-request/index.ts +1 -0
- package/src/repositories/recurrence-policy/actions.ts +53 -0
- package/src/repositories/recurrence-policy/index.ts +36 -0
- package/src/repositories/recurring-order/actions.ts +157 -0
- package/src/repositories/recurring-order/index.ts +52 -0
- package/src/repositories/review.test.ts +2 -0
- package/src/repositories/shopping-list/actions.ts +1 -0
- package/src/repositories/shopping-list/index.ts +1 -0
- package/src/services/cart.test.ts +282 -0
- package/src/services/discount-group.test.ts +270 -0
- package/src/services/discount-group.ts +16 -0
- package/src/services/index.ts +12 -0
- package/src/services/my-cart.test.ts +1 -0
- package/src/services/my-payment.test.ts +1 -0
- package/src/services/payment.test.ts +1 -0
- package/src/services/product-projection.test.ts +4 -0
- package/src/services/product-type.test.ts +1 -0
- package/src/services/product.test.ts +1 -0
- package/src/services/recurrence-policy.test.ts +316 -0
- package/src/services/recurrence-policy.ts +16 -0
- package/src/services/recurring-order.test.ts +424 -0
- package/src/services/recurring-order.ts +16 -0
- package/src/services/shopping-list.test.ts +3 -0
- package/src/storage/in-memory.ts +6 -0
- 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.
|
|
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.
|
|
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
|
|
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
|
})
|
|
@@ -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,
|
package/src/product-search.ts
CHANGED
|
@@ -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 {
|
|
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";
|
|
@@ -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
|
});
|