const path = require("path") const { DiscountRule, Discount, Customer, CustomerGroup, } = require("@medusajs/medusa") const setupServer = require("../../../environment-helpers/setup-server") const { useApi } = require("../../../environment-helpers/use-api") const { initDb, useDb } = require("../../../environment-helpers/use-db") const adminSeeder = require("../../../helpers/admin-seeder") const discountSeeder = require("../../../helpers/discount-seeder") const { simpleProductFactory } = require("../../../factories") const { simpleDiscountFactory, } = require("../../../factories/simple-discount-factory") jest.setTimeout(30000) const adminReqConfig = { headers: { "x-medusa-access-token": "test_token", }, } const validRegionId = "test-region" const invalidRegionId = "not-a-valid-region" describe("/admin/discounts", () => { let medusaProcess let dbConnection beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) dbConnection = await initDb({ cwd }) medusaProcess = await setupServer({ cwd }) }) afterAll(async () => { const db = useDb() await db.shutdown() medusaProcess.kill() }) describe("GET /admin/discounts/:id", () => { beforeEach(async () => { const manager = dbConnection.manager await adminSeeder(dbConnection) await manager.insert(DiscountRule, { id: "test-discount-rule-fixed", description: "Test discount rule", type: "fixed", value: 10, allocation: "total", }) const prod = await simpleProductFactory(dbConnection, { type: "pants" }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { type: "products", operator: "in", products: [prod.id], }, { type: "product_types", operator: "not_in", product_types: [prod.type_id], }, ], }, }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("should retrieve discount with customer conditions created with factory", async () => { const api = useApi() const group = await dbConnection.manager.insert(CustomerGroup, { id: "customer-group-1", name: "vip-customers", }) await dbConnection.manager.insert(Customer, { id: "cus_1234", email: "oli@email.com", groups: [group], }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { type: "customer_groups", operator: "in", customer_groups: ["customer-group-1"], }, ], }, }) const response = await api.get( "/admin/discounts/test-discount?expand=rule,rule.conditions,rule.conditions.customer_groups", adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc).toEqual( expect.objectContaining({ id: "test-discount", code: "TEST", }) ) expect(disc.rule.conditions).toEqual( expect.arrayContaining([ expect.objectContaining({ type: "customer_groups", operator: "in", discount_rule_id: disc.rule.id, }), ]) ) }) it("should retrieve discount and only select the id field and retrieve the relation parent_discount", async () => { const api = useApi() const group = await dbConnection.manager.insert(CustomerGroup, { id: "customer-group-1", name: "vip-customers", }) await dbConnection.manager.insert(Customer, { id: "cus_1234", email: "oli@email.com", groups: [group], }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { type: "customer_groups", operator: "in", customer_groups: ["customer-group-1"], }, ], }, }) const response = await api.get( "/admin/discounts/test-discount?fields=id&expand=parent_discount", adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc).toEqual({ id: "test-discount", parent_discount: null, }) }) it("should retrieve discount with product conditions created with factory", async () => { const api = useApi() const response = await api.get( "/admin/discounts/test-discount?expand=rule,rule.conditions,rule.conditions.products,rule.conditions.product_types", adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc).toEqual( expect.objectContaining({ id: "test-discount", code: "TEST", }) ) expect(disc.rule.conditions).toEqual( expect.arrayContaining([ expect.objectContaining({ type: "products", operator: "in", discount_rule_id: disc.rule.id, }), expect.objectContaining({ type: "product_types", operator: "not_in", discount_rule_id: disc.rule.id, }), ]) ) }) }) describe("GET /admin/discounts", () => { beforeEach(async () => { const manager = dbConnection.manager await adminSeeder(dbConnection) await manager.insert(DiscountRule, { id: "test-discount-rule", description: "Test discount rule", type: "percentage", value: 10, allocation: "total", }) await manager.insert(DiscountRule, { id: "test-discount-rule-fixed", description: "Test discount rule", type: "fixed", value: 10, allocation: "total", }) await manager.insert(Discount, { id: "test-discount", code: "TESTING", rule_id: "test-discount-rule", is_dynamic: false, is_disabled: false, }) await manager.insert(Discount, { id: "messi-discount", code: "BARCA100", rule_id: "test-discount-rule", is_dynamic: false, is_disabled: false, }) await manager.insert(Discount, { id: "dynamic-discount", code: "Dyn100", rule_id: "test-discount-rule", is_dynamic: true, is_disabled: false, }) await manager.insert(Discount, { id: "disabled-discount", code: "Dis100", rule_id: "test-discount-rule", is_dynamic: false, is_disabled: true, }) await manager.insert(Discount, { id: "fixed-discount", code: "fixed100", rule_id: "test-discount-rule-fixed", is_dynamic: false, is_disabled: false, }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("should list discounts that match a specific query in a case insensitive manner", async () => { const api = useApi() const response = await api.get("/admin/discounts?q=barca", adminReqConfig) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) expect(response.data.discounts).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "messi-discount", code: "BARCA100", }), ]) ) }) it("lists fixed discounts", async () => { const api = useApi() const response = await api.get( "/admin/discounts?rule[type]=fixed", adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) expect(response.data.discounts).toHaveLength(1) expect(response.data.discounts).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "fixed-discount", code: "fixed100", }), ]) ) }) it("fails when listing invalid discount types", async () => { expect.assertions(3) const api = useApi() await api .get("/admin/discounts?rule[type]=blah", adminReqConfig) .catch((err) => { expect(err.response.status).toEqual(400) expect(err.response.data.type).toEqual("invalid_data") expect(err.response.data.message).toEqual( "type must be one of the following values: fixed, percentage, free_shipping" ) }) }) it("lists percentage discounts ", async () => { const api = useApi() const notExpected = expect.objectContaining({ rule: expect.objectContaining({ type: "fixed" }), }) const response = await api.get( "/admin/discounts?rule[type]=percentage", adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discounts).toEqual( expect.not.arrayContaining([notExpected]) ) }) it("lists dynamic discounts ", async () => { const api = useApi() const response = await api.get( "/admin/discounts?is_dynamic=true", adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) expect(response.data.discounts).toHaveLength(1) expect(response.data.discounts).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "dynamic-discount", code: "Dyn100", }), ]) ) }) it("lists disabled discounts ", async () => { const api = useApi() const response = await api.get( "/admin/discounts?is_disabled=true", adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) expect(response.data.discounts).toHaveLength(1) expect(response.data.discounts).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "disabled-discount", code: "Dis100", }), ]) ) }) }) describe("POST /admin/discounts", () => { beforeEach(async () => { await adminSeeder(dbConnection) await discountSeeder(dbConnection) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("creates a discount with a rule", async () => { const api = useApi() const response = await api.post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( expect.objectContaining({ code: "HELLOWORLD", usage_limit: 10, }) ) }) it("creates a discount with conditions", async () => { const api = useApi() const product = await simpleProductFactory(dbConnection, { type: "pants", tags: ["ss22"], }) const anotherProduct = await simpleProductFactory(dbConnection, { type: "blouses", tags: ["ss23"], }) const response = await api.post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", conditions: [ { products: [product.id], operator: "in", }, { products: [anotherProduct.id], operator: "not_in", }, { product_types: [product.type_id], operator: "not_in", }, { product_types: [anotherProduct.type_id], operator: "in", }, { product_tags: [product.tags[0].id], operator: "not_in", }, { product_tags: [anotherProduct.tags[0].id], operator: "in", }, ], }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discount.rule.conditions).toEqual( expect.arrayContaining([ expect.objectContaining({ type: "products", operator: "in", }), expect.objectContaining({ type: "products", operator: "not_in", }), expect.objectContaining({ type: "product_types", operator: "not_in", }), expect.objectContaining({ type: "product_types", operator: "in", }), expect.objectContaining({ type: "product_tags", operator: "not_in", }), expect.objectContaining({ type: "product_tags", operator: "in", }), ]) ) }) it("creates a discount with conditions and updates said conditions", async () => { const api = useApi() const product = await simpleProductFactory(dbConnection, { type: "pants", }) const anotherProduct = await simpleProductFactory(dbConnection, { type: "pants", }) const response = await api.post( "/admin/discounts?expand=rule,rule.conditions", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", conditions: [ { products: [product.id], operator: "in", }, { product_types: [product.type_id], operator: "not_in", }, ], }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discount.rule.conditions).toHaveLength(2) expect(response.data.discount.rule.conditions).toEqual( expect.arrayContaining([ expect.objectContaining({ type: "products", operator: "in", }), expect.objectContaining({ type: "product_types", operator: "not_in", }), ]) ) const createdRule = response.data.discount.rule const condsToUpdate = createdRule.conditions[0] const updated = await api.post( `/admin/discounts/${response.data.discount.id}?expand=rule,rule.conditions,rule.conditions.products`, { rule: { id: createdRule.id, value: createdRule.value, allocation: createdRule.allocation, conditions: [ { id: condsToUpdate.id, operator: "not_in", products: [product.id, anotherProduct.id], }, ], }, regions: [validRegionId], }, adminReqConfig ) expect(updated.status).toEqual(200) expect(updated.data.discount.rule.conditions).toEqual( expect.arrayContaining([ expect.objectContaining({ type: "products", operator: "not_in", products: expect.arrayContaining([ expect.objectContaining({ id: product.id, }), expect.objectContaining({ id: anotherProduct.id, }), ]), }), expect.objectContaining({ type: "product_types", operator: "not_in", }), ]) ) }) it("fails to add condition on rule with existing comb. of type and operator", async () => { const api = useApi() const product = await simpleProductFactory(dbConnection, { type: "pants", }) const anotherProduct = await simpleProductFactory(dbConnection, { type: "pants", }) const response = await api.post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", conditions: [ { products: [product.id], operator: "in", }, ], }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) expect(response.status).toEqual(200) const createdRule = response.data.discount.rule await expect( api .post( `/admin/discounts/${response.data.discount.id}?expand=rule,rule.conditions,rule.conditions.products`, { rule: { id: createdRule.id, value: createdRule.value, conditions: [ { products: [anotherProduct.id], operator: "in", }, ], }, regions: [validRegionId], }, { headers: { "x-medusa-access-token": "test_token", }, } ) .catch((err) => err.response.data) ).resolves.toMatchSnapshot() }) it("fails if multiple types of resources are provided on create", async () => { const api = useApi() const product = await simpleProductFactory(dbConnection, { type: "pants", }) try { await api.post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", conditions: [ { products: [product.id], product_types: [product.type_id], operator: "in", }, ], }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) } catch (error) { expect(error.response.data.type).toEqual("invalid_data") expect(error.response.data.message).toEqual( "Only one of products, product_types is allowed, Only one of product_types, products is allowed" ) } }) it("fails if multiple types of resources are provided on update", async () => { const api = useApi() const product = await simpleProductFactory(dbConnection, { type: "pants", }) const anotherProduct = await simpleProductFactory(dbConnection, { type: "pants", }) const response = await api.post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", conditions: [ { products: [product.id], operator: "in", }, ], }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) expect(response.status).toEqual(200) const createdRule = response.data.discount.rule await expect( api .post( `/admin/discounts/${response.data.discount.id}?expand=rule,rule.conditions,rule.conditions.products`, { rule: { id: createdRule.id, value: createdRule.value, allocation: createdRule.allocation, conditions: [ { products: [anotherProduct.id], product_types: [product.type_id], operator: "in", }, ], }, regions: [validRegionId], }, { headers: { "x-medusa-access-token": "test_token", }, } ) .catch((err) => err.response.data) ).resolves.toMatchSnapshot() }) it("creates a discount and updates it", async () => { const api = useApi() const response = await api.post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) 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}`, { usage_limit: 20, }, adminReqConfig ) expect(updated.status).toEqual(200) expect(updated.data.discount).toEqual( expect.objectContaining({ code: "HELLOWORLD", usage_limit: 20, }) ) }) it("creates a discount and fails to update it because attempting type update", async () => { const api = useApi() const response = await api.post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( expect.objectContaining({ code: "HELLOWORLD", usage_limit: 10, }) ) await api .post( `/admin/discounts/${response.data.discount.id}`, { usage_limit: 20, rule: { id: response.data.discount.rule.id, type: "free_shipping", }, regions: [validRegionId], }, adminReqConfig ) .catch((err) => { expect(err.response.status).toEqual(400) expect(err.response.data.message).toEqual( "property type should not exist" ) }) }) it("creates a discount and fails to update it because attempting is_dynamic update", async () => { const api = useApi() const response = await api.post( "/admin/discounts", { code: "HELLOWORLD", is_dynamic: true, rule: { description: "test", type: "percentage", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( expect.objectContaining({ code: "HELLOWORLD", usage_limit: 10, is_dynamic: true, }) ) const err = await api .post( `/admin/discounts/${response.data.discount.id}`, { usage_limit: 20, is_dynamic: false, }, adminReqConfig ) .catch((e) => e) expect(err.response.status).toEqual(400) expect(err.response.data.message).toEqual( "property is_dynamic should not exist" ) }) 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", }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) 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, }, adminReqConfig ) 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", }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) 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, }, adminReqConfig ) 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 () => { const api = useApi() const err = await api .post( "/admin/discounts", { code: "HELLOWORLD", is_dynamic: true, rule: { description: "test", type: "fixed", value: 10, allocation: "total", }, usage_limit: 10, regions: [validRegionId, "test-region-2"], }, adminReqConfig ) .catch((e) => e) 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 () => { const api = useApi() const response = await api.post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "fixed", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) const err = await api .post( `/admin/discounts/${response.data.discount.id}`, { regions: [validRegionId, "test-region-2"], }, adminReqConfig ) .catch((e) => e) 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 () => { 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: [validRegionId], }, adminReqConfig ) const err = await api .post( `/admin/discounts/${response.data.discount.id}/regions/test-region-2`, {}, adminReqConfig ) .catch((e) => e) 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 () => { const api = useApi() const response = await api.post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, starts_at: new Date("09/15/2021 11:50"), ends_at: new Date("09/15/2021 17:50"), }, adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( expect.objectContaining({ code: "HELLOWORLD", usage_limit: 10, starts_at: expect.any(String), ends_at: expect.any(String), }) ) expect(new Date(response.data.discount.starts_at)).toEqual( new Date("09/15/2021 11:50") ) expect(new Date(response.data.discount.ends_at)).toEqual( new Date("09/15/2021 17:50") ) const updated = await api.post( `/admin/discounts/${response.data.discount.id}`, { usage_limit: 20, starts_at: new Date("09/14/2021 11:50"), ends_at: new Date("09/17/2021 17:50"), }, adminReqConfig ) expect(updated.status).toEqual(200) expect(updated.data.discount).toEqual( expect.objectContaining({ code: "HELLOWORLD", usage_limit: 20, starts_at: expect.any(String), ends_at: expect.any(String), }) ) expect(new Date(updated.data.discount.starts_at)).toEqual( new Date("09/14/2021 11:50") ) expect(new Date(updated.data.discount.ends_at)).toEqual( new Date("09/17/2021 17:50") ) }) it("fails to update end date to a date before start date", async () => { expect.assertions(6) const api = useApi() const response = await api.post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, starts_at: new Date("09/15/2021 11:50"), ends_at: new Date("09/15/2021 17:50"), }, adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( expect.objectContaining({ code: "HELLOWORLD", usage_limit: 10, starts_at: expect.any(String), ends_at: expect.any(String), }) ) expect(new Date(response.data.discount.starts_at)).toEqual( new Date("09/15/2021 11:50") ) expect(new Date(response.data.discount.ends_at)).toEqual( new Date("09/15/2021 17:50") ) await api .post( `/admin/discounts/${response.data.discount.id}`, { usage_limit: 20, ends_at: new Date("09/11/2021 17:50"), }, adminReqConfig ) .catch((err) => { expect(err.response.status).toEqual(400) expect(err.response.data.message).toEqual( `"ends_at" must be greater than "starts_at"` ) }) }) it("fails to create discount with end date before start date", async () => { const api = useApi() const err = await api .post( "/admin/discounts", { code: "HELLOWORLD", rule: { description: "test", type: "percentage", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, starts_at: new Date("09/15/2021 11:50"), ends_at: new Date("09/14/2021 17:50"), }, adminReqConfig ) .catch((e) => e) expect(err.response.status).toEqual(400) expect(err.response.data).toEqual( expect.objectContaining({ message: `"ends_at" must be greater than "starts_at"`, }) ) }) 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", }, regions: [validRegionId, invalidRegionId], }, adminReqConfig ) .catch((e) => e) expect(err.response.status).toEqual(404) expect(err.response.data.message).toEqual( `Region with not-a-valid-region was not found` ) }) 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", }, regions: [invalidRegionId], }, adminReqConfig ) .catch((e) => e) expect(err.response.status).toEqual(404) expect(err.response.data.message).toEqual( `Region with not-a-valid-region was not found` ) }) 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", }, }, adminReqConfig ) .catch((e) => e) expect(err.response.status).toEqual(400) expect(err.response.data.message).toEqual( `each value in regions must be a string, regions must be an array` ) }) }) describe("POST /admin/discounts/:id", () => { beforeEach(async () => { await adminSeeder(dbConnection) await dbConnection.manager.insert(DiscountRule, { id: "test-discount-rule", description: "Test discount rule", type: "percentage", value: 10, allocation: "total", }) await dbConnection.manager.insert(Discount, { id: "test-discount", code: "TESTING", rule_id: "test-discount-rule", is_dynamic: false, is_disabled: false, ends_at: new Date(), usage_limit: 10, valid_duration: "P1D", }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("Removes ends_at, valid_duration and usage_limit when fields are updated with null", async () => { const api = useApi() await api.post( "/admin/discounts/test-discount", { ends_at: null, valid_duration: null, usage_limit: null, }, adminReqConfig ) const resultingDiscount = await api.get( "/admin/discounts/test-discount", { headers: { "x-medusa-access-token": "test_token" } } ) expect(resultingDiscount.status).toEqual(200) expect(resultingDiscount.data.discount).toEqual( expect.objectContaining({ ends_at: null, valid_duration: null, usage_limit: null, }) ) }) }) describe("testing for soft-deletion + uniqueness on discount codes", () => { let manager beforeEach(async () => { manager = dbConnection.manager await adminSeeder(dbConnection) await discountSeeder(dbConnection) await manager.insert(DiscountRule, { id: "test-discount-rule", description: "Test discount rule", type: "percentage", value: 10, allocation: "total", }) await manager.insert(Discount, { id: "test-discount", code: "TESTING", rule_id: "test-discount-rule", is_dynamic: false, is_disabled: false, }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("successfully creates discount with soft-deleted discount code", async () => { const api = useApi() // First we soft-delete the discount await api.delete("/admin/discounts/test-discount", adminReqConfig) // Lets try to create a discount with same code as deleted one const response = await api.post( "/admin/discounts", { code: "TESTING", rule: { description: "test", type: "percentage", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( expect.objectContaining({ code: "TESTING", usage_limit: 10, }) ) }) it("should fails when creating a discount with already existing code", async () => { const api = useApi() // Lets try to create a discount with same code as deleted one try { await api.post( "/admin/discounts", { code: "TESTING", rule: { description: "test", type: "percentage", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) } catch (error) { expect(error.response.data.message).toEqual( "Discount with code TESTING already exists." ) } }) }) describe("POST /admin/discounts/:discount_id/dynamic-codes", () => { beforeEach(async () => { const manager = dbConnection.manager await adminSeeder(dbConnection) await manager.insert(DiscountRule, { id: "test-discount-rule", description: "Dynamic rule", type: "percentage", value: 10, allocation: "total", }) await manager.insert(Discount, { id: "test-discount", code: "DYNAMIC", is_dynamic: true, is_disabled: false, rule_id: "test-discount-rule", valid_duration: "P2Y", }) await manager.insert(DiscountRule, { id: "test-discount-rule1", description: "Dynamic rule", type: "percentage", value: 10, allocation: "total", }) await manager.insert(Discount, { id: "test-discount1", code: "DYNAMICCode", is_dynamic: true, is_disabled: false, rule_id: "test-discount-rule1", }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("creates a dynamic discount with ends_at", async () => { const api = useApi() const response = await api.post( "/admin/discounts/test-discount/dynamic-codes", { code: "HELLOWORLD", }, adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( expect.objectContaining({ code: "HELLOWORLD", ends_at: expect.any(String), }) ) }) it("creates a dynamic discount without ends_at", async () => { const api = useApi() const response = await api.post( "/admin/discounts/test-discount1/dynamic-codes", { code: "HELLOWORLD", }, adminReqConfig ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( expect.objectContaining({ code: "HELLOWORLD", ends_at: null, }) ) }) }) describe("DELETE /admin/discounts/:id/conditions/:condition_id", () => { beforeEach(async () => { const manager = dbConnection.manager await adminSeeder(dbConnection) await manager.insert(DiscountRule, { id: "test-discount-rule-fixed", description: "Test discount rule", type: "fixed", value: 10, allocation: "total", }) const prod = await simpleProductFactory(dbConnection, { type: "pants" }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { type: "products", operator: "in", products: [prod.id], }, ], }, }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("should delete condition", async () => { const api = useApi() const group = await dbConnection.manager.insert(CustomerGroup, { id: "customer-group-1", name: "vip-customers", }) await dbConnection.manager.insert(Customer, { id: "cus_1234", email: "oli@email.com", groups: [group], }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { id: "test-condition", type: "customer_groups", operator: "in", customer_groups: ["customer-group-1"], }, ], }, }) const response = await api.delete( "/admin/discounts/test-discount/conditions/test-condition", adminReqConfig ) const disc = response.data expect(response.status).toEqual(200) expect(disc).toEqual( expect.objectContaining({ id: "test-condition", deleted: true, object: "discount-condition", }) ) }) it("should not fail if condition does not exist", async () => { const api = useApi() const response = await api.delete( "/admin/discounts/test-discount/conditions/test-condition", adminReqConfig ) const disc = response.data expect(response.status).toEqual(200) expect(disc).toEqual( expect.objectContaining({ id: "test-condition", deleted: true, object: "discount-condition", }) ) }) it("should fail if discount does not exist", async () => { const api = useApi() const err = await api .delete( "/admin/discounts/not-exist/conditions/test-condition", adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "Discount with id not-exist was not found" ) }) }) describe("POST /admin/discounts/:id/conditions", () => { beforeEach(async () => { const manager = dbConnection.manager await adminSeeder(dbConnection) await manager.insert(DiscountRule, { id: "test-discount-rule-fixed", description: "Test discount rule", type: "fixed", value: 10, allocation: "total", }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", }, }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("should create a condition", async () => { const api = useApi() const prod = await simpleProductFactory(dbConnection, { type: "pants" }) const response = await api.post( "/admin/discounts/test-discount/conditions", { operator: "in", products: [prod.id], }, adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc).toMatchSnapshot({ id: "test-discount", code: "TEST", created_at: expect.any(String), updated_at: expect.any(String), rule_id: expect.any(String), starts_at: expect.any(String), rule: { id: expect.any(String), created_at: expect.any(String), updated_at: expect.any(String), conditions: [ { id: expect.any(String), discount_rule_id: expect.any(String), created_at: expect.any(String), updated_at: expect.any(String), }, ], }, }) }) it("fails if more than one condition type is provided", async () => { const api = useApi() const prod = await simpleProductFactory(dbConnection, { type: "pants" }) const group = await dbConnection.manager.insert(CustomerGroup, { id: "customer-group-1", name: "vip-customers", }) await dbConnection.manager.insert(Customer, { id: "cus_1234", email: "oli@email.com", groups: [group], }) const err = await api .post( "/admin/discounts/test-discount/conditions", { operator: "in", products: [prod.id], customer_groups: ["customer-group-1"], }, adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "Only one of products, customer_groups is allowed, Only one of customer_groups, products is allowed" ) }) it("throws if discount does not exist", async () => { expect.assertions(1) const api = useApi() const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) const err = await api .post( "/admin/discounts/does-not-exist/conditions/test-condition", { products: [prod2.id], }, adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "Discount with id does-not-exist was not found" ) }) }) describe("POST /admin/discounts/:id/conditions/:condition_id", () => { beforeEach(async () => { const manager = dbConnection.manager await adminSeeder(dbConnection) await manager.insert(DiscountRule, { id: "test-discount-rule-fixed", description: "Test discount rule", type: "fixed", value: 10, allocation: "total", }) const prod = await simpleProductFactory(dbConnection, { type: "pants" }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { id: "test-condition", type: "products", operator: "in", products: [prod.id], }, ], }, }) await simpleDiscountFactory(dbConnection, { id: "test-discount-2", code: "TEST2", rule: { type: "percentage", value: "10", allocation: "total", }, }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("should update a condition", async () => { const api = useApi() const prod2 = await simpleProductFactory(dbConnection, { id: "test-product", type: "pants", }) const discount = await api.get( "/admin/discounts/test-discount", adminReqConfig ) const cond = discount.data.discount.rule.conditions[0] const response = await api.post( `/admin/discounts/test-discount/conditions/${cond.id}?expand=rule.conditions.products.profiles`, { products: [prod2.id], }, adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc).toMatchSnapshot({ id: "test-discount", code: "TEST", created_at: expect.any(String), updated_at: expect.any(String), rule_id: expect.any(String), starts_at: expect.any(String), rule: { id: expect.any(String), created_at: expect.any(String), updated_at: expect.any(String), conditions: [ { id: expect.any(String), discount_rule_id: expect.any(String), created_at: expect.any(String), updated_at: expect.any(String), products: [ { created_at: expect.any(String), updated_at: expect.any(String), profile_id: expect.any(String), profiles: expect.any(Array), profile: expect.any(Object), type_id: expect.any(String), id: "test-product", }, ], }, ], }, }) }) it("throws if condition does not exist", async () => { const api = useApi() const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) const err = await api .post( "/admin/discounts/test-discount/conditions/does-not-exist", { products: [prod2.id], }, adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "DiscountCondition with id does-not-exist was not found" ) }) it("throws if discount does not exist", async () => { expect.assertions(1) const api = useApi() const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) const err = await api .post( "/admin/discounts/does-not-exist/conditions/test-condition", { products: [prod2.id], }, adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "Discount with id does-not-exist was not found" ) }) it("throws if condition does not belong to discount", async () => { const api = useApi() const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) const err = await api .post( "/admin/discounts/test-discount-2/conditions/test-condition", { products: [prod2.id], }, adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "Condition with id test-condition does not belong to Discount with id test-discount-2" ) }) }) describe("GET /admin/discounts/:id/conditions/:condition_id", () => { beforeEach(async () => { await adminSeeder(dbConnection) const prod = await simpleProductFactory(dbConnection, { type: "pants", id: "test-product", }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { id: "test-condition", type: "products", operator: "in", products: [prod.id], }, ], }, }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("should get condition", async () => { const api = useApi() const discountCondition = await api.get( "/admin/discounts/test-discount/conditions/test-condition", adminReqConfig ) const cond = discountCondition.data.discount_condition expect(discountCondition.status).toEqual(200) expect(cond).toMatchSnapshot({ id: "test-condition", type: "products", operator: "in", created_at: expect.any(String), updated_at: expect.any(String), discount_rule_id: expect.any(String), discount_rule: { id: expect.any(String), updated_at: expect.any(String), created_at: expect.any(String), }, }) }) it("should get condition with expand + fields", async () => { const api = useApi() const discountCondition = await api.get( "/admin/discounts/test-discount/conditions/test-condition?expand=products.profiles&fields=id,type", adminReqConfig ) const cond = discountCondition.data.discount_condition expect(discountCondition.status).toEqual(200) expect(cond).toMatchSnapshot({ id: "test-condition", type: "products", products: [ { id: "test-product", profile_id: expect.any(String), profiles: expect.any(Array), profile: expect.any(Object), type_id: expect.any(String), created_at: expect.any(String), updated_at: expect.any(String), }, ], }) }) it("throws if condition does not exist", async () => { const api = useApi() const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) const err = await api .post( "/admin/discounts/test-discount/conditions/does-not-exist", { products: [prod2.id], }, adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "DiscountCondition with id does-not-exist was not found" ) }) it("throws if condition does not belong to discount", async () => { const api = useApi() await simpleDiscountFactory(dbConnection, { id: "test-discount-2", code: "TEST2", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { id: "test-condition-2", type: "products", operator: "in", products: [], }, ], }, }) const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) const err = await api .post( "/admin/discounts/test-discount/conditions/test-condition-2", { products: [prod2.id], }, adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "Condition with id test-condition-2 does not belong to Discount with id test-discount" ) }) }) describe("GET /admin/discounts/code/:code", () => { beforeEach(async () => { const manager = dbConnection.manager await adminSeeder(dbConnection) await discountSeeder(dbConnection) await manager.insert(DiscountRule, { id: "test-discount-rule-fixed", description: "Test discount rule", type: "fixed", value: 10, allocation: "total", }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", }, }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("should retrieve discount using uppercase code", async () => { const api = useApi() const response = await api.get( "/admin/discounts/code/TEST", adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc).toEqual( expect.objectContaining({ id: "test-discount", code: "TEST", }) ) }) it("should retrieve discount using lowercase code", async () => { const api = useApi() const response = await api.get( "/admin/discounts/code/test", adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc).toEqual( expect.objectContaining({ id: "test-discount", code: "TEST", }) ) }) it("should retrieve discount using mixed casing code", async () => { const api = useApi() const response = await api.get( "/admin/discounts/code/TesT", adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc).toEqual( expect.objectContaining({ id: "test-discount", code: "TEST", }) ) }) it("should respond with 404 on non-existing code", async () => { const api = useApi() 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( `Discounts with code ${code} was not found` ) }) it("should trim and uppercase code on insert", async () => { const api = useApi() const response = await api.post( "/admin/discounts", { code: " Testing ", rule: { description: "test", type: "percentage", value: 10, allocation: "total", }, regions: [validRegionId], usage_limit: 10, }, adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc).toEqual( expect.objectContaining({ code: "TESTING", }) ) }) it("should trim and uppercase code on retrieve", async () => { const api = useApi() await simpleDiscountFactory(dbConnection, { code: "Testing", rule: { type: "percentage", value: "10", allocation: "total", }, }) const response = await api.get( "/admin/discounts/code/ testing", adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc).toEqual( expect.objectContaining({ code: "TESTING", }) ) }) }) describe("POST /admin/discounts/:id/conditions/:condition_id/batch", () => { let prod1 beforeEach(async () => { await adminSeeder(dbConnection) prod1 = await simpleProductFactory(dbConnection, { type: "pants" }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { id: "test-condition", type: "products", operator: "in", products: [prod1.id], }, ], }, }) await simpleDiscountFactory(dbConnection, { id: "test-discount-2", code: "TEST2", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { id: "test-condition-2", type: "products", operator: "in", products: [], }, ], }, }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("should update a condition with batch items", async () => { const api = useApi() const prod2 = await simpleProductFactory(dbConnection, { id: "test-product-2", type: "pants 2", }) const prod3 = await simpleProductFactory(dbConnection, { id: "test-product-3", type: "pants 3", }) const discount = await api.get( "/admin/discounts/test-discount", adminReqConfig ) const cond = discount.data.discount.rule.conditions[0] const response = await api.post( `/admin/discounts/test-discount/conditions/${cond.id}/batch?expand=rule.conditions.products.profiles`, { resources: [{ id: prod2.id }, { id: prod3.id }], }, adminReqConfig ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc.rule.conditions).toHaveLength(1) expect(disc.rule.conditions[0].products).toHaveLength(3) expect(disc).toEqual( expect.objectContaining({ id: "test-discount", code: "TEST", rule: expect.objectContaining({ conditions: expect.arrayContaining([ expect.objectContaining({ products: expect.arrayContaining([ expect.objectContaining({ id: prod1.id, }), expect.objectContaining({ id: prod2.id, }), expect.objectContaining({ id: prod3.id, }), ]), }), ]), }), }) ) }) it("should fail if condition does not belong to discount", async () => { const api = useApi() const err = await api .post( "/admin/discounts/test-discount/conditions/test-condition-2/batch", {}, adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "Condition with id test-condition-2 does not belong to Discount with id test-discount" ) }) it("should fail if discount does not exist", async () => { const api = useApi() const err = await api .post( "/admin/discounts/not-exist/conditions/test-condition/batch", {}, adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "Discount with id not-exist was not found" ) }) }) describe("DELETE /admin/discounts/:id/conditions/:condition_id/batch", () => { let prod1 let prod2 let prod3 let prod4 beforeEach(async () => { await adminSeeder(dbConnection) prod1 = await simpleProductFactory(dbConnection, { type: "pants" }) prod2 = await simpleProductFactory(dbConnection, { type: "pants2" }) prod3 = await simpleProductFactory(dbConnection, { type: "pants3" }) prod4 = await simpleProductFactory(dbConnection, { type: "pants4" }) await simpleDiscountFactory(dbConnection, { id: "test-discount", code: "TEST", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { id: "test-condition", type: "products", operator: "in", products: [prod1.id, prod2.id, prod3.id, prod4.id], }, ], }, }) await simpleDiscountFactory(dbConnection, { id: "test-discount-2", code: "TEST2", rule: { type: "percentage", value: "10", allocation: "total", conditions: [ { id: "test-condition-2", type: "products", operator: "in", products: [], }, ], }, }) }) afterEach(async () => { const db = useDb() await db.teardown() }) it("should update a condition with batch items to delete", async () => { const api = useApi() const discount = await api.get( "/admin/discounts/test-discount", adminReqConfig ) const cond = discount.data.discount.rule.conditions[0] const response = await api.delete( `/admin/discounts/test-discount/conditions/${cond.id}/batch?expand=rule.conditions.products.profiles`, { ...adminReqConfig, data: { resources: [{ id: prod2.id }, { id: prod3.id }, { id: prod4.id }], }, } ) const disc = response.data.discount expect(response.status).toEqual(200) expect(disc.rule.conditions).toHaveLength(1) expect(disc.rule.conditions[0].products).toHaveLength(1) expect(disc).toEqual( expect.objectContaining({ id: "test-discount", code: "TEST", rule: expect.objectContaining({ conditions: expect.arrayContaining([ expect.objectContaining({ products: expect.arrayContaining([ expect.objectContaining({ id: prod1.id, }), ]), }), ]), }), }) ) }) it("should fail if condition does not belong to discount", async () => { const api = useApi() const err = await api .delete( "/admin/discounts/test-discount/conditions/test-condition-2/batch", adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "Condition with id test-condition-2 does not belong to Discount with id test-discount" ) }) it("should fail if discount does not exist", async () => { const api = useApi() const err = await api .delete( "/admin/discounts/not-exist/conditions/test-condition/batch", adminReqConfig ) .catch((e) => e) expect(err.response.data.message).toBe( "Discount with id not-exist was not found" ) }) }) })