Fix/polish discount (#569)

* formatting and optional "is_dynamic" on updates

* formatting

* uppercasing discount code on update

* multiple regions fail with fixed discounts

* discount testing

* testing

* spelling

* removed comment

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>
This commit is contained in:
Philip Korsholm
2021-10-26 13:20:38 +02:00
committed by GitHub
co-authored by Sebastian Rindom
parent 9f1ae87605
commit 68c2a4fe3a
7 changed files with 408 additions and 29 deletions
@@ -5,6 +5,8 @@ const setupServer = require("../../../helpers/setup-server")
const { useApi } = require("../../../helpers/use-api") const { useApi } = require("../../../helpers/use-api")
const { initDb, useDb } = require("../../../helpers/use-db") const { initDb, useDb } = require("../../../helpers/use-db")
const adminSeeder = require("../../helpers/admin-seeder") const adminSeeder = require("../../helpers/admin-seeder")
const discountSeeder = require("../../helpers/discount-seeder")
const { exportAllDeclaration } = require("@babel/types")
jest.setTimeout(30000) jest.setTimeout(30000)
@@ -89,6 +91,7 @@ describe("/admin/discounts", () => {
beforeEach(async () => { beforeEach(async () => {
try { try {
await adminSeeder(dbConnection) await adminSeeder(dbConnection)
await discountSeeder(dbConnection)
} catch (err) { } catch (err) {
console.log(err) console.log(err)
throw err throw err
@@ -159,6 +162,256 @@ describe("/admin/discounts", () => {
) )
}) })
it("automatically sets the code to an uppercase string on update", async () => {
const api = useApi()
const response = await api
.post(
"/admin/discounts",
{
code: "HELLOworld",
rule: {
description: "test",
type: "percentage",
value: 10,
allocation: "total",
},
usage_limit: 10,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.discount).toEqual(
expect.objectContaining({
code: "HELLOWORLD",
usage_limit: 10,
})
)
const updated = await api
.post(
`/admin/discounts/${response.data.discount.id}`,
{
code: "HELLOWORLD_test",
usage_limit: 20,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
expect(updated.status).toEqual(200)
expect(updated.data.discount).toEqual(
expect.objectContaining({
code: "HELLOWORLD_TEST",
usage_limit: 20,
})
)
})
it("creates a dynamic discount and updates it", async () => {
const api = useApi()
const response = await api
.post(
"/admin/discounts",
{
code: "HELLOWORLD_DYNAMIC",
is_dynamic: true,
rule: {
description: "test",
type: "percentage",
value: 10,
allocation: "total",
},
usage_limit: 10,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.discount).toEqual(
expect.objectContaining({
code: "HELLOWORLD_DYNAMIC",
usage_limit: 10,
is_dynamic: true,
})
)
const updated = await api
.post(
`/admin/discounts/${response.data.discount.id}`,
{
usage_limit: 20,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
expect(updated.status).toEqual(200)
expect(updated.data.discount).toEqual(
expect.objectContaining({
code: "HELLOWORLD_DYNAMIC",
usage_limit: 20,
is_dynamic: true,
})
)
})
it("fails to create a fixed discount with multiple regions", async () => {
expect.assertions(2)
const api = useApi()
await api
.post(
"/admin/discounts",
{
code: "HELLOWORLD",
is_dynamic: true,
rule: {
description: "test",
type: "fixed",
value: 10,
allocation: "total",
},
usage_limit: 10,
regions: ["test-region", "test-region-2"],
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
expect(err.response.status).toEqual(400)
expect(err.response.data.message).toEqual(
`Fixed discounts can have one region`
)
})
})
it("fails to update a fixed discount with multiple regions", async () => {
expect.assertions(2)
const api = useApi()
const response = await api
.post(
"/admin/discounts",
{
code: "HELLOWORLD",
rule: {
description: "test",
type: "fixed",
value: 10,
allocation: "total",
},
usage_limit: 10,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
await api
.post(
`/admin/discounts/${response.data.discount.id}`,
{
regions: ["test-region", "test-region-2"],
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
expect(err.response.status).toEqual(400)
expect(err.response.data.message).toEqual(
`Fixed discounts can have one region`
)
})
})
it("fails to add a region to a fixed discount with an existing region", async () => {
expect.assertions(2)
const api = useApi()
const response = await api
.post(
"/admin/discounts",
{
code: "HELLOWORLD",
rule: {
description: "test",
type: "fixed",
value: 10,
allocation: "total",
},
usage_limit: 10,
regions: ["test-region"],
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
await api
.post(
`/admin/discounts/${response.data.discount.id}/regions/test-region-2`,
{},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
expect(err.response.status).toEqual(400)
expect(err.response.data.message).toEqual(
`Fixed discounts can have one region`
)
})
})
it("creates a discount with start and end dates", async () => { it("creates a discount with start and end dates", async () => {
const api = useApi() const api = useApi()
@@ -0,0 +1,35 @@
const {
ShippingProfile,
Region,
Discount,
DiscountRule,
} = require("@medusajs/medusa")
module.exports = async (connection, data = {}) => {
const manager = connection.manager
await manager.insert(Region, {
id: "test-region",
name: "Test Region",
currency_code: "usd",
tax_rate: 0,
payment_providers: [
{
id: "test-pay",
is_installed: true,
},
],
})
await manager.insert(Region, {
id: "test-region-2",
name: "Test Region 2",
currency_code: "eur",
tax_rate: 0,
payment_providers: [
{
id: "test-pay",
is_installed: true,
},
],
})
}
@@ -46,7 +46,6 @@ describe("POST /admin/discounts", () => {
value: 10, value: 10,
allocation: "total", allocation: "total",
}, },
is_dynamic: false,
} }
) )
}) })
@@ -55,7 +55,7 @@ export default async (req, res) => {
const { discount_id } = req.params const { discount_id } = req.params
const schema = Validator.object().keys({ const schema = Validator.object().keys({
code: Validator.string().optional(), code: Validator.string().optional(),
is_dynamic: Validator.boolean().default(false), is_dynamic: Validator.boolean().optional(),
rule: Validator.object() rule: Validator.object()
.keys({ .keys({
id: Validator.string().required(), id: Validator.string().required(),
@@ -86,7 +86,6 @@ export default async (req, res) => {
const discountService = req.scope.resolve("discountService") const discountService = req.scope.resolve("discountService")
await discountService.update(discount_id, value) await discountService.update(discount_id, value)
const discount = await discountService.retrieve(discount_id, { const discount = await discountService.retrieve(discount_id, {
select: defaultFields, select: defaultFields,
relations: defaultRelations, relations: defaultRelations,
@@ -1,5 +1,7 @@
import DiscountService from "../discount" import DiscountService from "../discount"
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { MedusaError } from "medusa-core-utils"
import { exportAllDeclaration } from "@babel/types"
describe("DiscountService", () => { describe("DiscountService", () => {
describe("create", () => { describe("create", () => {
@@ -13,7 +15,7 @@ describe("DiscountService", () => {
id: IdMap.getId("france"), id: IdMap.getId("france"),
} }
}, },
withTransaction: function() { withTransaction: function () {
return this return this
}, },
} }
@@ -29,6 +31,25 @@ describe("DiscountService", () => {
jest.clearAllMocks() jest.clearAllMocks()
}) })
it("fails to create a fixed discount with multiple regions", async () => {
expect.assertions(3)
try {
await discountService.create({
code: "test",
rule: {
type: "fixed",
allocation: "total",
value: 20,
},
regions: [IdMap.getId("france"), IdMap.getId("Italy")],
})
} catch (err) {
expect(err.type).toEqual("invalid_data")
expect(err.message).toEqual("Fixed discounts can have one region")
expect(discountRepository.create).toHaveBeenCalledTimes(0)
}
})
it("successfully creates discount", async () => { it("successfully creates discount", async () => {
await discountService.create({ await discountService.create({
code: "test", code: "test",
@@ -130,7 +151,7 @@ describe("DiscountService", () => {
describe("retrieve", () => { describe("retrieve", () => {
const discountRepository = MockRepository({ const discountRepository = MockRepository({
findOne: query => { findOne: (query) => {
if (query.where.id) { if (query.where.id) {
return Promise.resolve({ id: IdMap.getId("total10") }) return Promise.resolve({ id: IdMap.getId("total10") })
} }
@@ -170,7 +191,7 @@ describe("DiscountService", () => {
describe("retrieveByCode", () => { describe("retrieveByCode", () => {
const discountRepository = MockRepository({ const discountRepository = MockRepository({
findOne: query => { findOne: (query) => {
if (query.where.code === "10%OFF") { if (query.where.code === "10%OFF") {
return Promise.resolve({ id: IdMap.getId("total10"), code: "10%OFF" }) return Promise.resolve({ id: IdMap.getId("total10"), code: "10%OFF" })
} }
@@ -206,7 +227,11 @@ describe("DiscountService", () => {
describe("update", () => { describe("update", () => {
const discountRepository = MockRepository({ const discountRepository = MockRepository({
findOne: () => findOne: () =>
Promise.resolve({ id: IdMap.getId("total10"), code: "10%OFF" }), Promise.resolve({
id: IdMap.getId("total10"),
code: "10%OFF",
rule: { type: "fixed" },
}),
}) })
const discountRuleRepository = MockRepository({}) const discountRuleRepository = MockRepository({})
@@ -230,6 +255,20 @@ describe("DiscountService", () => {
jest.clearAllMocks() jest.clearAllMocks()
}) })
it("fails to update a fixed discount with multiple regions", async () => {
expect.assertions(3)
try {
await discountService.update(IdMap.getId("total10"), {
code: "test",
regions: [IdMap.getId("france"), IdMap.getId("Italy")],
})
} catch (err) {
expect(err.type).toEqual("invalid_data")
expect(err.message).toEqual("Fixed discounts can have one region")
expect(discountRepository.create).toHaveBeenCalledTimes(0)
}
})
it("successfully updates discount", async () => { it("successfully updates discount", async () => {
await discountService.update(IdMap.getId("total10"), { await discountService.update(IdMap.getId("total10"), {
code: "test", code: "test",
@@ -238,7 +277,8 @@ describe("DiscountService", () => {
expect(discountRepository.save).toHaveBeenCalledTimes(1) expect(discountRepository.save).toHaveBeenCalledTimes(1)
expect(discountRepository.save).toHaveBeenCalledWith({ expect(discountRepository.save).toHaveBeenCalledWith({
id: IdMap.getId("total10"), id: IdMap.getId("total10"),
code: "test", code: "TEST",
rule: { type: "fixed" },
regions: [{ id: IdMap.getId("france") }], regions: [{ id: IdMap.getId("france") }],
}) })
}) })
@@ -262,6 +302,7 @@ describe("DiscountService", () => {
expect(discountRepository.save).toHaveBeenCalledTimes(1) expect(discountRepository.save).toHaveBeenCalledTimes(1)
expect(discountRepository.save).toHaveBeenCalledWith({ expect(discountRepository.save).toHaveBeenCalledWith({
id: IdMap.getId("total10"), id: IdMap.getId("total10"),
rule: { type: "fixed" },
code: "10%OFF", code: "10%OFF",
metadata: { testKey: "testValue" }, metadata: { testKey: "testValue" },
}) })
@@ -385,11 +426,24 @@ describe("DiscountService", () => {
describe("addRegion", () => { describe("addRegion", () => {
const discountRepository = MockRepository({ const discountRepository = MockRepository({
findOne: () => findOne: (q) => {
Promise.resolve({ if (q.where.id === "fixed") {
return Promise.resolve({
id: IdMap.getId("total10"),
regions: [{ id: IdMap.getId("test-region") }],
rule: {
type: "fixed",
},
})
}
return Promise.resolve({
id: IdMap.getId("total10"), id: IdMap.getId("total10"),
regions: [{ id: IdMap.getId("test-region") }], regions: [{ id: IdMap.getId("test-region") }],
}), rule: {
type: "percentage",
},
})
},
}) })
const discountRuleRepository = MockRepository({}) const discountRuleRepository = MockRepository({})
@@ -413,6 +467,17 @@ describe("DiscountService", () => {
jest.clearAllMocks() jest.clearAllMocks()
}) })
it("fails to add a region to a fixed discount with an existing region", async () => {
expect.assertions(3)
try {
await discountService.addRegion("fixed", IdMap.getId("test-region-2"))
} catch (err) {
expect(err.type).toEqual("invalid_data")
expect(err.message).toEqual("Fixed discounts can have one region")
expect(discountRepository.save).toHaveBeenCalledTimes(0)
}
})
it("successfully adds a region", async () => { it("successfully adds a region", async () => {
await discountService.addRegion( await discountService.addRegion(
IdMap.getId("total10"), IdMap.getId("total10"),
@@ -426,6 +491,9 @@ describe("DiscountService", () => {
{ id: IdMap.getId("test-region") }, { id: IdMap.getId("test-region") },
{ id: IdMap.getId("test-region-2") }, { id: IdMap.getId("test-region-2") },
], ],
rule: {
type: "percentage",
},
}) })
}) })
@@ -441,7 +509,7 @@ describe("DiscountService", () => {
describe("createDynamicDiscount", () => { describe("createDynamicDiscount", () => {
const discountRepository = MockRepository({ const discountRepository = MockRepository({
create: d => d, create: (d) => d,
findOne: () => findOne: () =>
Promise.resolve({ Promise.resolve({
id: "parent", id: "parent",
@@ -4,7 +4,7 @@ import ProductVariantService from "../product-variant"
const eventBusService = { const eventBusService = {
emit: jest.fn(), emit: jest.fn(),
withTransaction: function() { withTransaction: function () {
return this return this
}, },
} }
@@ -12,7 +12,7 @@ const eventBusService = {
describe("ProductVariantService", () => { describe("ProductVariantService", () => {
describe("retrieve", () => { describe("retrieve", () => {
const productVariantRepository = MockRepository({ const productVariantRepository = MockRepository({
findOne: query => { findOne: (query) => {
if (query.where.id === IdMap.getId("batman")) { if (query.where.id === IdMap.getId("batman")) {
return Promise.resolve(undefined) return Promise.resolve(undefined)
} }
@@ -54,13 +54,13 @@ describe("ProductVariantService", () => {
describe("create", () => { describe("create", () => {
const productVariantRepository = MockRepository({ const productVariantRepository = MockRepository({
findOne: query => { findOne: (query) => {
return Promise.resolve() return Promise.resolve()
}, },
}) })
const productRepository = MockRepository({ const productRepository = MockRepository({
findOne: query => { findOne: (query) => {
if (query.where.id === IdMap.getId("ironmans")) { if (query.where.id === IdMap.getId("ironmans")) {
return Promise.resolve({ return Promise.resolve({
id: IdMap.getId("ironman"), id: IdMap.getId("ironman"),
@@ -229,7 +229,7 @@ describe("ProductVariantService", () => {
describe("publishVariant", () => { describe("publishVariant", () => {
const productVariantRepository = MockRepository({ const productVariantRepository = MockRepository({
findOne: query => Promise.resolve({ id: IdMap.getId("ironman") }), findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }),
}) })
const productVariantService = new ProductVariantService({ const productVariantService = new ProductVariantService({
@@ -268,7 +268,7 @@ describe("ProductVariantService", () => {
describe("update", () => { describe("update", () => {
const productVariantRepository = MockRepository({ const productVariantRepository = MockRepository({
findOne: query => Promise.resolve({ id: IdMap.getId("ironman") }), findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }),
}) })
const moneyAmountRepository = MockRepository({ const moneyAmountRepository = MockRepository({
@@ -457,11 +457,11 @@ describe("ProductVariantService", () => {
describe("setCurrencyPrice", () => { describe("setCurrencyPrice", () => {
const productVariantRepository = MockRepository({ const productVariantRepository = MockRepository({
findOne: query => Promise.resolve({ id: IdMap.getId("ironman") }), findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }),
}) })
const moneyAmountRepository = MockRepository({ const moneyAmountRepository = MockRepository({
findOne: query => { findOne: (query) => {
if (query.where.currency_code === "usd") { if (query.where.currency_code === "usd") {
return Promise.resolve(undefined) return Promise.resolve(undefined)
} }
@@ -521,18 +521,18 @@ describe("ProductVariantService", () => {
describe("getRegionPrice", () => { describe("getRegionPrice", () => {
const regionService = { const regionService = {
retrieve: function() { retrieve: function () {
return Promise.resolve({ return Promise.resolve({
id: IdMap.getId("california"), id: IdMap.getId("california"),
name: "California", name: "California",
}) })
}, },
withTransaction: function() { withTransaction: function () {
return this return this
}, },
} }
const moneyAmountRepository = MockRepository({ const moneyAmountRepository = MockRepository({
findOne: query => { findOne: (query) => {
if (query.where.variant_id === IdMap.getId("ironmanv2")) { if (query.where.variant_id === IdMap.getId("ironmanv2")) {
return Promise.resolve(undefined) return Promise.resolve(undefined)
} }
@@ -601,7 +601,7 @@ describe("ProductVariantService", () => {
describe("setRegionPrice", () => { describe("setRegionPrice", () => {
const moneyAmountRepository = MockRepository({ const moneyAmountRepository = MockRepository({
findOne: query => { findOne: (query) => {
if (query.where.region_id === IdMap.getId("cali")) { if (query.where.region_id === IdMap.getId("cali")) {
return Promise.resolve(undefined) return Promise.resolve(undefined)
} }
@@ -665,7 +665,7 @@ describe("ProductVariantService", () => {
describe("updateOptionValue", () => { describe("updateOptionValue", () => {
const productOptionValueRepository = MockRepository({ const productOptionValueRepository = MockRepository({
findOne: query => { findOne: (query) => {
if (query.where.variant_id === IdMap.getId("jibberish")) { if (query.where.variant_id === IdMap.getId("jibberish")) {
return Promise.resolve(undefined) return Promise.resolve(undefined)
} }
@@ -748,7 +748,7 @@ describe("ProductVariantService", () => {
describe("deleteOptionValue", () => { describe("deleteOptionValue", () => {
const productOptionValueRepository = MockRepository({ const productOptionValueRepository = MockRepository({
findOne: query => { findOne: (query) => {
if (query.where.option_id === IdMap.getId("size")) { if (query.where.option_id === IdMap.getId("size")) {
return Promise.resolve(undefined) return Promise.resolve(undefined)
} }
@@ -796,7 +796,7 @@ describe("ProductVariantService", () => {
describe("delete", () => { describe("delete", () => {
const productVariantRepository = MockRepository({ const productVariantRepository = MockRepository({
findOne: query => { findOne: (query) => {
if (query.where.id === IdMap.getId("ironmanv2")) { if (query.where.id === IdMap.getId("ironmanv2")) {
return Promise.resolve(undefined) return Promise.resolve(undefined)
} }
+27 -2
View File
@@ -176,6 +176,13 @@ class DiscountService extends BaseService {
const validatedRule = this.validateDiscountRule_(discount.rule) const validatedRule = this.validateDiscountRule_(discount.rule)
if (discount.regions?.length > 1 && discount.rule.type === "fixed") {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Fixed discounts can have one region"
)
}
if (discount.regions) { if (discount.regions) {
discount.regions = await Promise.all( discount.regions = await Promise.all(
discount.regions.map((regionId) => discount.regions.map((regionId) =>
@@ -264,7 +271,9 @@ class DiscountService extends BaseService {
return this.atomicPhase_(async (manager) => { return this.atomicPhase_(async (manager) => {
const discountRepo = manager.getCustomRepository(this.discountRepository_) const discountRepo = manager.getCustomRepository(this.discountRepository_)
const discount = await this.retrieve(discountId) const discount = await this.retrieve(discountId, {
relations: ["rule"],
})
const { rule, metadata, regions, ...rest } = update const { rule, metadata, regions, ...rest } = update
@@ -277,6 +286,13 @@ class DiscountService extends BaseService {
} }
} }
if (regions?.length > 1 && discount.rule.type === "fixed") {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Fixed discounts can have one region"
)
}
if (regions) { if (regions) {
discount.regions = await Promise.all( discount.regions = await Promise.all(
regions.map((regionId) => this.regionService_.retrieve(regionId)) regions.map((regionId) => this.regionService_.retrieve(regionId))
@@ -300,6 +316,8 @@ class DiscountService extends BaseService {
discount[key] = value discount[key] = value
} }
discount.code = discount.code.toUpperCase()
const updated = await discountRepo.save(discount) const updated = await discountRepo.save(discount)
return updated return updated
}) })
@@ -452,7 +470,7 @@ class DiscountService extends BaseService {
const discountRepo = manager.getCustomRepository(this.discountRepository_) const discountRepo = manager.getCustomRepository(this.discountRepository_)
const discount = await this.retrieve(discountId, { const discount = await this.retrieve(discountId, {
relations: ["regions"], relations: ["regions", "rule"],
}) })
const exists = discount.regions.find((r) => r.id === regionId) const exists = discount.regions.find((r) => r.id === regionId)
@@ -461,6 +479,13 @@ class DiscountService extends BaseService {
return discount return discount
} }
if (discount.regions?.length === 1 && discount.rule.type === "fixed") {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Fixed discounts can have one region"
)
}
const region = await this.regionService_.retrieve(regionId) const region = await this.regionService_.retrieve(regionId)
discount.regions = [...discount.regions, region] discount.regions = [...discount.regions, region]