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:
@@ -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 () => {
|
||||
|
||||
@@ -16,6 +16,16 @@ 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 } = require("../../factories")
|
||||
const {
|
||||
simpleDiscountFactory,
|
||||
} = require("../../factories/simple-discount-factory")
|
||||
const {
|
||||
simpleCustomerFactory,
|
||||
} = require("../../factories/simple-customer-factory")
|
||||
const {
|
||||
simpleCustomerGroupFactory,
|
||||
} = require("../../factories/simple-customer-group-factory")
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -354,6 +364,348 @@ describe("/store/carts", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully passes customer conditions with `in` operator and applies discount", async () => {
|
||||
const api = useApi()
|
||||
|
||||
await simpleCustomerFactory(dbConnection, {
|
||||
id: "cus_1234",
|
||||
email: "oli@medusajs.com",
|
||||
groups: [
|
||||
{
|
||||
id: "customer-group-1",
|
||||
name: "VIP Customer",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await simpleCustomerGroupFactory(dbConnection, {
|
||||
id: "customer-group-2",
|
||||
name: "Loyal",
|
||||
})
|
||||
|
||||
await simpleCartFactory(
|
||||
dbConnection,
|
||||
{
|
||||
id: "test-customer-discount",
|
||||
region: {
|
||||
id: "test-region",
|
||||
name: "Test region",
|
||||
tax_rate: 12,
|
||||
},
|
||||
customer: "cus_1234",
|
||||
line_items: [
|
||||
{
|
||||
variant_id: "test-variant",
|
||||
unit_price: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
100
|
||||
)
|
||||
|
||||
await simpleDiscountFactory(dbConnection, {
|
||||
id: "test-discount",
|
||||
code: "TEST",
|
||||
regions: ["test-region"],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
value: "10",
|
||||
allocation: "total",
|
||||
conditions: [
|
||||
{
|
||||
type: "customer_groups",
|
||||
operator: "in",
|
||||
customer_groups: ["customer-group-1", "customer-group-2"],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const response = await api.post("/store/carts/test-customer-discount", {
|
||||
discounts: [{ code: "TEST" }],
|
||||
})
|
||||
|
||||
const cartRes = response.data.cart
|
||||
expect(cartRes.discounts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "TEST",
|
||||
}),
|
||||
])
|
||||
)
|
||||
expect(response.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("successfully passes customer conditions with `not_in` operator and applies discount", async () => {
|
||||
const api = useApi()
|
||||
|
||||
await simpleCustomerFactory(dbConnection, {
|
||||
id: "cus_1234",
|
||||
email: "oli@medusajs.com",
|
||||
groups: [
|
||||
{
|
||||
id: "customer-group-2",
|
||||
name: "VIP Customer",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await simpleCustomerGroupFactory(dbConnection, {
|
||||
id: "customer-group-1",
|
||||
name: "Customer group 1",
|
||||
})
|
||||
|
||||
await simpleCustomerGroupFactory(dbConnection, {
|
||||
id: "customer-group-3",
|
||||
name: "Customer group 3",
|
||||
})
|
||||
|
||||
await simpleCartFactory(
|
||||
dbConnection,
|
||||
{
|
||||
id: "test-customer-discount",
|
||||
region: {
|
||||
id: "test-region",
|
||||
name: "Test region",
|
||||
tax_rate: 12,
|
||||
},
|
||||
customer: "cus_1234",
|
||||
line_items: [
|
||||
{
|
||||
variant_id: "test-variant",
|
||||
unit_price: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
100
|
||||
)
|
||||
|
||||
await simpleDiscountFactory(dbConnection, {
|
||||
id: "test-discount",
|
||||
code: "TEST",
|
||||
regions: ["test-region"],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
value: "10",
|
||||
allocation: "total",
|
||||
conditions: [
|
||||
{
|
||||
type: "customer_groups",
|
||||
operator: "not_in",
|
||||
customer_groups: ["customer-group-1", "customer-group-3"],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const response = await api.post("/store/carts/test-customer-discount", {
|
||||
discounts: [{ code: "TEST" }],
|
||||
})
|
||||
|
||||
const cartRes = response.data.cart
|
||||
expect(cartRes.discounts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "TEST",
|
||||
}),
|
||||
])
|
||||
)
|
||||
expect(response.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("successfully applies discount in case no conditions is defined for group", async () => {
|
||||
const api = useApi()
|
||||
|
||||
await simpleCustomerFactory(dbConnection, {
|
||||
id: "cus_1234",
|
||||
email: "oli@medusajs.com",
|
||||
groups: [
|
||||
{
|
||||
id: "customer-group-1",
|
||||
name: "VIP Customer",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await simpleCartFactory(
|
||||
dbConnection,
|
||||
{
|
||||
id: "test-customer-discount",
|
||||
region: {
|
||||
id: "test-region",
|
||||
name: "Test region",
|
||||
tax_rate: 12,
|
||||
},
|
||||
customer: "cus_1234",
|
||||
line_items: [
|
||||
{
|
||||
variant_id: "test-variant",
|
||||
unit_price: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
100
|
||||
)
|
||||
|
||||
await simpleDiscountFactory(dbConnection, {
|
||||
id: "test-discount",
|
||||
code: "TEST",
|
||||
regions: ["test-region"],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
value: "10",
|
||||
allocation: "total",
|
||||
},
|
||||
})
|
||||
|
||||
const response = await api.post("/store/carts/test-customer-discount", {
|
||||
discounts: [{ code: "TEST" }],
|
||||
})
|
||||
|
||||
const cartRes = response.data.cart
|
||||
expect(cartRes.discounts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "TEST",
|
||||
}),
|
||||
])
|
||||
)
|
||||
expect(response.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("fails to apply discount if customer group is part of `not_in` conditions", async () => {
|
||||
const api = useApi()
|
||||
|
||||
await simpleCustomerFactory(dbConnection, {
|
||||
id: "cus_1234",
|
||||
email: "oli@medusajs.com",
|
||||
groups: [
|
||||
{
|
||||
id: "customer-group-1",
|
||||
name: "VIP Customer",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await simpleCartFactory(
|
||||
dbConnection,
|
||||
{
|
||||
id: "test-customer-discount",
|
||||
region: {
|
||||
id: "test-region",
|
||||
name: "Test region",
|
||||
tax_rate: 12,
|
||||
},
|
||||
customer: "cus_1234",
|
||||
line_items: [
|
||||
{
|
||||
variant_id: "test-variant",
|
||||
unit_price: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
100
|
||||
)
|
||||
|
||||
await simpleDiscountFactory(dbConnection, {
|
||||
id: "test-discount",
|
||||
code: "TEST",
|
||||
regions: ["test-region"],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
value: "10",
|
||||
allocation: "total",
|
||||
conditions: [
|
||||
{
|
||||
type: "customer_groups",
|
||||
operator: "not_in",
|
||||
customer_groups: ["customer-group-1"],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
await api.post("/store/carts/test-customer-discount", {
|
||||
discounts: [{ code: "TEST" }],
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data.message).toEqual(
|
||||
"Discount is not valid for customer"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("fails to apply discount if customer group is not part of `in` conditions", async () => {
|
||||
const api = useApi()
|
||||
|
||||
await simpleCustomerFactory(dbConnection, {
|
||||
id: "cus_1234",
|
||||
email: "oli@medusajs.com",
|
||||
groups: [
|
||||
{
|
||||
id: "customer-group-2",
|
||||
name: "VIP Customer",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await simpleCustomerGroupFactory(dbConnection, {
|
||||
id: "customer-group-1",
|
||||
name: "Customer group 1",
|
||||
})
|
||||
|
||||
await simpleCartFactory(
|
||||
dbConnection,
|
||||
{
|
||||
id: "test-customer-discount",
|
||||
region: {
|
||||
id: "test-region",
|
||||
name: "Test region",
|
||||
tax_rate: 12,
|
||||
},
|
||||
customer: "cus_1234",
|
||||
line_items: [
|
||||
{
|
||||
variant_id: "test-variant",
|
||||
unit_price: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
100
|
||||
)
|
||||
|
||||
await simpleDiscountFactory(dbConnection, {
|
||||
id: "test-discount",
|
||||
code: "TEST",
|
||||
regions: ["test-region"],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
value: "10",
|
||||
allocation: "total",
|
||||
conditions: [
|
||||
{
|
||||
type: "customer_groups",
|
||||
operator: "in",
|
||||
customer_groups: ["customer-group-1"],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
await api.post("/store/carts/test-customer-discount", {
|
||||
discounts: [{ code: "TEST" }],
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.response.status).toEqual(400)
|
||||
expect(error.response.data.message).toEqual(
|
||||
"Discount is not valid for customer"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("fails to apply expired discount", async () => {
|
||||
expect.assertions(2)
|
||||
const api = useApi()
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Connection } from "typeorm"
|
||||
import faker from "faker"
|
||||
import { Cart } from "@medusajs/medusa"
|
||||
|
||||
import { RegionFactoryData, simpleRegionFactory } from "./simple-region-factory"
|
||||
import {
|
||||
LineItemFactoryData,
|
||||
simpleLineItemFactory,
|
||||
} from "./simple-line-item-factory"
|
||||
import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
import {
|
||||
AddressFactoryData,
|
||||
simpleAddressFactory,
|
||||
} from "./simple-address-factory"
|
||||
import { simpleCustomerFactory } from "./simple-customer-factory"
|
||||
import {
|
||||
LineItemFactoryData,
|
||||
simpleLineItemFactory,
|
||||
} from "./simple-line-item-factory"
|
||||
import { RegionFactoryData, simpleRegionFactory } from "./simple-region-factory"
|
||||
import {
|
||||
ShippingMethodFactoryData,
|
||||
simpleShippingMethodFactory,
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
|
||||
export type CartFactoryData = {
|
||||
id?: string
|
||||
customer?: string | { email: string }
|
||||
region?: RegionFactoryData | string
|
||||
email?: string | null
|
||||
line_items?: LineItemFactoryData[]
|
||||
@@ -43,6 +44,22 @@ export const simpleCartFactory = async (
|
||||
const region = await simpleRegionFactory(connection, data.region)
|
||||
regionId = region.id
|
||||
}
|
||||
|
||||
let customerId: string
|
||||
if (typeof data.customer === "string") {
|
||||
customerId = data.customer
|
||||
} else {
|
||||
if (data?.customer?.email) {
|
||||
const customer = await simpleCustomerFactory(connection, data.customer)
|
||||
customerId = customer.id
|
||||
} else if (data.email) {
|
||||
const customer = await simpleCustomerFactory(connection, {
|
||||
email: data.email,
|
||||
})
|
||||
customerId = customer.id
|
||||
}
|
||||
}
|
||||
|
||||
const address = await simpleAddressFactory(connection, data.shipping_address)
|
||||
|
||||
const id = data.id || `simple-cart-${Math.random() * 1000}`
|
||||
@@ -51,6 +68,7 @@ export const simpleCartFactory = async (
|
||||
email:
|
||||
typeof data.email !== "undefined" ? data.email : faker.internet.email(),
|
||||
region_id: regionId,
|
||||
customer_id: customerId,
|
||||
shipping_address_id: address.id,
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Customer } from "@medusajs/medusa"
|
||||
import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
import {
|
||||
CustomerGroupFactoryData,
|
||||
simpleCustomerGroupFactory,
|
||||
} from "./simple-customer-group-factory"
|
||||
|
||||
export type CustomerFactoryData = {
|
||||
id?: string
|
||||
email?: string
|
||||
groups?: CustomerGroupFactoryData[]
|
||||
}
|
||||
|
||||
export const simpleCustomerFactory = async (
|
||||
connection: Connection,
|
||||
data: CustomerFactoryData = {},
|
||||
seed?: number
|
||||
): Promise<Customer> => {
|
||||
if (typeof seed !== "undefined") {
|
||||
faker.seed(seed)
|
||||
}
|
||||
|
||||
const manager = connection.manager
|
||||
|
||||
const customerId = data.id || `simple-customer-${Math.random() * 1000}`
|
||||
const c = manager.create(Customer, {
|
||||
id: customerId,
|
||||
email: data.email,
|
||||
})
|
||||
|
||||
const customer = await manager.save(c)
|
||||
|
||||
if (data.groups) {
|
||||
const groups = []
|
||||
for (const g of data.groups) {
|
||||
const created = await simpleCustomerGroupFactory(connection, g)
|
||||
groups.push(created)
|
||||
}
|
||||
|
||||
customer.groups = groups
|
||||
await manager.save(customer)
|
||||
}
|
||||
|
||||
return customer
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { CustomerGroup } from "@medusajs/medusa"
|
||||
import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
|
||||
export type CustomerGroupFactoryData = {
|
||||
id?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export const simpleCustomerGroupFactory = async (
|
||||
connection: Connection,
|
||||
data: CustomerGroupFactoryData = {},
|
||||
seed?: number
|
||||
): Promise<CustomerGroup> => {
|
||||
if (typeof seed !== "undefined") {
|
||||
faker.seed(seed)
|
||||
}
|
||||
|
||||
const manager = connection.manager
|
||||
|
||||
const customerGroupId =
|
||||
data.id || `simple-customer-group-${Math.random() * 1000}`
|
||||
const c = manager.create(CustomerGroup, {
|
||||
id: customerGroupId,
|
||||
name: data.name,
|
||||
})
|
||||
|
||||
const group = await manager.save(c)
|
||||
|
||||
return group
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
DiscountCondition,
|
||||
DiscountConditionOperator,
|
||||
DiscountConditionType,
|
||||
} from "@medusajs/medusa/dist/models/discount-condition"
|
||||
import { DiscountConditionCustomerGroup } from "@medusajs/medusa/dist/models/discount-condition-customer-group"
|
||||
import { DiscountConditionProduct } from "@medusajs/medusa/dist/models/discount-condition-product"
|
||||
import { DiscountConditionProductCollection } from "@medusajs/medusa/dist/models/discount-condition-product-collection"
|
||||
import { DiscountConditionProductTag } from "@medusajs/medusa/dist/models/discount-condition-product-tag"
|
||||
import { DiscountConditionProductType } from "@medusajs/medusa/dist/models/discount-condition-product-type"
|
||||
import { DiscountConditionJoinTableForeignKey } from "@medusajs/medusa/dist/repositories/discount-condition"
|
||||
import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
|
||||
export type DiscuntConditionFactoryData = {
|
||||
rule_id: string
|
||||
type: DiscountConditionType
|
||||
operator: DiscountConditionOperator
|
||||
products: string[]
|
||||
product_collections: string[]
|
||||
product_types: string[]
|
||||
product_tags: string[]
|
||||
customer_groups: string[]
|
||||
}
|
||||
|
||||
const getJoinTableResourceIdentifiers = (type: string) => {
|
||||
let conditionTable: any
|
||||
let resourceKey
|
||||
|
||||
switch (type) {
|
||||
case DiscountConditionType.PRODUCTS: {
|
||||
resourceKey = DiscountConditionJoinTableForeignKey.PRODUCT_ID
|
||||
conditionTable = DiscountConditionProduct
|
||||
break
|
||||
}
|
||||
case DiscountConditionType.PRODUCT_TYPES: {
|
||||
resourceKey = DiscountConditionJoinTableForeignKey.PRODUCT_TYPE_ID
|
||||
conditionTable = DiscountConditionProductType
|
||||
break
|
||||
}
|
||||
case DiscountConditionType.PRODUCT_COLLECTIONS: {
|
||||
resourceKey = DiscountConditionJoinTableForeignKey.PRODUCT_COLLECTION_ID
|
||||
conditionTable = DiscountConditionProductCollection
|
||||
break
|
||||
}
|
||||
case DiscountConditionType.PRODUCT_TAGS: {
|
||||
resourceKey = DiscountConditionJoinTableForeignKey.PRODUCT_TAG_ID
|
||||
|
||||
conditionTable = DiscountConditionProductTag
|
||||
break
|
||||
}
|
||||
case DiscountConditionType.CUSTOMER_GROUPS: {
|
||||
resourceKey = DiscountConditionJoinTableForeignKey.CUSTOMER_GROUP_ID
|
||||
conditionTable = DiscountConditionCustomerGroup
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
resourceKey,
|
||||
conditionTable,
|
||||
}
|
||||
}
|
||||
|
||||
export const simpleDiscountConditionFactory = async (
|
||||
connection: Connection,
|
||||
data: DiscuntConditionFactoryData,
|
||||
seed?: number
|
||||
): Promise<void> => {
|
||||
if (typeof seed !== "undefined") {
|
||||
faker.seed(seed)
|
||||
}
|
||||
|
||||
const manager = connection.manager
|
||||
|
||||
let resources = []
|
||||
|
||||
if (data.products) {
|
||||
resources = data.products
|
||||
}
|
||||
if (data.product_collections) {
|
||||
resources = data.product_collections
|
||||
}
|
||||
if (data.product_types) {
|
||||
resources = data.product_types
|
||||
}
|
||||
if (data.product_tags) {
|
||||
resources = data.product_tags
|
||||
}
|
||||
if (data.customer_groups) {
|
||||
resources = data.customer_groups
|
||||
}
|
||||
|
||||
const condToSave = manager.create(DiscountCondition, {
|
||||
type: data.type,
|
||||
operator: data.operator,
|
||||
discount_rule_id: data.rule_id,
|
||||
})
|
||||
|
||||
const { conditionTable, resourceKey } = getJoinTableResourceIdentifiers(
|
||||
data.type
|
||||
)
|
||||
|
||||
const condition = await manager.save(condToSave)
|
||||
|
||||
for (const resourceCond of resources) {
|
||||
const toSave = manager.create(conditionTable, {
|
||||
[resourceKey]: resourceCond,
|
||||
condition_id: condition.id,
|
||||
})
|
||||
|
||||
await manager.save(toSave)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Connection } from "typeorm"
|
||||
import faker from "faker"
|
||||
import {
|
||||
AllocationType,
|
||||
Discount,
|
||||
DiscountRule,
|
||||
DiscountRuleType,
|
||||
AllocationType,
|
||||
} from "@medusajs/medusa"
|
||||
import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
import {
|
||||
DiscuntConditionFactoryData,
|
||||
simpleDiscountConditionFactory,
|
||||
} from "./simple-discount-condition-factory"
|
||||
|
||||
export type DiscountRuleFactoryData = {
|
||||
type?: DiscountRuleType
|
||||
value?: number
|
||||
allocation?: AllocationType
|
||||
conditions: DiscuntConditionFactoryData[]
|
||||
}
|
||||
|
||||
export type DiscountFactoryData = {
|
||||
@@ -41,6 +46,16 @@ export const simpleDiscountFactory = async (
|
||||
|
||||
const dRule = await manager.save(ruleToSave)
|
||||
|
||||
if (data?.rule?.conditions) {
|
||||
for (const condition of data.rule.conditions) {
|
||||
await simpleDiscountConditionFactory(
|
||||
connection,
|
||||
{ ...condition, rule_id: dRule.id },
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const toSave = manager.create(Discount, {
|
||||
id: data.id,
|
||||
is_dynamic: data.is_dynamic ?? false,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Connection } from "typeorm"
|
||||
import faker from "faker"
|
||||
import {
|
||||
ShippingProfileType,
|
||||
ShippingProfile,
|
||||
Product,
|
||||
ProductType,
|
||||
ProductOption,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
ShippingProfile,
|
||||
ShippingProfileType,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
import faker from "faker"
|
||||
import { Connection } from "typeorm"
|
||||
import {
|
||||
simpleProductVariantFactory,
|
||||
ProductVariantFactoryData,
|
||||
simpleProductVariantFactory,
|
||||
} from "./simple-product-variant-factory"
|
||||
|
||||
export type ProductFactoryData = {
|
||||
@@ -19,6 +19,7 @@ export type ProductFactoryData = {
|
||||
status?: string
|
||||
title?: string
|
||||
type?: string
|
||||
tags?: string[]
|
||||
options?: { id: string; title: string }[]
|
||||
variants?: ProductVariantFactoryData[]
|
||||
}
|
||||
@@ -42,27 +43,40 @@ export const simpleProductFactory = async (
|
||||
type: ShippingProfileType.GIFT_CARD,
|
||||
})
|
||||
|
||||
let typeId: string
|
||||
const prodId = data.id || `simple-product-${Math.random() * 1000}`
|
||||
const productToCreate = {
|
||||
id: prodId,
|
||||
title: data.title || faker.commerce.productName(),
|
||||
is_giftcard: data.is_giftcard || false,
|
||||
discountable: !data.is_giftcard,
|
||||
tags: [],
|
||||
profile_id: data.is_giftcard ? gcProfile.id : defaultProfile.id,
|
||||
}
|
||||
|
||||
if (typeof data.tags !== "undefined") {
|
||||
for (let i = 0; i < data.tags.length; i++) {
|
||||
const createdTag = manager.create(ProductTag, {
|
||||
id: `tag-${Math.random() * 1000}`,
|
||||
value: data.tags[i],
|
||||
})
|
||||
|
||||
const tagRes = await manager.save(createdTag)
|
||||
|
||||
productToCreate.tags.push(tagRes)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data.type !== "undefined") {
|
||||
const toSave = manager.create(ProductType, {
|
||||
value: data.type,
|
||||
})
|
||||
const res = await manager.save(toSave)
|
||||
typeId = res.id
|
||||
productToCreate["type_id"] = res.id
|
||||
}
|
||||
|
||||
const prodId = data.id || `simple-product-${Math.random() * 1000}`
|
||||
const toSave = manager.create(Product, {
|
||||
id: prodId,
|
||||
type_id: typeId,
|
||||
status: data.status,
|
||||
title: data.title || faker.commerce.productName(),
|
||||
is_giftcard: data.is_giftcard || false,
|
||||
discountable: !data.is_giftcard,
|
||||
profile_id: data.is_giftcard ? gcProfile.id : defaultProfile.id,
|
||||
})
|
||||
const toSave = manager.create(Product, productToCreate)
|
||||
|
||||
const product = await manager.save(toSave)
|
||||
await manager.save(toSave)
|
||||
|
||||
const optionId = `${prodId}-option`
|
||||
const options = data.options || [{ id: optionId, title: "Size" }]
|
||||
@@ -97,5 +111,5 @@ export const simpleProductFactory = async (
|
||||
await simpleProductVariantFactory(connection, factoryData)
|
||||
}
|
||||
|
||||
return product
|
||||
return await manager.findOne(Product, { id: prodId }, { relations: ["tags"] })
|
||||
}
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/medusa": "1.2.0-dev-1647336201011",
|
||||
"@medusajs/medusa": "1.2.1-dev-1648026403166",
|
||||
"faker": "^5.5.3",
|
||||
"medusa-interfaces": "1.2.0-dev-1647336201011",
|
||||
"medusa-interfaces": "1.2.1-dev-1648026403166",
|
||||
"typeorm": "^0.2.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/node": "^7.12.10",
|
||||
"babel-preset-medusa-package": "1.1.19-dev-1647336201011",
|
||||
"babel-preset-medusa-package": "1.1.19-dev-1648026403166",
|
||||
"jest": "^26.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
+1175
-1201
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user