@labdigital/commercetools-mock 2.59.1 → 2.60.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 +178 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/tax.test.ts +119 -0
- package/src/lib/tax.ts +186 -0
- package/src/repositories/cart/actions.ts +67 -0
- package/src/repositories/cart/helpers.ts +1 -80
- package/src/repositories/cart/index.test.ts +48 -0
- package/src/repositories/cart/index.ts +5 -4
- package/src/repositories/order/index.test.ts +183 -0
- package/src/repositories/order/index.ts +48 -5
- package/src/services/cart.test.ts +211 -0
package/dist/index.js
CHANGED
|
@@ -8,8 +8,8 @@ import auth from "basic-auth";
|
|
|
8
8
|
import bodyParser from "body-parser";
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
10
10
|
import { isDeepStrictEqual } from "node:util";
|
|
11
|
-
import { Decimal } from "decimal.js";
|
|
12
|
-
import { Decimal as Decimal$1 } from "decimal.js
|
|
11
|
+
import { Decimal } from "decimal.js/decimal";
|
|
12
|
+
import { Decimal as Decimal$1 } from "decimal.js";
|
|
13
13
|
import assert from "node:assert";
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
import { fromZodError } from "zod-validation-error";
|
|
@@ -555,9 +555,9 @@ const createPrice = (draft) => ({
|
|
|
555
555
|
*/
|
|
556
556
|
const roundDecimal = (decimal, roundingMode) => {
|
|
557
557
|
switch (roundingMode) {
|
|
558
|
-
case "HalfEven": return decimal.toDecimalPlaces(0, Decimal
|
|
559
|
-
case "HalfUp": return decimal.toDecimalPlaces(0, Decimal
|
|
560
|
-
case "HalfDown": return decimal.toDecimalPlaces(0, Decimal
|
|
558
|
+
case "HalfEven": return decimal.toDecimalPlaces(0, Decimal.ROUND_HALF_EVEN);
|
|
559
|
+
case "HalfUp": return decimal.toDecimalPlaces(0, Decimal.ROUND_HALF_UP);
|
|
560
|
+
case "HalfDown": return decimal.toDecimalPlaces(0, Decimal.ROUND_HALF_DOWN);
|
|
561
561
|
default: throw new Error(`Unknown rounding mode: ${roundingMode}`);
|
|
562
562
|
}
|
|
563
563
|
};
|
|
@@ -686,6 +686,109 @@ const getBusinessUnitKeyReference = (id, projectKey, storage) => {
|
|
|
686
686
|
};
|
|
687
687
|
};
|
|
688
688
|
|
|
689
|
+
//#endregion
|
|
690
|
+
//#region src/lib/tax.ts
|
|
691
|
+
const calculateTaxTotals = (resource) => {
|
|
692
|
+
const taxedItemPrices = [];
|
|
693
|
+
resource.lineItems.forEach((item) => {
|
|
694
|
+
if (item.taxedPrice) taxedItemPrices.push(item.taxedPrice);
|
|
695
|
+
});
|
|
696
|
+
resource.customLineItems.forEach((item) => {
|
|
697
|
+
if (item.taxedPrice) taxedItemPrices.push(item.taxedPrice);
|
|
698
|
+
});
|
|
699
|
+
let taxedShippingPrice;
|
|
700
|
+
if (resource.shippingInfo?.taxedPrice) {
|
|
701
|
+
taxedShippingPrice = resource.shippingInfo.taxedPrice;
|
|
702
|
+
taxedItemPrices.push(resource.shippingInfo.taxedPrice);
|
|
703
|
+
}
|
|
704
|
+
if (!taxedItemPrices.length) return {
|
|
705
|
+
taxedPrice: void 0,
|
|
706
|
+
taxedShippingPrice
|
|
707
|
+
};
|
|
708
|
+
const currencyCode = resource.totalPrice.currencyCode;
|
|
709
|
+
const toMoney = (centAmount) => createCentPrecisionMoney({
|
|
710
|
+
currencyCode,
|
|
711
|
+
centAmount
|
|
712
|
+
});
|
|
713
|
+
let totalNet = 0;
|
|
714
|
+
let totalGross = 0;
|
|
715
|
+
let totalTax = 0;
|
|
716
|
+
const taxPortionsByRate = new Map();
|
|
717
|
+
taxedItemPrices.forEach((price) => {
|
|
718
|
+
totalNet += price.totalNet.centAmount;
|
|
719
|
+
totalGross += price.totalGross.centAmount;
|
|
720
|
+
const priceTax = price.totalTax ? price.totalTax.centAmount : price.totalGross.centAmount - price.totalNet.centAmount;
|
|
721
|
+
totalTax += Math.max(priceTax, 0);
|
|
722
|
+
price.taxPortions?.forEach((portion) => {
|
|
723
|
+
const key = `${portion.rate}-${portion.name ?? ""}`;
|
|
724
|
+
const existing = taxPortionsByRate.get(key) ?? {
|
|
725
|
+
rate: portion.rate,
|
|
726
|
+
name: portion.name,
|
|
727
|
+
centAmount: 0
|
|
728
|
+
};
|
|
729
|
+
existing.centAmount += portion.amount.centAmount;
|
|
730
|
+
taxPortionsByRate.set(key, existing);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
const taxPortions = Array.from(taxPortionsByRate.values()).map((portion) => ({
|
|
734
|
+
rate: portion.rate,
|
|
735
|
+
name: portion.name,
|
|
736
|
+
amount: toMoney(portion.centAmount)
|
|
737
|
+
}));
|
|
738
|
+
return {
|
|
739
|
+
taxedPrice: {
|
|
740
|
+
totalNet: toMoney(totalNet),
|
|
741
|
+
totalGross: toMoney(totalGross),
|
|
742
|
+
taxPortions,
|
|
743
|
+
totalTax: totalTax > 0 ? toMoney(totalTax) : void 0
|
|
744
|
+
},
|
|
745
|
+
taxedShippingPrice
|
|
746
|
+
};
|
|
747
|
+
};
|
|
748
|
+
const buildTaxedPriceFromRate = (amount, currencyCode, taxRate) => {
|
|
749
|
+
if (!taxRate) return void 0;
|
|
750
|
+
const toMoney = (centAmount) => createCentPrecisionMoney({
|
|
751
|
+
type: "centPrecision",
|
|
752
|
+
currencyCode,
|
|
753
|
+
centAmount
|
|
754
|
+
});
|
|
755
|
+
let netAmount;
|
|
756
|
+
let grossAmount;
|
|
757
|
+
let taxAmount;
|
|
758
|
+
if (taxRate.includedInPrice) {
|
|
759
|
+
grossAmount = amount;
|
|
760
|
+
taxAmount = Math.round(grossAmount * taxRate.amount / (1 + taxRate.amount));
|
|
761
|
+
netAmount = grossAmount - taxAmount;
|
|
762
|
+
} else {
|
|
763
|
+
netAmount = amount;
|
|
764
|
+
taxAmount = Math.round(netAmount * taxRate.amount);
|
|
765
|
+
grossAmount = netAmount + taxAmount;
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
totalNet: toMoney(netAmount),
|
|
769
|
+
totalGross: toMoney(grossAmount),
|
|
770
|
+
totalTax: taxAmount > 0 ? toMoney(taxAmount) : void 0,
|
|
771
|
+
taxPortions: taxAmount > 0 ? [{
|
|
772
|
+
rate: taxRate.amount,
|
|
773
|
+
name: taxRate.name,
|
|
774
|
+
amount: toMoney(taxAmount)
|
|
775
|
+
}] : []
|
|
776
|
+
};
|
|
777
|
+
};
|
|
778
|
+
const calculateTaxedPriceFromRate = (amount, currencyCode, taxRate) => buildTaxedPriceFromRate(amount, currencyCode, taxRate);
|
|
779
|
+
const calculateTaxedPrice = (amount, taxCategory, currency, country) => {
|
|
780
|
+
if (!taxCategory || !taxCategory.rates.length) return void 0;
|
|
781
|
+
const taxRate = taxCategory.rates.find((rate) => !rate.country || rate.country === country) || taxCategory.rates[0];
|
|
782
|
+
const taxedItemPrice = buildTaxedPriceFromRate(amount, currency, taxRate);
|
|
783
|
+
if (!taxedItemPrice) return void 0;
|
|
784
|
+
return {
|
|
785
|
+
totalNet: taxedItemPrice.totalNet,
|
|
786
|
+
totalGross: taxedItemPrice.totalGross,
|
|
787
|
+
taxPortions: taxedItemPrice.taxPortions,
|
|
788
|
+
totalTax: taxedItemPrice.totalTax
|
|
789
|
+
};
|
|
790
|
+
};
|
|
791
|
+
|
|
689
792
|
//#endregion
|
|
690
793
|
//#region src/shipping.ts
|
|
691
794
|
const markMatchingShippingRate = (cart, shippingRate) => {
|
|
@@ -774,11 +877,11 @@ const createShippingInfoFromMethod = (context, storage, resource, method) => {
|
|
|
774
877
|
};
|
|
775
878
|
const totalGross = taxRate.includedInPrice ? shippingPrice : {
|
|
776
879
|
...shippingPrice,
|
|
777
|
-
centAmount: roundDecimal(new Decimal(shippingPrice.centAmount).mul(1 + taxRate.amount), resource.taxRoundingMode || "HalfEven").toNumber()
|
|
880
|
+
centAmount: roundDecimal(new Decimal$1(shippingPrice.centAmount).mul(1 + taxRate.amount), resource.taxRoundingMode || "HalfEven").toNumber()
|
|
778
881
|
};
|
|
779
882
|
const totalNet = taxRate.includedInPrice ? {
|
|
780
883
|
...shippingPrice,
|
|
781
|
-
centAmount: roundDecimal(new Decimal(shippingPrice.centAmount).div(1 + taxRate.amount), resource.taxRoundingMode || "HalfEven").toNumber()
|
|
884
|
+
centAmount: roundDecimal(new Decimal$1(shippingPrice.centAmount).div(1 + taxRate.amount), resource.taxRoundingMode || "HalfEven").toNumber()
|
|
782
885
|
} : shippingPrice;
|
|
783
886
|
const taxPortions = [{
|
|
784
887
|
name: taxRate.name,
|
|
@@ -829,53 +932,6 @@ const calculateCartTotalPrice = (cart) => {
|
|
|
829
932
|
const customLineItemsTotal = cart.customLineItems.reduce((cur, item) => cur + item.totalPrice.centAmount, 0);
|
|
830
933
|
return lineItemsTotal + customLineItemsTotal;
|
|
831
934
|
};
|
|
832
|
-
const calculateTaxedPrice = (amount, taxCategory, currency, country) => {
|
|
833
|
-
if (!taxCategory || !taxCategory.rates.length) return void 0;
|
|
834
|
-
const taxRate = taxCategory.rates.find((rate) => !rate.country || rate.country === country) || taxCategory.rates[0];
|
|
835
|
-
if (!taxRate) return void 0;
|
|
836
|
-
let netAmount;
|
|
837
|
-
let grossAmount;
|
|
838
|
-
let taxAmount;
|
|
839
|
-
if (taxRate.includedInPrice) {
|
|
840
|
-
grossAmount = amount;
|
|
841
|
-
taxAmount = Math.round(grossAmount * taxRate.amount / (1 + taxRate.amount));
|
|
842
|
-
netAmount = grossAmount - taxAmount;
|
|
843
|
-
} else {
|
|
844
|
-
netAmount = amount;
|
|
845
|
-
taxAmount = Math.round(netAmount * taxRate.amount);
|
|
846
|
-
grossAmount = netAmount + taxAmount;
|
|
847
|
-
}
|
|
848
|
-
return {
|
|
849
|
-
totalNet: {
|
|
850
|
-
type: "centPrecision",
|
|
851
|
-
currencyCode: currency,
|
|
852
|
-
centAmount: netAmount,
|
|
853
|
-
fractionDigits: 2
|
|
854
|
-
},
|
|
855
|
-
totalGross: {
|
|
856
|
-
type: "centPrecision",
|
|
857
|
-
currencyCode: currency,
|
|
858
|
-
centAmount: grossAmount,
|
|
859
|
-
fractionDigits: 2
|
|
860
|
-
},
|
|
861
|
-
taxPortions: taxAmount > 0 ? [{
|
|
862
|
-
rate: taxRate.amount,
|
|
863
|
-
amount: {
|
|
864
|
-
type: "centPrecision",
|
|
865
|
-
currencyCode: currency,
|
|
866
|
-
centAmount: taxAmount,
|
|
867
|
-
fractionDigits: 2
|
|
868
|
-
},
|
|
869
|
-
name: taxRate.name
|
|
870
|
-
}] : [],
|
|
871
|
-
totalTax: taxAmount > 0 ? {
|
|
872
|
-
type: "centPrecision",
|
|
873
|
-
currencyCode: currency,
|
|
874
|
-
centAmount: taxAmount,
|
|
875
|
-
fractionDigits: 2
|
|
876
|
-
} : void 0
|
|
877
|
-
};
|
|
878
|
-
};
|
|
879
935
|
const createCustomLineItemFromDraft = (projectKey, draft, storage, country) => {
|
|
880
936
|
const quantity = draft.quantity ?? 1;
|
|
881
937
|
const taxCategoryRef = draft.taxCategory ? getReferenceFromResourceIdentifier(draft.taxCategory, projectKey, storage) : void 0;
|
|
@@ -1228,6 +1284,39 @@ var CartUpdateHandler = class extends AbstractUpdateHandler {
|
|
|
1228
1284
|
};
|
|
1229
1285
|
}
|
|
1230
1286
|
}
|
|
1287
|
+
setLineItemPrice(context, resource, { lineItemId, lineItemKey, externalPrice }) {
|
|
1288
|
+
const lineItem = resource.lineItems.find((x) => lineItemId && x.id === lineItemId || lineItemKey && x.key === lineItemKey);
|
|
1289
|
+
if (!lineItem) throw new CommercetoolsError({
|
|
1290
|
+
code: "General",
|
|
1291
|
+
message: lineItemKey ? `A line item with key '${lineItemKey}' not found.` : `A line item with ID '${lineItemId}' not found.`
|
|
1292
|
+
});
|
|
1293
|
+
if (!externalPrice && lineItem.priceMode !== "ExternalPrice") return;
|
|
1294
|
+
if (externalPrice && externalPrice.currencyCode !== resource.totalPrice.currencyCode) throw new CommercetoolsError({
|
|
1295
|
+
code: "General",
|
|
1296
|
+
message: `Currency mismatch. Expected '${resource.totalPrice.currencyCode}' but got '${externalPrice.currencyCode}'.`
|
|
1297
|
+
});
|
|
1298
|
+
if (externalPrice) {
|
|
1299
|
+
lineItem.priceMode = "ExternalPrice";
|
|
1300
|
+
const priceValue = createTypedMoney(externalPrice);
|
|
1301
|
+
lineItem.price = lineItem.price ?? { id: v4() };
|
|
1302
|
+
lineItem.price.value = priceValue;
|
|
1303
|
+
} else {
|
|
1304
|
+
lineItem.priceMode = "Platform";
|
|
1305
|
+
const price = selectPrice({
|
|
1306
|
+
prices: lineItem.variant.prices,
|
|
1307
|
+
currency: resource.totalPrice.currencyCode,
|
|
1308
|
+
country: resource.country
|
|
1309
|
+
});
|
|
1310
|
+
if (!price) throw new Error(`No valid price found for ${lineItem.productId} for country ${resource.country} and currency ${resource.totalPrice.currencyCode}`);
|
|
1311
|
+
lineItem.price = price;
|
|
1312
|
+
}
|
|
1313
|
+
const lineItemTotal = calculateLineItemTotalPrice(lineItem);
|
|
1314
|
+
lineItem.totalPrice = createCentPrecisionMoney({
|
|
1315
|
+
...lineItem.price.value,
|
|
1316
|
+
centAmount: lineItemTotal
|
|
1317
|
+
});
|
|
1318
|
+
resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
|
|
1319
|
+
}
|
|
1231
1320
|
setLineItemShippingDetails(context, resource, { action, shippingDetails, lineItemId, lineItemKey }) {
|
|
1232
1321
|
const lineItem = resource.lineItems.find((x) => lineItemId && x.id === lineItemId || lineItemKey && x.key === lineItemKey);
|
|
1233
1322
|
if (!lineItem) throw new CommercetoolsError({
|
|
@@ -1360,6 +1449,9 @@ var CartRepository = class extends AbstractResourceRepository {
|
|
|
1360
1449
|
key: draft.store.key
|
|
1361
1450
|
} : void 0;
|
|
1362
1451
|
if (draft.shippingMethod) resource.shippingInfo = this.createShippingInfo(context, resource, draft.shippingMethod);
|
|
1452
|
+
const { taxedPrice, taxedShippingPrice } = calculateTaxTotals(resource);
|
|
1453
|
+
resource.taxedPrice = taxedPrice;
|
|
1454
|
+
resource.taxedShippingPrice = taxedShippingPrice;
|
|
1363
1455
|
return this.saveNew(context, resource);
|
|
1364
1456
|
}
|
|
1365
1457
|
getActiveCart(projectKey) {
|
|
@@ -1647,6 +1739,14 @@ var OrderRepository = class extends AbstractResourceRepository {
|
|
|
1647
1739
|
totalPrice: cart.totalPrice,
|
|
1648
1740
|
store: cart.store
|
|
1649
1741
|
};
|
|
1742
|
+
const { taxedPrice, taxedShippingPrice } = calculateTaxTotals({
|
|
1743
|
+
lineItems: cart.lineItems,
|
|
1744
|
+
customLineItems: cart.customLineItems,
|
|
1745
|
+
shippingInfo: cart.shippingInfo,
|
|
1746
|
+
totalPrice: cart.totalPrice
|
|
1747
|
+
});
|
|
1748
|
+
resource.taxedPrice = resource.taxedPrice ?? taxedPrice;
|
|
1749
|
+
resource.taxedShippingPrice = resource.taxedShippingPrice ?? taxedShippingPrice;
|
|
1650
1750
|
return this.saveNew(context, resource);
|
|
1651
1751
|
}
|
|
1652
1752
|
import(context, draft) {
|
|
@@ -1692,6 +1792,14 @@ var OrderRepository = class extends AbstractResourceRepository {
|
|
|
1692
1792
|
id: shippingMethodRef.id
|
|
1693
1793
|
});
|
|
1694
1794
|
}
|
|
1795
|
+
const { taxedPrice, taxedShippingPrice } = calculateTaxTotals({
|
|
1796
|
+
lineItems: resource.lineItems,
|
|
1797
|
+
customLineItems: resource.customLineItems,
|
|
1798
|
+
shippingInfo: resource.shippingInfo,
|
|
1799
|
+
totalPrice: resource.totalPrice
|
|
1800
|
+
});
|
|
1801
|
+
resource.taxedPrice = resource.taxedPrice ?? taxedPrice;
|
|
1802
|
+
resource.taxedShippingPrice = resource.taxedShippingPrice ?? taxedShippingPrice;
|
|
1695
1803
|
return this.saveNew(context, resource);
|
|
1696
1804
|
}
|
|
1697
1805
|
lineItemFromImportDraft(context, draft) {
|
|
@@ -1712,6 +1820,11 @@ var OrderRepository = class extends AbstractResourceRepository {
|
|
|
1712
1820
|
else variant = product.masterData.current.variants.find((v) => v.sku === draft.variant.sku);
|
|
1713
1821
|
if (!variant) throw new Error("Internal state error");
|
|
1714
1822
|
} else throw new Error("No product found");
|
|
1823
|
+
const quantity = draft.quantity ?? 1;
|
|
1824
|
+
const totalPrice = createCentPrecisionMoney({
|
|
1825
|
+
...draft.price.value,
|
|
1826
|
+
centAmount: (draft.price.value.centAmount ?? 0) * quantity
|
|
1827
|
+
});
|
|
1715
1828
|
const lineItem = {
|
|
1716
1829
|
...getBaseResourceProperties(),
|
|
1717
1830
|
custom: createCustomFields(draft.custom, context.projectKey, this._storage),
|
|
@@ -1722,12 +1835,13 @@ var OrderRepository = class extends AbstractResourceRepository {
|
|
|
1722
1835
|
priceMode: "Platform",
|
|
1723
1836
|
productId: product.id,
|
|
1724
1837
|
productType: product.productType,
|
|
1725
|
-
quantity
|
|
1838
|
+
quantity,
|
|
1726
1839
|
state: draft.state || [],
|
|
1727
1840
|
taxRate: draft.taxRate,
|
|
1841
|
+
taxedPrice: calculateTaxedPriceFromRate(totalPrice.centAmount, totalPrice.currencyCode, draft.taxRate),
|
|
1728
1842
|
taxedPricePortions: [],
|
|
1729
1843
|
perMethodTaxRate: [],
|
|
1730
|
-
totalPrice
|
|
1844
|
+
totalPrice,
|
|
1731
1845
|
variant: {
|
|
1732
1846
|
id: variant.id,
|
|
1733
1847
|
sku: variant.sku,
|
|
@@ -1738,18 +1852,24 @@ var OrderRepository = class extends AbstractResourceRepository {
|
|
|
1738
1852
|
return lineItem;
|
|
1739
1853
|
}
|
|
1740
1854
|
customLineItemFromImportDraft(context, draft) {
|
|
1855
|
+
const quantity = draft.quantity ?? 1;
|
|
1856
|
+
const totalPrice = createCentPrecisionMoney({
|
|
1857
|
+
...draft.money,
|
|
1858
|
+
centAmount: (draft.money.centAmount ?? 0) * quantity
|
|
1859
|
+
});
|
|
1741
1860
|
const lineItem = {
|
|
1742
1861
|
...getBaseResourceProperties(),
|
|
1743
1862
|
custom: createCustomFields(draft.custom, context.projectKey, this._storage),
|
|
1744
1863
|
discountedPricePerQuantity: [],
|
|
1745
1864
|
money: createTypedMoney(draft.money),
|
|
1746
1865
|
name: draft.name,
|
|
1747
|
-
quantity
|
|
1866
|
+
quantity,
|
|
1748
1867
|
perMethodTaxRate: [],
|
|
1749
1868
|
priceMode: draft.priceMode ?? "Standard",
|
|
1750
1869
|
slug: draft.slug,
|
|
1751
1870
|
state: [],
|
|
1752
|
-
totalPrice
|
|
1871
|
+
totalPrice,
|
|
1872
|
+
taxedPrice: calculateTaxedPriceFromRate(totalPrice.centAmount, totalPrice.currencyCode, draft.taxRate),
|
|
1753
1873
|
taxedPricePortions: []
|
|
1754
1874
|
};
|
|
1755
1875
|
return lineItem;
|