fix(medusa): Applying Discounts (with Conditions) on DraftOrders and Carts (#3197)
This commit is contained in:
committed by
GitHub
parent
4d3210bfbb
commit
bfa33f444c
@@ -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,
|
||||
},
|
||||
adminReqConfig
|
||||
).catch(e => e)
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.response.status).toEqual(400)
|
||||
expect(err.response.data.message).toEqual(
|
||||
@@ -1026,7 +1027,8 @@ describe("/admin/discounts", () => {
|
||||
regions: [validRegionId, "test-region-2"],
|
||||
},
|
||||
adminReqConfig
|
||||
).catch(e => e)
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.response.status).toEqual(400)
|
||||
expect(err.response.data.message).toEqual(
|
||||
@@ -1060,7 +1062,8 @@ describe("/admin/discounts", () => {
|
||||
regions: [validRegionId, "test-region-2"],
|
||||
},
|
||||
adminReqConfig
|
||||
).catch(e => e)
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.response.status).toEqual(400)
|
||||
expect(err.response.data.message).toEqual(
|
||||
@@ -1092,7 +1095,8 @@ describe("/admin/discounts", () => {
|
||||
`/admin/discounts/${response.data.discount.id}/regions/test-region-2`,
|
||||
{},
|
||||
adminReqConfig
|
||||
).catch(e => e)
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.response.status).toEqual(400)
|
||||
expect(err.response.data.message).toEqual(
|
||||
@@ -1246,7 +1250,8 @@ describe("/admin/discounts", () => {
|
||||
ends_at: new Date("09/14/2021 17:50"),
|
||||
},
|
||||
adminReqConfig
|
||||
).catch(e => e)
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.response.status).toEqual(400)
|
||||
expect(err.response.data).toEqual(
|
||||
@@ -1259,20 +1264,22 @@ describe("/admin/discounts", () => {
|
||||
it("fails to create a discount if the regions contains an invalid regionId ", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const err = await api.post(
|
||||
"/admin/discounts",
|
||||
{
|
||||
code: "HELLOWORLD",
|
||||
rule: {
|
||||
description: "test",
|
||||
type: "percentage",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
const err = await api
|
||||
.post(
|
||||
"/admin/discounts",
|
||||
{
|
||||
code: "HELLOWORLD",
|
||||
rule: {
|
||||
description: "test",
|
||||
type: "percentage",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
regions: [validRegionId, invalidRegionId],
|
||||
},
|
||||
regions: [validRegionId, invalidRegionId],
|
||||
},
|
||||
adminReqConfig
|
||||
).catch(e => e)
|
||||
adminReqConfig
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.response.status).toEqual(404)
|
||||
expect(err.response.data.message).toEqual(
|
||||
@@ -1283,20 +1290,22 @@ describe("/admin/discounts", () => {
|
||||
it("fails to create a discount if the regions contains only invalid regionIds ", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const err = await api.post(
|
||||
"/admin/discounts",
|
||||
{
|
||||
code: "HELLOWORLD",
|
||||
rule: {
|
||||
description: "test",
|
||||
type: "percentage",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
const err = await api
|
||||
.post(
|
||||
"/admin/discounts",
|
||||
{
|
||||
code: "HELLOWORLD",
|
||||
rule: {
|
||||
description: "test",
|
||||
type: "percentage",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
regions: [invalidRegionId],
|
||||
},
|
||||
regions: [invalidRegionId],
|
||||
},
|
||||
adminReqConfig
|
||||
).catch(e => e)
|
||||
adminReqConfig
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.response.status).toEqual(404)
|
||||
expect(err.response.data.message).toEqual(
|
||||
@@ -1307,19 +1316,21 @@ describe("/admin/discounts", () => {
|
||||
it("fails to create a discount if regions are not present ", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const err = await api.post(
|
||||
"/admin/discounts",
|
||||
{
|
||||
code: "HELLOWORLD",
|
||||
rule: {
|
||||
description: "test",
|
||||
type: "percentage",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
const err = await api
|
||||
.post(
|
||||
"/admin/discounts",
|
||||
{
|
||||
code: "HELLOWORLD",
|
||||
rule: {
|
||||
description: "test",
|
||||
type: "percentage",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
},
|
||||
},
|
||||
adminReqConfig
|
||||
).catch((e) => e)
|
||||
adminReqConfig
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.response.status).toEqual(400)
|
||||
expect(err.response.data.message).toEqual(
|
||||
@@ -2204,11 +2215,16 @@ describe("/admin/discounts", () => {
|
||||
|
||||
it("should respond with 404 on non-existing code", async () => {
|
||||
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.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 adminSeeder = require("../../../helpers/admin-seeder")
|
||||
const {
|
||||
simpleRegionFactory,
|
||||
simpleDiscountFactory,
|
||||
} = require("../../../factories")
|
||||
const { simpleDiscountFactory } = require("../../../factories")
|
||||
|
||||
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 () => {
|
||||
const api = useApi()
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ const { useApi } = require("../../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../../helpers/use-db")
|
||||
|
||||
const cartSeeder = require("../../../helpers/cart-seeder")
|
||||
const productSeeder = require("../../../helpers/product-seeder")
|
||||
const swapSeeder = require("../../../helpers/swap-seeder")
|
||||
const {
|
||||
simpleCartFactory,
|
||||
@@ -980,16 +979,18 @@ describe("/store/carts", () => {
|
||||
it("fails on apply discount if limit has been reached", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const code = "SPENT"
|
||||
|
||||
const err = await api
|
||||
.post("/store/carts/test-cart", {
|
||||
discounts: [{ code: "SPENT" }],
|
||||
discounts: [{ code }],
|
||||
})
|
||||
.catch((err) => err)
|
||||
|
||||
expect(err).toBeTruthy()
|
||||
expect(err.response.status).toEqual(400)
|
||||
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 {
|
||||
await api.post("/store/carts/test-customer-discount", {
|
||||
discounts: [{ code: "TEST" }],
|
||||
discounts: [{ code }],
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.response.status).toEqual(400)
|
||||
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 {
|
||||
await api.post("/store/carts/test-customer-discount", {
|
||||
discounts: [{ code: "TEST" }],
|
||||
discounts: [{ code }],
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.response.status).toEqual(400)
|
||||
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)
|
||||
const api = useApi()
|
||||
|
||||
try {
|
||||
await api.post("/store/carts/test-cart", {
|
||||
discounts: [{ code: "EXP_DISC" }],
|
||||
const code = "EXP_DISC"
|
||||
|
||||
const err = await api
|
||||
.post("/store/carts/test-cart", {
|
||||
discounts: [{ code }],
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data.message).toEqual("Discount is expired")
|
||||
}
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.response.status).toEqual(400)
|
||||
expect(err.response.data.message).toEqual(`Discount ${code} is expired`)
|
||||
})
|
||||
|
||||
it("fails on discount before start day", async () => {
|
||||
expect.assertions(2)
|
||||
const api = useApi()
|
||||
|
||||
try {
|
||||
await api.post("/store/carts/test-cart", {
|
||||
discounts: [{ code: "PREM_DISC" }],
|
||||
const code = "PREM_DISC"
|
||||
|
||||
const err = await api
|
||||
.post("/store/carts/test-cart", {
|
||||
discounts: [{ code }],
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data.message).toEqual("Discount is not valid yet")
|
||||
}
|
||||
.catch((e) => e)
|
||||
|
||||
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 () => {
|
||||
const api = useApi()
|
||||
|
||||
try {
|
||||
await api.post("/store/carts/test-cart", {
|
||||
discounts: [{ code: "INV_DYN_DISC" }],
|
||||
const code = "INV_DYN_DISC"
|
||||
|
||||
const err = await api
|
||||
.post("/store/carts/test-cart", {
|
||||
discounts: [{ code }],
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data.message).toEqual("Discount is expired")
|
||||
}
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.response.status).toEqual(400)
|
||||
expect(err.response.data.message).toEqual(`Discount ${code} is expired`)
|
||||
})
|
||||
|
||||
it("Applies dynamic discount to cart correctly", async () => {
|
||||
|
||||
@@ -327,7 +327,7 @@ module.exports = async (connection, data = {}) => {
|
||||
is_dynamic: true,
|
||||
is_disabled: false,
|
||||
starts_at: tenDaysAgo,
|
||||
ends_at: tenDaysFromToday,
|
||||
ends_at: yesterday,
|
||||
valid_duration: "P1D", // one day
|
||||
})
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@ import {
|
||||
Not,
|
||||
Repository,
|
||||
} from "typeorm"
|
||||
import { Discount } from "../models"
|
||||
import {
|
||||
Discount,
|
||||
DiscountCondition,
|
||||
DiscountConditionCustomerGroup,
|
||||
DiscountConditionOperator,
|
||||
DiscountConditionProduct,
|
||||
DiscountConditionProductCollection,
|
||||
DiscountConditionProductTag,
|
||||
DiscountConditionProductType,
|
||||
DiscountConditionType,
|
||||
} from "../models/discount-condition"
|
||||
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"
|
||||
} from "../models"
|
||||
import { isString } from "../utils"
|
||||
|
||||
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 `not_in` and the query for conditions defined for the given type is not empty, the discount is invalid
|
||||
for (const condition of discountConditions) {
|
||||
if (condition.type === DiscountConditionType.CUSTOMER_GROUPS) {
|
||||
continue
|
||||
}
|
||||
|
||||
const numConditions = await this.queryConditionTable({
|
||||
type: condition.type,
|
||||
condId: condition.id,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
export const LineItemAdjustmentServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
|
||||
@@ -2062,126 +2062,141 @@ describe("CartService", () => {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieveByCode: jest.fn().mockImplementation((code) => {
|
||||
if (code === "US10") {
|
||||
return Promise.resolve({
|
||||
regions: [{ id: IdMap.getId("bad") }],
|
||||
})
|
||||
listByCodes: jest.fn().mockImplementation((code) => {
|
||||
const codes = Array.isArray(code) ? code : [code]
|
||||
|
||||
const data = []
|
||||
|
||||
for (const code of codes) {
|
||||
if (code === "US10") {
|
||||
data.push({
|
||||
regions: [{ id: IdMap.getId("bad") }],
|
||||
})
|
||||
}
|
||||
if (code === "limit-reached") {
|
||||
data.push({
|
||||
id: IdMap.getId("limit-reached"),
|
||||
code: "limit-reached",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {},
|
||||
usage_count: 2,
|
||||
usage_limit: 2,
|
||||
})
|
||||
}
|
||||
if (code === "null-count") {
|
||||
data.push({
|
||||
id: IdMap.getId("null-count"),
|
||||
code: "null-count",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {},
|
||||
usage_count: null,
|
||||
usage_limit: 2,
|
||||
})
|
||||
}
|
||||
if (code === "FREESHIPPING") {
|
||||
data.push({
|
||||
id: IdMap.getId("freeship"),
|
||||
code: "FREESHIPPING",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "free_shipping",
|
||||
},
|
||||
})
|
||||
}
|
||||
if (code === "EarlyDiscount") {
|
||||
data.push({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(1),
|
||||
ends_at: getOffsetDate(10),
|
||||
})
|
||||
}
|
||||
if (code === "ExpiredDiscount") {
|
||||
data.push({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
ends_at: getOffsetDate(-1),
|
||||
starts_at: getOffsetDate(-10),
|
||||
})
|
||||
}
|
||||
if (code === "ExpiredDynamicDiscount") {
|
||||
data.push({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
is_dynamic: true,
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(-1),
|
||||
})
|
||||
}
|
||||
if (code === "ExpiredDynamicDiscountEndDate") {
|
||||
data.push({
|
||||
id: IdMap.getId("10off"),
|
||||
is_dynamic: true,
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(-3),
|
||||
valid_duration: "P0Y0M1D",
|
||||
})
|
||||
}
|
||||
if (code === "ValidDiscount") {
|
||||
data.push({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(10),
|
||||
})
|
||||
}
|
||||
if (code === "ApplicableForCustomer") {
|
||||
data.push({
|
||||
id: "ApplicableForCustomer",
|
||||
code: "ApplicableForCustomer",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
id: "test-rule",
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(10),
|
||||
})
|
||||
}
|
||||
|
||||
if (!data.length) {
|
||||
data.push({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if (code === "limit-reached") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("limit-reached"),
|
||||
code: "limit-reached",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {},
|
||||
usage_count: 2,
|
||||
usage_limit: 2,
|
||||
})
|
||||
|
||||
if (Array.isArray(code)) {
|
||||
return Promise.resolve(data)
|
||||
}
|
||||
if (code === "null-count") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("null-count"),
|
||||
code: "null-count",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {},
|
||||
usage_count: null,
|
||||
usage_limit: 2,
|
||||
})
|
||||
}
|
||||
if (code === "FREESHIPPING") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("freeship"),
|
||||
code: "FREESHIPPING",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "free_shipping",
|
||||
},
|
||||
})
|
||||
}
|
||||
if (code === "EarlyDiscount") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(1),
|
||||
ends_at: getOffsetDate(10),
|
||||
})
|
||||
}
|
||||
if (code === "ExpiredDiscount") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
ends_at: getOffsetDate(-1),
|
||||
starts_at: getOffsetDate(-10),
|
||||
})
|
||||
}
|
||||
if (code === "ExpiredDynamicDiscount") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
is_dynamic: true,
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(-1),
|
||||
})
|
||||
}
|
||||
if (code === "ExpiredDynamicDiscountEndDate") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
is_dynamic: true,
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(-3),
|
||||
valid_duration: "P0Y0M1D",
|
||||
})
|
||||
}
|
||||
if (code === "ValidDiscount") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(10),
|
||||
})
|
||||
}
|
||||
if (code === "ApplicableForCustomer") {
|
||||
return Promise.resolve({
|
||||
id: "ApplicableForCustomer",
|
||||
code: "ApplicableForCustomer",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
id: "test-rule",
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(10),
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
})
|
||||
|
||||
return Promise.resolve(data[0])
|
||||
}),
|
||||
canApplyForCustomer: jest
|
||||
.fn()
|
||||
@@ -2333,7 +2348,7 @@ describe("CartService", () => {
|
||||
discounts: [{ code: "10%OFF" }, { code: "FREESHIPPING" }],
|
||||
})
|
||||
|
||||
expect(discountService.retrieveByCode).toHaveBeenCalledTimes(2)
|
||||
expect(discountService.listByCodes).toHaveBeenCalledTimes(1)
|
||||
expect(cartRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(cartRepository.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FlagRouter } from "../../utils/flag-router"
|
||||
import DiscountService from "../discount"
|
||||
import { TotalsServiceMock } from "../__mocks__/totals"
|
||||
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
||||
import { In } from "typeorm"
|
||||
|
||||
const featureFlagRouter = new FlagRouter({})
|
||||
|
||||
@@ -54,14 +55,16 @@ describe("DiscountService", () => {
|
||||
})
|
||||
|
||||
it("fails to create a discount without regions", async () => {
|
||||
const err = await discountService.create({
|
||||
code: "test",
|
||||
rule: {
|
||||
type: "fixed",
|
||||
allocation: "total",
|
||||
value: 20,
|
||||
},
|
||||
}).catch(e => e)
|
||||
const err = await discountService
|
||||
.create({
|
||||
code: "test",
|
||||
rule: {
|
||||
type: "fixed",
|
||||
allocation: "total",
|
||||
value: 20,
|
||||
},
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err.type).toEqual("invalid_data")
|
||||
expect(err.message).toEqual("Discount must have atleast 1 region")
|
||||
@@ -237,7 +240,6 @@ describe("DiscountService", () => {
|
||||
expect(discountRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
code: "10%OFF",
|
||||
is_dynamic: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -248,7 +250,54 @@ describe("DiscountService", () => {
|
||||
expect(discountRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
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", () => {
|
||||
const discount = { code: "TEST" }
|
||||
const getCart = (id) => {
|
||||
if (id === "with-d") {
|
||||
return {
|
||||
@@ -878,50 +928,46 @@ describe("DiscountService", () => {
|
||||
})
|
||||
|
||||
it("throws when hasReachedLimit returns true", async () => {
|
||||
const discount = {}
|
||||
const cart = getCart("with-d-and-customer")
|
||||
discountService.hasReachedLimit = jest.fn().mockImplementation(() => true)
|
||||
|
||||
expect(
|
||||
discountService.validateDiscountForCartOrThrow(cart, discount)
|
||||
).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 () => {
|
||||
const discount = {}
|
||||
const cart = getCart("with-d-and-customer")
|
||||
discountService.hasNotStarted = jest.fn().mockImplementation(() => true)
|
||||
|
||||
expect(
|
||||
discountService.validateDiscountForCartOrThrow(cart, discount)
|
||||
).rejects.toThrow({
|
||||
message: "Discount is not valid yet",
|
||||
message: `Discount ${discount.code} is not valid yet`,
|
||||
})
|
||||
})
|
||||
|
||||
it("throws when hasExpired returns true", async () => {
|
||||
const discount = {}
|
||||
const cart = getCart("with-d-and-customer")
|
||||
discountService.hasExpired = jest.fn().mockImplementation(() => true)
|
||||
|
||||
expect(
|
||||
discountService.validateDiscountForCartOrThrow(cart, discount)
|
||||
).rejects.toThrow({
|
||||
message: "Discount is expired",
|
||||
message: `Discount ${discount.code} is expired`,
|
||||
})
|
||||
})
|
||||
|
||||
it("throws when isDisabled returns true", async () => {
|
||||
const discount = {}
|
||||
const cart = getCart("with-d-and-customer")
|
||||
discountService.isDisabled = jest.fn().mockImplementation(() => true)
|
||||
|
||||
expect(
|
||||
discountService.validateDiscountForCartOrThrow(cart, discount)
|
||||
).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 () => {
|
||||
const discount = { rule: { id: "" } }
|
||||
const discount_ = { code: discount.code, rule: { id: "" } }
|
||||
const cart = getCart("with-d-and-customer")
|
||||
discountService.canApplyForCustomer = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(false))
|
||||
|
||||
expect(
|
||||
discountService.validateDiscountForCartOrThrow(cart, discount)
|
||||
discountService.validateDiscountForCartOrThrow(cart, discount_)
|
||||
).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 { EventBusServiceMock } from "../__mocks__/event-bus"
|
||||
import DraftOrderService from "../draft-order"
|
||||
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
@@ -10,30 +11,7 @@ const eventBusService = {
|
||||
}
|
||||
|
||||
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", () => {
|
||||
let result
|
||||
|
||||
const regionService = {
|
||||
retrieve: () =>
|
||||
Promise.resolve({ id: "test-region", countries: [{ iso_2: "dk" }] }),
|
||||
@@ -62,7 +40,7 @@ describe("DraftOrderService", () => {
|
||||
variant_id: "test-variant",
|
||||
})
|
||||
),
|
||||
create: jest.fn(),
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
@@ -86,7 +64,9 @@ describe("DraftOrderService", () => {
|
||||
shipping_address_id: "test-shipping",
|
||||
billing_address_id: "test-billing",
|
||||
customer_id: "test-customer",
|
||||
items: [{ variant_id: "test-variant", quantity: 2, metadata: {} }],
|
||||
items: [
|
||||
{ id: "test", variant_id: "test-variant", quantity: 2, metadata: {} },
|
||||
],
|
||||
shipping_methods: [
|
||||
{
|
||||
option_id: "test-option",
|
||||
@@ -108,6 +88,8 @@ describe("DraftOrderService", () => {
|
||||
...testOrder,
|
||||
})
|
||||
),
|
||||
update: jest.fn(),
|
||||
applyDiscount: jest.fn(),
|
||||
addShippingMethod: jest.fn(),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
@@ -136,6 +118,7 @@ describe("DraftOrderService", () => {
|
||||
cartService,
|
||||
shippingOptionService,
|
||||
lineItemService,
|
||||
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||
productVariantService,
|
||||
draftOrderRepository,
|
||||
addressRepository,
|
||||
@@ -147,11 +130,14 @@ describe("DraftOrderService", () => {
|
||||
})
|
||||
|
||||
it("creates a draft order", async () => {
|
||||
const cartId = "test-cart"
|
||||
const title = "test-item"
|
||||
|
||||
await draftOrderService.create(testOrder)
|
||||
|
||||
expect(draftOrderRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(draftOrderRepository.create).toHaveBeenCalledWith({
|
||||
cart_id: "test-cart",
|
||||
cart_id: cartId,
|
||||
})
|
||||
|
||||
expect(cartService.create).toHaveBeenCalledTimes(1)
|
||||
@@ -163,11 +149,6 @@ describe("DraftOrderService", () => {
|
||||
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).toHaveBeenCalledWith(
|
||||
"test-cart",
|
||||
@@ -183,18 +164,69 @@ describe("DraftOrderService", () => {
|
||||
{
|
||||
metadata: {},
|
||||
unit_price: undefined,
|
||||
cart: expect.objectContaining({
|
||||
id: "test-cart",
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
expect(lineItemService.create).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.create).toHaveBeenCalledWith({
|
||||
cart_id: "test-cart",
|
||||
title: "test-item",
|
||||
cart_id: cartId,
|
||||
title,
|
||||
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 () => {
|
||||
@@ -213,7 +245,7 @@ describe("DraftOrderService", () => {
|
||||
await draftOrderService.create({
|
||||
region_id: "test-region",
|
||||
items: [],
|
||||
shipping_methods: []
|
||||
shipping_methods: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -262,6 +294,7 @@ describe("DraftOrderService", () => {
|
||||
cartService: undefined,
|
||||
shippingOptionService: undefined,
|
||||
lineItemService: undefined,
|
||||
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
|
||||
productVariantService: undefined,
|
||||
draftOrderRepository,
|
||||
addressRepository: undefined,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Customer,
|
||||
CustomShippingOption,
|
||||
Discount,
|
||||
DiscountRule,
|
||||
DiscountRuleType,
|
||||
LineItem,
|
||||
PaymentSession,
|
||||
@@ -1122,10 +1123,9 @@ class CartService extends TransactionBaseService {
|
||||
const previousDiscounts = [...cart.discounts]
|
||||
cart.discounts.length = 0
|
||||
|
||||
await Promise.all(
|
||||
data.discounts.map(async ({ code }) => {
|
||||
return this.applyDiscount(cart, code)
|
||||
})
|
||||
await this.applyDiscounts(
|
||||
cart,
|
||||
data.discounts.map((d) => d.code)
|
||||
)
|
||||
|
||||
const hasFreeShipping = cart.discounts.some(
|
||||
@@ -1410,41 +1410,65 @@ class CartService extends TransactionBaseService {
|
||||
* Throws if discount regions does not include the cart region
|
||||
* @param cart - the cart to update
|
||||
* @param discountCode - the discount code
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
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_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const discount = await this.discountService_
|
||||
const discounts = await this.discountService_
|
||||
.withTransaction(transactionManager)
|
||||
.retrieveByCode(discountCode, { relations: ["rule", "regions"] })
|
||||
.listByCodes(discountCodes, { relations: ["rule", "regions"] })
|
||||
|
||||
await this.discountService_
|
||||
.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
|
||||
if (cart.discounts.find(({ id }) => id === discount.id)) {
|
||||
return
|
||||
}
|
||||
cart.discounts.forEach((discount) => {
|
||||
if (discountsMap.has(discount.id)) {
|
||||
discountsMap.delete(discount.id)
|
||||
}
|
||||
})
|
||||
|
||||
const toParse = [...cart.discounts, discount]
|
||||
const toParse = [...cart.discounts, ...discountsMap.values()]
|
||||
|
||||
let sawNotShipping = false
|
||||
const newDiscounts = toParse.map((discountToParse) => {
|
||||
switch (discountToParse.rule?.type) {
|
||||
case DiscountRuleType.FREE_SHIPPING:
|
||||
if (discountToParse.rule.type === rule.type) {
|
||||
return discount
|
||||
if (
|
||||
discountToParse.rule.type ===
|
||||
rules.get(discountToParse.id)!.type
|
||||
) {
|
||||
return discountsMap.get(discountToParse.id)
|
||||
}
|
||||
return discountToParse
|
||||
default:
|
||||
if (!sawNotShipping) {
|
||||
sawNotShipping = true
|
||||
if (rule?.type !== DiscountRuleType.FREE_SHIPPING) {
|
||||
return discount
|
||||
if (
|
||||
rules.get(discountToParse.id)!.type !==
|
||||
DiscountRuleType.FREE_SHIPPING
|
||||
) {
|
||||
return discountsMap.get(discountToParse.id)
|
||||
}
|
||||
return discountToParse
|
||||
}
|
||||
@@ -1458,8 +1482,11 @@ class CartService extends TransactionBaseService {
|
||||
}
|
||||
)
|
||||
|
||||
// ignore if free shipping
|
||||
if (rule?.type !== DiscountRuleType.FREE_SHIPPING && cart?.items) {
|
||||
const hadNonFreeShippingDiscounts = [...rules.values()].some(
|
||||
(rule) => rule.type !== DiscountRuleType.FREE_SHIPPING
|
||||
)
|
||||
|
||||
if (hadNonFreeShippingDiscounts && cart?.items) {
|
||||
await this.refreshAdjustments_(cart)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
DeepPartial,
|
||||
EntityManager,
|
||||
ILike,
|
||||
In,
|
||||
SelectQueryBuilder,
|
||||
} from "typeorm"
|
||||
import {
|
||||
@@ -277,10 +278,10 @@ class DiscountService extends TransactionBaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a discount by discount code.
|
||||
* @param {string} discountCode - discount code of discount to retrieve
|
||||
* @param {Object} config - the config object containing query settings
|
||||
* @return {Promise<Discount>} the discount document
|
||||
* Gets the discount by discount code.
|
||||
* @param discountCode - discount code of discount to retrieve
|
||||
* @param config - the config object containing query settings
|
||||
* @return the discount
|
||||
*/
|
||||
async retrieveByCode(
|
||||
discountCode: string,
|
||||
@@ -291,24 +292,49 @@ class DiscountService extends TransactionBaseService {
|
||||
|
||||
const normalizedCode = discountCode.toUpperCase().trim()
|
||||
|
||||
let query = buildQuery({ code: normalizedCode, is_dynamic: false }, config)
|
||||
let discount = await discountRepo.findOne(query)
|
||||
const query = buildQuery({ code: normalizedCode }, config)
|
||||
const discount = await discountRepo.findOne(query)
|
||||
|
||||
if (!discount) {
|
||||
query = buildQuery({ code: normalizedCode, is_dynamic: true }, config)
|
||||
discount = await discountRepo.findOne(query)
|
||||
|
||||
if (!discount) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Discount with code ${discountCode} was not found`
|
||||
)
|
||||
}
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Discounts with code ${discountCode} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
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.
|
||||
* @param {string} discountId - discount id of discount to update
|
||||
@@ -660,61 +686,66 @@ class DiscountService extends TransactionBaseService {
|
||||
|
||||
async validateDiscountForCartOrThrow(
|
||||
cart: Cart,
|
||||
discount: Discount
|
||||
discount: Discount | Discount[]
|
||||
): Promise<void> {
|
||||
const discounts = Array.isArray(discount) ? discount : [discount]
|
||||
return await this.atomicPhase_(async () => {
|
||||
if (this.hasReachedLimit(discount)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Discount has been used maximum allowed times"
|
||||
)
|
||||
}
|
||||
await Promise.all(
|
||||
discounts.map(async (disc) => {
|
||||
if (this.hasReachedLimit(disc)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Discount ${disc.code} has been used maximum allowed times`
|
||||
)
|
||||
}
|
||||
|
||||
if (this.hasNotStarted(discount)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Discount is not valid yet"
|
||||
)
|
||||
}
|
||||
if (this.hasNotStarted(disc)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Discount ${disc.code} is not valid yet`
|
||||
)
|
||||
}
|
||||
|
||||
if (this.hasExpired(discount)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Discount is expired"
|
||||
)
|
||||
}
|
||||
if (this.hasExpired(disc)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Discount ${disc.code} is expired`
|
||||
)
|
||||
}
|
||||
|
||||
if (this.isDisabled(discount)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"The discount code is disabled"
|
||||
)
|
||||
}
|
||||
if (this.isDisabled(disc)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`The discount code ${disc.code} is disabled`
|
||||
)
|
||||
}
|
||||
|
||||
const isValidForRegion = await this.isValidForRegion(
|
||||
discount,
|
||||
cart.region_id
|
||||
)
|
||||
if (!isValidForRegion) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"The discount is not available in current region"
|
||||
)
|
||||
}
|
||||
|
||||
if (cart.customer_id) {
|
||||
const canApplyForCustomer = await this.canApplyForCustomer(
|
||||
discount.rule.id,
|
||||
cart.customer_id
|
||||
)
|
||||
|
||||
if (!canApplyForCustomer) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Discount is not valid for customer"
|
||||
const isValidForRegion = await this.isValidForRegion(
|
||||
disc,
|
||||
cart.region_id
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!isValidForRegion) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"The discount is not available in current region"
|
||||
)
|
||||
}
|
||||
|
||||
if (cart.customer_id) {
|
||||
const canApplyForCustomer = await this.canApplyForCustomer(
|
||||
disc.rule.id,
|
||||
cart.customer_id
|
||||
)
|
||||
|
||||
if (!canApplyForCustomer) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Discount ${disc.code} is not valid for customer`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { Brackets, EntityManager, FindManyOptions, UpdateResult } from "typeorm"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { Cart, CartType, DraftOrder, DraftOrderStatus } from "../models"
|
||||
import { CartType, DraftOrder, DraftOrderStatus } from "../models"
|
||||
import { DraftOrderRepository } from "../repositories/draft-order"
|
||||
import { OrderRepository } from "../repositories/order"
|
||||
import { PaymentRepository } from "../repositories/payment"
|
||||
@@ -264,34 +264,24 @@ class DraftOrderService extends TransactionBaseService {
|
||||
no_notification_order,
|
||||
items,
|
||||
idempotency_key,
|
||||
discounts,
|
||||
...rawCart
|
||||
} = data
|
||||
|
||||
const cartServiceTx =
|
||||
this.cartService_.withTransaction(transactionManager)
|
||||
|
||||
if (rawCart.discounts) {
|
||||
const { discounts } = rawCart
|
||||
rawCart.discounts = []
|
||||
|
||||
for (const { code } of discounts) {
|
||||
await cartServiceTx.applyDiscount(rawCart as Cart, code)
|
||||
}
|
||||
}
|
||||
|
||||
let createdCart = await cartServiceTx.create({
|
||||
const createdCart = await cartServiceTx.create({
|
||||
type: CartType.DRAFT_ORDER,
|
||||
...rawCart,
|
||||
})
|
||||
createdCart = await cartServiceTx.retrieve(createdCart.id, {
|
||||
relations: ["discounts", "discounts.rule", "items", "region"],
|
||||
})
|
||||
|
||||
const draftOrder = draftOrderRepo.create({
|
||||
cart_id: createdCart.id,
|
||||
no_notification_order,
|
||||
idempotency_key,
|
||||
})
|
||||
|
||||
const result = await draftOrderRepo.save(draftOrder)
|
||||
|
||||
await this.eventBus_
|
||||
@@ -303,7 +293,7 @@ class DraftOrderService extends TransactionBaseService {
|
||||
const lineItemServiceTx =
|
||||
this.lineItemService_.withTransaction(transactionManager)
|
||||
|
||||
for (const item of (items || [])) {
|
||||
for (const item of items || []) {
|
||||
if (item.variant_id) {
|
||||
const line = await lineItemServiceTx.generate(
|
||||
item.variant_id,
|
||||
@@ -312,7 +302,6 @@ class DraftOrderService extends TransactionBaseService {
|
||||
{
|
||||
metadata: item?.metadata || {},
|
||||
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) {
|
||||
if (typeof method.price !== "undefined") {
|
||||
await this.customShippingOptionService_
|
||||
|
||||
@@ -367,7 +367,7 @@ class LineItemService extends TransactionBaseService {
|
||||
*/
|
||||
async create<
|
||||
T = LineItem | LineItem[],
|
||||
TResult = T extends LineItem ? LineItem : LineItem[]
|
||||
TResult = T extends LineItem[] ? LineItem[] : LineItem
|
||||
>(data: T): Promise<TResult> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
|
||||
Reference in New Issue
Block a user