Skip to content

Commit 375f7a2

Browse files
feat(cart): support setLineItemPrice (#346)
Add support for the `setLineItemPrice` on the Cart.
1 parent 043acdf commit 375f7a2

File tree

3 files changed

+283
-0
lines changed

3 files changed

+283
-0
lines changed

.changeset/kind-flowers-start.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(cart): add support for set line-item prices

src/repositories/cart/actions.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
CartSetDirectDiscountsAction,
2929
CartSetLineItemCustomFieldAction,
3030
CartSetLineItemCustomTypeAction,
31+
CartSetLineItemPriceAction,
3132
CartSetLineItemShippingDetailsAction,
3233
CartSetLocaleAction,
3334
CartSetShippingAddressAction,
@@ -740,6 +741,72 @@ export class CartUpdateHandler
740741
}
741742
}
742743

744+
setLineItemPrice(
745+
context: RepositoryContext,
746+
resource: Writable<Cart>,
747+
{ lineItemId, lineItemKey, externalPrice }: CartSetLineItemPriceAction,
748+
) {
749+
const lineItem = resource.lineItems.find(
750+
(x) =>
751+
(lineItemId && x.id === lineItemId) ||
752+
(lineItemKey && x.key === lineItemKey),
753+
);
754+
755+
if (!lineItem) {
756+
throw new CommercetoolsError<GeneralError>({
757+
code: "General",
758+
message: lineItemKey
759+
? `A line item with key '${lineItemKey}' not found.`
760+
: `A line item with ID '${lineItemId}' not found.`,
761+
});
762+
}
763+
764+
if (!externalPrice && lineItem.priceMode !== "ExternalPrice") {
765+
return;
766+
}
767+
768+
if (
769+
externalPrice &&
770+
externalPrice.currencyCode !== resource.totalPrice.currencyCode
771+
) {
772+
throw new CommercetoolsError<GeneralError>({
773+
code: "General",
774+
message: `Currency mismatch. Expected '${resource.totalPrice.currencyCode}' but got '${externalPrice.currencyCode}'.`,
775+
});
776+
}
777+
778+
if (externalPrice) {
779+
lineItem.priceMode = "ExternalPrice";
780+
const priceValue = createTypedMoney(externalPrice);
781+
782+
lineItem.price = lineItem.price ?? { id: uuidv4() };
783+
lineItem.price.value = priceValue;
784+
} else {
785+
lineItem.priceMode = "Platform";
786+
787+
const price = selectPrice({
788+
prices: lineItem.variant.prices,
789+
currency: resource.totalPrice.currencyCode,
790+
country: resource.country,
791+
});
792+
793+
if (!price) {
794+
throw new Error(
795+
`No valid price found for ${lineItem.productId} for country ${resource.country} and currency ${resource.totalPrice.currencyCode}`,
796+
);
797+
}
798+
799+
lineItem.price = price;
800+
}
801+
802+
const lineItemTotal = calculateLineItemTotalPrice(lineItem);
803+
lineItem.totalPrice = createCentPrecisionMoney({
804+
...lineItem.price!.value,
805+
centAmount: lineItemTotal,
806+
});
807+
resource.totalPrice.centAmount = calculateCartTotalPrice(resource);
808+
}
809+
743810
setLineItemShippingDetails(
744811
context: RepositoryContext,
745812
resource: Writable<Cart>,

src/services/cart.test.ts

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,217 @@ describe("Cart Update Actions", () => {
711711
]);
712712
});
713713

