@labdigital/commercetools-mock 2.34.3 → 2.35.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.cjs +244 -158
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +244 -158
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/product-projection-search.ts +1 -1
- package/src/product-search.ts +1 -1
- package/src/repositories/cart/actions.ts +160 -4
- package/src/repositories/helpers.ts +21 -0
- package/src/repositories/product-projection.ts +1 -1
- package/src/repositories/shipping-method/index.ts +2 -54
- package/src/services/cart.test.ts +417 -0
- package/src/{shippingCalculator.test.ts → shipping.test.ts} +1 -1
- package/src/shipping.ts +147 -0
- package/src/shippingCalculator.ts +0 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@labdigital/commercetools-mock",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.35.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Michael van Tellingen",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"basic-auth": "^2.0.1",
|
|
23
23
|
"body-parser": "^1.20.2",
|
|
24
|
+
"decimal.js": "10.4.3",
|
|
24
25
|
"deep-equal": "^2.2.3",
|
|
25
26
|
"express": "^4.19.2",
|
|
26
27
|
"light-my-request": "^5.11.1",
|
|
@@ -61,7 +61,7 @@ export class ProductProjectionSearch {
|
|
|
61
61
|
.all(projectKey, "product")
|
|
62
62
|
.map((r) => this.transform(r, params.staged ?? false))
|
|
63
63
|
.filter((p) => {
|
|
64
|
-
if (!params.staged ?? false) {
|
|
64
|
+
if (!(params.staged ?? false)) {
|
|
65
65
|
return p.published;
|
|
66
66
|
}
|
|
67
67
|
return true;
|
package/src/product-search.ts
CHANGED
|
@@ -29,7 +29,7 @@ export class ProductSearch {
|
|
|
29
29
|
this.transform(r, params.productProjectionParameters?.staged ?? false),
|
|
30
30
|
)
|
|
31
31
|
.filter((p) => {
|
|
32
|
-
if (!params.productProjectionParameters?.staged ?? false) {
|
|
32
|
+
if (!(params.productProjectionParameters?.staged ?? false)) {
|
|
33
33
|
return p.published;
|
|
34
34
|
}
|
|
35
35
|
return true;
|
|
@@ -2,6 +2,10 @@ import {
|
|
|
2
2
|
CartSetAnonymousIdAction,
|
|
3
3
|
CartSetCustomerIdAction,
|
|
4
4
|
CartUpdateAction,
|
|
5
|
+
CentPrecisionMoney,
|
|
6
|
+
InvalidOperationError,
|
|
7
|
+
MissingTaxRateForCountryError,
|
|
8
|
+
ShippingMethodDoesNotMatchCartError,
|
|
5
9
|
type Address,
|
|
6
10
|
type AddressDraft,
|
|
7
11
|
type Cart,
|
|
@@ -32,9 +36,15 @@ import {
|
|
|
32
36
|
type ProductPagedQueryResponse,
|
|
33
37
|
type ProductVariant,
|
|
34
38
|
} from "@commercetools/platform-sdk";
|
|
35
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
DirectDiscount,
|
|
41
|
+
TaxPortion,
|
|
42
|
+
TaxedItemPrice,
|
|
43
|
+
} from "@commercetools/platform-sdk/dist/declarations/src/generated/models/cart";
|
|
44
|
+
import { Decimal } from "decimal.js/decimal";
|
|
36
45
|
import { v4 as uuidv4 } from "uuid";
|
|
37
46
|
import { CommercetoolsError } from "~src/exceptions";
|
|
47
|
+
import { getShippingMethodsMatchingCart } from "~src/shipping";
|
|
38
48
|
import type { Writable } from "~src/types";
|
|
39
49
|
import {
|
|
40
50
|
AbstractUpdateHandler,
|
|
@@ -46,6 +56,7 @@ import {
|
|
|
46
56
|
createCentPrecisionMoney,
|
|
47
57
|
createCustomFields,
|
|
48
58
|
createTypedMoney,
|
|
59
|
+
roundDecimal,
|
|
49
60
|
} from "../helpers";
|
|
50
61
|
import {
|
|
51
62
|
calculateCartTotalPrice,
|
|
@@ -575,13 +586,153 @@ export class CartUpdateHandler
|
|
|
575
586
|
{ shippingMethod }: CartSetShippingMethodAction,
|
|
576
587
|
) {
|
|
577
588
|
if (shippingMethod) {
|
|
578
|
-
|
|
589
|
+
if (resource.taxMode === "External") {
|
|
590
|
+
throw new Error("External tax rate is not supported");
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const country = resource.shippingAddress?.country;
|
|
594
|
+
|
|
595
|
+
if (!country) {
|
|
596
|
+
throw new CommercetoolsError<InvalidOperationError>({
|
|
597
|
+
code: "InvalidOperation",
|
|
598
|
+
message: `The cart with ID '${resource.id}' does not have a shipping address set.`,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Bit of a hack: calling this checks that the resource identifier is
|
|
603
|
+
// valid (i.e. id xor key) and that the shipping method exists.
|
|
604
|
+
this._storage.getByResourceIdentifier<"shipping-method">(
|
|
579
605
|
context.projectKey,
|
|
580
606
|
shippingMethod,
|
|
581
607
|
);
|
|
582
608
|
|
|
583
|
-
//
|
|
584
|
-
//
|
|
609
|
+
// getShippingMethodsMatchingCart does the work of determining whether the
|
|
610
|
+
// shipping method is allowed for the cart, and which shipping rate to use
|
|
611
|
+
const shippingMethods = getShippingMethodsMatchingCart(
|
|
612
|
+
context,
|
|
613
|
+
this._storage,
|
|
614
|
+
resource,
|
|
615
|
+
{
|
|
616
|
+
expand: ["zoneRates[*].zone"],
|
|
617
|
+
},
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const method = shippingMethods.results.find((candidate) =>
|
|
621
|
+
shippingMethod.id
|
|
622
|
+
? candidate.id === shippingMethod.id
|
|
623
|
+
: candidate.key === shippingMethod.key,
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
// Not finding the method in the results means it's not allowed, since
|
|
627
|
+
// getShippingMethodsMatchingCart only returns allowed methods and we
|
|
628
|
+
// already checked that the method exists.
|
|
629
|
+
if (!method) {
|
|
630
|
+
throw new CommercetoolsError<ShippingMethodDoesNotMatchCartError>({
|
|
631
|
+
code: "ShippingMethodDoesNotMatchCart",
|
|
632
|
+
message: `The shipping method with ${shippingMethod.id ? `ID '${shippingMethod.id}'` : `key '${shippingMethod.key}'`} is not allowed for the cart with ID '${resource.id}'.`,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const taxCategory = this._storage.getByResourceIdentifier<"tax-category">(
|
|
637
|
+
context.projectKey,
|
|
638
|
+
method.taxCategory,
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
// TODO: match state in addition to country
|
|
642
|
+
const taxRate = taxCategory.rates.find(
|
|
643
|
+
(rate) => rate.country === country,
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
if (!taxRate) {
|
|
647
|
+
throw new CommercetoolsError<MissingTaxRateForCountryError>({
|
|
648
|
+
code: "MissingTaxRateForCountry",
|
|
649
|
+
message: `Tax category '${taxCategory.id}' is missing a tax rate for country '${country}'.`,
|
|
650
|
+
taxCategoryId: taxCategory.id,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// There should only be one zone rate matching the address, since
|
|
655
|
+
// Locations cannot be assigned to more than one zone.
|
|
656
|
+
// See https://docs.commercetools.com/api/projects/zones#location
|
|
657
|
+
const zoneRate = method.zoneRates.find((rate) =>
|
|
658
|
+
rate.zone.obj!.locations.some((loc) => loc.country === country),
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
if (!zoneRate) {
|
|
662
|
+
// This shouldn't happen because getShippingMethodsMatchingCart already
|
|
663
|
+
// filtered out shipping methods without any zones matching the address
|
|
664
|
+
throw new Error("Zone rate not found");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Shipping rates are defined by currency, and getShippingMethodsMatchingCart
|
|
668
|
+
// also matches on currency, so there should only be one in the array.
|
|
669
|
+
// See https://docs.commercetools.com/api/projects/shippingMethods#zonerate
|
|
670
|
+
const shippingRate = zoneRate.shippingRates[0];
|
|
671
|
+
if (!shippingRate) {
|
|
672
|
+
// This shouldn't happen because getShippingMethodsMatchingCart already
|
|
673
|
+
// filtered out shipping methods without any matching rates
|
|
674
|
+
throw new Error("Shipping rate not found");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const shippingRateTier = shippingRate.tiers.find(
|
|
678
|
+
(tier) => tier.isMatching,
|
|
679
|
+
);
|
|
680
|
+
if (shippingRateTier && shippingRateTier.type !== "CartValue") {
|
|
681
|
+
throw new Error("Non-CartValue shipping rate tier is not supported");
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const shippingPrice = shippingRateTier
|
|
685
|
+
? createCentPrecisionMoney(shippingRateTier.price)
|
|
686
|
+
: shippingRate.price;
|
|
687
|
+
|
|
688
|
+
// TODO: handle freeAbove
|
|
689
|
+
|
|
690
|
+
const totalGross: CentPrecisionMoney = taxRate.includedInPrice
|
|
691
|
+
? shippingPrice
|
|
692
|
+
: {
|
|
693
|
+
...shippingPrice,
|
|
694
|
+
centAmount: roundDecimal(
|
|
695
|
+
new Decimal(shippingPrice.centAmount).mul(1 + taxRate.amount),
|
|
696
|
+
resource.taxRoundingMode,
|
|
697
|
+
).toNumber(),
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const totalNet: CentPrecisionMoney = taxRate.includedInPrice
|
|
701
|
+
? {
|
|
702
|
+
...shippingPrice,
|
|
703
|
+
centAmount: roundDecimal(
|
|
704
|
+
new Decimal(shippingPrice.centAmount).div(1 + taxRate.amount),
|
|
705
|
+
resource.taxRoundingMode,
|
|
706
|
+
).toNumber(),
|
|
707
|
+
}
|
|
708
|
+
: shippingPrice;
|
|
709
|
+
|
|
710
|
+
const taxPortions: TaxPortion[] = [
|
|
711
|
+
{
|
|
712
|
+
name: taxRate.name,
|
|
713
|
+
rate: taxRate.amount,
|
|
714
|
+
amount: {
|
|
715
|
+
...shippingPrice,
|
|
716
|
+
centAmount: totalGross.centAmount - totalNet.centAmount,
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
];
|
|
720
|
+
|
|
721
|
+
const totalTax: CentPrecisionMoney = {
|
|
722
|
+
...shippingPrice,
|
|
723
|
+
centAmount: taxPortions.reduce(
|
|
724
|
+
(acc, portion) => acc + portion.amount.centAmount,
|
|
725
|
+
0,
|
|
726
|
+
),
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const taxedPrice: TaxedItemPrice = {
|
|
730
|
+
totalNet,
|
|
731
|
+
totalGross,
|
|
732
|
+
taxPortions,
|
|
733
|
+
totalTax,
|
|
734
|
+
};
|
|
735
|
+
|
|
585
736
|
// @ts-ignore
|
|
586
737
|
resource.shippingInfo = {
|
|
587
738
|
shippingMethod: {
|
|
@@ -589,6 +740,11 @@ export class CartUpdateHandler
|
|
|
589
740
|
id: method.id,
|
|
590
741
|
},
|
|
591
742
|
shippingMethodName: method.name,
|
|
743
|
+
price: shippingPrice,
|
|
744
|
+
shippingRate,
|
|
745
|
+
taxedPrice,
|
|
746
|
+
taxRate,
|
|
747
|
+
taxCategory: method.taxCategory,
|
|
592
748
|
};
|
|
593
749
|
} else {
|
|
594
750
|
resource.shippingInfo = undefined;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
BusinessUnitKeyReference,
|
|
4
4
|
BusinessUnitReference,
|
|
5
5
|
BusinessUnitResourceIdentifier,
|
|
6
|
+
RoundingMode,
|
|
6
7
|
type Address,
|
|
7
8
|
type Associate,
|
|
8
9
|
type AssociateDraft,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
type Type,
|
|
30
31
|
type _Money,
|
|
31
32
|
} from "@commercetools/platform-sdk";
|
|
33
|
+
import { Decimal } from "decimal.js/decimal";
|
|
32
34
|
import type { Request } from "express";
|
|
33
35
|
import { v4 as uuidv4 } from "uuid";
|
|
34
36
|
import { CommercetoolsError } from "~src/exceptions";
|
|
@@ -84,6 +86,25 @@ export const createPrice = (draft: PriceDraft): Price => ({
|
|
|
84
86
|
value: createTypedMoney(draft.value),
|
|
85
87
|
});
|
|
86
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Rounds a decimal to the nearest whole number using the specified
|
|
91
|
+
* (Commercetools) rounding mode.
|
|
92
|
+
*
|
|
93
|
+
* @see https://docs.commercetools.com/api/projects/carts#roundingmode
|
|
94
|
+
*/
|
|
95
|
+
export const roundDecimal = (decimal: Decimal, roundingMode: RoundingMode) => {
|
|
96
|
+
switch (roundingMode) {
|
|
97
|
+
case "HalfEven":
|
|
98
|
+
return decimal.toDecimalPlaces(0, Decimal.ROUND_HALF_EVEN);
|
|
99
|
+
case "HalfUp":
|
|
100
|
+
return decimal.toDecimalPlaces(0, Decimal.ROUND_HALF_UP);
|
|
101
|
+
case "HalfDown":
|
|
102
|
+
return decimal.toDecimalPlaces(0, Decimal.ROUND_HALF_DOWN);
|
|
103
|
+
default:
|
|
104
|
+
throw new Error(`Unknown rounding mode: ${roundingMode}`);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
87
108
|
export const createCentPrecisionMoney = (value: _Money): CentPrecisionMoney => {
|
|
88
109
|
// Taken from https://docs.adyen.com/development-resources/currency-codes
|
|
89
110
|
let fractionDigits = 2;
|
|
@@ -66,7 +66,7 @@ export class ProductProjectionRepository extends AbstractResourceRepository<"pro
|
|
|
66
66
|
.all(context.projectKey, "product")
|
|
67
67
|
.map((r) => this._searchService.transform(r, params.staged ?? false))
|
|
68
68
|
.filter((p) => {
|
|
69
|
-
if (!params.staged ?? false) {
|
|
69
|
+
if (!(params.staged ?? false)) {
|
|
70
70
|
return p.published;
|
|
71
71
|
}
|
|
72
72
|
return true;
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
InvalidOperationError,
|
|
3
2
|
type ShippingMethod,
|
|
4
3
|
type ShippingMethodDraft,
|
|
5
4
|
type ZoneRate,
|
|
6
5
|
type ZoneRateDraft,
|
|
7
6
|
type ZoneReference,
|
|
8
7
|
} from "@commercetools/platform-sdk";
|
|
9
|
-
import { CommercetoolsError } from "~src/exceptions";
|
|
10
8
|
import { getBaseResourceProperties } from "../../helpers";
|
|
11
|
-
import {
|
|
9
|
+
import { getShippingMethodsMatchingCart } from "../../shipping";
|
|
12
10
|
import { AbstractStorage } from "../../storage/abstract";
|
|
13
11
|
import {
|
|
14
12
|
AbstractResourceRepository,
|
|
@@ -69,57 +67,7 @@ export class ShippingMethodRepository extends AbstractResourceRepository<"shippi
|
|
|
69
67
|
return undefined;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
throw new CommercetoolsError<InvalidOperationError>({
|
|
74
|
-
code: "InvalidOperation",
|
|
75
|
-
message: `The cart with ID '${cart.id}' does not have a shipping address set.`,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Get all shipping methods that have a zone that matches the shipping address
|
|
80
|
-
const zones = this._storage.query<"zone">(context.projectKey, "zone", {
|
|
81
|
-
where: [`locations(country="${cart.shippingAddress.country}"))`],
|
|
82
|
-
limit: 100,
|
|
83
|
-
});
|
|
84
|
-
const zoneIds = zones.results.map((zone) => zone.id);
|
|
85
|
-
const shippingMethods = this.query(context, {
|
|
86
|
-
"where": [
|
|
87
|
-
`zoneRates(zone(id in (:zoneIds)))`,
|
|
88
|
-
`zoneRates(shippingRates(price(currencyCode="${cart.totalPrice.currencyCode}")))`,
|
|
89
|
-
],
|
|
90
|
-
"var.zoneIds": zoneIds,
|
|
91
|
-
"expand": params.expand,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Make sure that each shipping method has exactly one shipping rate and
|
|
95
|
-
// that the shipping rate is marked as matching
|
|
96
|
-
const results = shippingMethods.results
|
|
97
|
-
.map((shippingMethod) => {
|
|
98
|
-
// Iterate through the zoneRates, process the shipping rates and filter
|
|
99
|
-
// out all zoneRates which have no matching shipping rates left
|
|
100
|
-
const rates = shippingMethod.zoneRates
|
|
101
|
-
.map((zoneRate) => ({
|
|
102
|
-
zone: zoneRate.zone,
|
|
103
|
-
|
|
104
|
-
// Iterate through the shippingRates and mark the matching ones
|
|
105
|
-
// then we filter out the non-matching ones
|
|
106
|
-
shippingRates: zoneRate.shippingRates
|
|
107
|
-
.map((rate) => markMatchingShippingRate(cart, rate))
|
|
108
|
-
.filter((rate) => rate.isMatching),
|
|
109
|
-
}))
|
|
110
|
-
.filter((zoneRate) => zoneRate.shippingRates.length > 0);
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
...shippingMethod,
|
|
114
|
-
zoneRates: rates,
|
|
115
|
-
};
|
|
116
|
-
})
|
|
117
|
-
.filter((shippingMethod) => shippingMethod.zoneRates.length > 0);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
...shippingMethods,
|
|
121
|
-
results: results,
|
|
122
|
-
};
|
|
70
|
+
return getShippingMethodsMatchingCart(context, this._storage, cart, params);
|
|
123
71
|
}
|
|
124
72
|
|
|
125
73
|
private _transformZoneRateDraft(
|