Skip to content

Commit a5cc7d7

Browse files
authored
feat: consume shipping method draft when creating a cart (#338)
1 parent 8d15b05 commit a5cc7d7

File tree

4 files changed

+580
-172
lines changed

4 files changed

+580
-172
lines changed

.changeset/rare-towns-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@labdigital/commercetools-mock": minor
3+
---
4+
5+
Feat: set shipping method from cart draft as shipping method

src/repositories/cart/actions.ts

Lines changed: 9 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ import type {
22
CartSetAnonymousIdAction,
33
CartSetCustomerIdAction,
44
CartUpdateAction,
5-
CentPrecisionMoney,
6-
InvalidOperationError,
7-
MissingTaxRateForCountryError,
8-
ShippingMethodDoesNotMatchCartError,
95
} from "@commercetools/platform-sdk";
106
import type {
117
Address,
@@ -48,29 +44,23 @@ import type {
4844
import type {
4945
CustomLineItem,
5046
DirectDiscount,
51-
TaxPortion,
52-
TaxedItemPrice,
5347
} from "@commercetools/platform-sdk/dist/declarations/src/generated/models/cart";
5448
import type { ShippingMethodResourceIdentifier } from "@commercetools/platform-sdk/dist/declarations/src/generated/models/shipping-method";
55-
import { Decimal } from "decimal.js/decimal";
5649
import { v4 as uuidv4 } from "uuid";
5750
import { CommercetoolsError } from "~src/exceptions";
58-
import { getShippingMethodsMatchingCart } from "~src/shipping";
5951
import type { Writable } from "~src/types";
52+
import type { CartRepository } from ".";
6053
import type { UpdateHandlerInterface } from "../abstract";
6154
import { AbstractUpdateHandler, type RepositoryContext } from "../abstract";
6255
import {
6356
createAddress,
6457
createCentPrecisionMoney,
6558
createCustomFields,
6659
createTypedMoney,
67-
getReferenceFromResourceIdentifier,
68-
roundDecimal,
6960
} from "../helpers";
7061
import {
7162
calculateCartTotalPrice,
7263
calculateLineItemTotalPrice,
73-
calculateTaxedPrice,
7464
createCustomLineItemFromDraft,
7565
selectPrice,
7666
} from "./helpers";
@@ -79,6 +69,12 @@ export class CartUpdateHandler
7969
extends AbstractUpdateHandler
8070
implements Partial<UpdateHandlerInterface<Cart, CartUpdateAction>>
8171
{
72+
private repository: CartRepository;
73+
74+
constructor(storage: any, repository: CartRepository) {
75+
super(storage);
76+
this.repository = repository;
77+
}
8278
addItemShippingAddress(
8379
context: RepositoryContext,
8480
resource: Writable<Cart>,
@@ -769,166 +765,11 @@ export class CartUpdateHandler
769765
{ shippingMethod }: CartSetShippingMethodAction,
770766
) {
771767
if (shippingMethod) {
772-
if (resource.taxMode === "External") {
773-
throw new Error("External tax rate is not supported");
774-
}
775-
776-
const country = resource.shippingAddress?.country;
777-
778-
if (!country) {
779-
throw new CommercetoolsError<InvalidOperationError>({
780-
code: "InvalidOperation",
781-
message: `The cart with ID '${resource.id}' does not have a shipping address set.`,
782-
});
783-
}
784-
785-
// Bit of a hack: calling this checks that the resource identifier is
786-
// valid (i.e. id xor key) and that the shipping method exists.
787-
this._storage.getByResourceIdentifier<"shipping-method">(
788-
context.projectKey,
789-
shippingMethod,
790-
);
791-
792-
// getShippingMethodsMatchingCart does the work of determining whether the
793-
// shipping method is allowed for the cart, and which shipping rate to use
794-
const shippingMethods = getShippingMethodsMatchingCart(
768+
resource.shippingInfo = this.repository.createShippingInfo(
795769
context,
796-
this._storage,
797770
resource,
798-
{
799-
expand: ["zoneRates[*].zone"],
800-
},
801-
);
802-
803-
const method = shippingMethods.results.find((candidate) =>
804-
shippingMethod.id
805-
? candidate.id === shippingMethod.id
806-
: candidate.key === shippingMethod.key,
807-
);
808-
809-
// Not finding the method in the results means it's not allowed, since
810-
// getShippingMethodsMatchingCart only returns allowed methods and we
811-
// already checked that the method exists.
812-
if (!method) {
813-
throw new CommercetoolsError<ShippingMethodDoesNotMatchCartError>({
814-
code: "ShippingMethodDoesNotMatchCart",
815-
message: `The shipping method with ${shippingMethod.id ? `ID '${shippingMethod.id}'` : `key '${shippingMethod.key}'`} is not allowed for the cart with ID '${resource.id}'.`,
816-
});
817-
}
818-
819-
const taxCategory = this._storage.getByResourceIdentifier<"tax-category">(
820-
context.projectKey,
821-
method.taxCategory,
822-
);
823-
824-
// TODO: match state in addition to country
825-
const taxRate = taxCategory.rates.find(
826-
(rate) => rate.country === country,
827-
);
828-
829-
if (!taxRate) {
830-
throw new CommercetoolsError<MissingTaxRateForCountryError>({
831-
code: "MissingTaxRateForCountry",
832-
message: `Tax category '${taxCategory.id}' is missing a tax rate for country '${country}'.`,
833-
taxCategoryId: taxCategory.id,
834-
});
835-
}
836-
837-
// There should only be one zone rate matching the address, since
838-
// Locations cannot be assigned to more than one zone.
839-
// See https://docs.commercetools.com/api/projects/zones#location
840-
const zoneRate = method.zoneRates.find((rate) =>
841-
rate.zone.obj?.locations.some((loc) => loc.country === country),
842-
);
843-
844-
if (!zoneRate) {
845-
// This shouldn't happen because getShippingMethodsMatchingCart already
846-
// filtered out shipping methods without any zones matching the address
847-
throw new Error("Zone rate not found");
848-
}
849-
850-
// Shipping rates are defined by currency, and getShippingMethodsMatchingCart
851-
// also matches on currency, so there should only be one in the array.
852-
// See https://docs.commercetools.com/api/projects/shippingMethods#zonerate
853-
const shippingRate = zoneRate.shippingRates[0];
854-
if (!shippingRate) {
855-
// This shouldn't happen because getShippingMethodsMatchingCart already
856-
// filtered out shipping methods without any matching rates
857-
throw new Error("Shipping rate not found");
858-
}
859-
860-
const shippingRateTier = shippingRate.tiers.find(
861-
(tier) => tier.isMatching,
771+
shippingMethod,
862772
);
863-
if (shippingRateTier && shippingRateTier.type !== "CartValue") {
864-
throw new Error("Non-CartValue shipping rate tier is not supported");
865-
}
866-
867-
const shippingPrice = shippingRateTier
868-
? createCentPrecisionMoney(shippingRateTier.price)
869-
: shippingRate.price;
870-
871-
// TODO: handle freeAbove
872-
873-
const totalGross: CentPrecisionMoney = taxRate.includedInPrice
874-
? shippingPrice
875-
: {
876-
...shippingPrice,
877-
centAmount: roundDecimal(
878-
new Decimal(shippingPrice.centAmount).mul(1 + taxRate.amount),
879-
resource.taxRoundingMode,
880-
).toNumber(),
881-
};
882-
883-
const totalNet: CentPrecisionMoney = taxRate.includedInPrice
884-
? {
885-
...shippingPrice,
886-
centAmount: roundDecimal(
887-
new Decimal(shippingPrice.centAmount).div(1 + taxRate.amount),
888-
resource.taxRoundingMode,
889-
).toNumber(),
890-
}
891-
: shippingPrice;
892-
893-
const taxPortions: TaxPortion[] = [
894-
{
895-
name: taxRate.name,
896-
rate: taxRate.amount,
897-
amount: {
898-
...shippingPrice,
899-
centAmount: totalGross.centAmount - totalNet.centAmount,
900-
},
901-
},
902-
];
903-
904-
const totalTax: CentPrecisionMoney = {
905-
...shippingPrice,
906-
centAmount: taxPortions.reduce(
907-
(acc, portion) => acc + portion.amount.centAmount,
908-
0,
909-
),
910-
};
911-
912-
const taxedPrice: TaxedItemPrice = {
913-
totalNet,
914-
totalGross,
915-
taxPortions,
916-
totalTax,
917-
};
918-
919-
// @ts-ignore
920-
resource.shippingInfo = {
921-
shippingMethod: {
922-
typeId: "shipping-method",
923-
id: method.id,
924-
},
925-
shippingMethodName: method.name,
926-
price: shippingPrice,
927-
shippingRate,
928-
taxedPrice,
929-
taxRate,
930-
taxCategory: method.taxCategory,
931-
};
932773
} else {
933774
resource.shippingInfo = undefined;
934775
}

0 commit comments

Comments
 (0)