fix(medusa): Applying Discounts (with Conditions) on DraftOrders and Carts (#3197)
This commit is contained in:
1 parent
4d3210bfbb
commit
bfa33f444c
14 files changed
+452
-181
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(medusa): Applying Discounts (with Conditions) on DraftOrders and Carts
|
||||||
@@ -907,7 +907,8 @@ describe("/admin/discounts", () => {
|
|||||||
is_dynamic: false,
|
is_dynamic: false,
|
||||||
},
|
},
|
||||||
adminReqConfig
|
adminReqConfig
|
||||||
).catch(e => e)
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(err.response.status).toEqual(400)
|
expect(err.response.status).toEqual(400)
|
||||||
expect(err.response.data.message).toEqual(
|
expect(err.response.data.message).toEqual(
|
||||||
@@ -1026,7 +1027,8 @@ describe("/admin/discounts", () => {
|
|||||||
regions: [validRegionId, "test-region-2"],
|
regions: [validRegionId, "test-region-2"],
|
||||||
},
|
},
|
||||||
adminReqConfig
|
adminReqConfig
|
||||||
).catch(e => e)
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(err.response.status).toEqual(400)
|
expect(err.response.status).toEqual(400)
|
||||||
expect(err.response.data.message).toEqual(
|
expect(err.response.data.message).toEqual(
|
||||||
@@ -1060,7 +1062,8 @@ describe("/admin/discounts", () => {
|
|||||||
regions: [validRegionId, "test-region-2"],
|
regions: [validRegionId, "test-region-2"],
|
||||||
},
|
},
|
||||||
adminReqConfig
|
adminReqConfig
|
||||||
).catch(e => e)
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(err.response.status).toEqual(400)
|
expect(err.response.status).toEqual(400)
|
||||||
expect(err.response.data.message).toEqual(
|
expect(err.response.data.message).toEqual(
|
||||||
@@ -1092,7 +1095,8 @@ describe("/admin/discounts", () => {
|
|||||||
`/admin/discounts/${response.data.discount.id}/regions/test-region-2`,
|
`/admin/discounts/${response.data.discount.id}/regions/test-region-2`,
|
||||||
{},
|
{},
|
||||||
adminReqConfig
|
adminReqConfig
|
||||||
).catch(e => e)
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(err.response.status).toEqual(400)
|
expect(err.response.status).toEqual(400)
|
||||||
expect(err.response.data.message).toEqual(
|
expect(err.response.data.message).toEqual(
|
||||||
@@ -1246,7 +1250,8 @@ describe("/admin/discounts", () => {
|
|||||||
ends_at: new Date("09/14/2021 17:50"),
|
ends_at: new Date("09/14/2021 17:50"),
|
||||||
},
|
},
|
||||||
adminReqConfig
|
adminReqConfig
|
||||||
).catch(e => e)
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(err.response.status).toEqual(400)
|
expect(err.response.status).toEqual(400)
|
||||||
expect(err.response.data).toEqual(
|
expect(err.response.data).toEqual(
|
||||||
@@ -1259,7 +1264,8 @@ describe("/admin/discounts", () => {
|
|||||||
it("fails to create a discount if the regions contains an invalid regionId ", async () => {
|
it("fails to create a discount if the regions contains an invalid regionId ", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const err = await api.post(
|
const err = await api
|
||||||
|
.post(
|
||||||
"/admin/discounts",
|
"/admin/discounts",
|
||||||
{
|
{
|
||||||
code: "HELLOWORLD",
|
code: "HELLOWORLD",
|
||||||
@@ -1272,7 +1278,8 @@ describe("/admin/discounts", () => {
|
|||||||
regions: [validRegionId, invalidRegionId],
|
regions: [validRegionId, invalidRegionId],
|
||||||
},
|
},
|
||||||
adminReqConfig
|
adminReqConfig
|
||||||
).catch(e => e)
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(err.response.status).toEqual(404)
|
expect(err.response.status).toEqual(404)
|
||||||
expect(err.response.data.message).toEqual(
|
expect(err.response.data.message).toEqual(
|
||||||
@@ -1283,7 +1290,8 @@ describe("/admin/discounts", () => {
|
|||||||
it("fails to create a discount if the regions contains only invalid regionIds ", async () => {
|
it("fails to create a discount if the regions contains only invalid regionIds ", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const err = await api.post(
|
const err = await api
|
||||||
|
.post(
|
||||||
"/admin/discounts",
|
"/admin/discounts",
|
||||||
{
|
{
|
||||||
code: "HELLOWORLD",
|
code: "HELLOWORLD",
|
||||||
@@ -1296,7 +1304,8 @@ describe("/admin/discounts", () => {
|
|||||||
regions: [invalidRegionId],
|
regions: [invalidRegionId],
|
||||||
},
|
},
|
||||||
adminReqConfig
|
adminReqConfig
|
||||||
).catch(e => e)
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(err.response.status).toEqual(404)
|
expect(err.response.status).toEqual(404)
|
||||||
expect(err.response.data.message).toEqual(
|
expect(err.response.data.message).toEqual(
|
||||||
@@ -1307,7 +1316,8 @@ describe("/admin/discounts", () => {
|
|||||||
it("fails to create a discount if regions are not present ", async () => {
|
it("fails to create a discount if regions are not present ", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const err = await api.post(
|
const err = await api
|
||||||
|
.post(
|
||||||
"/admin/discounts",
|
"/admin/discounts",
|
||||||
{
|
{
|
||||||
code: "HELLOWORLD",
|
code: "HELLOWORLD",
|
||||||
@@ -1319,7 +1329,8 @@ describe("/admin/discounts", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
adminReqConfig
|
adminReqConfig
|
||||||
).catch((e) => e)
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(err.response.status).toEqual(400)
|
expect(err.response.status).toEqual(400)
|
||||||
expect(err.response.data.message).toEqual(
|
expect(err.response.data.message).toEqual(
|
||||||
@@ -2204,11 +2215,16 @@ describe("/admin/discounts", () => {
|
|||||||
|
|
||||||
it("should respond with 404 on non-existing code", async () => {
|
it("should respond with 404 on non-existing code", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const err = await api.get("/admin/discounts/code/non-existing", adminReqConfig).catch(e => e)
|
|
||||||
|
const code = "non-existing"
|
||||||
|
|
||||||
|
const err = await api
|
||||||
|
.get(`/admin/discounts/code/${code}`, adminReqConfig)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(err.response.status).toEqual(404)
|
expect(err.response.status).toEqual(404)
|
||||||
expect(err.response.data.message).toBe(
|
expect(err.response.data.message).toBe(
|
||||||
"Discount with code non-existing was not found"
|
`Discounts with code ${code} was not found`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ const { initDb, useDb } = require("../../../../helpers/use-db")
|
|||||||
|
|
||||||
const draftOrderSeeder = require("../../../helpers/draft-order-seeder")
|
const draftOrderSeeder = require("../../../helpers/draft-order-seeder")
|
||||||
const adminSeeder = require("../../../helpers/admin-seeder")
|
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||||
const {
|
const { simpleDiscountFactory } = require("../../../factories")
|
||||||
simpleRegionFactory,
|
|
||||||
simpleDiscountFactory,
|
|
||||||
} = require("../../../factories")
|
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -517,6 +514,102 @@ describe("/admin/draft-orders", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("creates a draft order with fixed discount amount allocated to the total", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const testVariantId = "test-variant"
|
||||||
|
const testVariant2Id = "test-variant-2"
|
||||||
|
const discountAmount = 1000
|
||||||
|
|
||||||
|
const discount = await simpleDiscountFactory(dbConnection, {
|
||||||
|
code: "test-fixed",
|
||||||
|
regions: ["test-region"],
|
||||||
|
rule: {
|
||||||
|
type: "fixed",
|
||||||
|
allocation: "total",
|
||||||
|
value: discountAmount,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
email: "oli@test.dk",
|
||||||
|
shipping_address: "oli-shipping",
|
||||||
|
discounts: [{ code: discount.code }],
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: testVariantId,
|
||||||
|
quantity: 2,
|
||||||
|
metadata: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variant_id: testVariant2Id,
|
||||||
|
quantity: 2,
|
||||||
|
metadata: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
region_id: "test-region",
|
||||||
|
customer_id: "oli-test",
|
||||||
|
shipping_methods: [
|
||||||
|
{
|
||||||
|
option_id: "test-option",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
"/admin/draft-orders",
|
||||||
|
payload,
|
||||||
|
adminReqConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
const draftOrder = response.data.draft_order
|
||||||
|
const lineItem1 = draftOrder.cart.items.find(
|
||||||
|
(item) => item.variant_id === testVariantId
|
||||||
|
)
|
||||||
|
const lineItem2 = draftOrder.cart.items.find(
|
||||||
|
(item) => item.variant_id === testVariant2Id
|
||||||
|
)
|
||||||
|
|
||||||
|
const lineItem1WeightInTotal = 0.444 // line item amount / amount
|
||||||
|
const lineItem2WeightInTotal = 0.556 // line item amount / amount
|
||||||
|
|
||||||
|
expect(draftOrder.cart.items).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(lineItem1).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
variant_id: testVariantId,
|
||||||
|
unit_price: lineItem1.unit_price,
|
||||||
|
quantity: lineItem1.quantity,
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
item_id: lineItem1.id,
|
||||||
|
amount: discountAmount * lineItem1WeightInTotal,
|
||||||
|
description: "discount",
|
||||||
|
discount_id: discount.id,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(lineItem2).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
variant_id: testVariant2Id,
|
||||||
|
unit_price: lineItem2.unit_price,
|
||||||
|
quantity: lineItem2.quantity,
|
||||||
|
adjustments: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
item_id: lineItem2.id,
|
||||||
|
amount: discountAmount * lineItem2WeightInTotal,
|
||||||
|
description: "discount",
|
||||||
|
discount_id: discount.id,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("creates a draft order with discount and free shipping along the line item", async () => {
|
it("creates a draft order with discount and free shipping along the line item", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const { useApi } = require("../../../../helpers/use-api")
|
|||||||
const { initDb, useDb } = require("../../../../helpers/use-db")
|
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||||
|
|
||||||
const cartSeeder = require("../../../helpers/cart-seeder")
|
const cartSeeder = require("../../../helpers/cart-seeder")
|
||||||
const productSeeder = require("../../../helpers/product-seeder")
|
|
||||||
const swapSeeder = require("../../../helpers/swap-seeder")
|
const swapSeeder = require("../../../helpers/swap-seeder")
|
||||||
const {
|
const {
|
||||||
simpleCartFactory,
|
simpleCartFactory,
|
||||||
@@ -980,16 +979,18 @@ describe("/store/carts", () => {
|
|||||||
it("fails on apply discount if limit has been reached", async () => {
|
it("fails on apply discount if limit has been reached", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
|
const code = "SPENT"
|
||||||
|
|
||||||
const err = await api
|
const err = await api
|
||||||
.post("/store/carts/test-cart", {
|
.post("/store/carts/test-cart", {
|
||||||
discounts: [{ code: "SPENT" }],
|
discounts: [{ code }],
|
||||||
})
|
})
|
||||||
.catch((err) => err)
|
.catch((err) => err)
|
||||||
|
|
||||||
expect(err).toBeTruthy()
|
expect(err).toBeTruthy()
|
||||||
expect(err.response.status).toEqual(400)
|
expect(err.response.status).toEqual(400)
|
||||||
expect(err.response.data.message).toEqual(
|
expect(err.response.data.message).toEqual(
|
||||||
"Discount has been used maximum allowed times"
|
`Discount ${code} has been used maximum allowed times`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1330,14 +1331,15 @@ describe("/store/carts", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const code = "TEST"
|
||||||
try {
|
try {
|
||||||
await api.post("/store/carts/test-customer-discount", {
|
await api.post("/store/carts/test-customer-discount", {
|
||||||
discounts: [{ code: "TEST" }],
|
discounts: [{ code }],
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.response.status).toEqual(400)
|
expect(error.response.status).toEqual(400)
|
||||||
expect(error.response.data.message).toEqual(
|
expect(error.response.data.message).toEqual(
|
||||||
"Discount is not valid for customer"
|
`Discount ${code} is not valid for customer`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1399,14 +1401,15 @@ describe("/store/carts", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const code = "TEST"
|
||||||
try {
|
try {
|
||||||
await api.post("/store/carts/test-customer-discount", {
|
await api.post("/store/carts/test-customer-discount", {
|
||||||
discounts: [{ code: "TEST" }],
|
discounts: [{ code }],
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.response.status).toEqual(400)
|
expect(error.response.status).toEqual(400)
|
||||||
expect(error.response.data.message).toEqual(
|
expect(error.response.data.message).toEqual(
|
||||||
"Discount is not valid for customer"
|
`Discount ${code} is not valid for customer`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1415,41 +1418,49 @@ describe("/store/carts", () => {
|
|||||||
expect.assertions(2)
|
expect.assertions(2)
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
try {
|
const code = "EXP_DISC"
|
||||||
await api.post("/store/carts/test-cart", {
|
|
||||||
discounts: [{ code: "EXP_DISC" }],
|
const err = await api
|
||||||
|
.post("/store/carts/test-cart", {
|
||||||
|
discounts: [{ code }],
|
||||||
})
|
})
|
||||||
} catch (error) {
|
.catch((e) => e)
|
||||||
expect(error.response.status).toEqual(400)
|
|
||||||
expect(error.response.data.message).toEqual("Discount is expired")
|
expect(err.response.status).toEqual(400)
|
||||||
}
|
expect(err.response.data.message).toEqual(`Discount ${code} is expired`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails on discount before start day", async () => {
|
it("fails on discount before start day", async () => {
|
||||||
expect.assertions(2)
|
expect.assertions(2)
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
try {
|
const code = "PREM_DISC"
|
||||||
await api.post("/store/carts/test-cart", {
|
|
||||||
discounts: [{ code: "PREM_DISC" }],
|
const err = await api
|
||||||
|
.post("/store/carts/test-cart", {
|
||||||
|
discounts: [{ code }],
|
||||||
})
|
})
|
||||||
} catch (error) {
|
.catch((e) => e)
|
||||||
expect(error.response.status).toEqual(400)
|
|
||||||
expect(error.response.data.message).toEqual("Discount is not valid yet")
|
expect(err.response.status).toEqual(400)
|
||||||
}
|
expect(err.response.data.message).toEqual(
|
||||||
|
`Discount ${code} is not valid yet`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails on apply invalid dynamic discount", async () => {
|
it("fails on apply invalid dynamic discount", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
try {
|
const code = "INV_DYN_DISC"
|
||||||
await api.post("/store/carts/test-cart", {
|
|
||||||
discounts: [{ code: "INV_DYN_DISC" }],
|
const err = await api
|
||||||
|
.post("/store/carts/test-cart", {
|
||||||
|
discounts: [{ code }],
|
||||||
})
|
})
|
||||||
} catch (error) {
|
.catch((e) => e)
|
||||||
expect(error.response.status).toEqual(400)
|
|
||||||
expect(error.response.data.message).toEqual("Discount is expired")
|
expect(err.response.status).toEqual(400)
|
||||||
}
|
expect(err.response.data.message).toEqual(`Discount ${code} is expired`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Applies dynamic discount to cart correctly", async () => {
|
it("Applies dynamic discount to cart correctly", async () => {
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ module.exports = async (connection, data = {}) => {
|
|||||||
is_dynamic: true,
|
is_dynamic: true,
|
||||||
is_disabled: false,
|
is_disabled: false,
|
||||||
starts_at: tenDaysAgo,
|
starts_at: tenDaysAgo,
|
||||||
ends_at: tenDaysFromToday,
|
ends_at: yesterday,
|
||||||
valid_duration: "P1D", // one day
|
valid_duration: "P1D", // one day
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import {
|
|||||||
Not,
|
Not,
|
||||||
Repository,
|
Repository,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import { Discount } from "../models"
|
|
||||||
import {
|
import {
|
||||||
|
Discount,
|
||||||
DiscountCondition,
|
DiscountCondition,
|
||||||
|
DiscountConditionCustomerGroup,
|
||||||
DiscountConditionOperator,
|
DiscountConditionOperator,
|
||||||
|
DiscountConditionProduct,
|
||||||
|
DiscountConditionProductCollection,
|
||||||
|
DiscountConditionProductTag,
|
||||||
|
DiscountConditionProductType,
|
||||||
DiscountConditionType,
|
DiscountConditionType,
|
||||||
} from "../models/discount-condition"
|
} from "../models"
|
||||||
import { DiscountConditionCustomerGroup } from "../models/discount-condition-customer-group"
|
|
||||||
import { DiscountConditionProduct } from "../models/discount-condition-product"
|
|
||||||
import { DiscountConditionProductCollection } from "../models/discount-condition-product-collection"
|
|
||||||
import { DiscountConditionProductTag } from "../models/discount-condition-product-tag"
|
|
||||||
import { DiscountConditionProductType } from "../models/discount-condition-product-type"
|
|
||||||
import { isString } from "../utils"
|
import { isString } from "../utils"
|
||||||
|
|
||||||
export enum DiscountConditionJoinTableForeignKey {
|
export enum DiscountConditionJoinTableForeignKey {
|
||||||
@@ -256,6 +256,10 @@ export class DiscountConditionRepository extends Repository<DiscountCondition> {
|
|||||||
// if condition operation is `in` and the query for conditions defined for the given type is empty, the discount is invalid
|
// if condition operation is `in` and the query for conditions defined for the given type is empty, the discount is invalid
|
||||||
// if condition operation is `not_in` and the query for conditions defined for the given type is not empty, the discount is invalid
|
// if condition operation is `not_in` and the query for conditions defined for the given type is not empty, the discount is invalid
|
||||||
for (const condition of discountConditions) {
|
for (const condition of discountConditions) {
|
||||||
|
if (condition.type === DiscountConditionType.CUSTOMER_GROUPS) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const numConditions = await this.queryConditionTable({
|
const numConditions = await this.queryConditionTable({
|
||||||
type: condition.type,
|
type: condition.type,
|
||||||
condId: condition.id,
|
condId: condition.id,
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import { IdMap } from "medusa-test-utils"
|
|
||||||
import { MedusaError } from "medusa-core-utils"
|
|
||||||
|
|
||||||
export const LineItemAdjustmentServiceMock = {
|
export const LineItemAdjustmentServiceMock = {
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
|
|||||||
@@ -2062,14 +2062,19 @@ describe("CartService", () => {
|
|||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
retrieveByCode: jest.fn().mockImplementation((code) => {
|
listByCodes: jest.fn().mockImplementation((code) => {
|
||||||
|
const codes = Array.isArray(code) ? code : [code]
|
||||||
|
|
||||||
|
const data = []
|
||||||
|
|
||||||
|
for (const code of codes) {
|
||||||
if (code === "US10") {
|
if (code === "US10") {
|
||||||
return Promise.resolve({
|
data.push({
|
||||||
regions: [{ id: IdMap.getId("bad") }],
|
regions: [{ id: IdMap.getId("bad") }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (code === "limit-reached") {
|
if (code === "limit-reached") {
|
||||||
return Promise.resolve({
|
data.push({
|
||||||
id: IdMap.getId("limit-reached"),
|
id: IdMap.getId("limit-reached"),
|
||||||
code: "limit-reached",
|
code: "limit-reached",
|
||||||
regions: [{ id: IdMap.getId("good") }],
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
@@ -2079,7 +2084,7 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (code === "null-count") {
|
if (code === "null-count") {
|
||||||
return Promise.resolve({
|
data.push({
|
||||||
id: IdMap.getId("null-count"),
|
id: IdMap.getId("null-count"),
|
||||||
code: "null-count",
|
code: "null-count",
|
||||||
regions: [{ id: IdMap.getId("good") }],
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
@@ -2089,7 +2094,7 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (code === "FREESHIPPING") {
|
if (code === "FREESHIPPING") {
|
||||||
return Promise.resolve({
|
data.push({
|
||||||
id: IdMap.getId("freeship"),
|
id: IdMap.getId("freeship"),
|
||||||
code: "FREESHIPPING",
|
code: "FREESHIPPING",
|
||||||
regions: [{ id: IdMap.getId("good") }],
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
@@ -2099,7 +2104,7 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (code === "EarlyDiscount") {
|
if (code === "EarlyDiscount") {
|
||||||
return Promise.resolve({
|
data.push({
|
||||||
id: IdMap.getId("10off"),
|
id: IdMap.getId("10off"),
|
||||||
code: "10%OFF",
|
code: "10%OFF",
|
||||||
regions: [{ id: IdMap.getId("good") }],
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
@@ -2111,7 +2116,7 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (code === "ExpiredDiscount") {
|
if (code === "ExpiredDiscount") {
|
||||||
return Promise.resolve({
|
data.push({
|
||||||
id: IdMap.getId("10off"),
|
id: IdMap.getId("10off"),
|
||||||
code: "10%OFF",
|
code: "10%OFF",
|
||||||
regions: [{ id: IdMap.getId("good") }],
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
@@ -2123,7 +2128,7 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (code === "ExpiredDynamicDiscount") {
|
if (code === "ExpiredDynamicDiscount") {
|
||||||
return Promise.resolve({
|
data.push({
|
||||||
id: IdMap.getId("10off"),
|
id: IdMap.getId("10off"),
|
||||||
code: "10%OFF",
|
code: "10%OFF",
|
||||||
is_dynamic: true,
|
is_dynamic: true,
|
||||||
@@ -2136,7 +2141,7 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (code === "ExpiredDynamicDiscountEndDate") {
|
if (code === "ExpiredDynamicDiscountEndDate") {
|
||||||
return Promise.resolve({
|
data.push({
|
||||||
id: IdMap.getId("10off"),
|
id: IdMap.getId("10off"),
|
||||||
is_dynamic: true,
|
is_dynamic: true,
|
||||||
code: "10%OFF",
|
code: "10%OFF",
|
||||||
@@ -2150,7 +2155,7 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (code === "ValidDiscount") {
|
if (code === "ValidDiscount") {
|
||||||
return Promise.resolve({
|
data.push({
|
||||||
id: IdMap.getId("10off"),
|
id: IdMap.getId("10off"),
|
||||||
code: "10%OFF",
|
code: "10%OFF",
|
||||||
regions: [{ id: IdMap.getId("good") }],
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
@@ -2162,7 +2167,7 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (code === "ApplicableForCustomer") {
|
if (code === "ApplicableForCustomer") {
|
||||||
return Promise.resolve({
|
data.push({
|
||||||
id: "ApplicableForCustomer",
|
id: "ApplicableForCustomer",
|
||||||
code: "ApplicableForCustomer",
|
code: "ApplicableForCustomer",
|
||||||
regions: [{ id: IdMap.getId("good") }],
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
@@ -2174,7 +2179,9 @@ describe("CartService", () => {
|
|||||||
ends_at: getOffsetDate(10),
|
ends_at: getOffsetDate(10),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return Promise.resolve({
|
|
||||||
|
if (!data.length) {
|
||||||
|
data.push({
|
||||||
id: IdMap.getId("10off"),
|
id: IdMap.getId("10off"),
|
||||||
code: "10%OFF",
|
code: "10%OFF",
|
||||||
regions: [{ id: IdMap.getId("good") }],
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
@@ -2182,6 +2189,14 @@ describe("CartService", () => {
|
|||||||
type: "percentage",
|
type: "percentage",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(code)) {
|
||||||
|
return Promise.resolve(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(data[0])
|
||||||
}),
|
}),
|
||||||
canApplyForCustomer: jest
|
canApplyForCustomer: jest
|
||||||
.fn()
|
.fn()
|
||||||
@@ -2333,7 +2348,7 @@ describe("CartService", () => {
|
|||||||
discounts: [{ code: "10%OFF" }, { code: "FREESHIPPING" }],
|
discounts: [{ code: "10%OFF" }, { code: "FREESHIPPING" }],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(discountService.retrieveByCode).toHaveBeenCalledTimes(2)
|
expect(discountService.listByCodes).toHaveBeenCalledTimes(1)
|
||||||
expect(cartRepository.save).toHaveBeenCalledTimes(1)
|
expect(cartRepository.save).toHaveBeenCalledTimes(1)
|
||||||
expect(cartRepository.save).toHaveBeenCalledWith(
|
expect(cartRepository.save).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FlagRouter } from "../../utils/flag-router"
|
|||||||
import DiscountService from "../discount"
|
import DiscountService from "../discount"
|
||||||
import { TotalsServiceMock } from "../__mocks__/totals"
|
import { TotalsServiceMock } from "../__mocks__/totals"
|
||||||
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
||||||
|
import { In } from "typeorm"
|
||||||
|
|
||||||
const featureFlagRouter = new FlagRouter({})
|
const featureFlagRouter = new FlagRouter({})
|
||||||
|
|
||||||
@@ -54,14 +55,16 @@ describe("DiscountService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("fails to create a discount without regions", async () => {
|
it("fails to create a discount without regions", async () => {
|
||||||
const err = await discountService.create({
|
const err = await discountService
|
||||||
|
.create({
|
||||||
code: "test",
|
code: "test",
|
||||||
rule: {
|
rule: {
|
||||||
type: "fixed",
|
type: "fixed",
|
||||||
allocation: "total",
|
allocation: "total",
|
||||||
value: 20,
|
value: 20,
|
||||||
},
|
},
|
||||||
}).catch(e => e)
|
})
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(err.type).toEqual("invalid_data")
|
expect(err.type).toEqual("invalid_data")
|
||||||
expect(err.message).toEqual("Discount must have atleast 1 region")
|
expect(err.message).toEqual("Discount must have atleast 1 region")
|
||||||
@@ -237,7 +240,6 @@ describe("DiscountService", () => {
|
|||||||
expect(discountRepository.findOne).toHaveBeenCalledWith({
|
expect(discountRepository.findOne).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
code: "10%OFF",
|
code: "10%OFF",
|
||||||
is_dynamic: false,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -248,7 +250,54 @@ describe("DiscountService", () => {
|
|||||||
expect(discountRepository.findOne).toHaveBeenCalledWith({
|
expect(discountRepository.findOne).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
code: "10%OFF",
|
code: "10%OFF",
|
||||||
is_dynamic: false,
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("listByCodes", () => {
|
||||||
|
const discountRepository = MockRepository({
|
||||||
|
find: (query) => {
|
||||||
|
if (query.where.code.value.includes("10%OFF")) {
|
||||||
|
return Promise.resolve([
|
||||||
|
{ id: IdMap.getId("total10"), code: "10%OFF" },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
if (query.where.code.value.includes("DYNAMIC")) {
|
||||||
|
return Promise.resolve([
|
||||||
|
{ id: IdMap.getId("total10"), code: "10%OFF" },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
return Promise.resolve([])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const discountService = new DiscountService({
|
||||||
|
manager: MockManager,
|
||||||
|
discountRepository,
|
||||||
|
featureFlagRouter,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully finds discount by code", async () => {
|
||||||
|
await discountService.listByCodes(["10%OFF"])
|
||||||
|
expect(discountRepository.find).toHaveBeenCalledTimes(1)
|
||||||
|
expect(discountRepository.find).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
code: In(["10%OFF"]),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully trims, uppdercases, and finds discount by code", async () => {
|
||||||
|
await discountService.listByCodes([" 10%Off "])
|
||||||
|
expect(discountRepository.find).toHaveBeenCalledTimes(1)
|
||||||
|
expect(discountRepository.find).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
code: In(["10%OFF"]),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -746,6 +795,7 @@ describe("DiscountService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("validateDiscountForCartOrThrow", () => {
|
describe("validateDiscountForCartOrThrow", () => {
|
||||||
|
const discount = { code: "TEST" }
|
||||||
const getCart = (id) => {
|
const getCart = (id) => {
|
||||||
if (id === "with-d") {
|
if (id === "with-d") {
|
||||||
return {
|
return {
|
||||||
@@ -878,50 +928,46 @@ describe("DiscountService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("throws when hasReachedLimit returns true", async () => {
|
it("throws when hasReachedLimit returns true", async () => {
|
||||||
const discount = {}
|
|
||||||
const cart = getCart("with-d-and-customer")
|
const cart = getCart("with-d-and-customer")
|
||||||
discountService.hasReachedLimit = jest.fn().mockImplementation(() => true)
|
discountService.hasReachedLimit = jest.fn().mockImplementation(() => true)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
discountService.validateDiscountForCartOrThrow(cart, discount)
|
discountService.validateDiscountForCartOrThrow(cart, discount)
|
||||||
).rejects.toThrow({
|
).rejects.toThrow({
|
||||||
message: "Discount has been used maximum allowed times",
|
message: `Discount ${discount.code} has been used maximum allowed times`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throws when hasNotStarted returns true", async () => {
|
it("throws when hasNotStarted returns true", async () => {
|
||||||
const discount = {}
|
|
||||||
const cart = getCart("with-d-and-customer")
|
const cart = getCart("with-d-and-customer")
|
||||||
discountService.hasNotStarted = jest.fn().mockImplementation(() => true)
|
discountService.hasNotStarted = jest.fn().mockImplementation(() => true)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
discountService.validateDiscountForCartOrThrow(cart, discount)
|
discountService.validateDiscountForCartOrThrow(cart, discount)
|
||||||
).rejects.toThrow({
|
).rejects.toThrow({
|
||||||
message: "Discount is not valid yet",
|
message: `Discount ${discount.code} is not valid yet`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throws when hasExpired returns true", async () => {
|
it("throws when hasExpired returns true", async () => {
|
||||||
const discount = {}
|
|
||||||
const cart = getCart("with-d-and-customer")
|
const cart = getCart("with-d-and-customer")
|
||||||
discountService.hasExpired = jest.fn().mockImplementation(() => true)
|
discountService.hasExpired = jest.fn().mockImplementation(() => true)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
discountService.validateDiscountForCartOrThrow(cart, discount)
|
discountService.validateDiscountForCartOrThrow(cart, discount)
|
||||||
).rejects.toThrow({
|
).rejects.toThrow({
|
||||||
message: "Discount is expired",
|
message: `Discount ${discount.code} is expired`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throws when isDisabled returns true", async () => {
|
it("throws when isDisabled returns true", async () => {
|
||||||
const discount = {}
|
|
||||||
const cart = getCart("with-d-and-customer")
|
const cart = getCart("with-d-and-customer")
|
||||||
discountService.isDisabled = jest.fn().mockImplementation(() => true)
|
discountService.isDisabled = jest.fn().mockImplementation(() => true)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
discountService.validateDiscountForCartOrThrow(cart, discount)
|
discountService.validateDiscountForCartOrThrow(cart, discount)
|
||||||
).rejects.toThrow({
|
).rejects.toThrow({
|
||||||
message: "The discount code is disabled",
|
message: `The discount code ${discount.code} is disabled`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -940,16 +986,16 @@ describe("DiscountService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("throws when canApplyForCustomer returns false", async () => {
|
it("throws when canApplyForCustomer returns false", async () => {
|
||||||
const discount = { rule: { id: "" } }
|
const discount_ = { code: discount.code, rule: { id: "" } }
|
||||||
const cart = getCart("with-d-and-customer")
|
const cart = getCart("with-d-and-customer")
|
||||||
discountService.canApplyForCustomer = jest
|
discountService.canApplyForCustomer = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(() => Promise.resolve(false))
|
.mockImplementation(() => Promise.resolve(false))
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
discountService.validateDiscountForCartOrThrow(cart, discount)
|
discountService.validateDiscountForCartOrThrow(cart, discount_)
|
||||||
).rejects.toThrow({
|
).rejects.toThrow({
|
||||||
message: "Discount is not valid for customer",
|
message: `Discount ${discount.code} is not valid for customer`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { MockManager, MockRepository } from "medusa-test-utils"
|
import { MockManager, MockRepository } from "medusa-test-utils"
|
||||||
import { EventBusServiceMock } from "../__mocks__/event-bus"
|
import { EventBusServiceMock } from "../__mocks__/event-bus"
|
||||||
import DraftOrderService from "../draft-order"
|
import DraftOrderService from "../draft-order"
|
||||||
|
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
||||||
|
|
||||||
const eventBusService = {
|
const eventBusService = {
|
||||||
emit: jest.fn(),
|
emit: jest.fn(),
|
||||||
@@ -10,30 +11,7 @@ const eventBusService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("DraftOrderService", () => {
|
describe("DraftOrderService", () => {
|
||||||
const totalsService = {
|
|
||||||
getTotal: (o) => {
|
|
||||||
return o.total || 0
|
|
||||||
},
|
|
||||||
getSubtotal: (o) => {
|
|
||||||
return o.subtotal || 0
|
|
||||||
},
|
|
||||||
getTaxTotal: (o) => {
|
|
||||||
return o.tax_total || 0
|
|
||||||
},
|
|
||||||
getDiscountTotal: (o) => {
|
|
||||||
return o.discount_total || 0
|
|
||||||
},
|
|
||||||
getShippingTotal: (o) => {
|
|
||||||
return o.shipping_total || 0
|
|
||||||
},
|
|
||||||
getGiftCardTotal: (o) => {
|
|
||||||
return o.gift_card_total || 0
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
let result
|
|
||||||
|
|
||||||
const regionService = {
|
const regionService = {
|
||||||
retrieve: () =>
|
retrieve: () =>
|
||||||
Promise.resolve({ id: "test-region", countries: [{ iso_2: "dk" }] }),
|
Promise.resolve({ id: "test-region", countries: [{ iso_2: "dk" }] }),
|
||||||
@@ -62,7 +40,7 @@ describe("DraftOrderService", () => {
|
|||||||
variant_id: "test-variant",
|
variant_id: "test-variant",
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
create: jest.fn(),
|
create: jest.fn().mockImplementation((data) => data),
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
@@ -86,7 +64,9 @@ describe("DraftOrderService", () => {
|
|||||||
shipping_address_id: "test-shipping",
|
shipping_address_id: "test-shipping",
|
||||||
billing_address_id: "test-billing",
|
billing_address_id: "test-billing",
|
||||||
customer_id: "test-customer",
|
customer_id: "test-customer",
|
||||||
items: [{ variant_id: "test-variant", quantity: 2, metadata: {} }],
|
items: [
|
||||||
|
{ id: "test", variant_id: "test-variant", quantity: 2, metadata: {} },
|
||||||
|
],
|
||||||
shipping_methods: [
|
shipping_methods: [
|
||||||
{
|
{
|
||||||
option_id: "test-option",
|
option_id: "test-option",
|
||||||
@@ -108,6 +88,8 @@ describe("DraftOrderService", () => {
|
|||||||
...testOrder,
|
...testOrder,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
update: jest.fn(),
|
||||||
|
applyDiscount: jest.fn(),
|
||||||
addShippingMethod: jest.fn(),
|
addShippingMethod: jest.fn(),
|
||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
@@ -136,6 +118,7 @@ describe("DraftOrderService", () => {
|
|||||||
cartService,
|
cartService,
|
||||||
shippingOptionService,
|
shippingOptionService,
|
||||||
lineItemService,
|
lineItemService,
|
||||||
|
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||||
productVariantService,
|
productVariantService,
|
||||||
draftOrderRepository,
|
draftOrderRepository,
|
||||||
addressRepository,
|
addressRepository,
|
||||||
@@ -147,11 +130,14 @@ describe("DraftOrderService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("creates a draft order", async () => {
|
it("creates a draft order", async () => {
|
||||||
|
const cartId = "test-cart"
|
||||||
|
const title = "test-item"
|
||||||
|
|
||||||
await draftOrderService.create(testOrder)
|
await draftOrderService.create(testOrder)
|
||||||
|
|
||||||
expect(draftOrderRepository.create).toHaveBeenCalledTimes(1)
|
expect(draftOrderRepository.create).toHaveBeenCalledTimes(1)
|
||||||
expect(draftOrderRepository.create).toHaveBeenCalledWith({
|
expect(draftOrderRepository.create).toHaveBeenCalledWith({
|
||||||
cart_id: "test-cart",
|
cart_id: cartId,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(cartService.create).toHaveBeenCalledTimes(1)
|
expect(cartService.create).toHaveBeenCalledTimes(1)
|
||||||
@@ -163,11 +149,6 @@ describe("DraftOrderService", () => {
|
|||||||
type: "draft_order",
|
type: "draft_order",
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(cartService.retrieve).toHaveBeenCalledTimes(1)
|
|
||||||
expect(cartService.retrieve).toHaveBeenCalledWith("test-cart", {
|
|
||||||
relations: ["discounts", "discounts.rule", "items", "region"],
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(cartService.addShippingMethod).toHaveBeenCalledTimes(1)
|
expect(cartService.addShippingMethod).toHaveBeenCalledTimes(1)
|
||||||
expect(cartService.addShippingMethod).toHaveBeenCalledWith(
|
expect(cartService.addShippingMethod).toHaveBeenCalledWith(
|
||||||
"test-cart",
|
"test-cart",
|
||||||
@@ -183,18 +164,69 @@ describe("DraftOrderService", () => {
|
|||||||
{
|
{
|
||||||
metadata: {},
|
metadata: {},
|
||||||
unit_price: undefined,
|
unit_price: undefined,
|
||||||
cart: expect.objectContaining({
|
|
||||||
id: "test-cart",
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(lineItemService.create).toHaveBeenCalledTimes(1)
|
expect(lineItemService.create).toHaveBeenCalledTimes(1)
|
||||||
expect(lineItemService.create).toHaveBeenCalledWith({
|
expect(lineItemService.create).toHaveBeenCalledWith({
|
||||||
cart_id: "test-cart",
|
cart_id: cartId,
|
||||||
title: "test-item",
|
title,
|
||||||
variant_id: "test-variant",
|
variant_id: "test-variant",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
expect(cartService.applyDiscount).toHaveBeenCalledTimes(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("creates a draft order with a discount", async () => {
|
||||||
|
const cartId = "test-cart"
|
||||||
|
const title = "test-item"
|
||||||
|
|
||||||
|
testOrder["discounts"] = [{ code: "TEST" }]
|
||||||
|
await draftOrderService.create(testOrder)
|
||||||
|
|
||||||
|
expect(draftOrderRepository.create).toHaveBeenCalledTimes(1)
|
||||||
|
expect(draftOrderRepository.create).toHaveBeenCalledWith({
|
||||||
|
cart_id: cartId,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(cartService.create).toHaveBeenCalledTimes(1)
|
||||||
|
expect(cartService.create).toHaveBeenCalledWith({
|
||||||
|
region_id: "test-region",
|
||||||
|
shipping_address_id: "test-shipping",
|
||||||
|
billing_address_id: "test-billing",
|
||||||
|
customer_id: "test-customer",
|
||||||
|
type: "draft_order",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(cartService.addShippingMethod).toHaveBeenCalledTimes(1)
|
||||||
|
expect(cartService.addShippingMethod).toHaveBeenCalledWith(
|
||||||
|
"test-cart",
|
||||||
|
"test-option",
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(lineItemService.generate).toHaveBeenCalledTimes(1)
|
||||||
|
expect(lineItemService.generate).toHaveBeenCalledWith(
|
||||||
|
"test-variant",
|
||||||
|
"test-region",
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
metadata: {},
|
||||||
|
unit_price: undefined,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(lineItemService.create).toHaveBeenCalledTimes(1)
|
||||||
|
expect(lineItemService.create).toHaveBeenCalledWith({
|
||||||
|
cart_id: cartId,
|
||||||
|
title,
|
||||||
|
variant_id: "test-variant",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(cartService.update).toHaveBeenCalledTimes(1)
|
||||||
|
expect(cartService.update).toHaveBeenCalledWith(cartId, {
|
||||||
|
discounts: testOrder.discounts,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails on missing region", async () => {
|
it("fails on missing region", async () => {
|
||||||
@@ -213,7 +245,7 @@ describe("DraftOrderService", () => {
|
|||||||
await draftOrderService.create({
|
await draftOrderService.create({
|
||||||
region_id: "test-region",
|
region_id: "test-region",
|
||||||
items: [],
|
items: [],
|
||||||
shipping_methods: []
|
shipping_methods: [],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -262,6 +294,7 @@ describe("DraftOrderService", () => {
|
|||||||
cartService: undefined,
|
cartService: undefined,
|
||||||
shippingOptionService: undefined,
|
shippingOptionService: undefined,
|
||||||
lineItemService: undefined,
|
lineItemService: undefined,
|
||||||
|
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||||
productVariantService: undefined,
|
productVariantService: undefined,
|
||||||
draftOrderRepository,
|
draftOrderRepository,
|
||||||
addressRepository: undefined,
|
addressRepository: undefined,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Customer,
|
Customer,
|
||||||
CustomShippingOption,
|
CustomShippingOption,
|
||||||
Discount,
|
Discount,
|
||||||
|
DiscountRule,
|
||||||
DiscountRuleType,
|
DiscountRuleType,
|
||||||
LineItem,
|
LineItem,
|
||||||
PaymentSession,
|
PaymentSession,
|
||||||
@@ -1122,10 +1123,9 @@ class CartService extends TransactionBaseService {
|
|||||||
const previousDiscounts = [...cart.discounts]
|
const previousDiscounts = [...cart.discounts]
|
||||||
cart.discounts.length = 0
|
cart.discounts.length = 0
|
||||||
|
|
||||||
await Promise.all(
|
await this.applyDiscounts(
|
||||||
data.discounts.map(async ({ code }) => {
|
cart,
|
||||||
return this.applyDiscount(cart, code)
|
data.discounts.map((d) => d.code)
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasFreeShipping = cart.discounts.some(
|
const hasFreeShipping = cart.discounts.some(
|
||||||
@@ -1410,41 +1410,65 @@ class CartService extends TransactionBaseService {
|
|||||||
* Throws if discount regions does not include the cart region
|
* Throws if discount regions does not include the cart region
|
||||||
* @param cart - the cart to update
|
* @param cart - the cart to update
|
||||||
* @param discountCode - the discount code
|
* @param discountCode - the discount code
|
||||||
* @return the result of the update operation
|
|
||||||
*/
|
*/
|
||||||
async applyDiscount(cart: Cart, discountCode: string): Promise<void> {
|
async applyDiscount(cart: Cart, discountCode: string): Promise<void> {
|
||||||
|
return await this.applyDiscounts(cart, [discountCode])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the cart's discounts.
|
||||||
|
* If discount besides free shipping is already applied, this
|
||||||
|
* will be overwritten
|
||||||
|
* Throws if discount regions does not include the cart region
|
||||||
|
* @param cart - the cart to update
|
||||||
|
* @param discountCodes - the discount code(s) to apply
|
||||||
|
*/
|
||||||
|
async applyDiscounts(cart: Cart, discountCodes: string[]): Promise<void> {
|
||||||
return await this.atomicPhase_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const discount = await this.discountService_
|
const discounts = await this.discountService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.retrieveByCode(discountCode, { relations: ["rule", "regions"] })
|
.listByCodes(discountCodes, { relations: ["rule", "regions"] })
|
||||||
|
|
||||||
await this.discountService_
|
await this.discountService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.validateDiscountForCartOrThrow(cart, discount)
|
.validateDiscountForCartOrThrow(cart, discounts)
|
||||||
|
|
||||||
const rule = discount.rule
|
const rules: Map<string, DiscountRule> = new Map()
|
||||||
|
const discountsMap = new Map(
|
||||||
|
discounts.map((d) => {
|
||||||
|
rules.set(d.id, d.rule)
|
||||||
|
return [d.id, d]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// if discount is already there, we simply resolve
|
cart.discounts.forEach((discount) => {
|
||||||
if (cart.discounts.find(({ id }) => id === discount.id)) {
|
if (discountsMap.has(discount.id)) {
|
||||||
return
|
discountsMap.delete(discount.id)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const toParse = [...cart.discounts, discount]
|
const toParse = [...cart.discounts, ...discountsMap.values()]
|
||||||
|
|
||||||
let sawNotShipping = false
|
let sawNotShipping = false
|
||||||
const newDiscounts = toParse.map((discountToParse) => {
|
const newDiscounts = toParse.map((discountToParse) => {
|
||||||
switch (discountToParse.rule?.type) {
|
switch (discountToParse.rule?.type) {
|
||||||
case DiscountRuleType.FREE_SHIPPING:
|
case DiscountRuleType.FREE_SHIPPING:
|
||||||
if (discountToParse.rule.type === rule.type) {
|
if (
|
||||||
return discount
|
discountToParse.rule.type ===
|
||||||
|
rules.get(discountToParse.id)!.type
|
||||||
|
) {
|
||||||
|
return discountsMap.get(discountToParse.id)
|
||||||
}
|
}
|
||||||
return discountToParse
|
return discountToParse
|
||||||
default:
|
default:
|
||||||
if (!sawNotShipping) {
|
if (!sawNotShipping) {
|
||||||
sawNotShipping = true
|
sawNotShipping = true
|
||||||
if (rule?.type !== DiscountRuleType.FREE_SHIPPING) {
|
if (
|
||||||
return discount
|
rules.get(discountToParse.id)!.type !==
|
||||||
|
DiscountRuleType.FREE_SHIPPING
|
||||||
|
) {
|
||||||
|
return discountsMap.get(discountToParse.id)
|
||||||
}
|
}
|
||||||
return discountToParse
|
return discountToParse
|
||||||
}
|
}
|
||||||
@@ -1458,8 +1482,11 @@ class CartService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ignore if free shipping
|
const hadNonFreeShippingDiscounts = [...rules.values()].some(
|
||||||
if (rule?.type !== DiscountRuleType.FREE_SHIPPING && cart?.items) {
|
(rule) => rule.type !== DiscountRuleType.FREE_SHIPPING
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hadNonFreeShippingDiscounts && cart?.items) {
|
||||||
await this.refreshAdjustments_(cart)
|
await this.refreshAdjustments_(cart)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
DeepPartial,
|
DeepPartial,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
ILike,
|
ILike,
|
||||||
|
In,
|
||||||
SelectQueryBuilder,
|
SelectQueryBuilder,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import {
|
import {
|
||||||
@@ -277,10 +278,10 @@ class DiscountService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a discount by discount code.
|
* Gets the discount by discount code.
|
||||||
* @param {string} discountCode - discount code of discount to retrieve
|
* @param discountCode - discount code of discount to retrieve
|
||||||
* @param {Object} config - the config object containing query settings
|
* @param config - the config object containing query settings
|
||||||
* @return {Promise<Discount>} the discount document
|
* @return the discount
|
||||||
*/
|
*/
|
||||||
async retrieveByCode(
|
async retrieveByCode(
|
||||||
discountCode: string,
|
discountCode: string,
|
||||||
@@ -291,24 +292,49 @@ class DiscountService extends TransactionBaseService {
|
|||||||
|
|
||||||
const normalizedCode = discountCode.toUpperCase().trim()
|
const normalizedCode = discountCode.toUpperCase().trim()
|
||||||
|
|
||||||
let query = buildQuery({ code: normalizedCode, is_dynamic: false }, config)
|
const query = buildQuery({ code: normalizedCode }, config)
|
||||||
let discount = await discountRepo.findOne(query)
|
const discount = await discountRepo.findOne(query)
|
||||||
|
|
||||||
if (!discount) {
|
|
||||||
query = buildQuery({ code: normalizedCode, is_dynamic: true }, config)
|
|
||||||
discount = await discountRepo.findOne(query)
|
|
||||||
|
|
||||||
if (!discount) {
|
if (!discount) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_FOUND,
|
MedusaError.Types.NOT_FOUND,
|
||||||
`Discount with code ${discountCode} was not found`
|
`Discounts with code ${discountCode} was not found`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return discount
|
return discount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all the discounts corresponding to the given codes
|
||||||
|
* @param discountCodes - discount codes of discounts to retrieve
|
||||||
|
* @param config - the config object containing query settings
|
||||||
|
* @return the discounts
|
||||||
|
*/
|
||||||
|
async listByCodes(
|
||||||
|
discountCodes: string[],
|
||||||
|
config: FindConfig<Discount> = {}
|
||||||
|
): Promise<Discount[]> {
|
||||||
|
const manager = this.manager_
|
||||||
|
const discountRepo = manager.getCustomRepository(this.discountRepository_)
|
||||||
|
|
||||||
|
const normalizedCodes = discountCodes.map((code) =>
|
||||||
|
code.toUpperCase().trim()
|
||||||
|
)
|
||||||
|
|
||||||
|
const query = buildQuery({ code: In(normalizedCodes) }, config)
|
||||||
|
const discounts = await discountRepo.find(query)
|
||||||
|
|
||||||
|
if (discounts?.length !== discountCodes.length) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Discounts with code [${normalizedCodes.join(", ")}] was not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return discounts
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a discount.
|
* Updates a discount.
|
||||||
* @param {string} discountId - discount id of discount to update
|
* @param {string} discountId - discount id of discount to update
|
||||||
@@ -660,39 +686,42 @@ class DiscountService extends TransactionBaseService {
|
|||||||
|
|
||||||
async validateDiscountForCartOrThrow(
|
async validateDiscountForCartOrThrow(
|
||||||
cart: Cart,
|
cart: Cart,
|
||||||
discount: Discount
|
discount: Discount | Discount[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const discounts = Array.isArray(discount) ? discount : [discount]
|
||||||
return await this.atomicPhase_(async () => {
|
return await this.atomicPhase_(async () => {
|
||||||
if (this.hasReachedLimit(discount)) {
|
await Promise.all(
|
||||||
|
discounts.map(async (disc) => {
|
||||||
|
if (this.hasReachedLimit(disc)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
"Discount has been used maximum allowed times"
|
`Discount ${disc.code} has been used maximum allowed times`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasNotStarted(discount)) {
|
if (this.hasNotStarted(disc)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
"Discount is not valid yet"
|
`Discount ${disc.code} is not valid yet`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasExpired(discount)) {
|
if (this.hasExpired(disc)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
"Discount is expired"
|
`Discount ${disc.code} is expired`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isDisabled(discount)) {
|
if (this.isDisabled(disc)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
"The discount code is disabled"
|
`The discount code ${disc.code} is disabled`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidForRegion = await this.isValidForRegion(
|
const isValidForRegion = await this.isValidForRegion(
|
||||||
discount,
|
disc,
|
||||||
cart.region_id
|
cart.region_id
|
||||||
)
|
)
|
||||||
if (!isValidForRegion) {
|
if (!isValidForRegion) {
|
||||||
@@ -704,18 +733,20 @@ class DiscountService extends TransactionBaseService {
|
|||||||
|
|
||||||
if (cart.customer_id) {
|
if (cart.customer_id) {
|
||||||
const canApplyForCustomer = await this.canApplyForCustomer(
|
const canApplyForCustomer = await this.canApplyForCustomer(
|
||||||
discount.rule.id,
|
disc.rule.id,
|
||||||
cart.customer_id
|
cart.customer_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!canApplyForCustomer) {
|
if (!canApplyForCustomer) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
"Discount is not valid for customer"
|
`Discount ${disc.code} is not valid for customer`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
hasReachedLimit(discount: Discount): boolean {
|
hasReachedLimit(discount: Discount): boolean {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||||
import { Brackets, EntityManager, FindManyOptions, UpdateResult } from "typeorm"
|
import { Brackets, EntityManager, FindManyOptions, UpdateResult } from "typeorm"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import { Cart, CartType, DraftOrder, DraftOrderStatus } from "../models"
|
import { CartType, DraftOrder, DraftOrderStatus } from "../models"
|
||||||
import { DraftOrderRepository } from "../repositories/draft-order"
|
import { DraftOrderRepository } from "../repositories/draft-order"
|
||||||
import { OrderRepository } from "../repositories/order"
|
import { OrderRepository } from "../repositories/order"
|
||||||
import { PaymentRepository } from "../repositories/payment"
|
import { PaymentRepository } from "../repositories/payment"
|
||||||
@@ -264,34 +264,24 @@ class DraftOrderService extends TransactionBaseService {
|
|||||||
no_notification_order,
|
no_notification_order,
|
||||||
items,
|
items,
|
||||||
idempotency_key,
|
idempotency_key,
|
||||||
|
discounts,
|
||||||
...rawCart
|
...rawCart
|
||||||
} = data
|
} = data
|
||||||
|
|
||||||
const cartServiceTx =
|
const cartServiceTx =
|
||||||
this.cartService_.withTransaction(transactionManager)
|
this.cartService_.withTransaction(transactionManager)
|
||||||
|
|
||||||
if (rawCart.discounts) {
|
const createdCart = await cartServiceTx.create({
|
||||||
const { discounts } = rawCart
|
|
||||||
rawCart.discounts = []
|
|
||||||
|
|
||||||
for (const { code } of discounts) {
|
|
||||||
await cartServiceTx.applyDiscount(rawCart as Cart, code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let createdCart = await cartServiceTx.create({
|
|
||||||
type: CartType.DRAFT_ORDER,
|
type: CartType.DRAFT_ORDER,
|
||||||
...rawCart,
|
...rawCart,
|
||||||
})
|
})
|
||||||
createdCart = await cartServiceTx.retrieve(createdCart.id, {
|
|
||||||
relations: ["discounts", "discounts.rule", "items", "region"],
|
|
||||||
})
|
|
||||||
|
|
||||||
const draftOrder = draftOrderRepo.create({
|
const draftOrder = draftOrderRepo.create({
|
||||||
cart_id: createdCart.id,
|
cart_id: createdCart.id,
|
||||||
no_notification_order,
|
no_notification_order,
|
||||||
idempotency_key,
|
idempotency_key,
|
||||||
})
|
})
|
||||||
|
|
||||||
const result = await draftOrderRepo.save(draftOrder)
|
const result = await draftOrderRepo.save(draftOrder)
|
||||||
|
|
||||||
await this.eventBus_
|
await this.eventBus_
|
||||||
@@ -303,7 +293,7 @@ class DraftOrderService extends TransactionBaseService {
|
|||||||
const lineItemServiceTx =
|
const lineItemServiceTx =
|
||||||
this.lineItemService_.withTransaction(transactionManager)
|
this.lineItemService_.withTransaction(transactionManager)
|
||||||
|
|
||||||
for (const item of (items || [])) {
|
for (const item of items || []) {
|
||||||
if (item.variant_id) {
|
if (item.variant_id) {
|
||||||
const line = await lineItemServiceTx.generate(
|
const line = await lineItemServiceTx.generate(
|
||||||
item.variant_id,
|
item.variant_id,
|
||||||
@@ -312,7 +302,6 @@ class DraftOrderService extends TransactionBaseService {
|
|||||||
{
|
{
|
||||||
metadata: item?.metadata || {},
|
metadata: item?.metadata || {},
|
||||||
unit_price: item.unit_price,
|
unit_price: item.unit_price,
|
||||||
cart: createdCart,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -340,6 +329,10 @@ class DraftOrderService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (discounts?.length) {
|
||||||
|
await cartServiceTx.update(createdCart.id, { discounts })
|
||||||
|
}
|
||||||
|
|
||||||
for (const method of shipping_methods) {
|
for (const method of shipping_methods) {
|
||||||
if (typeof method.price !== "undefined") {
|
if (typeof method.price !== "undefined") {
|
||||||
await this.customShippingOptionService_
|
await this.customShippingOptionService_
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ class LineItemService extends TransactionBaseService {
|
|||||||
*/
|
*/
|
||||||
async create<
|
async create<
|
||||||
T = LineItem | LineItem[],
|
T = LineItem | LineItem[],
|
||||||
TResult = T extends LineItem ? LineItem : LineItem[]
|
TResult = T extends LineItem[] ? LineItem[] : LineItem
|
||||||
>(data: T): Promise<TResult> {
|
>(data: T): Promise<TResult> {
|
||||||
return await this.atomicPhase_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
|
|||||||
Reference in new issue
Block a user