fix(medusa): Add free shipping functionality (#241)
This commit is contained in:
@@ -14,6 +14,19 @@ describe("/store/carts", () => {
|
|||||||
let medusaProcess;
|
let medusaProcess;
|
||||||
let dbConnection;
|
let dbConnection;
|
||||||
|
|
||||||
|
const doAfterEach = async (manager) => {
|
||||||
|
await manager.query(`DELETE FROM "discount"`);
|
||||||
|
await manager.query(`DELETE FROM "discount_rule"`);
|
||||||
|
await manager.query(`DELETE FROM "shipping_method"`);
|
||||||
|
await manager.query(`DELETE FROM "shipping_option"`);
|
||||||
|
await manager.query(`DELETE FROM "cart"`);
|
||||||
|
await manager.query(`DELETE FROM "customer"`);
|
||||||
|
await manager.query(
|
||||||
|
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
|
||||||
|
);
|
||||||
|
await manager.query(`DELETE FROM "region"`);
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const cwd = path.resolve(path.join(__dirname, "..", ".."));
|
const cwd = path.resolve(path.join(__dirname, "..", ".."));
|
||||||
dbConnection = await initDb({ cwd });
|
dbConnection = await initDb({ cwd });
|
||||||
@@ -43,13 +56,7 @@ describe("/store/carts", () => {
|
|||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
const manager = dbConnection.manager;
|
const manager = dbConnection.manager;
|
||||||
await manager.query(`DELETE FROM "cart"`);
|
await doAfterEach(manager);
|
||||||
await manager.query(`DELETE FROM "discount"`);
|
|
||||||
await manager.query(`DELETE FROM "discount_rule"`);
|
|
||||||
await manager.query(
|
|
||||||
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
|
|
||||||
);
|
|
||||||
await manager.query(`DELETE FROM "region"`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates a cart", async () => {
|
it("creates a cart", async () => {
|
||||||
@@ -108,14 +115,7 @@ describe("/store/carts", () => {
|
|||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
const manager = dbConnection.manager;
|
const manager = dbConnection.manager;
|
||||||
await manager.query(`DELETE FROM "cart"`);
|
await doAfterEach(manager);
|
||||||
await manager.query(`DELETE FROM "discount"`);
|
|
||||||
await manager.query(`DELETE FROM "discount_rule"`);
|
|
||||||
await manager.query(`DELETE FROM "customer"`);
|
|
||||||
await manager.query(
|
|
||||||
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
|
|
||||||
);
|
|
||||||
await manager.query(`DELETE FROM "region"`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fails on apply discount if limit has been reached", async () => {
|
it("fails on apply discount if limit has been reached", async () => {
|
||||||
@@ -142,6 +142,73 @@ describe("/store/carts", () => {
|
|||||||
|
|
||||||
expect(response.status).toEqual(200);
|
expect(response.status).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds free shipping to cart then removes it again", async () => {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
let cart = await api.post(
|
||||||
|
"/store/carts/test-cart",
|
||||||
|
{
|
||||||
|
discounts: [{ code: "FREE_SHIPPING" }, { code: "CREATED" }],
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cart.data.cart.shipping_total).toBe(0);
|
||||||
|
expect(cart.status).toEqual(200);
|
||||||
|
|
||||||
|
cart = await api.post(
|
||||||
|
"/store/carts/test-cart",
|
||||||
|
{
|
||||||
|
discounts: [{ code: "CREATED" }],
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cart.data.cart.shipping_total).toBe(1000);
|
||||||
|
expect(cart.status).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DELETE /store/carts/:id/discounts/:code", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
try {
|
||||||
|
await cartSeeder(dbConnection);
|
||||||
|
await dbConnection.manager.query(
|
||||||
|
`INSERT INTO "cart_discounts" (cart_id, discount_id) VALUES ('test-cart', 'free-shipping')`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
const manager = dbConnection.manager;
|
||||||
|
await doAfterEach(manager);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes free shipping and updates shipping total", async () => {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const cartWithFreeShipping = await api.post(
|
||||||
|
"/store/carts/test-cart",
|
||||||
|
{
|
||||||
|
discounts: [{ code: "FREE_SHIPPING" }],
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cartWithFreeShipping.data.cart.shipping_total).toBe(0);
|
||||||
|
expect(cartWithFreeShipping.status).toEqual(200);
|
||||||
|
|
||||||
|
const response = await api.delete(
|
||||||
|
"/store/carts/test-cart/discounts/FREE_SHIPPING"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.data.cart.shipping_total).toBe(1000);
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("get-cart with session customer", () => {
|
describe("get-cart with session customer", () => {
|
||||||
@@ -156,14 +223,7 @@ describe("/store/carts", () => {
|
|||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
const manager = dbConnection.manager;
|
const manager = dbConnection.manager;
|
||||||
await manager.query(`DELETE FROM "cart"`);
|
await doAfterEach(manager);
|
||||||
await manager.query(`DELETE FROM "discount"`);
|
|
||||||
await manager.query(`DELETE FROM "discount_rule"`);
|
|
||||||
await manager.query(`DELETE FROM "customer"`);
|
|
||||||
await manager.query(
|
|
||||||
`UPDATE "country" SET region_id=NULL WHERE iso_2 = 'us'`
|
|
||||||
);
|
|
||||||
await manager.query(`DELETE FROM "region"`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("updates empty cart.customer_id on cart retrieval", async () => {
|
it("updates empty cart.customer_id on cart retrieval", async () => {
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ const {
|
|||||||
Cart,
|
Cart,
|
||||||
DiscountRule,
|
DiscountRule,
|
||||||
Discount,
|
Discount,
|
||||||
|
ShippingProfile,
|
||||||
|
ShippingOption,
|
||||||
|
ShippingMethod,
|
||||||
} = require("@medusajs/medusa");
|
} = require("@medusajs/medusa");
|
||||||
|
|
||||||
module.exports = async (connection, data = {}) => {
|
module.exports = async (connection, data = {}) => {
|
||||||
const manager = connection.manager;
|
const manager = connection.manager;
|
||||||
|
|
||||||
|
const defaultProfile = await manager.findOne(ShippingProfile, {
|
||||||
|
type: "default",
|
||||||
|
});
|
||||||
|
|
||||||
const r = manager.create(Region, {
|
const r = manager.create(Region, {
|
||||||
id: "test-region",
|
id: "test-region",
|
||||||
name: "Test Region",
|
name: "Test Region",
|
||||||
@@ -16,24 +23,26 @@ module.exports = async (connection, data = {}) => {
|
|||||||
tax_rate: 0,
|
tax_rate: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
await manager.insert(DiscountRule, {
|
const freeRule = manager.create(DiscountRule, {
|
||||||
id: "test-discount-rule",
|
id: "free-shipping-rule",
|
||||||
description: "Dynamic rule",
|
description: "Free shipping rule",
|
||||||
type: "percentage",
|
type: "free_shipping",
|
||||||
value: 10,
|
value: 100,
|
||||||
allocation: "total",
|
allocation: "total",
|
||||||
});
|
});
|
||||||
|
|
||||||
await manager.insert(Discount, {
|
const freeDisc = manager.create(Discount, {
|
||||||
id: "test-discount",
|
id: "free-shipping",
|
||||||
code: "DYNAMIC",
|
code: "FREE_SHIPPING",
|
||||||
rule_id: "test-discount-rule",
|
is_dynamic: false,
|
||||||
is_dynamic: true,
|
|
||||||
usage_count: 0,
|
|
||||||
usage_limit: 1,
|
|
||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
freeDisc.regions = [r];
|
||||||
|
freeDisc.rule = freeRule;
|
||||||
|
|
||||||
|
await manager.save(freeDisc);
|
||||||
|
|
||||||
const d = await manager.create(Discount, {
|
const d = await manager.create(Discount, {
|
||||||
id: "test-discount",
|
id: "test-discount",
|
||||||
code: "CREATED",
|
code: "CREATED",
|
||||||
@@ -73,6 +82,17 @@ module.exports = async (connection, data = {}) => {
|
|||||||
email: "some-customer@email.com",
|
email: "some-customer@email.com",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await manager.insert(ShippingOption, {
|
||||||
|
id: "test-option",
|
||||||
|
name: "test-option",
|
||||||
|
provider_id: "test-ful",
|
||||||
|
region_id: "test-region",
|
||||||
|
profile_id: defaultProfile.id,
|
||||||
|
price_type: "flat_rate",
|
||||||
|
amount: 1000,
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
const cart = manager.create(Cart, {
|
const cart = manager.create(Cart, {
|
||||||
id: "test-cart",
|
id: "test-cart",
|
||||||
customer_id: "some-customer",
|
customer_id: "some-customer",
|
||||||
@@ -88,4 +108,12 @@ module.exports = async (connection, data = {}) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await manager.save(cart);
|
await manager.save(cart);
|
||||||
|
|
||||||
|
await manager.insert(ShippingMethod, {
|
||||||
|
id: "test-method",
|
||||||
|
shipping_option_id: "test-option",
|
||||||
|
cart_id: "test-cart",
|
||||||
|
price: 1000,
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe("POST /store/carts/:id", () => {
|
|||||||
expect(CartServiceMock.retrieve).toHaveBeenCalledWith(
|
expect(CartServiceMock.retrieve).toHaveBeenCalledWith(
|
||||||
IdMap.getId("emptyCart"),
|
IdMap.getId("emptyCart"),
|
||||||
{
|
{
|
||||||
relations: ["payment_sessions"],
|
relations: ["payment_sessions", "shipping_methods"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
expect(CartServiceMock.retrieve).toHaveBeenCalledWith(
|
expect(CartServiceMock.retrieve).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
// If the cart has payment sessions update these
|
// If the cart has payment sessions update these
|
||||||
const updated = await cartService.retrieve(id, {
|
const updated = await cartService.retrieve(id, {
|
||||||
relations: ["payment_sessions"],
|
relations: ["payment_sessions", "shipping_methods"],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (updated.payment_sessions?.length && !value.region_id) {
|
if (updated.payment_sessions?.length && !value.region_id) {
|
||||||
|
|||||||
@@ -1281,6 +1281,7 @@ describe("CartService", () => {
|
|||||||
profile_id: IdMap.getId(m.profile),
|
profile_id: IdMap.getId(m.profile),
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
discounts: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
import { Validator, MedusaError } from "medusa-core-utils"
|
import { Validator, MedusaError } from "medusa-core-utils"
|
||||||
import { BaseService } from "medusa-interfaces"
|
import { BaseService } from "medusa-interfaces"
|
||||||
import { defaultFields, defaultRelations } from "../api/routes/store/carts"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides layer to manipulate carts.
|
* Provides layer to manipulate carts.
|
||||||
@@ -574,6 +573,49 @@ class CartService extends BaseService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures shipping total on cart is correct in regards to a potential free
|
||||||
|
* shipping discount
|
||||||
|
* If a free shipping is present, we set shipping methods price to 0
|
||||||
|
* if a free shipping was present, we set shipping methods to original amount
|
||||||
|
* @param {Cart} cart - the the cart to adjust free shipping for
|
||||||
|
* @param {boolean} shouldAdd - flag to indicate, if we should add or remove
|
||||||
|
*/
|
||||||
|
async adjustFreeShipping_(cart, shouldAdd) {
|
||||||
|
if (cart.shipping_methods?.length) {
|
||||||
|
// if any free shipping discounts, we ensure to update shipping method amount
|
||||||
|
if (shouldAdd) {
|
||||||
|
await Promise.all(
|
||||||
|
cart.shipping_methods.map(async sm => {
|
||||||
|
const smRepo = this.manager_.getCustomRepository(
|
||||||
|
this.shippingMethodRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const method = await smRepo.findOne({ where: { id: sm.id } })
|
||||||
|
|
||||||
|
if (method) {
|
||||||
|
method.price = 0
|
||||||
|
await smRepo.save(method)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await Promise.all(
|
||||||
|
cart.shipping_methods.map(async sm => {
|
||||||
|
const smRepo = this.manager_.getCustomRepository(
|
||||||
|
this.shippingMethodRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
// if free shipping discount is removed, we adjust the shipping
|
||||||
|
// back to its original amount
|
||||||
|
sm.price = sm.shipping_option.amount
|
||||||
|
await smRepo.save(sm)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async update(cartId, update) {
|
async update(cartId, update) {
|
||||||
return this.atomicPhase_(async manager => {
|
return this.atomicPhase_(async manager => {
|
||||||
const cartRepo = manager.getCustomRepository(this.cartRepository_)
|
const cartRepo = manager.getCustomRepository(this.cartRepository_)
|
||||||
@@ -627,10 +669,29 @@ class CartService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("discounts" in update) {
|
if ("discounts" in update) {
|
||||||
|
const previousDiscounts = cart.discounts
|
||||||
cart.discounts = []
|
cart.discounts = []
|
||||||
|
|
||||||
for (const { code } of update.discounts) {
|
for (const { code } of update.discounts) {
|
||||||
await this.applyDiscount_(cart, code)
|
await this.applyDiscount_(cart, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasFreeShipping = cart.discounts.some(
|
||||||
|
({ rule }) => rule.type === "free_shipping"
|
||||||
|
)
|
||||||
|
|
||||||
|
// if we previously had a free shipping discount and then removed it,
|
||||||
|
// we need to update shipping methods to original price
|
||||||
|
if (
|
||||||
|
previousDiscounts.some(({ rule }) => rule.type === "free_shipping") &&
|
||||||
|
!hasFreeShipping
|
||||||
|
) {
|
||||||
|
await this.adjustFreeShipping_(cart, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFreeShipping) {
|
||||||
|
await this.adjustFreeShipping_(cart, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("gift_cards" in update) {
|
if ("gift_cards" in update) {
|
||||||
@@ -890,8 +951,13 @@ class CartService extends BaseService {
|
|||||||
async removeDiscount(cartId, discountCode) {
|
async removeDiscount(cartId, discountCode) {
|
||||||
return this.atomicPhase_(async manager => {
|
return this.atomicPhase_(async manager => {
|
||||||
const cart = await this.retrieve(cartId, {
|
const cart = await this.retrieve(cartId, {
|
||||||
relations: ["discounts", "payment_sessions"],
|
relations: ["discounts", "payment_sessions", "shipping_methods"],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (cart.discounts.some(({ rule }) => rule.type === "free_shipping")) {
|
||||||
|
await this.adjustFreeShipping_(cart, false)
|
||||||
|
}
|
||||||
|
|
||||||
cart.discounts = cart.discounts.filter(d => d.code !== discountCode)
|
cart.discounts = cart.discounts.filter(d => d.code !== discountCode)
|
||||||
|
|
||||||
const cartRepo = manager.getCustomRepository(this.cartRepository_)
|
const cartRepo = manager.getCustomRepository(this.cartRepository_)
|
||||||
@@ -1232,6 +1298,7 @@ class CartService extends BaseService {
|
|||||||
select: ["subtotal"],
|
select: ["subtotal"],
|
||||||
relations: [
|
relations: [
|
||||||
"shipping_methods",
|
"shipping_methods",
|
||||||
|
"discounts",
|
||||||
"shipping_methods.shipping_option",
|
"shipping_methods.shipping_option",
|
||||||
"items",
|
"items",
|
||||||
"items.variant",
|
"items.variant",
|
||||||
@@ -1267,7 +1334,15 @@ class CartService extends BaseService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.retrieve(cartId)
|
const result = await this.retrieve(cartId, {
|
||||||
|
relations: ["discounts", "shipping_methods"],
|
||||||
|
})
|
||||||
|
|
||||||
|
// if cart has freeshipping, adjust price
|
||||||
|
if (result.discounts.some(({ rule }) => rule.type === "free_shipping")) {
|
||||||
|
await this.adjustFreeShipping_(result, true)
|
||||||
|
}
|
||||||
|
|
||||||
await this.eventBus_
|
await this.eventBus_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.emit(CartService.Events.UPDATED, result)
|
.emit(CartService.Events.UPDATED, result)
|
||||||
|
|||||||
@@ -400,8 +400,13 @@ class SwapService extends BaseService {
|
|||||||
|
|
||||||
const order = swap.order
|
const order = swap.order
|
||||||
|
|
||||||
|
// filter out free shipping discounts
|
||||||
|
const discounts =
|
||||||
|
order?.discounts?.filter(({ rule }) => rule.type !== "free_shipping") ||
|
||||||
|
undefined
|
||||||
|
|
||||||
const cart = await this.cartService_.withTransaction(manager).create({
|
const cart = await this.cartService_.withTransaction(manager).create({
|
||||||
discounts: order.discounts,
|
discounts,
|
||||||
email: order.email,
|
email: order.email,
|
||||||
billing_address_id: order.billing_address_id,
|
billing_address_id: order.billing_address_id,
|
||||||
shipping_address_id: order.shipping_address_id,
|
shipping_address_id: order.shipping_address_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user