feat: Add DiscountConditions (#1230)

* feat: Add DiscountCondition entity + Join table per relation (#1146)

* feat: Convert DiscountService to TypeScript (#1149)

* feat: Add DiscountRepository + bulk insert and remove (#1156)

* feat: Add `conditions` to payload in `POST /discounts` and `POST /discounts/:id` (#1170)

* feat: Add DiscountRuleCondition entity

* fix relation

* fix join key

* Add discount rule condition repo

* add join table per relation

* Convert DiscountService to TypeScript

* feat: Add DiscountConditionRepository

* Add migration + remove use of valid_for

* revert changes to files, not done yet

* init work on create discount endpoint

* Add conditions to create discount endpoint

* Add conditions to update discount endpoint

* Add unique constraint to discount condition

* integration tests passing

* fix imports of models

* fix tests (excluding totals calculations)

* Fix commented code

* add unique constraint on discount condition

* Add generic way of generating retrieve configs

* Requested changes + ExactlyOne validator

* Remove isLocal flag from error handler

* Use postgres error constant

* remove commented code

* feat: Add `isValidForProduct` to check if Discount is valid for a given Product (#1172)

* feat: Add `canApplyForCustomer` to check if Discount is valid for customer groups (#1212)

* feat: Add `calculateDiscountForLineItem` (#1224)

* feat: Adds discount condition test factory (#1228)

* Remove use of valid_for

* Tests passing

* Remove valid_for form relations

* Add integration tests for applying discounts to cart
This commit is contained in:
Oliver Windall Juhl
2022-03-24 16:47:50 +01:00
committed by GitHub
parent b7f699654b
commit a610805917
60 changed files with 4805 additions and 2021 deletions
+547 -17
View File
@@ -1,5 +1,11 @@
const path = require("path")
const { Region, DiscountRule, Discount } = require("@medusajs/medusa")
const {
Region,
DiscountRule,
Discount,
Customer,
CustomerGroup,
} = require("@medusajs/medusa")
const setupServer = require("../../../helpers/setup-server")
const { useApi } = require("../../../helpers/use-api")
@@ -7,6 +13,10 @@ const { initDb, useDb } = require("../../../helpers/use-db")
const adminSeeder = require("../../helpers/admin-seeder")
const discountSeeder = require("../../helpers/discount-seeder")
const { exportAllDeclaration } = require("@babel/types")
const { simpleProductFactory } = require("../../factories")
const {
simpleDiscountFactory,
} = require("../../factories/simple-discount-factory")
jest.setTimeout(30000)
@@ -26,6 +36,153 @@ describe("/admin/discounts", () => {
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",
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
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 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",
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
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
@@ -267,25 +424,398 @@ describe("/admin/discounts", () => {
usage_limit: 10,
})
)
})
const test = await api.get(
`/admin/discounts/${response.data.discount.id}`,
{ headers: { Authorization: "Bearer test_token" } }
)
it("creates a discount with conditions", async () => {
const api = useApi()
expect(test.status).toEqual(200)
expect(test.data.discount).toEqual(
expect.objectContaining({
code: "HELLOWORLD",
usage_limit: 10,
rule: expect.objectContaining({
value: 10,
type: "percentage",
description: "test",
allocation: "total",
}),
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",
},
],
},
usage_limit: 10,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
)
expect(response.status).toEqual(200)
expect(response.data.discount.rule.conditions).toEqual([
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",
},
],
},
usage_limit: 10,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.discount.rule.conditions).toEqual([
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,
type: createdRule.type,
value: createdRule.value,
allocation: createdRule.allocation,
conditions: [
{
id: condsToUpdate.id,
products: [product.id, anotherProduct.id],
},
],
},
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
expect(updated.status).toEqual(200)
expect(updated.data.discount.rule.conditions).toEqual([
expect.objectContaining({
type: "products",
operator: "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",
},
],
},
usage_limit: 10,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
const createdRule = response.data.discount.rule
try {
await api.post(
`/admin/discounts/${response.data.discount.id}?expand=rule,rule.conditions,rule.conditions.products`,
{
rule: {
id: createdRule.id,
type: createdRule.type,
value: createdRule.value,
allocation: createdRule.allocation,
conditions: [
{
products: [anotherProduct.id],
operator: "in",
},
],
},
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
} catch (error) {
console.log(error)
expect(error.response.data.type).toEqual("duplicate_error")
expect(error.response.data.message).toEqual(
`Discount Condition with operator 'in' and type 'products' already exist on a Discount Rule`
)
}
})
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",
},
],
},
usage_limit: 10,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
} 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",
},
],
},
usage_limit: 10,
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
const createdRule = response.data.discount.rule
try {
await api.post(
`/admin/discounts/${response.data.discount.id}?expand=rule,rule.conditions,rule.conditions.products`,
{
rule: {
id: createdRule.id,
type: createdRule.type,
value: createdRule.value,
allocation: createdRule.allocation,
conditions: [
{
products: [anotherProduct.id],
product_types: [product.type_id],
operator: "in",
},
],
},
},
{
headers: {
Authorization: "Bearer test_token",
},
}
)
} catch (error) {
console.log(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("creates a discount and updates it", async () => {