714+
test("setLineItemPrice sets an external price for a line item", async () => {
715+
const product = await supertest(ctMock.app)
716+
.post("/dummy/products")
717+
.send(productDraft)
718+
.then((x) => x.body);
719+
720+
assert(product, "product not created");
721+
722+
const baseCartResponse = await supertest(ctMock.app)
723+
.post("/dummy/carts")
724+
.send({ currency: "EUR" });
725+
expect(baseCartResponse.status).toBe(201);
726+
const baseCart = baseCartResponse.body as Cart;
727+
728+
const addLineItemResponse = await supertest(ctMock.app)
729+
.post(`/dummy/carts/${baseCart.id}`)
730+
.send({
731+
version: baseCart.version,
732+
actions: [
733+
{
734+
action: "addLineItem",
735+
sku: product.masterData.current.masterVariant.sku,
736+
quantity: 2,
737+
key: "line-item-key",
738+
},
739+
],
740+
});
741+
expect(addLineItemResponse.status).toBe(200);
742+
const cartWithLineItem = addLineItemResponse.body as Cart;
743+
const lineItem = cartWithLineItem.lineItems[0];
744+
assert(lineItem, "lineItem not created");
745+
746+
const externalPrice: CentPrecisionMoney = {
747+
type: "centPrecision",
748+
currencyCode: "EUR",
749+
centAmount: 2500,
750+
fractionDigits: 2,
751+
};
752+
753+
const response = await supertest(ctMock.app)
754+
.post(`/dummy/carts/${cartWithLineItem.id}`)
755+
.send({
756+
version: cartWithLineItem.version,
757+
actions: [
758+
{
759+
action: "setLineItemPrice",
760+
lineItemKey: lineItem.key,
761+
externalPrice,
762+
},
763+
],
764+
});
765+
766+
expect(response.status).toBe(200);
767+
expect(response.body.version).toBe(cartWithLineItem.version + 1);
768+
expect(response.body.lineItems).toHaveLength(1);
769+
770+
const updatedLineItem = response.body.lineItems[0];
771+
expect(updatedLineItem.priceMode).toBe("ExternalPrice");
772+
expect(updatedLineItem.price.value.centAmount).toBe(
773+
externalPrice.centAmount,
774+
);
775+
expect(updatedLineItem.price.value.currencyCode).toBe(
776+
externalPrice.currencyCode,
777+
);
778+
expect(updatedLineItem.totalPrice.centAmount).toBe(
779+
externalPrice.centAmount * updatedLineItem.quantity,
780+
);
781+
expect(response.body.totalPrice.centAmount).toBe(
782+
externalPrice.centAmount * updatedLineItem.quantity,
783+
);
784+
});
785+
786+
test("setLineItemPrice fails when the money uses another currency", async () => {
787+
const product = await supertest(ctMock.app)
788+
.post("/dummy/products")
789+
.send(productDraft)
790+
.then((x) => x.body);
791+
792+
assert(product, "product not created");
793+
794+
const baseCartResponse = await supertest(ctMock.app)
795+
.post("/dummy/carts")
796+
.send({ currency: "EUR" });
797+
expect(baseCartResponse.status).toBe(201);
798+
const baseCart = baseCartResponse.body as Cart;
799+
800+
const addLineItemResponse = await supertest(ctMock.app)
801+
.post(`/dummy/carts/${baseCart.id}`)
802+
.send({
803+
version: baseCart.version,
804+
actions: [
805+
{
806+
action: "addLineItem",
807+
sku: product.masterData.current.masterVariant.sku,
808+
quantity: 1,
809+
},
810+
],
811+
});
812+
expect(addLineItemResponse.status).toBe(200);
813+
const cartWithLineItem = addLineItemResponse.body as Cart;
814+
const lineItem = cartWithLineItem.lineItems[0];
815+
assert(lineItem, "lineItem not created");
816+
817+
const response = await supertest(ctMock.app)
818+
.post(`/dummy/carts/${cartWithLineItem.id}`)
819+
.send({
820+
version: cartWithLineItem.version,
821+
actions: [
822+
{
823+
action: "setLineItemPrice",
824+
lineItemId: lineItem.id,
825+
externalPrice: {
826+
type: "centPrecision",
827+
currencyCode: "USD",
828+
centAmount: 5000,
829+
fractionDigits: 2,
830+
},
831+
},
832+
],
833+
});
834+
835+
expect(response.status).toBe(400);
836+
expect(response.body.message).toContain("Currency mismatch");
837+
});
838+
839+
test("setLineItemPrice removes external price when no value is provided", async () => {
840+
const product = await supertest(ctMock.app)
841+
.post("/dummy/products")
842+
.send(productDraft)
843+
.then((x) => x.body);
844+
845+
assert(product, "product not created");
846+
847+
const baseCartResponse = await supertest(ctMock.app)
848+
.post("/dummy/carts")
849+
.send({ currency: "EUR" });
850+
expect(baseCartResponse.status).toBe(201);
851+
const baseCart = baseCartResponse.body as Cart;
852+
853+
const addLineItemResponse = await supertest(ctMock.app)
854+
.post(`/dummy/carts/${baseCart.id}`)
855+
.send({
856+
version: baseCart.version,
857+
actions: [
858+
{
859+
action: "addLineItem",
860+
sku: product.masterData.current.masterVariant.sku,
861+
quantity: 1,
862+
},
863+
],
864+
});
865+
expect(addLineItemResponse.status).toBe(200);
866+
const cartWithLineItem = addLineItemResponse.body as Cart;
867+
const lineItem = cartWithLineItem.lineItems[0];
868+
assert(lineItem, "lineItem not created");
869+
870+
const externalPrice: CentPrecisionMoney = {
871+
type: "centPrecision",
872+
currencyCode: "EUR",
873+
centAmount: 1000,
874+
fractionDigits: 2,
875+
};
876+
877+
const setExternalPriceResponse = await supertest(ctMock.app)
878+
.post(`/dummy/carts/${cartWithLineItem.id}`)
879+
.send({
880+
version: cartWithLineItem.version,
881+
actions: [
882+
{
883+
action: "setLineItemPrice",
884+
lineItemId: lineItem.id,
885+
externalPrice,
886+
},
887+
],
888+
});
889+
expect(setExternalPriceResponse.status).toBe(200);
890+
const cartWithExternalPrice = setExternalPriceResponse.body as Cart;
891+
expect(cartWithExternalPrice.lineItems[0].priceMode).toBe("ExternalPrice");
892+
893+
const resetResponse = await supertest(ctMock.app)
894+
.post(`/dummy/carts/${cartWithExternalPrice.id}`)
895+
.send({
896+
version: cartWithExternalPrice.version,
897+
actions: [
898+
{
899+
action: "setLineItemPrice",
900+
lineItemId: lineItem.id,
901+
},
902+
],
903+
});
904+
905+
expect(resetResponse.status).toBe(200);
906+
expect(resetResponse.body.version).toBe(cartWithExternalPrice.version + 1);
907+
expect(resetResponse.body.lineItems).toHaveLength(1);
908+
909+
const revertedLineItem = resetResponse.body.lineItems[0];
910+
const expectedCentAmount =
911+
product.masterData.current.masterVariant.prices?.[0].value.centAmount;
912+
if (typeof expectedCentAmount !== "number") {
913+
throw new Error("product price not found");
914+
}
915+
expect(revertedLineItem.priceMode).toBe("Platform");
916+
expect(revertedLineItem.price.value.centAmount).toBe(expectedCentAmount);
917+
expect(revertedLineItem.totalPrice.centAmount).toBe(
918+
expectedCentAmount * revertedLineItem.quantity,
919+
);
920+
expect(resetResponse.body.totalPrice.centAmount).toBe(
921+
expectedCentAmount * revertedLineItem.quantity,
922+
);
923+
});
924+
714925
test("setLineItemCustomField", async () => {
715926
const product = await supertest(ctMock.app)
716927
.post("/dummy/products")

0 commit comments

Comments
 (0)