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:
committed by
GitHub
parent
b7f699654b
commit
a610805917
@@ -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
@@ -1,4 +1,3 @@
|
||||
import { TotalsService } from "@medusajs/medusa"
|
||||
import { humanizeAmount } from "medusa-core-utils"
|
||||
import { FulfillmentService } from "medusa-interfaces"
|
||||
import Webshipper from "../utils/webshipper"
|
||||
@@ -114,13 +113,7 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
||||
|
||||
const fromOrder = await this.orderService_.retrieve(orderId, {
|
||||
select: ["total"],
|
||||
relations: [
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_address",
|
||||
"returns",
|
||||
],
|
||||
relations: ["discounts", "discounts.rule", "shipping_address", "returns"],
|
||||
})
|
||||
|
||||
const methodData = returnOrder.shipping_method.data
|
||||
@@ -145,7 +138,7 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
||||
}
|
||||
}
|
||||
|
||||
let docs = []
|
||||
const docs = []
|
||||
if (this.invoiceGenerator_) {
|
||||
const base64Invoice = await this.invoiceGenerator_.createReturnInvoice(
|
||||
fromOrder,
|
||||
@@ -338,7 +331,7 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
||||
}
|
||||
}
|
||||
|
||||
let id = fulfillment.id
|
||||
const id = fulfillment.id
|
||||
let visible_ref = `${fromOrder.display_id}-${id.substr(id.length - 4)}`
|
||||
let ext_ref = `${fromOrder.id}.${fulfillment.id}`
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ class OrderSubscriber {
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
@@ -149,7 +148,6 @@ class OrderSubscriber {
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
@@ -258,7 +256,6 @@ class OrderSubscriber {
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import SendGrid from "@sendgrid/mail"
|
||||
|
||||
import { NotificationService } from "medusa-interfaces"
|
||||
import { humanizeAmount, zeroDecimalCurrencies } from "medusa-core-utils"
|
||||
import { NotificationService } from "medusa-interfaces"
|
||||
|
||||
class SendGridService extends NotificationService {
|
||||
static identifier = "sendgrid"
|
||||
@@ -293,7 +292,7 @@ class SendGridService extends NotificationService {
|
||||
* @param {string} from - sender of email
|
||||
* @param {string} to - receiver of email
|
||||
* @param {Object} data - data to send in mail (match with template)
|
||||
* @returns {Promise} result of the send operation
|
||||
* @return {Promise} result of the send operation
|
||||
*/
|
||||
async sendEmail(options) {
|
||||
try {
|
||||
@@ -321,7 +320,6 @@ class SendGridService extends NotificationService {
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"shipping_methods.shipping_option",
|
||||
"payments",
|
||||
@@ -366,7 +364,6 @@ class SendGridService extends NotificationService {
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"shipping_methods.shipping_option",
|
||||
"payments",
|
||||
@@ -465,7 +462,6 @@ class SendGridService extends NotificationService {
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"shipping_methods.shipping_option",
|
||||
"payments",
|
||||
@@ -624,7 +620,6 @@ class SendGridService extends NotificationService {
|
||||
"items.tax_lines",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_address",
|
||||
"returns",
|
||||
],
|
||||
@@ -747,7 +742,6 @@ class SendGridService extends NotificationService {
|
||||
"items",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_address",
|
||||
"swaps",
|
||||
"swaps.additional_items",
|
||||
@@ -875,7 +869,6 @@ class SendGridService extends NotificationService {
|
||||
"items.tax_lines",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_address",
|
||||
"swaps",
|
||||
"swaps.additional_items",
|
||||
@@ -985,7 +978,6 @@ class SendGridService extends NotificationService {
|
||||
"items.tax_lines",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"swaps",
|
||||
"swaps.additional_items",
|
||||
"swaps.additional_items.tax_lines",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from "axios"
|
||||
import { zeroDecimalCurrencies, humanizeAmount } from "medusa-core-utils"
|
||||
import { humanizeAmount, zeroDecimalCurrencies } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
class SlackService extends BaseService {
|
||||
@@ -41,7 +41,6 @@ class SlackService extends BaseService {
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
@@ -69,7 +68,7 @@ class SlackService extends BaseService {
|
||||
return humanAmount.toFixed(2)
|
||||
}
|
||||
|
||||
let blocks = [
|
||||
const blocks = [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
@@ -147,7 +146,7 @@ class SlackService extends BaseService {
|
||||
include_tax: true,
|
||||
}
|
||||
)
|
||||
let line = {
|
||||
const line = {
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||
import { Type } from "class-transformer"
|
||||
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||
import omit from "lodash/omit"
|
||||
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { CustomerGroupService } from "../../../../services"
|
||||
import { CustomerGroup } from "../../../../models/customer-group"
|
||||
import { FindConfig } from "../../../../types/common"
|
||||
import { defaultAdminCustomerGroupsRelations } from "."
|
||||
import { CustomerGroup } from "../../../../models/customer-group"
|
||||
import { CustomerGroupService } from "../../../../services"
|
||||
import { FindConfig } from "../../../../types/common"
|
||||
import { FilterableCustomerGroupProps } from "../../../../types/customer-groups"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [get] /customer-groups
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("POST /admin/discounts/:discount_id/regions/:region_id", () => {
|
||||
"metadata",
|
||||
"valid_duration",
|
||||
],
|
||||
relations: ["rule", "parent_discount", "regions", "rule.valid_for"],
|
||||
relations: ["rule", "parent_discount", "regions", "rule.conditions"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("POST /admin/discounts/:discount_id/products/:product_id", () => {
|
||||
"metadata",
|
||||
"valid_duration",
|
||||
],
|
||||
relations: ["rule", "parent_discount", "regions", "rule.valid_for"],
|
||||
relations: ["rule", "parent_discount", "regions", "rule.conditions"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -166,6 +166,49 @@ describe("POST /admin/discounts", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails on xor constraint for conditions", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", "/admin/discounts", {
|
||||
payload: {
|
||||
code: "TEST",
|
||||
rule: {
|
||||
description: "Test",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
conditions: [
|
||||
{
|
||||
products: ["product1"],
|
||||
operator: "in",
|
||||
product_types: ["producttype1"],
|
||||
},
|
||||
],
|
||||
},
|
||||
starts_at: "02/02/2021 13:45",
|
||||
is_dynamic: true,
|
||||
valid_duration: "P1Y2M03DT04H05M",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("returns error", () => {
|
||||
expect(subject.body.message).toEqual(
|
||||
`Only one of products, product_types is allowed, Only one of product_types, products is allowed`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails on invalid date intervals", () => {
|
||||
let subject
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const defaultRelations = [
|
||||
"rule",
|
||||
"parent_discount",
|
||||
"regions",
|
||||
"rule.valid_for",
|
||||
"rule.conditions",
|
||||
]
|
||||
|
||||
describe("GET /admin/discounts/:discount_id", () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ const defaultRelations = [
|
||||
"rule",
|
||||
"parent_discount",
|
||||
"regions",
|
||||
"rule.valid_for",
|
||||
"rule.conditions",
|
||||
]
|
||||
|
||||
describe("DELETE /admin/discounts/:discount_id/regions/region_id", () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ const defaultRelations = [
|
||||
"rule",
|
||||
"parent_discount",
|
||||
"regions",
|
||||
"rule.valid_for",
|
||||
"rule.conditions",
|
||||
]
|
||||
|
||||
describe("DELETE /admin/discounts/:discount_id/products/:product_id", () => {
|
||||
|
||||
@@ -11,11 +11,16 @@ import {
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { defaultAdminDiscountsRelations } from "."
|
||||
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
|
||||
import { Discount } from "../../../../models/discount"
|
||||
import { DiscountConditionOperator } from "../../../../models/discount-condition"
|
||||
import DiscountService from "../../../../services/discount"
|
||||
import { IsGreaterThan } from "../../../../utils/validators/greater-than"
|
||||
import { AdminUpsertConditionsReq } from "../../../../types/discount"
|
||||
import { getRetrieveConfig } from "../../../../utils/get-query-config"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsGreaterThan } from "../../../../utils/validators/greater-than"
|
||||
import { IsISO8601Duration } from "../../../../utils/validators/iso8601-duration"
|
||||
import { AdminPostDiscountsDiscountParams } from "./update-discount"
|
||||
/**
|
||||
* @oas [post] /discounts
|
||||
* operationId: "PostDiscounts"
|
||||
@@ -74,16 +79,30 @@ import { IsISO8601Duration } from "../../../../utils/validators/iso8601-duration
|
||||
* discount:
|
||||
* $ref: "#/components/schemas/discount"
|
||||
*/
|
||||
|
||||
export default async (req, res) => {
|
||||
const validated = await validator(AdminPostDiscountsReq, req.body)
|
||||
|
||||
const discountService: DiscountService = req.scope.resolve("discountService")
|
||||
const created = await discountService.create(validated)
|
||||
const discount = await discountService.retrieve(
|
||||
created.id,
|
||||
defaultAdminDiscountsRelations
|
||||
console.log(validated.rule.conditions)
|
||||
|
||||
const validatedParams = await validator(
|
||||
AdminPostDiscountsDiscountParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const discountService: DiscountService = req.scope.resolve("discountService")
|
||||
|
||||
const created = await discountService.create(validated)
|
||||
|
||||
const config = getRetrieveConfig<Discount>(
|
||||
defaultAdminDiscountsFields,
|
||||
defaultAdminDiscountsRelations,
|
||||
validatedParams?.fields?.split(",") as (keyof Discount)[],
|
||||
validatedParams?.expand?.split(",")
|
||||
)
|
||||
|
||||
const discount = await discountService.retrieve(created.id, config)
|
||||
|
||||
res.status(200).json({ discount })
|
||||
}
|
||||
|
||||
@@ -132,7 +151,7 @@ export class AdminPostDiscountsReq {
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: object
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export class AdminPostDiscountsDiscountRule {
|
||||
@@ -153,6 +172,22 @@ export class AdminPostDiscountsDiscountRule {
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
valid_for?: string[]
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AdminCreateCondition)
|
||||
conditions?: AdminCreateCondition[]
|
||||
}
|
||||
|
||||
export class AdminCreateCondition extends AdminUpsertConditionsReq {
|
||||
@IsString()
|
||||
operator: DiscountConditionOperator
|
||||
}
|
||||
|
||||
export class AdminPostDiscountsParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from "class-validator"
|
||||
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
|
||||
import DiscountService from "../../../../services/discount"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
/**
|
||||
@@ -16,6 +17,7 @@ import { validator } from "../../../../utils/validator"
|
||||
* parameters:
|
||||
* - (path) id=* {string} The id of the Discount to create the dynamic code from."
|
||||
* - (body) code=* {string} The unique code that will be used to redeem the Discount.
|
||||
* - (body) usage_limit=* {number} amount of times the discount can be applied
|
||||
* - (body) metadata {object} An optional set of key-value paris to hold additional information.
|
||||
* tags:
|
||||
* - Discount
|
||||
@@ -44,7 +46,8 @@ export default async (req, res) => {
|
||||
)
|
||||
|
||||
const discount = await discountService.retrieve(created.id, {
|
||||
relations: ["rule", "rule.valid_for", "regions"],
|
||||
select: defaultAdminDiscountsFields,
|
||||
relations: defaultAdminDiscountsRelations,
|
||||
})
|
||||
|
||||
res.status(200).json({ discount })
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
|
||||
import DiscountService from "../../../../services/discount"
|
||||
|
||||
/**
|
||||
@@ -28,7 +29,8 @@ export default async (req, res) => {
|
||||
await discountService.deleteDynamicCode(discount_id, code)
|
||||
|
||||
const discount = await discountService.retrieve(discount_id, {
|
||||
relations: ["rule", "rule.valid_for", "regions"],
|
||||
select: defaultAdminDiscountsFields,
|
||||
relations: defaultAdminDiscountsRelations,
|
||||
})
|
||||
|
||||
res.status(200).json({ discount })
|
||||
|
||||
@@ -62,7 +62,7 @@ export default (app) => {
|
||||
return app
|
||||
}
|
||||
|
||||
export const defaultAdminDiscountsFields = [
|
||||
export const defaultAdminDiscountsFields: (keyof Discount)[] = [
|
||||
"id",
|
||||
"code",
|
||||
"is_dynamic",
|
||||
@@ -84,7 +84,7 @@ export const defaultAdminDiscountsRelations = [
|
||||
"rule",
|
||||
"parent_discount",
|
||||
"regions",
|
||||
"rule.valid_for",
|
||||
"rule.conditions",
|
||||
]
|
||||
|
||||
export type AdminDiscountsRes = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Type, Transform } from "class-transformer"
|
||||
import { Transform, Type } from "class-transformer"
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
@@ -9,12 +8,10 @@ import {
|
||||
} from "class-validator"
|
||||
import _, { pickBy } from "lodash"
|
||||
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
|
||||
import {
|
||||
AllocationType,
|
||||
DiscountRuleType,
|
||||
} from "../../../../models/discount-rule"
|
||||
import { Discount } from "../../../.."
|
||||
import DiscountService from "../../../../services/discount"
|
||||
import { DateComparisonOperator } from "../../../../types/common"
|
||||
import { FindConfig } from "../../../../types/common"
|
||||
import { AdminGetDiscountsDiscountRuleParams } from "../../../../types/discount"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
/**
|
||||
* @oas [get] /discounts
|
||||
@@ -46,7 +43,7 @@ export default async (req, res) => {
|
||||
|
||||
const discountService: DiscountService = req.scope.resolve("discountService")
|
||||
|
||||
const listConfig = {
|
||||
const listConfig: FindConfig<Discount> = {
|
||||
select: defaultAdminDiscountsFields,
|
||||
relations: defaultAdminDiscountsRelations,
|
||||
skip: validated.offset,
|
||||
@@ -69,17 +66,12 @@ export default async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
class AdminGetDiscountsDiscountRuleParams {
|
||||
@IsOptional()
|
||||
@IsEnum(DiscountRuleType)
|
||||
type: DiscountRuleType
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AllocationType)
|
||||
allocation: AllocationType
|
||||
}
|
||||
|
||||
export class AdminGetDiscountsParams {
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => AdminGetDiscountsDiscountRuleParams)
|
||||
rule?: AdminGetDiscountsDiscountRuleParams
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
q?: string
|
||||
@@ -94,11 +86,6 @@ export class AdminGetDiscountsParams {
|
||||
@Transform(({ value }) => value === "true")
|
||||
is_disabled?: boolean
|
||||
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => AdminGetDiscountsDiscountRuleParams)
|
||||
rule?: AdminGetDiscountsDiscountRuleParams
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
|
||||
import { Discount } from "../../../../models/discount"
|
||||
import { DiscountConditionOperator } from "../../../../models/discount-condition"
|
||||
import DiscountService from "../../../../services/discount"
|
||||
import { AdminUpsertConditionsReq } from "../../../../types/discount"
|
||||
import { getRetrieveConfig } from "../../../../utils/get-query-config"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsGreaterThan } from "../../../../utils/validators/greater-than"
|
||||
import { IsISO8601Duration } from "../../../../utils/validators/iso8601-duration"
|
||||
@@ -73,12 +77,24 @@ export default async (req, res) => {
|
||||
const { discount_id } = req.params
|
||||
|
||||
const validated = await validator(AdminPostDiscountsDiscountReq, req.body)
|
||||
|
||||
const validatedParams = await validator(
|
||||
AdminPostDiscountsDiscountParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const discountService: DiscountService = req.scope.resolve("discountService")
|
||||
|
||||
await discountService.update(discount_id, validated)
|
||||
const discount = await discountService.retrieve(discount_id, {
|
||||
select: defaultAdminDiscountsFields,
|
||||
relations: defaultAdminDiscountsRelations,
|
||||
})
|
||||
|
||||
const config = getRetrieveConfig<Discount>(
|
||||
defaultAdminDiscountsFields,
|
||||
defaultAdminDiscountsRelations,
|
||||
validatedParams?.fields?.split(",") as (keyof Discount)[],
|
||||
validatedParams?.expand?.split(",")
|
||||
)
|
||||
|
||||
const discount = await discountService.retrieve(discount_id, config)
|
||||
|
||||
res.status(200).json({ discount })
|
||||
}
|
||||
@@ -128,7 +144,7 @@ export class AdminPostDiscountsDiscountReq {
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: object
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export class AdminUpdateDiscountRule {
|
||||
@@ -151,8 +167,29 @@ export class AdminUpdateDiscountRule {
|
||||
@IsNotEmpty()
|
||||
allocation: string
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
valid_for?: string[]
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AdminUpsertCondition)
|
||||
conditions?: AdminUpsertCondition[]
|
||||
}
|
||||
|
||||
export class AdminUpsertCondition extends AdminUpsertConditionsReq {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
id?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
operator: DiscountConditionOperator
|
||||
}
|
||||
|
||||
export class AdminPostDiscountsDiscountParams {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
expand?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
fields?: string
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ export default async (req, res) => {
|
||||
relations: [
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"region",
|
||||
"items",
|
||||
|
||||
@@ -8,7 +8,6 @@ const defaultRelations = [
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Router } from "express"
|
||||
import { Order } from "../../../.."
|
||||
import middlewares from "../../../middlewares"
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import "reflect-metadata"
|
||||
import { Order } from "../../../.."
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -231,7 +231,6 @@ export const defaultAdminOrdersRelations = [
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
@@ -332,7 +331,6 @@ export const allowedAdminOrdersRelations = [
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defaultAdminTaxRatesFields, defaultAdminTaxRatesRelations } from "../"
|
||||
import { pick } from "lodash"
|
||||
import { FindConfig } from "../../../../../types/common"
|
||||
import { defaultAdminTaxRatesFields, defaultAdminTaxRatesRelations } from "../"
|
||||
import { TaxRate } from "../../../../.."
|
||||
import { FindConfig } from "../../../../../types/common"
|
||||
|
||||
export function pickByConfig<T>(
|
||||
obj: T | T[],
|
||||
@@ -64,7 +64,9 @@ export function getListConfig(
|
||||
expandFields = expand
|
||||
}
|
||||
|
||||
const orderBy = order ?? { created_at: "DESC" }
|
||||
const orderBy: Record<string, "DESC" | "ASC"> = order ?? {
|
||||
created_at: "DESC",
|
||||
}
|
||||
|
||||
return {
|
||||
select: includeFields.length ? includeFields : defaultAdminTaxRatesFields,
|
||||
|
||||
@@ -125,23 +125,25 @@ export const defaultStoreCartRelations = [
|
||||
"shipping_methods.shipping_option",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
]
|
||||
|
||||
export type StoreCartsRes = {
|
||||
cart: Omit<Cart, "refundable_amount" | "refunded_total">
|
||||
}
|
||||
|
||||
export type StoreCompleteCartRes = {
|
||||
type: "cart"
|
||||
data: Cart
|
||||
} | {
|
||||
type: "order"
|
||||
data: Order
|
||||
} | {
|
||||
type: "swap"
|
||||
data: Swap
|
||||
}
|
||||
export type StoreCompleteCartRes =
|
||||
| {
|
||||
type: "cart"
|
||||
data: Cart
|
||||
}
|
||||
| {
|
||||
type: "order"
|
||||
data: Order
|
||||
}
|
||||
| {
|
||||
type: "swap"
|
||||
data: Swap
|
||||
}
|
||||
|
||||
export type StoreCartsDeleteRes = DeleteResponse
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ export const defaultStoreOrdersRelations = [
|
||||
"shipping_methods",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"customer",
|
||||
"payments",
|
||||
"region",
|
||||
@@ -79,7 +78,6 @@ export const allowedStoreOrdersRelations = [
|
||||
"shipping_methods",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"customer",
|
||||
"payments",
|
||||
"region",
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class discountConditions1646324713514 implements MigrationInterface {
|
||||
name = "discountConditions1646324713514"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "discount_condition_type_enum" AS ENUM('products', 'product_types', 'product_collections', 'product_tags', 'customer_groups')`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "discount_condition_operator_enum" AS ENUM('in', 'not_in')`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "discount_condition" ("id" character varying NOT NULL, "type" "discount_condition_type_enum" NOT NULL, "operator" "discount_condition_operator_enum" NOT NULL, "discount_rule_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_e6b81d83133ddc21a2baf2e2204" PRIMARY KEY ("id"), CONSTRAINT "dctypeuniq" UNIQUE ("type", "operator", "discount_rule_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_efff700651718e452ca9580a62" ON "discount_condition" ("discount_rule_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "discount_condition_customer_group" ("customer_group_id" character varying NOT NULL, "condition_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "PK_cdc8b2277169a16b8b7d4c73e0e" PRIMARY KEY ("customer_group_id", "condition_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "discount_condition_product_collection" ("product_collection_id" character varying NOT NULL, "condition_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "PK_b3508fc787aa4a38705866cbb6d" PRIMARY KEY ("product_collection_id", "condition_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "discount_condition_product_tag" ("product_tag_id" character varying NOT NULL, "condition_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "PK_a95382c1e62205b121aa058682b" PRIMARY KEY ("product_tag_id", "condition_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "discount_condition_product_type" ("product_type_id" character varying NOT NULL, "condition_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "PK_35d538a5a24399d0df978df12ed" PRIMARY KEY ("product_type_id", "condition_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "discount_condition_product" ("product_id" character varying NOT NULL, "condition_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "PK_994eb4529fdbf14450d64ec17e8" PRIMARY KEY ("product_id", "condition_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f05132301e95bdab4ba1cf29a2" ON "discount_condition_product" ("condition_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_c759f53b2e48e8cfb50638fe4e" ON "discount_condition_product" ("product_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6ef23ce0b1d9cf9b5b833e52b9" ON "discount_condition_product_type" ("condition_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_e706deb68f52ab2756119b9e70" ON "discount_condition_product_type" ("product_type_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_fbb2499551ed074526f3ee3624" ON "discount_condition_product_tag" ("condition_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_01486cc9dc6b36bf658685535f" ON "discount_condition_product_tag" ("product_tag_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_a1c4f9cfb599ad1f0db39cadd5" ON "discount_condition_product_collection" ("condition_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_a0b05dc4257abe639cb75f8eae" ON "discount_condition_product_collection" ("product_collection_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_8486ee16e69013c645d0b8716b" ON "discount_condition_customer_group" ("condition_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_4d5f98645a67545d8dea42e2eb" ON "discount_condition_customer_group" ("customer_group_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition" ADD CONSTRAINT "FK_efff700651718e452ca9580a624" FOREIGN KEY ("discount_rule_id") REFERENCES "discount_rule"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_customer_group" ADD CONSTRAINT "FK_4d5f98645a67545d8dea42e2eb8" FOREIGN KEY ("customer_group_id") REFERENCES "customer_group"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_customer_group" ADD CONSTRAINT "FK_8486ee16e69013c645d0b8716b6" FOREIGN KEY ("condition_id") REFERENCES "discount_condition"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_collection" ADD CONSTRAINT "FK_a0b05dc4257abe639cb75f8eae2" FOREIGN KEY ("product_collection_id") REFERENCES "product_collection"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_collection" ADD CONSTRAINT "FK_a1c4f9cfb599ad1f0db39cadd5f" FOREIGN KEY ("condition_id") REFERENCES "discount_condition"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_tag" ADD CONSTRAINT "FK_01486cc9dc6b36bf658685535f6" FOREIGN KEY ("product_tag_id") REFERENCES "product_tag"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_tag" ADD CONSTRAINT "FK_fbb2499551ed074526f3ee36241" FOREIGN KEY ("condition_id") REFERENCES "discount_condition"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_type" ADD CONSTRAINT "FK_e706deb68f52ab2756119b9e704" FOREIGN KEY ("product_type_id") REFERENCES "product_type"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_type" ADD CONSTRAINT "FK_6ef23ce0b1d9cf9b5b833e52b9d" FOREIGN KEY ("condition_id") REFERENCES "discount_condition"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product" ADD CONSTRAINT "FK_c759f53b2e48e8cfb50638fe4e0" FOREIGN KEY ("product_id") REFERENCES "product"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product" ADD CONSTRAINT "FK_f05132301e95bdab4ba1cf29a24" FOREIGN KEY ("condition_id") REFERENCES "discount_condition"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product" DROP CONSTRAINT "FK_f05132301e95bdab4ba1cf29a24"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product" DROP CONSTRAINT "FK_c759f53b2e48e8cfb50638fe4e0"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_type" DROP CONSTRAINT "FK_6ef23ce0b1d9cf9b5b833e52b9d"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_type" DROP CONSTRAINT "FK_e706deb68f52ab2756119b9e704"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_tag" DROP CONSTRAINT "FK_fbb2499551ed074526f3ee36241"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_tag" DROP CONSTRAINT "FK_01486cc9dc6b36bf658685535f6"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_collection" DROP CONSTRAINT "FK_a1c4f9cfb599ad1f0db39cadd5f"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_collection" DROP CONSTRAINT "FK_a0b05dc4257abe639cb75f8eae2"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_customer_group" DROP CONSTRAINT "FK_8486ee16e69013c645d0b8716b6"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_customer_group" DROP CONSTRAINT "FK_4d5f98645a67545d8dea42e2eb8"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition" DROP CONSTRAINT "FK_efff700651718e452ca9580a624"`
|
||||
)
|
||||
await queryRunner.query(`DROP INDEX "IDX_4d5f98645a67545d8dea42e2eb"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_8486ee16e69013c645d0b8716b"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_a0b05dc4257abe639cb75f8eae"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_a1c4f9cfb599ad1f0db39cadd5"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_01486cc9dc6b36bf658685535f"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_fbb2499551ed074526f3ee3624"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_e706deb68f52ab2756119b9e70"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_6ef23ce0b1d9cf9b5b833e52b9"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_c759f53b2e48e8cfb50638fe4e"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_f05132301e95bdab4ba1cf29a2"`)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product" DROP COLUMN "metadata"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product" DROP COLUMN "updated_at"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product" DROP COLUMN "created_at"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_type" DROP COLUMN "metadata"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_type" DROP COLUMN "updated_at"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_type" DROP COLUMN "created_at"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_tag" DROP COLUMN "metadata"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_tag" DROP COLUMN "updated_at"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_tag" DROP COLUMN "created_at"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_collection" DROP COLUMN "metadata"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_collection" DROP COLUMN "updated_at"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_collection" DROP COLUMN "created_at"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_customer_group" DROP COLUMN "metadata"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_customer_group" DROP COLUMN "updated_at"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_customer_group" DROP COLUMN "created_at"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_customer_group" ADD "metadata" jsonb`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_customer_group" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_customer_group" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_collection" ADD "metadata" jsonb`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_collection" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_collection" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_tag" ADD "metadata" jsonb`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_tag" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_tag" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_type" ADD "metadata" jsonb`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_type" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product_type" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product" ADD "metadata" jsonb`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product" ADD "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_condition_product" ADD "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`
|
||||
)
|
||||
await queryRunner.query(`DROP TABLE "discount_condition_product"`)
|
||||
await queryRunner.query(`DROP TABLE "discount_condition_product_type"`)
|
||||
await queryRunner.query(`DROP TABLE "discount_condition_product_tag"`)
|
||||
await queryRunner.query(
|
||||
`DROP TABLE "discount_condition_product_collection"`
|
||||
)
|
||||
await queryRunner.query(`DROP TABLE "discount_condition_customer_group"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_efff700651718e452ca9580a62"`)
|
||||
await queryRunner.query(`DROP TABLE "discount_condition"`)
|
||||
await queryRunner.query(`DROP TYPE "discount_condition_operator_enum"`)
|
||||
await queryRunner.query(`DROP TYPE "discount_condition_type_enum"`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm"
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { CustomerGroup } from "./customer-group"
|
||||
import { DiscountCondition } from "./discount-condition"
|
||||
|
||||
@Entity()
|
||||
export class DiscountConditionCustomerGroup {
|
||||
@PrimaryColumn()
|
||||
customer_group_id: string
|
||||
|
||||
@PrimaryColumn()
|
||||
condition_id: string
|
||||
|
||||
@ManyToOne(() => CustomerGroup, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "customer_group_id" })
|
||||
customer_group?: CustomerGroup
|
||||
|
||||
@ManyToOne(() => DiscountCondition, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "condition_id" })
|
||||
discount_condition?: DiscountCondition
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema discount_condition_customer_group
|
||||
* title: "Product Tag Discount Condition"
|
||||
* description: "Associates a discount condition with a customer group"
|
||||
* x-resourceId: discount_condition_customer_group
|
||||
* properties:
|
||||
* customer_group_id:
|
||||
* description: "The id of the Product Tag"
|
||||
* type: string
|
||||
* condition_id:
|
||||
* description: "The id of the Discount Condition"
|
||||
* type: string
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted_at:
|
||||
* description: "The date with timezone at which the resource was deleted."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
*/
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm"
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { DiscountCondition } from "./discount-condition"
|
||||
import { ProductCollection } from "./product-collection"
|
||||
|
||||
@Entity()
|
||||
export class DiscountConditionProductCollection {
|
||||
@PrimaryColumn()
|
||||
product_collection_id: string
|
||||
|
||||
@PrimaryColumn()
|
||||
condition_id: string
|
||||
|
||||
@ManyToOne(() => ProductCollection, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "product_collection_id" })
|
||||
product_collection?: ProductCollection
|
||||
|
||||
@ManyToOne(() => DiscountCondition, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "condition_id" })
|
||||
discount_condition?: DiscountCondition
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema discount_condition_product_collection
|
||||
* title: "Product Collection Discount Condition"
|
||||
* description: "Associates a discount condition with a product collection"
|
||||
* x-resourceId: discount_condition_product_collection
|
||||
* properties:
|
||||
* product_collection_id:
|
||||
* description: "The id of the Product Collection"
|
||||
* type: string
|
||||
* condition_id:
|
||||
* description: "The id of the Discount Condition"
|
||||
* type: string
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted_at:
|
||||
* description: "The date with timezone at which the resource was deleted."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
*/
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm"
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { DiscountCondition } from "./discount-condition"
|
||||
import { ProductTag } from "./product-tag"
|
||||
|
||||
@Entity()
|
||||
export class DiscountConditionProductTag {
|
||||
@PrimaryColumn()
|
||||
product_tag_id: string
|
||||
|
||||
@PrimaryColumn()
|
||||
condition_id: string
|
||||
|
||||
@ManyToOne(() => ProductTag, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "product_tag_id" })
|
||||
product_tag?: ProductTag
|
||||
|
||||
@ManyToOne(() => DiscountCondition, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "condition_id" })
|
||||
discount_condition?: DiscountCondition
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema discount_condition_product_tag
|
||||
* title: "Product Tag Discount Condition"
|
||||
* description: "Associates a discount condition with a product tag"
|
||||
* x-resourceId: discount_condition_product_tag
|
||||
* properties:
|
||||
* product_tag_id:
|
||||
* description: "The id of the Product Tag"
|
||||
* type: string
|
||||
* condition_id:
|
||||
* description: "The id of the Discount Condition"
|
||||
* type: string
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted_at:
|
||||
* description: "The date with timezone at which the resource was deleted."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
*/
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm"
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { DiscountCondition } from "./discount-condition"
|
||||
import { ProductType } from "./product-type"
|
||||
|
||||
@Entity()
|
||||
export class DiscountConditionProductType {
|
||||
@PrimaryColumn()
|
||||
product_type_id: string
|
||||
|
||||
@PrimaryColumn()
|
||||
condition_id: string
|
||||
|
||||
@ManyToOne(() => ProductType, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "product_type_id" })
|
||||
product_type?: ProductType
|
||||
|
||||
@ManyToOne(() => DiscountCondition, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "condition_id" })
|
||||
discount_condition?: DiscountCondition
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema discount_condition_product_type
|
||||
* title: "Product Type Discount Condition"
|
||||
* description: "Associates a discount condition with a product type"
|
||||
* x-resourceId: discount_condition_product
|
||||
* properties:
|
||||
* product_type_id:
|
||||
* description: "The id of the Product Type"
|
||||
* type: string
|
||||
* condition_id:
|
||||
* description: "The id of the Discount Condition"
|
||||
* type: string
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted_at:
|
||||
* description: "The date with timezone at which the resource was deleted."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
*/
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm"
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { DiscountCondition } from "./discount-condition"
|
||||
import { Product } from "./product"
|
||||
|
||||
@Entity()
|
||||
export class DiscountConditionProduct {
|
||||
@PrimaryColumn()
|
||||
product_id: string
|
||||
|
||||
@PrimaryColumn()
|
||||
condition_id: string
|
||||
|
||||
@ManyToOne(() => Product, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "product_id" })
|
||||
product?: Product
|
||||
|
||||
@ManyToOne(() => DiscountCondition, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "condition_id" })
|
||||
discount_condition?: DiscountCondition
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema discount_condition_product
|
||||
* title: "Product Discount Condition"
|
||||
* description: "Associates a discount condition with a product"
|
||||
* x-resourceId: discount_condition_product
|
||||
* properties:
|
||||
* product_id:
|
||||
* description: "The id of the Product"
|
||||
* type: string
|
||||
* condition_id:
|
||||
* description: "The id of the Discount Condition"
|
||||
* type: string
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted_at:
|
||||
* description: "The date with timezone at which the resource was deleted."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
*/
|
||||
@@ -0,0 +1,186 @@
|
||||
import {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { CustomerGroup } from "./customer-group"
|
||||
import { DiscountRule } from "./discount-rule"
|
||||
import { Product } from "./product"
|
||||
import { ProductCollection } from "./product-collection"
|
||||
import { ProductTag } from "./product-tag"
|
||||
import { ProductType } from "./product-type"
|
||||
|
||||
export enum DiscountConditionType {
|
||||
PRODUCTS = "products",
|
||||
PRODUCT_TYPES = "product_types",
|
||||
PRODUCT_COLLECTIONS = "product_collections",
|
||||
PRODUCT_TAGS = "product_tags",
|
||||
CUSTOMER_GROUPS = "customer_groups",
|
||||
}
|
||||
|
||||
export enum DiscountConditionOperator {
|
||||
IN = "in",
|
||||
NOT_IN = "not_in",
|
||||
}
|
||||
|
||||
@Entity()
|
||||
@Unique("dctypeuniq", ["type", "operator", "discount_rule_id"])
|
||||
export class DiscountCondition {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@DbAwareColumn({
|
||||
type: "enum",
|
||||
enum: DiscountConditionType,
|
||||
})
|
||||
type: DiscountConditionType
|
||||
|
||||
@DbAwareColumn({
|
||||
type: "enum",
|
||||
enum: DiscountConditionOperator,
|
||||
})
|
||||
operator: DiscountConditionOperator
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
discount_rule_id: string
|
||||
|
||||
@ManyToOne(() => DiscountRule, (dr) => dr.conditions)
|
||||
@JoinColumn({ name: "discount_rule_id" })
|
||||
discount_rule: DiscountRule
|
||||
|
||||
@ManyToMany(() => Product)
|
||||
@JoinTable({
|
||||
name: "discount_condition_product",
|
||||
joinColumn: {
|
||||
name: "condition_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "product_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
products: Product[]
|
||||
|
||||
@ManyToMany(() => ProductType)
|
||||
@JoinTable({
|
||||
name: "discount_condition_product_type",
|
||||
joinColumn: {
|
||||
name: "condition_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "product_type_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
product_types: ProductType[]
|
||||
|
||||
@ManyToMany(() => ProductTag)
|
||||
@JoinTable({
|
||||
name: "discount_condition_product_tag",
|
||||
joinColumn: {
|
||||
name: "condition_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "product_tag_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
product_tags: ProductTag[]
|
||||
|
||||
@ManyToMany(() => ProductCollection)
|
||||
@JoinTable({
|
||||
name: "discount_condition_product_collection",
|
||||
joinColumn: {
|
||||
name: "condition_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "product_collection_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
product_collections: ProductCollection[]
|
||||
|
||||
@ManyToMany(() => CustomerGroup)
|
||||
@JoinTable({
|
||||
name: "discount_condition_customer_group",
|
||||
joinColumn: {
|
||||
name: "condition_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "customer_group_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
customer_groups: CustomerGroup[]
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DeleteDateColumn({ type: resolveDbType("timestamptz") })
|
||||
deleted_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert() {
|
||||
const id = ulid()
|
||||
this.id = `discon_${id}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema discount_condition
|
||||
* title: "Discount Condition"
|
||||
* description: "Holds rule conditions for when a discount is applicable"
|
||||
* x-resourceId: discount_condition
|
||||
* properties:
|
||||
* id:
|
||||
* description: "The id of the Discount Condition. Will be prefixed by `discon_`."
|
||||
* type: string
|
||||
* type:
|
||||
* description: "The type of the Condition"
|
||||
* type: string
|
||||
* enum:
|
||||
* - products
|
||||
* - product_types
|
||||
* - product_collections
|
||||
* - product_tags
|
||||
* - customer_groups
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* update_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted_at:
|
||||
* description: "The date with timezone at which the resource was deleted."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
*/
|
||||
@@ -1,19 +1,16 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Index,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryColumn,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
|
||||
|
||||
import { Product } from "./product"
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { DiscountCondition } from "./discount-condition"
|
||||
|
||||
export enum DiscountRuleType {
|
||||
FIXED = "fixed",
|
||||
@@ -50,19 +47,8 @@ export class DiscountRule {
|
||||
})
|
||||
allocation: AllocationType
|
||||
|
||||
@ManyToMany(() => Product, { cascade: true })
|
||||
@JoinTable({
|
||||
name: "discount_rule_products",
|
||||
joinColumn: {
|
||||
name: "discount_rule_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "product_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
valid_for: Product[]
|
||||
@OneToMany(() => DiscountCondition, (conditions) => conditions.discount_rule)
|
||||
conditions: DiscountCondition[]
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
@@ -111,11 +97,11 @@ export class DiscountRule {
|
||||
* enum:
|
||||
* - total
|
||||
* - item
|
||||
* valid_for:
|
||||
* description: "A set of Products that the discount can be used for."
|
||||
* conditions:
|
||||
* description: "A set of conditions that can be used to limit when the discount can be used"
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/product"
|
||||
* $ref: "#/components/schemas/discount_condition"
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToOne,
|
||||
JoinTable,
|
||||
JoinColumn,
|
||||
PrimaryColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
|
||||
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { DiscountRule } from "./discount-rule"
|
||||
import { Region } from "./region"
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ export const discounts = {
|
||||
type: "percentage",
|
||||
allocation: "item",
|
||||
value: 10,
|
||||
valid_for: [IdMap.getId("eur-8-us-10"), IdMap.getId("eur-10-us-12")],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -50,7 +49,6 @@ export const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 9,
|
||||
valid_for: [IdMap.getId("eur-8-us-10"), IdMap.getId("eur-10-us-12")],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -61,7 +59,6 @@ export const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 2,
|
||||
valid_for: [IdMap.getId("eur-8-us-10"), IdMap.getId("eur-10-us-12")],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -72,7 +69,6 @@ export const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 10,
|
||||
valid_for: [],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -84,7 +80,6 @@ export const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 10,
|
||||
valid_for: [],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -95,7 +90,6 @@ export const discounts = {
|
||||
type: "free_shipping",
|
||||
allocation: "total",
|
||||
value: 10,
|
||||
valid_for: [],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -106,7 +100,6 @@ export const discounts = {
|
||||
type: "free_shipping",
|
||||
allocation: "total",
|
||||
value: 10,
|
||||
valid_for: [],
|
||||
},
|
||||
regions: [IdMap.getId("us")],
|
||||
},
|
||||
@@ -122,12 +115,12 @@ export const discounts = {
|
||||
}
|
||||
|
||||
export const DiscountModelMock = {
|
||||
create: jest.fn().mockImplementation(data => Promise.resolve(data)),
|
||||
create: jest.fn().mockImplementation((data) => Promise.resolve(data)),
|
||||
updateOne: jest.fn().mockImplementation((query, update) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
findOne: jest.fn().mockImplementation(query => {
|
||||
findOne: jest.fn().mockImplementation((query) => {
|
||||
if (query._id === IdMap.getId("dynamic")) {
|
||||
return Promise.resolve(discounts.dynamic)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
import {
|
||||
DeleteResult,
|
||||
EntityRepository,
|
||||
EntityTarget,
|
||||
In,
|
||||
Not,
|
||||
Repository,
|
||||
} from "typeorm"
|
||||
import {
|
||||
DiscountCondition,
|
||||
DiscountConditionOperator,
|
||||
DiscountConditionType,
|
||||
} from "../models/discount-condition"
|
||||
import { DiscountConditionCustomerGroup } from "../models/discount-condition-customer-group"
|
||||
import { DiscountConditionProduct } from "../models/discount-condition-product"
|
||||
import { DiscountConditionProductCollection } from "../models/discount-condition-product-collection"
|
||||
import { DiscountConditionProductTag } from "../models/discount-condition-product-tag"
|
||||
import { DiscountConditionProductType } from "../models/discount-condition-product-type"
|
||||
|
||||
export enum DiscountConditionJoinTableForeignKey {
|
||||
PRODUCT_ID = "product_id",
|
||||
PRODUCT_TYPE_ID = "product_type_id",
|
||||
PRODUCT_COLLECTION_ID = "product_collection_id",
|
||||
PRODUCT_TAG_ID = "product_tag_id",
|
||||
CUSTOMER_GROUP_ID = "customer_group_id",
|
||||
}
|
||||
|
||||
type DiscountConditionResourceType = EntityTarget<
|
||||
| DiscountConditionProduct
|
||||
| DiscountConditionProductType
|
||||
| DiscountConditionProductCollection
|
||||
| DiscountConditionProductTag
|
||||
| DiscountConditionCustomerGroup
|
||||
>
|
||||
|
||||
@EntityRepository(DiscountCondition)
|
||||
export class DiscountConditionRepository extends Repository<DiscountCondition> {
|
||||
getJoinTableResourceIdentifiers(type: string): {
|
||||
joinTable: string
|
||||
resourceKey: string
|
||||
joinTableForeignKey: DiscountConditionJoinTableForeignKey
|
||||
conditionTable: DiscountConditionResourceType
|
||||
joinTableKey: string
|
||||
} {
|
||||
let conditionTable: DiscountConditionResourceType = DiscountConditionProduct
|
||||
|
||||
let joinTable = "product"
|
||||
let joinTableForeignKey: DiscountConditionJoinTableForeignKey =
|
||||
DiscountConditionJoinTableForeignKey.PRODUCT_ID
|
||||
let joinTableKey = "id"
|
||||
|
||||
// On the joined table (e.g. `product`), what key should be match on
|
||||
// (e.g `type_id` for product types and `id` for products)
|
||||
let resourceKey
|
||||
|
||||
switch (type) {
|
||||
case DiscountConditionType.PRODUCTS: {
|
||||
resourceKey = "id"
|
||||
joinTableForeignKey = DiscountConditionJoinTableForeignKey.PRODUCT_ID
|
||||
joinTable = "product"
|
||||
|
||||
conditionTable = DiscountConditionProduct
|
||||
break
|
||||
}
|
||||
case DiscountConditionType.PRODUCT_TYPES: {
|
||||
resourceKey = "type_id"
|
||||
joinTableForeignKey =
|
||||
DiscountConditionJoinTableForeignKey.PRODUCT_TYPE_ID
|
||||
joinTable = "product"
|
||||
|
||||
conditionTable = DiscountConditionProductType
|
||||
break
|
||||
}
|
||||
case DiscountConditionType.PRODUCT_COLLECTIONS: {
|
||||
resourceKey = "collection_id"
|
||||
joinTableForeignKey =
|
||||
DiscountConditionJoinTableForeignKey.PRODUCT_COLLECTION_ID
|
||||
joinTable = "product"
|
||||
|
||||
conditionTable = DiscountConditionProductCollection
|
||||
break
|
||||
}
|
||||
case DiscountConditionType.PRODUCT_TAGS: {
|
||||
joinTableKey = "product_id"
|
||||
resourceKey = "product_tag_id"
|
||||
joinTableForeignKey =
|
||||
DiscountConditionJoinTableForeignKey.PRODUCT_TAG_ID
|
||||
joinTable = "product_tags"
|
||||
|
||||
conditionTable = DiscountConditionProductTag
|
||||
break
|
||||
}
|
||||
case DiscountConditionType.CUSTOMER_GROUPS: {
|
||||
joinTableKey = "customer_id"
|
||||
resourceKey = "customer_group_id"
|
||||
joinTable = "customer_group_customers"
|
||||
joinTableForeignKey =
|
||||
DiscountConditionJoinTableForeignKey.CUSTOMER_GROUP_ID
|
||||
|
||||
conditionTable = DiscountConditionCustomerGroup
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
joinTable,
|
||||
joinTableKey,
|
||||
resourceKey,
|
||||
joinTableForeignKey,
|
||||
conditionTable,
|
||||
}
|
||||
}
|
||||
|
||||
async removeConditionResources(
|
||||
id: string,
|
||||
type: DiscountConditionType,
|
||||
resourceIds: string[]
|
||||
): Promise<DeleteResult | void> {
|
||||
const { conditionTable, joinTableForeignKey } =
|
||||
this.getJoinTableResourceIdentifiers(type)
|
||||
|
||||
if (!conditionTable || !joinTableForeignKey) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return await this.createQueryBuilder()
|
||||
.delete()
|
||||
.from(conditionTable)
|
||||
.where({ condition_id: id, [joinTableForeignKey]: In(resourceIds) })
|
||||
.execute()
|
||||
}
|
||||
|
||||
async addConditionResources(
|
||||
conditionId: string,
|
||||
resourceIds: string[],
|
||||
type: DiscountConditionType,
|
||||
overrideExisting = false
|
||||
): Promise<
|
||||
(
|
||||
| DiscountConditionProduct
|
||||
| DiscountConditionProductType
|
||||
| DiscountConditionProductCollection
|
||||
| DiscountConditionProductTag
|
||||
| DiscountConditionCustomerGroup
|
||||
)[]
|
||||
> {
|
||||
let toInsert: { condition_id: string; [x: string]: string }[] | [] = []
|
||||
|
||||
const { conditionTable, joinTableForeignKey } =
|
||||
this.getJoinTableResourceIdentifiers(type)
|
||||
|
||||
if (!conditionTable || !joinTableForeignKey) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
toInsert = resourceIds.map((rId) => ({
|
||||
condition_id: conditionId,
|
||||
[joinTableForeignKey]: rId,
|
||||
}))
|
||||
|
||||
const insertResult = await this.createQueryBuilder()
|
||||
.insert()
|
||||
.orIgnore(true)
|
||||
.into(conditionTable)
|
||||
.values(toInsert)
|
||||
.execute()
|
||||
|
||||
if (overrideExisting) {
|
||||
await this.createQueryBuilder()
|
||||
.delete()
|
||||
.from(conditionTable)
|
||||
.where({
|
||||
condition_id: conditionId,
|
||||
[joinTableForeignKey]: Not(In(resourceIds)),
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
|
||||
return await this.manager
|
||||
.createQueryBuilder(conditionTable, "discon")
|
||||
.select()
|
||||
.where(insertResult.identifiers)
|
||||
.getMany()
|
||||
}
|
||||
|
||||
async queryConditionTable({ type, condId, resourceId }): Promise<number> {
|
||||
const {
|
||||
conditionTable,
|
||||
joinTable,
|
||||
joinTableForeignKey,
|
||||
resourceKey,
|
||||
joinTableKey,
|
||||
} = this.getJoinTableResourceIdentifiers(type)
|
||||
|
||||
return await this.manager
|
||||
.createQueryBuilder(conditionTable, "dc")
|
||||
.innerJoin(
|
||||
joinTable,
|
||||
"resource",
|
||||
`dc.${joinTableForeignKey} = resource.${resourceKey} and resource.${joinTableKey} = :resourceId `,
|
||||
{
|
||||
resourceId,
|
||||
}
|
||||
)
|
||||
.where(`dc.condition_id = :conditionId`, {
|
||||
conditionId: condId,
|
||||
})
|
||||
.getCount()
|
||||
}
|
||||
|
||||
async isValidForProduct(
|
||||
discountRuleId: string,
|
||||
productId: string
|
||||
): Promise<boolean> {
|
||||
const discountConditions = await this.createQueryBuilder("discon")
|
||||
.select(["discon.id", "discon.type", "discon.operator"])
|
||||
.where("discon.discount_rule_id = :discountRuleId", {
|
||||
discountRuleId,
|
||||
})
|
||||
.getMany()
|
||||
|
||||
// in case of no discount conditions, we assume that the discount
|
||||
// is valid for all
|
||||
if (!discountConditions.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
// retrieve all conditions for each type where condition type id is in jointable (products, product_types, product_collections, product_tags)
|
||||
// "E.g. for a given product condition, give me all products affected by it"
|
||||
// for each of these types, we check:
|
||||
// if condition operation is `in` and the query for conditions defined for the given type is empty, the discount is invalid
|
||||
// if condition operation is `not_in` and the query for conditions defined for the given type is not empty, the discount is invalid
|
||||
for (const condition of discountConditions) {
|
||||
const numConditions = await this.queryConditionTable({
|
||||
type: condition.type,
|
||||
condId: condition.id,
|
||||
resourceId: productId,
|
||||
})
|
||||
|
||||
if (
|
||||
condition.operator === DiscountConditionOperator.IN &&
|
||||
numConditions === 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
condition.operator === DiscountConditionOperator.NOT_IN &&
|
||||
numConditions > 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async canApplyForCustomer(
|
||||
discountRuleId: string,
|
||||
customerId: string
|
||||
): Promise<boolean> {
|
||||
const discountConditions = await this.createQueryBuilder("discon")
|
||||
.select(["discon.id", "discon.type", "discon.operator"])
|
||||
.where("discon.discount_rule_id = :discountRuleId", {
|
||||
discountRuleId,
|
||||
})
|
||||
.getMany()
|
||||
|
||||
// in case of no discount conditions, we assume that the discount
|
||||
// is valid for all
|
||||
if (!discountConditions.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
// retrieve conditions for customer groups
|
||||
// for each customer group
|
||||
// if condition operation is `in` and the query for customer group conditions is empty, the discount is invalid
|
||||
// if condition operation is `not_in` and the query for customer group conditions is not empty, the discount is invalid
|
||||
for (const condition of discountConditions) {
|
||||
const numConditions = await this.queryConditionTable({
|
||||
type: "customer_groups",
|
||||
condId: condition.id,
|
||||
resourceId: customerId,
|
||||
})
|
||||
|
||||
if (
|
||||
condition.operator === DiscountConditionOperator.IN &&
|
||||
numConditions === 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
condition.operator === DiscountConditionOperator.NOT_IN &&
|
||||
numConditions > 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ export const discounts = {
|
||||
type: "percentage",
|
||||
allocation: "item",
|
||||
value: 10,
|
||||
valid_for: [IdMap.getId("eur-8-us-10"), IdMap.getId("eur-10-us-12")],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -50,7 +49,6 @@ export const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 9,
|
||||
valid_for: [IdMap.getId("eur-8-us-10"), IdMap.getId("eur-10-us-12")],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -61,7 +59,6 @@ export const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 2,
|
||||
valid_for: [IdMap.getId("eur-8-us-10"), IdMap.getId("eur-10-us-12")],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -72,7 +69,6 @@ export const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 10,
|
||||
valid_for: [],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -84,7 +80,6 @@ export const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 10,
|
||||
valid_for: [],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -95,7 +90,6 @@ export const discounts = {
|
||||
type: "free_shipping",
|
||||
allocation: "total",
|
||||
value: 10,
|
||||
valid_for: [],
|
||||
},
|
||||
regions: [IdMap.getId("region-france")],
|
||||
},
|
||||
@@ -106,7 +100,6 @@ export const discounts = {
|
||||
type: "free_shipping",
|
||||
allocation: "total",
|
||||
value: 10,
|
||||
valid_for: [],
|
||||
},
|
||||
regions: [IdMap.getId("us")],
|
||||
},
|
||||
@@ -125,10 +118,10 @@ export const DiscountServiceMock = {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
create: jest.fn().mockImplementation(data => {
|
||||
create: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve(data)
|
||||
}),
|
||||
retrieveByCode: jest.fn().mockImplementation(data => {
|
||||
retrieveByCode: jest.fn().mockImplementation((data) => {
|
||||
if (data === "10%OFF") {
|
||||
return Promise.resolve(discounts.total10Percent)
|
||||
}
|
||||
@@ -140,7 +133,7 @@ export const DiscountServiceMock = {
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation(data => {
|
||||
retrieve: jest.fn().mockImplementation((data) => {
|
||||
if (data === IdMap.getId("total10")) {
|
||||
return Promise.resolve(discounts.total10Percent)
|
||||
}
|
||||
@@ -152,20 +145,20 @@ export const DiscountServiceMock = {
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
update: jest.fn().mockImplementation(data => {
|
||||
update: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
delete: jest.fn().mockImplementation(data => {
|
||||
delete: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("total10"),
|
||||
object: "discount",
|
||||
deleted: true,
|
||||
})
|
||||
}),
|
||||
list: jest.fn().mockImplementation(data => {
|
||||
list: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve([{}])
|
||||
}),
|
||||
listAndCount: jest.fn().mockImplementation(data => {
|
||||
listAndCount: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve([{}])
|
||||
}),
|
||||
addRegion: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import _ from "lodash"
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import CartService from "../cart"
|
||||
import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -209,7 +209,7 @@ describe("CartService", () => {
|
||||
email: "email@test.com",
|
||||
})
|
||||
),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -317,14 +317,14 @@ describe("CartService", () => {
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
create: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const shippingOptionService = {
|
||||
deleteShippingMethod: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -505,7 +505,7 @@ describe("CartService", () => {
|
||||
const lineItemService = {
|
||||
delete: jest.fn(),
|
||||
update: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -547,7 +547,7 @@ describe("CartService", () => {
|
||||
|
||||
const shippingOptionService = {
|
||||
deleteShippingMethod: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -653,7 +653,6 @@ describe("CartService", () => {
|
||||
"region.countries",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"discounts.regions",
|
||||
"items.tax_lines",
|
||||
"region.tax_rates",
|
||||
@@ -668,7 +667,7 @@ describe("CartService", () => {
|
||||
describe("updateLineItem", () => {
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -764,7 +763,7 @@ describe("CartService", () => {
|
||||
email: data.email,
|
||||
})
|
||||
),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -986,7 +985,7 @@ describe("CartService", () => {
|
||||
const lineItemService = {
|
||||
update: jest.fn((r) => r),
|
||||
delete: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1036,7 +1035,7 @@ describe("CartService", () => {
|
||||
deleteSession: jest.fn(),
|
||||
updateSession: jest.fn(),
|
||||
createSession: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1254,7 +1253,7 @@ describe("CartService", () => {
|
||||
deleteSession: jest.fn(),
|
||||
updateSession: jest.fn(),
|
||||
createSession: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1412,7 +1411,7 @@ describe("CartService", () => {
|
||||
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1425,7 +1424,7 @@ describe("CartService", () => {
|
||||
})
|
||||
}),
|
||||
deleteShippingMethod: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1469,11 +1468,9 @@ describe("CartService", () => {
|
||||
IdMap.getId("option"),
|
||||
data
|
||||
)
|
||||
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
|
||||
IdMap.getId("option"),
|
||||
data,
|
||||
{ cart: cart1 }
|
||||
)
|
||||
expect(
|
||||
shippingOptionService.createShippingMethod
|
||||
).toHaveBeenCalledWith(IdMap.getId("option"), data, { cart: cart1 })
|
||||
})
|
||||
|
||||
it("successfully overrides existing profile shipping method", async () => {
|
||||
@@ -1485,11 +1482,9 @@ describe("CartService", () => {
|
||||
IdMap.getId("profile1"),
|
||||
data
|
||||
)
|
||||
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
|
||||
IdMap.getId("profile1"),
|
||||
data,
|
||||
{ cart: cart2 }
|
||||
)
|
||||
expect(
|
||||
shippingOptionService.createShippingMethod
|
||||
).toHaveBeenCalledWith(IdMap.getId("profile1"), data, { cart: cart2 })
|
||||
expect(shippingOptionService.deleteShippingMethod).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("ship1"),
|
||||
shipping_option: {
|
||||
@@ -1515,11 +1510,9 @@ describe("CartService", () => {
|
||||
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes(
|
||||
1
|
||||
)
|
||||
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
|
||||
IdMap.getId("additional"),
|
||||
data,
|
||||
{ cart: cart2 }
|
||||
)
|
||||
expect(
|
||||
shippingOptionService.createShippingMethod
|
||||
).toHaveBeenCalledWith(IdMap.getId("additional"), data, { cart: cart2 })
|
||||
})
|
||||
|
||||
it("updates item shipping", async () => {
|
||||
@@ -1539,11 +1532,9 @@ describe("CartService", () => {
|
||||
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledTimes(
|
||||
1
|
||||
)
|
||||
expect(shippingOptionService.createShippingMethod).toHaveBeenCalledWith(
|
||||
IdMap.getId("profile1"),
|
||||
data,
|
||||
{ cart: cart3 }
|
||||
)
|
||||
expect(
|
||||
shippingOptionService.createShippingMethod
|
||||
).toHaveBeenCalledWith(IdMap.getId("profile1"), data, { cart: cart3 })
|
||||
|
||||
expect(lineItemService.update).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.update).toHaveBeenCalledWith(IdMap.getId("line"), {
|
||||
@@ -1610,6 +1601,20 @@ describe("CartService", () => {
|
||||
region_id: IdMap.getId("good"),
|
||||
})
|
||||
}
|
||||
if (q.where.id === "with-d-and-customer") {
|
||||
return Promise.resolve({
|
||||
id: "with-d-and-customer",
|
||||
discounts: [
|
||||
{
|
||||
code: "ApplicableForCustomer",
|
||||
rule: {
|
||||
type: "fixed",
|
||||
},
|
||||
},
|
||||
],
|
||||
region_id: IdMap.getId("good"),
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("cart"),
|
||||
discounts: [],
|
||||
@@ -1718,6 +1723,19 @@ describe("CartService", () => {
|
||||
ends_at: getOffsetDate(10),
|
||||
})
|
||||
}
|
||||
if (code === "ApplicableForCustomer") {
|
||||
return Promise.resolve({
|
||||
id: "ApplicableForCustomer",
|
||||
code: "ApplicableForCustomer",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
id: "test-rule",
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(10),
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
@@ -1727,6 +1745,17 @@ describe("CartService", () => {
|
||||
},
|
||||
})
|
||||
}),
|
||||
canApplyForCustomer: jest
|
||||
.fn()
|
||||
.mockImplementation((ruleId, customerId) => {
|
||||
if (ruleId === "test-rule") {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
if (!customerId) {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
return Promise.resolve(false)
|
||||
}),
|
||||
}
|
||||
|
||||
const cartService = new CartService({
|
||||
@@ -1953,6 +1982,45 @@ describe("CartService", () => {
|
||||
})
|
||||
).rejects.toThrow("The discount is not available in current region")
|
||||
})
|
||||
|
||||
it("successfully applies discount with a check for customer applicableness", async () => {
|
||||
await cartService.update("with-d-and-customer", {
|
||||
discounts: [
|
||||
{
|
||||
code: "ApplicableForCustomer",
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
||||
"cart.updated",
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
expect(cartRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(cartRepository.save).toHaveBeenCalledWith({
|
||||
id: "with-d-and-customer",
|
||||
region_id: IdMap.getId("good"),
|
||||
discount_total: 0,
|
||||
shipping_total: 0,
|
||||
subtotal: 0,
|
||||
tax_total: 0,
|
||||
total: 0,
|
||||
discounts: [
|
||||
{
|
||||
id: "ApplicableForCustomer",
|
||||
code: "ApplicableForCustomer",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
id: "test-rule",
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: expect.any(Date),
|
||||
ends_at: expect.any(Date),
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("removeDiscount", () => {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import DiscountService from "../discount"
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { exportAllDeclaration } from "@babel/types"
|
||||
import DiscountService from "../discount"
|
||||
|
||||
describe("DiscountService", () => {
|
||||
describe("create", () => {
|
||||
@@ -15,7 +13,7 @@ describe("DiscountService", () => {
|
||||
id: IdMap.getId("france"),
|
||||
}
|
||||
},
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -309,121 +307,6 @@ describe("DiscountService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("addValidProduct", () => {
|
||||
const discountRepository = MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("total10"),
|
||||
rule: {
|
||||
id: IdMap.getId("test-rule"),
|
||||
valid_for: [{ id: IdMap.getId("test-product") }],
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const discountRuleRepository = MockRepository({})
|
||||
|
||||
const productService = {
|
||||
retrieve: () => {
|
||||
return {
|
||||
id: IdMap.getId("test-product-2"),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const discountService = new DiscountService({
|
||||
manager: MockManager,
|
||||
discountRepository,
|
||||
discountRuleRepository,
|
||||
productService,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully adds a product", async () => {
|
||||
await discountService.addValidProduct(
|
||||
IdMap.getId("total10"),
|
||||
IdMap.getId("test-product-2")
|
||||
)
|
||||
|
||||
expect(discountRuleRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(discountRuleRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("test-rule"),
|
||||
valid_for: [
|
||||
{ id: IdMap.getId("test-product") },
|
||||
{ id: IdMap.getId("test-product-2") },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully resolves if product already exists", async () => {
|
||||
await discountService.addValidProduct(
|
||||
IdMap.getId("total10"),
|
||||
IdMap.getId("test-product")
|
||||
)
|
||||
|
||||
expect(discountRuleRepository.save).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("removeValidVariant", () => {
|
||||
const discountRepository = MockRepository({
|
||||
findOne: () =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("total10"),
|
||||
rule: {
|
||||
id: IdMap.getId("test-rule"),
|
||||
valid_for: [{ id: IdMap.getId("test-product") }],
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const discountRuleRepository = MockRepository({})
|
||||
|
||||
const productService = {
|
||||
retrieve: () => {
|
||||
return {
|
||||
id: IdMap.getId("test-product"),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const discountService = new DiscountService({
|
||||
manager: MockManager,
|
||||
discountRepository,
|
||||
discountRuleRepository,
|
||||
productService,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully removes a product", async () => {
|
||||
await discountService.removeValidProduct(
|
||||
IdMap.getId("total10"),
|
||||
IdMap.getId("test-product")
|
||||
)
|
||||
|
||||
expect(discountRuleRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(discountRuleRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("test-rule"),
|
||||
valid_for: [],
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully resolve if product does not exist", async () => {
|
||||
await discountService.removeValidProduct(
|
||||
IdMap.getId("total10"),
|
||||
IdMap.getId("test-product-2")
|
||||
)
|
||||
|
||||
expect(discountRuleRepository.save).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("addRegion", () => {
|
||||
const discountRepository = MockRepository({
|
||||
findOne: (q) => {
|
||||
@@ -638,7 +521,7 @@ describe("DiscountService", () => {
|
||||
expect(discountRepository.findAndCount).toHaveBeenCalledWith({
|
||||
where: expect.anything(),
|
||||
skip: 0,
|
||||
take: 50,
|
||||
take: 20,
|
||||
order: { created_at: "DESC" },
|
||||
})
|
||||
})
|
||||
@@ -658,4 +541,198 @@ describe("DiscountService", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("calculateDiscountForLineItem", () => {
|
||||
const discountRepository = MockRepository({
|
||||
findOne: ({ where }) => {
|
||||
if (where.id === "disc_percentage") {
|
||||
return Promise.resolve({
|
||||
code: "MEDUSA",
|
||||
rule: {
|
||||
type: "percentage",
|
||||
allocation: "total",
|
||||
value: 15,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (where.id === "disc_fixed_total") {
|
||||
return Promise.resolve({
|
||||
code: "MEDUSA",
|
||||
rule: {
|
||||
type: "fixed",
|
||||
allocation: "total",
|
||||
value: 400,
|
||||
},
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: "disc_fixed",
|
||||
code: "MEDUSA",
|
||||
rule: {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 200,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const totalsService = {
|
||||
getSubtotal: () => {
|
||||
return 1100
|
||||
},
|
||||
}
|
||||
|
||||
const discountService = new DiscountService({
|
||||
manager: MockManager,
|
||||
discountRepository,
|
||||
totalsService,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("correctly calculates fixed + item discount", async () => {
|
||||
const adjustment = await discountService.calculateDiscountForLineItem(
|
||||
"disc_fixed",
|
||||
{
|
||||
unit_price: 300,
|
||||
quantity: 2,
|
||||
allow_discounts: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(adjustment).toBe(400)
|
||||
})
|
||||
|
||||
it("correctly calculates fixed + total discount", async () => {
|
||||
const adjustment1 = await discountService.calculateDiscountForLineItem(
|
||||
"disc_fixed_total",
|
||||
{
|
||||
unit_price: 400,
|
||||
quantity: 2,
|
||||
allow_discounts: true,
|
||||
}
|
||||
)
|
||||
|
||||
const adjustment2 = await discountService.calculateDiscountForLineItem(
|
||||
"disc_fixed_total",
|
||||
{
|
||||
unit_price: 300,
|
||||
quantity: 1,
|
||||
allow_discounts: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(adjustment1).toBe(291)
|
||||
expect(adjustment2).toBe(109)
|
||||
})
|
||||
|
||||
it("returns line item amount if discount exceeds lime item price", async () => {
|
||||
const adjustment = await discountService.calculateDiscountForLineItem(
|
||||
"disc_fixed",
|
||||
{
|
||||
unit_price: 100,
|
||||
quantity: 1,
|
||||
allow_discounts: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(adjustment).toBe(100)
|
||||
})
|
||||
|
||||
it("correctly calculates percentage discount", async () => {
|
||||
const adjustment = await discountService.calculateDiscountForLineItem(
|
||||
"disc_percentage",
|
||||
{
|
||||
unit_price: 400,
|
||||
quantity: 2,
|
||||
allow_discounts: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(adjustment).toBe(120)
|
||||
})
|
||||
|
||||
it("returns full amount if exceeds total line item amount", async () => {
|
||||
const adjustment = await discountService.calculateDiscountForLineItem(
|
||||
"disc_fixed",
|
||||
{
|
||||
unit_price: 50,
|
||||
quantity: 2,
|
||||
allow_discounts: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(adjustment).toBe(100)
|
||||
})
|
||||
|
||||
it("returns early if discounts are not allowed", async () => {
|
||||
const adjustment = await discountService.calculateDiscountForLineItem(
|
||||
"disc_percentage",
|
||||
{
|
||||
unit_price: 400,
|
||||
quantity: 2,
|
||||
allow_discounts: false,
|
||||
}
|
||||
)
|
||||
|
||||
expect(adjustment).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("canApplyForCustomer", () => {
|
||||
const discountConditionRepository = {
|
||||
canApplyForCustomer: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(true)),
|
||||
}
|
||||
|
||||
const customerService = {
|
||||
retrieve: jest.fn().mockImplementation((id) => {
|
||||
if (id === "customer-no-groups") {
|
||||
return Promise.resolve({ id: "customer-no-groups" })
|
||||
}
|
||||
if (id === "customer-with-groups") {
|
||||
return Promise.resolve({
|
||||
id: "customer-with-groups",
|
||||
groups: [{ id: "group-1" }],
|
||||
})
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const discountService = new DiscountService({
|
||||
manager: MockManager,
|
||||
discountConditionRepository,
|
||||
customerService,
|
||||
})
|
||||
|
||||
it("returns false on undefined customer id", async () => {
|
||||
const res = await discountService.canApplyForCustomer("rule-1")
|
||||
|
||||
expect(res).toBe(false)
|
||||
|
||||
expect(
|
||||
discountConditionRepository.canApplyForCustomer
|
||||
).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it("returns true on customer with groups", async () => {
|
||||
const res = await discountService.canApplyForCustomer(
|
||||
"rule-1",
|
||||
"customer-with-groups"
|
||||
)
|
||||
|
||||
expect(res).toBe(true)
|
||||
|
||||
expect(
|
||||
discountConditionRepository.canApplyForCustomer
|
||||
).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
discountConditionRepository.canApplyForCustomer
|
||||
).toHaveBeenCalledWith("rule-1", "customer-with-groups")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("OrderService", () => {
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -78,20 +78,20 @@ describe("OrderService", () => {
|
||||
})
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
const shippingOptionService = {
|
||||
updateShippingMethod: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
const giftCardService = {
|
||||
update: jest.fn(),
|
||||
createTransaction: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -103,7 +103,7 @@ describe("OrderService", () => {
|
||||
cancelPayment: jest.fn().mockImplementation((payment) => {
|
||||
return Promise.resolve({ ...payment, status: "cancelled" })
|
||||
}),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -142,7 +142,7 @@ describe("OrderService", () => {
|
||||
total: 100,
|
||||
})
|
||||
}),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -230,7 +230,6 @@ describe("OrderService", () => {
|
||||
"items",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"gift_cards",
|
||||
"shipping_methods",
|
||||
],
|
||||
@@ -456,7 +455,7 @@ describe("OrderService", () => {
|
||||
await expect(res).rejects.toThrow(
|
||||
"Variant with id: variant-1 does not have the required inventory"
|
||||
)
|
||||
//check to see if payment is cancelled
|
||||
// check to see if payment is cancelled
|
||||
expect(
|
||||
orderService.paymentProviderService_.cancelPayment
|
||||
).toHaveBeenCalledTimes(1)
|
||||
@@ -634,14 +633,14 @@ describe("OrderService", () => {
|
||||
|
||||
const fulfillmentService = {
|
||||
cancelFulfillment: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const paymentProviderService = {
|
||||
cancelPayment: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -738,7 +737,7 @@ describe("OrderService", () => {
|
||||
? Promise.reject()
|
||||
: Promise.resolve({ ...p, captured_at: "notnull" })
|
||||
),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -843,7 +842,7 @@ describe("OrderService", () => {
|
||||
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -856,7 +855,7 @@ describe("OrderService", () => {
|
||||
},
|
||||
])
|
||||
}),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1023,7 +1022,7 @@ describe("OrderService", () => {
|
||||
})
|
||||
}
|
||||
}),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1092,7 +1091,7 @@ describe("OrderService", () => {
|
||||
.mockImplementation((p) =>
|
||||
p.id === "payment_fail" ? Promise.reject() : Promise.resolve()
|
||||
),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1233,7 +1232,7 @@ describe("OrderService", () => {
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({})),
|
||||
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1367,7 +1366,7 @@ describe("OrderService", () => {
|
||||
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1390,7 +1389,7 @@ describe("OrderService", () => {
|
||||
],
|
||||
})
|
||||
}),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1417,7 +1416,9 @@ describe("OrderService", () => {
|
||||
)
|
||||
|
||||
expect(fulfillmentService.createShipment).toHaveBeenCalledTimes(1)
|
||||
expect(fulfillmentService.createShipment).toHaveBeenCalledWith(
|
||||
expect(
|
||||
fulfillmentService.createShipment
|
||||
).toHaveBeenCalledWith(
|
||||
IdMap.getId("fulfillment"),
|
||||
[{ tracking_number: "1234" }, { tracking_number: "2345" }],
|
||||
{ metadata: undefined, no_notification: true }
|
||||
@@ -1509,7 +1510,7 @@ describe("OrderService", () => {
|
||||
refundPayment: jest
|
||||
.fn()
|
||||
.mockImplementation((p) => Promise.resolve({ id: "ref" })),
|
||||
withTransaction: function () {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import TotalsService from "../totals"
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import TotalsService from "../totals"
|
||||
|
||||
const discounts = {
|
||||
total10Percent: {
|
||||
@@ -19,7 +19,6 @@ const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 2,
|
||||
valid_for: [{ id: "testp2" }],
|
||||
},
|
||||
regions: [{ id: "fr" }],
|
||||
},
|
||||
@@ -30,7 +29,6 @@ const discounts = {
|
||||
type: "percentage",
|
||||
allocation: "item",
|
||||
value: 10,
|
||||
valid_for: [{ id: "testp2" }],
|
||||
},
|
||||
regions: [{ id: "fr" }],
|
||||
},
|
||||
@@ -41,7 +39,6 @@ const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "total",
|
||||
value: 10,
|
||||
valid_for: [],
|
||||
},
|
||||
regions: [{ id: "fr" }],
|
||||
},
|
||||
@@ -53,7 +50,6 @@ const discounts = {
|
||||
type: "fixed",
|
||||
allocation: "item",
|
||||
value: 10,
|
||||
valid_for: [],
|
||||
},
|
||||
regions: [{ id: "fr" }],
|
||||
},
|
||||
@@ -65,131 +61,130 @@ describe("TotalsService", () => {
|
||||
taxCalculationStrategy: {},
|
||||
}
|
||||
|
||||
describe("getAllocationItemDiscounts", () => {
|
||||
let res
|
||||
// TODO: Redo tests to include new line item adjustments
|
||||
|
||||
const totalsService = new TotalsService(container)
|
||||
// describe("getAllocationItemDiscounts", () => {
|
||||
// let res
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
// const totalsService = new TotalsService(container)
|
||||
|
||||
it("calculates item with percentage discount", async () => {
|
||||
const cart = {
|
||||
items: [
|
||||
{
|
||||
id: "test",
|
||||
allow_discounts: true,
|
||||
unit_price: 10,
|
||||
quantity: 10,
|
||||
variant: {
|
||||
id: "testv",
|
||||
product_id: "testp",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
// beforeEach(() => {
|
||||
// jest.clearAllMocks()
|
||||
// })
|
||||
|
||||
const discount = {
|
||||
rule: {
|
||||
type: "percentage",
|
||||
value: 10,
|
||||
valid_for: [{ id: "testp" }],
|
||||
},
|
||||
}
|
||||
// it("calculates item with percentage discount", async () => {
|
||||
// const cart = {
|
||||
// items: [
|
||||
// {
|
||||
// id: "test",
|
||||
// allow_discounts: true,
|
||||
// unit_price: 10,
|
||||
// quantity: 10,
|
||||
// variant: {
|
||||
// id: "testv",
|
||||
// product_id: "testp",
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
|
||||
res = totalsService.getAllocationItemDiscounts(discount, cart)
|
||||
// const discount = {
|
||||
// rule: {
|
||||
// type: "percentage",
|
||||
// value: 10,
|
||||
// },
|
||||
// }
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
lineItem: {
|
||||
id: "test",
|
||||
allow_discounts: true,
|
||||
unit_price: 10,
|
||||
quantity: 10,
|
||||
variant: {
|
||||
id: "testv",
|
||||
product_id: "testp",
|
||||
},
|
||||
},
|
||||
variant: "testv",
|
||||
amount: 10,
|
||||
},
|
||||
])
|
||||
})
|
||||
// res = totalsService.getAllocationItemDiscounts(discount, cart)
|
||||
|
||||
it("calculates item with fixed discount", async () => {
|
||||
const cart = {
|
||||
items: [
|
||||
{
|
||||
id: "exists",
|
||||
allow_discounts: true,
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
id: "testv",
|
||||
product_id: "testp",
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
}
|
||||
// expect(res).toEqual([
|
||||
// {
|
||||
// lineItem: {
|
||||
// id: "test",
|
||||
// allow_discounts: true,
|
||||
// unit_price: 10,
|
||||
// quantity: 10,
|
||||
// variant: {
|
||||
// id: "testv",
|
||||
// product_id: "testp",
|
||||
// },
|
||||
// },
|
||||
// variant: "testv",
|
||||
// amount: 10,
|
||||
// },
|
||||
// ])
|
||||
// })
|
||||
|
||||
const discount = {
|
||||
rule: {
|
||||
type: "fixed",
|
||||
value: 9,
|
||||
valid_for: [{ id: "testp" }],
|
||||
},
|
||||
}
|
||||
// it("calculates item with fixed discount", async () => {
|
||||
// const cart = {
|
||||
// items: [
|
||||
// {
|
||||
// id: "exists",
|
||||
// allow_discounts: true,
|
||||
// unit_price: 10,
|
||||
// variant: {
|
||||
// id: "testv",
|
||||
// product_id: "testp",
|
||||
// },
|
||||
// quantity: 10,
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
|
||||
res = totalsService.getAllocationItemDiscounts(discount, cart)
|
||||
// const discount = {
|
||||
// rule: {
|
||||
// type: "fixed",
|
||||
// value: 9,
|
||||
// },
|
||||
// }
|
||||
|
||||
expect(res).toEqual([
|
||||
{
|
||||
lineItem: {
|
||||
id: "exists",
|
||||
allow_discounts: true,
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
id: "testv",
|
||||
product_id: "testp",
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
variant: "testv",
|
||||
amount: 90,
|
||||
},
|
||||
])
|
||||
})
|
||||
// res = totalsService.getAllocationItemDiscounts(discount, cart)
|
||||
|
||||
it("does not apply discount if no valid variants are provided", async () => {
|
||||
const cart = {
|
||||
items: [
|
||||
{
|
||||
id: "exists",
|
||||
allow_discounts: true,
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
id: "testv",
|
||||
product_id: "testp",
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
}
|
||||
// expect(res).toEqual([
|
||||
// {
|
||||
// lineItem: {
|
||||
// id: "exists",
|
||||
// allow_discounts: true,
|
||||
// unit_price: 10,
|
||||
// variant: {
|
||||
// id: "testv",
|
||||
// product_id: "testp",
|
||||
// },
|
||||
// quantity: 10,
|
||||
// },
|
||||
// variant: "testv",
|
||||
// amount: 90,
|
||||
// },
|
||||
// ])
|
||||
// })
|
||||
|
||||
const discount = {
|
||||
rule: {
|
||||
type: "fixed",
|
||||
value: 9,
|
||||
valid_for: [],
|
||||
},
|
||||
}
|
||||
res = totalsService.getAllocationItemDiscounts(discount, cart)
|
||||
// it("does not apply discount if no valid variants are provided", async () => {
|
||||
// const cart = {
|
||||
// items: [
|
||||
// {
|
||||
// id: "exists",
|
||||
// allow_discounts: true,
|
||||
// unit_price: 10,
|
||||
// variant: {
|
||||
// id: "testv",
|
||||
// product_id: "testp",
|
||||
// },
|
||||
// quantity: 10,
|
||||
// },
|
||||
// ],
|
||||
// }
|
||||
|
||||
expect(res).toEqual([])
|
||||
})
|
||||
})
|
||||
// const discount = {
|
||||
// rule: {
|
||||
// type: "fixed",
|
||||
// value: 9,
|
||||
// },
|
||||
// }
|
||||
// res = totalsService.getAllocationItemDiscounts(discount, cart)
|
||||
|
||||
// expect(res).toEqual([])
|
||||
// })
|
||||
// })
|
||||
|
||||
describe("getDiscountTotal", () => {
|
||||
let res
|
||||
@@ -235,19 +230,21 @@ describe("TotalsService", () => {
|
||||
expect(res).toEqual(28)
|
||||
})
|
||||
|
||||
it("calculate item fixed discount", async () => {
|
||||
discountCart.discounts.push(discounts.item2Fixed)
|
||||
res = totalsService.getDiscountTotal(discountCart)
|
||||
// TODO: Redo tests to include new line item adjustments
|
||||
|
||||
expect(res).toEqual(20)
|
||||
})
|
||||
// it("calculate item fixed discount", async () => {
|
||||
// discountCart.discounts.push(discounts.item2Fixed)
|
||||
// res = totalsService.getDiscountTotal(discountCart)
|
||||
|
||||
it("calculate item percentage discount", async () => {
|
||||
discountCart.discounts.push(discounts.item10Percent)
|
||||
res = totalsService.getDiscountTotal(discountCart)
|
||||
// expect(res).toEqual(20)
|
||||
// })
|
||||
|
||||
expect(res).toEqual(10)
|
||||
})
|
||||
// it("calculate item percentage discount", async () => {
|
||||
// discountCart.discounts.push(discounts.item10Percent)
|
||||
// res = totalsService.getDiscountTotal(discountCart)
|
||||
|
||||
// expect(res).toEqual(10)
|
||||
// })
|
||||
|
||||
it("calculate total fixed discount", async () => {
|
||||
discountCart.discounts.push(discounts.total10Fixed)
|
||||
@@ -350,82 +347,84 @@ describe("TotalsService", () => {
|
||||
expect(res).toEqual(1250)
|
||||
})
|
||||
|
||||
it("calculates refund with total precentage discount", async () => {
|
||||
orderToRefund.discounts.push(discounts.total10Percent)
|
||||
res = totalsService.getRefundTotal(orderToRefund, [
|
||||
{
|
||||
id: "line2",
|
||||
unit_price: 100,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "product2",
|
||||
},
|
||||
returned_quantity: 0,
|
||||
metadata: {},
|
||||
quantity: 10,
|
||||
},
|
||||
])
|
||||
// TODO: Redo tests to include new line item adjustments
|
||||
|
||||
expect(res).toEqual(1125)
|
||||
})
|
||||
// it("calculates refund with total precentage discount", async () => {
|
||||
// orderToRefund.discounts.push(discounts.total10Percent)
|
||||
// res = totalsService.getRefundTotal(orderToRefund, [
|
||||
// {
|
||||
// id: "line2",
|
||||
// unit_price: 100,
|
||||
// allow_discounts: true,
|
||||
// variant: {
|
||||
// id: "variant",
|
||||
// product_id: "product2",
|
||||
// },
|
||||
// returned_quantity: 0,
|
||||
// metadata: {},
|
||||
// quantity: 10,
|
||||
// },
|
||||
// ])
|
||||
|
||||
it("calculates refund with total fixed discount", async () => {
|
||||
orderToRefund.discounts.push(discounts.total10Fixed)
|
||||
res = totalsService.getRefundTotal(orderToRefund, [
|
||||
{
|
||||
id: "line",
|
||||
unit_price: 100,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "product",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
},
|
||||
])
|
||||
// expect(res).toEqual(1125)
|
||||
// })
|
||||
|
||||
expect(res).toEqual(1244)
|
||||
})
|
||||
// it("calculates refund with total fixed discount", async () => {
|
||||
// orderToRefund.discounts.push(discounts.total10Fixed)
|
||||
// res = totalsService.getRefundTotal(orderToRefund, [
|
||||
// {
|
||||
// id: "line",
|
||||
// unit_price: 100,
|
||||
// allow_discounts: true,
|
||||
// variant: {
|
||||
// id: "variant",
|
||||
// product_id: "product",
|
||||
// },
|
||||
// quantity: 10,
|
||||
// returned_quantity: 0,
|
||||
// },
|
||||
// ])
|
||||
|
||||
it("calculates refund with item fixed discount", async () => {
|
||||
orderToRefund.discounts.push(discounts.item2Fixed)
|
||||
res = totalsService.getRefundTotal(orderToRefund, [
|
||||
{
|
||||
id: "line2",
|
||||
unit_price: 100,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "testp2",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
},
|
||||
])
|
||||
// expect(res).toEqual(1244)
|
||||
// })
|
||||
|
||||
expect(res).toEqual(1225)
|
||||
})
|
||||
// it("calculates refund with item fixed discount", async () => {
|
||||
// orderToRefund.discounts.push(discounts.item2Fixed)
|
||||
// res = totalsService.getRefundTotal(orderToRefund, [
|
||||
// {
|
||||
// id: "line2",
|
||||
// unit_price: 100,
|
||||
// allow_discounts: true,
|
||||
// variant: {
|
||||
// id: "variant",
|
||||
// product_id: "testp2",
|
||||
// },
|
||||
// quantity: 10,
|
||||
// returned_quantity: 0,
|
||||
// },
|
||||
// ])
|
||||
|
||||
it("calculates refund with item percentage discount", async () => {
|
||||
orderToRefund.discounts.push(discounts.item10Percent)
|
||||
res = totalsService.getRefundTotal(orderToRefund, [
|
||||
{
|
||||
id: "line2",
|
||||
unit_price: 100,
|
||||
allow_discounts: true,
|
||||
variant: {
|
||||
id: "variant",
|
||||
product_id: "testp2",
|
||||
},
|
||||
quantity: 10,
|
||||
returned_quantity: 0,
|
||||
},
|
||||
])
|
||||
// expect(res).toEqual(1225)
|
||||
// })
|
||||
|
||||
expect(res).toEqual(1125)
|
||||
})
|
||||
// it("calculates refund with item percentage discount", async () => {
|
||||
// orderToRefund.discounts.push(discounts.item10Percent)
|
||||
// res = totalsService.getRefundTotal(orderToRefund, [
|
||||
// {
|
||||
// id: "line2",
|
||||
// unit_price: 100,
|
||||
// allow_discounts: true,
|
||||
// variant: {
|
||||
// id: "variant",
|
||||
// product_id: "testp2",
|
||||
// },
|
||||
// quantity: 10,
|
||||
// returned_quantity: 0,
|
||||
// },
|
||||
// ])
|
||||
|
||||
// expect(res).toEqual(1125)
|
||||
// })
|
||||
|
||||
it("throws if line items to return is not in order", async () => {
|
||||
const work = () =>
|
||||
|
||||
@@ -1,48 +1,40 @@
|
||||
import _ from "lodash"
|
||||
import {
|
||||
EntityManager,
|
||||
DeepPartial,
|
||||
AlreadyHasActiveConnectionError,
|
||||
} from "typeorm"
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
import { ShippingMethodRepository } from "../repositories/shipping-method"
|
||||
import { CartRepository } from "../repositories/cart"
|
||||
import { AddressRepository } from "../repositories/address"
|
||||
import { PaymentSessionRepository } from "../repositories/payment-session"
|
||||
|
||||
import { DeepPartial, EntityManager } from "typeorm"
|
||||
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
||||
import { Address } from "../models/address"
|
||||
import { Discount } from "../models/discount"
|
||||
import { Cart } from "../models/cart"
|
||||
import { CustomShippingOption } from "../models/custom-shipping-option"
|
||||
import { Customer } from "../models/customer"
|
||||
import { Discount } from "../models/discount"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { ShippingMethod } from "../models/shipping-method"
|
||||
import { CustomShippingOption } from "../models/custom-shipping-option"
|
||||
|
||||
import { TotalField, FindConfig } from "../types/common"
|
||||
import { AddressRepository } from "../repositories/address"
|
||||
import { CartRepository } from "../repositories/cart"
|
||||
import { PaymentSessionRepository } from "../repositories/payment-session"
|
||||
import { ShippingMethodRepository } from "../repositories/shipping-method"
|
||||
import {
|
||||
CartCreateProps,
|
||||
CartUpdateProps,
|
||||
FilterableCartProps,
|
||||
LineItemUpdate,
|
||||
CartUpdateProps,
|
||||
CartCreateProps,
|
||||
} from "../types/cart"
|
||||
|
||||
import { FindConfig, TotalField } from "../types/common"
|
||||
import CustomShippingOptionService from "./custom-shipping-option"
|
||||
import CustomerService from "./customer"
|
||||
import DiscountService from "./discount"
|
||||
import EventBusService from "./event-bus"
|
||||
import ProductVariantService from "./product-variant"
|
||||
import ProductService from "./product"
|
||||
import RegionService from "./region"
|
||||
import GiftCardService from "./gift-card"
|
||||
import InventoryService from "./inventory"
|
||||
import LineItemService from "./line-item"
|
||||
import PaymentProviderService from "./payment-provider"
|
||||
import ProductService from "./product"
|
||||
import ProductVariantService from "./product-variant"
|
||||
import RegionService from "./region"
|
||||
import ShippingOptionService from "./shipping-option"
|
||||
import CustomerService from "./customer"
|
||||
import TaxProviderService from "./tax-provider"
|
||||
import DiscountService from "./discount"
|
||||
import GiftCardService from "./gift-card"
|
||||
import TotalsService from "./totals"
|
||||
import InventoryService from "./inventory"
|
||||
import CustomShippingOptionService from "./custom-shipping-option"
|
||||
import { IPriceSelectionStrategy } from "../interfaces/price-selection-strategy"
|
||||
|
||||
type CartConstructorProps = {
|
||||
manager: EntityManager
|
||||
@@ -213,7 +205,6 @@ class CartService extends BaseService {
|
||||
relationSet.add("gift_cards")
|
||||
relationSet.add("discounts")
|
||||
relationSet.add("discounts.rule")
|
||||
relationSet.add("discounts.rule.valid_for")
|
||||
// relationSet.add("discounts.parent_discount")
|
||||
// relationSet.add("discounts.parent_discount.rule")
|
||||
// relationSet.add("discounts.parent_discount.regions")
|
||||
@@ -706,7 +697,6 @@ class CartService extends BaseService {
|
||||
"region.countries",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"discounts.regions",
|
||||
],
|
||||
})
|
||||
@@ -1016,10 +1006,23 @@ class CartService extends BaseService {
|
||||
async applyDiscount(cart: Cart, discountCode: string): Promise<void> {
|
||||
const discount = await this.discountService_.retrieveByCode(discountCode, [
|
||||
"rule",
|
||||
"rule.valid_for",
|
||||
"regions",
|
||||
])
|
||||
|
||||
if (cart.customer_id) {
|
||||
const canApply = await this.discountService_.canApplyForCustomer(
|
||||
discount.rule.id,
|
||||
cart.customer_id
|
||||
)
|
||||
|
||||
if (!canApply) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Discount is not valid for customer"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const rule = discount.rule
|
||||
|
||||
// if limit is set and reached, we make an early exit
|
||||
@@ -1103,7 +1106,7 @@ class CartService extends BaseService {
|
||||
}
|
||||
})
|
||||
|
||||
cart.discounts = newDiscounts.filter(Boolean)
|
||||
cart.discounts = newDiscounts.filter(Boolean) as Discount[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1118,7 +1121,6 @@ class CartService extends BaseService {
|
||||
relations: [
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"payment_sessions",
|
||||
"shipping_methods",
|
||||
],
|
||||
@@ -1333,7 +1335,6 @@ class CartService extends BaseService {
|
||||
"items",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"gift_cards",
|
||||
"shipping_methods",
|
||||
"billing_address",
|
||||
@@ -1508,7 +1509,6 @@ class CartService extends BaseService {
|
||||
"shipping_methods",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods.shipping_option",
|
||||
"items",
|
||||
"items.variant",
|
||||
@@ -1566,12 +1566,7 @@ class CartService extends BaseService {
|
||||
}
|
||||
|
||||
const result = await this.retrieve(cartId, {
|
||||
relations: [
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
],
|
||||
relations: ["discounts", "discounts.rule", "shipping_methods"],
|
||||
})
|
||||
|
||||
// if cart has freeshipping, adjust price
|
||||
@@ -1801,13 +1796,7 @@ class CartService extends BaseService {
|
||||
async delete(cartId: string): Promise<string> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const cart = await this.retrieve(cartId, {
|
||||
relations: [
|
||||
"items",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"payment_sessions",
|
||||
],
|
||||
relations: ["items", "discounts", "discounts.rule", "payment_sessions"],
|
||||
})
|
||||
|
||||
if (cart.completed_at) {
|
||||
@@ -1878,7 +1867,6 @@ class CartService extends BaseService {
|
||||
"gift_cards",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"region",
|
||||
"region.tax_rates",
|
||||
|
||||
+301
-112
@@ -1,22 +1,62 @@
|
||||
import { parse, toSeconds } from "iso8601-duration"
|
||||
import { isEmpty, omit } from "lodash"
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Brackets, ILike } from "typeorm"
|
||||
import { formatException } from "../utils/exception-formatter"
|
||||
import { Brackets, EntityManager, ILike, SelectQueryBuilder } from "typeorm"
|
||||
import {
|
||||
EventBusService,
|
||||
ProductService,
|
||||
RegionService,
|
||||
TotalsService,
|
||||
} from "."
|
||||
import { Cart } from "../models/cart"
|
||||
import { Discount } from "../models/discount"
|
||||
import { DiscountConditionType } from "../models/discount-condition"
|
||||
import {
|
||||
AllocationType as DiscountAllocation,
|
||||
DiscountRule,
|
||||
DiscountRuleType,
|
||||
} from "../models/discount-rule"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { DiscountRepository } from "../repositories/discount"
|
||||
import { DiscountConditionRepository } from "../repositories/discount-condition"
|
||||
import { DiscountRuleRepository } from "../repositories/discount-rule"
|
||||
import { GiftCardRepository } from "../repositories/gift-card"
|
||||
import { FindConfig } from "../types/common"
|
||||
import {
|
||||
CreateDiscountInput,
|
||||
CreateDynamicDiscountInput,
|
||||
FilterableDiscountProps,
|
||||
UpdateDiscountInput,
|
||||
UpsertDiscountConditionInput,
|
||||
} from "../types/discount"
|
||||
import { formatException, PostgresError } from "../utils/exception-formatter"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate discounts.
|
||||
* @implements {BaseService}
|
||||
*/
|
||||
class DiscountService extends BaseService {
|
||||
private manager_: EntityManager
|
||||
private discountRepository_: typeof DiscountRepository
|
||||
private discountRuleRepository_: typeof DiscountRuleRepository
|
||||
private giftCardRepository_: typeof GiftCardRepository
|
||||
private discountConditionRepository_: typeof DiscountConditionRepository
|
||||
private totalsService_: TotalsService
|
||||
private productService_: ProductService
|
||||
private regionService_: RegionService
|
||||
private eventBus_: EventBusService
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
discountRepository,
|
||||
discountRuleRepository,
|
||||
giftCardRepository,
|
||||
discountConditionRepository,
|
||||
totalsService,
|
||||
productService,
|
||||
regionService,
|
||||
customerService,
|
||||
eventBusService,
|
||||
}) {
|
||||
super()
|
||||
@@ -33,6 +73,9 @@ class DiscountService extends BaseService {
|
||||
/** @private @const {GiftCardRepository} */
|
||||
this.giftCardRepository_ = giftCardRepository
|
||||
|
||||
/** @private @const {DiscountConditionRepository} */
|
||||
this.discountConditionRepository_ = discountConditionRepository
|
||||
|
||||
/** @private @const {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
@@ -42,11 +85,14 @@ class DiscountService extends BaseService {
|
||||
/** @private @const {RegionService} */
|
||||
this.regionService_ = regionService
|
||||
|
||||
/** @private @const {CustomerService} */
|
||||
this.customerService_ = customerService
|
||||
|
||||
/** @private @const {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
withTransaction(transactionManager: EntityManager): DiscountService {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
@@ -56,13 +102,16 @@ class DiscountService extends BaseService {
|
||||
discountRepository: this.discountRepository_,
|
||||
discountRuleRepository: this.discountRuleRepository_,
|
||||
giftCardRepository: this.giftCardRepository_,
|
||||
discountConditionRepository: this.discountConditionRepository_,
|
||||
totalsService: this.totalsService_,
|
||||
productService: this.productService_,
|
||||
regionService: this.regionService_,
|
||||
customerService: this.customerService_,
|
||||
eventBusService: this.eventBus_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
cloned.manager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
@@ -72,14 +121,13 @@ class DiscountService extends BaseService {
|
||||
* @param {DiscountRule} discountRule - the discount rule to create
|
||||
* @return {Promise} the result of the create operation
|
||||
*/
|
||||
validateDiscountRule_(discountRule) {
|
||||
validateDiscountRule_(discountRule): DiscountRule {
|
||||
const schema = Validator.object().keys({
|
||||
id: Validator.string().optional(),
|
||||
description: Validator.string().optional(),
|
||||
type: Validator.string().required(),
|
||||
value: Validator.number().min(0).required(),
|
||||
allocation: Validator.string().required(),
|
||||
valid_for: Validator.array().optional(),
|
||||
created_at: Validator.date().optional(),
|
||||
updated_at: Validator.date().allow(null).optional(),
|
||||
deleted_at: Validator.date().allow(null).optional(),
|
||||
@@ -109,7 +157,10 @@ class DiscountService extends BaseService {
|
||||
* @param {Object} config - the config object containing query settings
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async list(selector = {}, config = { relations: [], skip: 0, take: 10 }) {
|
||||
async list(
|
||||
selector: FilterableDiscountProps = {},
|
||||
config: FindConfig<Discount> = { relations: [], skip: 0, take: 10 }
|
||||
): Promise<Discount[]> {
|
||||
const discountRepo = this.manager_.getCustomRepository(
|
||||
this.discountRepository_
|
||||
)
|
||||
@@ -124,9 +175,13 @@ class DiscountService extends BaseService {
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async listAndCount(
|
||||
selector = {},
|
||||
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
|
||||
) {
|
||||
selector: FilterableDiscountProps = {},
|
||||
config: FindConfig<Discount> = {
|
||||
take: 20,
|
||||
skip: 0,
|
||||
order: { created_at: "DESC" },
|
||||
}
|
||||
): Promise<[Discount[], number]> {
|
||||
const discountRepo = this.manager_.getCustomRepository(
|
||||
this.discountRepository_
|
||||
)
|
||||
@@ -144,7 +199,7 @@ class DiscountService extends BaseService {
|
||||
|
||||
delete where.code
|
||||
|
||||
query.where = (qb) => {
|
||||
query.where = (qb: SelectQueryBuilder<Discount>): void => {
|
||||
qb.where(where)
|
||||
|
||||
qb.andWhere(
|
||||
@@ -166,18 +221,23 @@ class DiscountService extends BaseService {
|
||||
* @param {Discount} discount - the discount data to create
|
||||
* @return {Promise} the result of the create operation
|
||||
*/
|
||||
async create(discount) {
|
||||
async create(discount: CreateDiscountInput): Promise<Discount> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountRepo = manager.getCustomRepository(this.discountRepository_)
|
||||
const ruleRepo = manager.getCustomRepository(this.discountRuleRepository_)
|
||||
|
||||
if (discount.rule?.valid_for) {
|
||||
discount.rule.valid_for = discount.rule.valid_for.map((id) => ({ id }))
|
||||
}
|
||||
const conditions = discount.rule?.conditions
|
||||
|
||||
const ruleToCreate = omit(discount.rule, ["conditions"])
|
||||
discount.rule = ruleToCreate
|
||||
|
||||
const validatedRule = this.validateDiscountRule_(discount.rule)
|
||||
|
||||
if (discount.regions?.length > 1 && discount.rule.type === "fixed") {
|
||||
if (
|
||||
discount?.regions &&
|
||||
discount?.regions.length > 1 &&
|
||||
discount?.rule?.type === "fixed"
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Fixed discounts can have one region"
|
||||
@@ -195,11 +255,18 @@ class DiscountService extends BaseService {
|
||||
const discountRule = await ruleRepo.create(validatedRule)
|
||||
const createdDiscountRule = await ruleRepo.save(discountRule)
|
||||
|
||||
discount.code = discount.code.toUpperCase()
|
||||
discount.code = discount.code!.toUpperCase()
|
||||
discount.rule = createdDiscountRule
|
||||
|
||||
const created = await discountRepo.create(discount)
|
||||
const result = await discountRepo.save(created)
|
||||
|
||||
if (conditions?.length) {
|
||||
for (const cond of conditions) {
|
||||
await this.upsertDiscountCondition_(result.id, cond)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
throw formatException(error)
|
||||
@@ -213,7 +280,10 @@ class DiscountService extends BaseService {
|
||||
* @param {Object} config - the config object containing query settings
|
||||
* @return {Promise<Discount>} the discount
|
||||
*/
|
||||
async retrieve(discountId, config = {}) {
|
||||
async retrieve(
|
||||
discountId: string,
|
||||
config: FindConfig<Discount> = {}
|
||||
): Promise<Discount> {
|
||||
const discountRepo = this.manager_.getCustomRepository(
|
||||
this.discountRepository_
|
||||
)
|
||||
@@ -238,7 +308,10 @@ class DiscountService extends BaseService {
|
||||
* @param {array} relations - list of relations
|
||||
* @return {Promise<Discount>} the discount document
|
||||
*/
|
||||
async retrieveByCode(discountCode, relations = []) {
|
||||
async retrieveByCode(
|
||||
discountCode: string,
|
||||
relations: string[] = []
|
||||
): Promise<Discount> {
|
||||
const discountRepo = this.manager_.getCustomRepository(
|
||||
this.discountRepository_
|
||||
)
|
||||
@@ -271,7 +344,10 @@ class DiscountService extends BaseService {
|
||||
* @param {Discount} update - the data to update the discount with
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async update(discountId, update) {
|
||||
async update(
|
||||
discountId: string,
|
||||
update: UpdateDiscountInput
|
||||
): Promise<Discount> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountRepo = manager.getCustomRepository(this.discountRepository_)
|
||||
|
||||
@@ -279,10 +355,17 @@ class DiscountService extends BaseService {
|
||||
relations: ["rule"],
|
||||
})
|
||||
|
||||
const conditions = update?.rule?.conditions
|
||||
const ruleToUpdate = omit(update.rule, "conditions")
|
||||
|
||||
if (!isEmpty(ruleToUpdate)) {
|
||||
update.rule = ruleToUpdate
|
||||
}
|
||||
|
||||
const { rule, metadata, regions, ...rest } = update
|
||||
|
||||
if (rest.ends_at) {
|
||||
if (discount.starts_at >= new Date(update.ends_at)) {
|
||||
if (discount.starts_at >= new Date(rest.ends_at)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`"ends_at" must be greater than "starts_at"`
|
||||
@@ -290,13 +373,19 @@ class DiscountService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if (regions?.length > 1 && discount.rule.type === "fixed") {
|
||||
if (regions && regions?.length > 1 && discount.rule.type === "fixed") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Fixed discounts can have one region"
|
||||
)
|
||||
}
|
||||
|
||||
if (conditions?.length) {
|
||||
for (const cond of conditions) {
|
||||
await this.upsertDiscountCondition_(discount.id, cond)
|
||||
}
|
||||
}
|
||||
|
||||
if (regions) {
|
||||
discount.regions = await Promise.all(
|
||||
regions.map((regionId) => this.regionService_.retrieve(regionId))
|
||||
@@ -308,16 +397,11 @@ class DiscountService extends BaseService {
|
||||
}
|
||||
|
||||
if (rule) {
|
||||
discount.rule = this.validateDiscountRule_(rule)
|
||||
if (rule.valid_for) {
|
||||
discount.rule.valid_for = discount.rule.valid_for.map((id) => ({
|
||||
id,
|
||||
}))
|
||||
}
|
||||
discount.rule = this.validateDiscountRule_(ruleToUpdate)
|
||||
}
|
||||
|
||||
for (const key of Object.keys(rest).filter(
|
||||
(k) => rest[k] !== undefined
|
||||
(k) => typeof rest[k] !== `undefined`
|
||||
)) {
|
||||
discount[key] = rest[key]
|
||||
}
|
||||
@@ -335,7 +419,10 @@ class DiscountService extends BaseService {
|
||||
* @param {Object} data - the object containing a code to identify the discount by
|
||||
* @return {Promise} the newly created dynamic code
|
||||
*/
|
||||
async createDynamicCode(discountId, data) {
|
||||
async createDynamicCode(
|
||||
discountId: string,
|
||||
data: CreateDynamicDiscountInput
|
||||
): Promise<Discount> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountRepo = manager.getCustomRepository(this.discountRepository_)
|
||||
|
||||
@@ -384,7 +471,7 @@ class DiscountService extends BaseService {
|
||||
* @param {string} code - the code to identify the discount by
|
||||
* @return {Promise} the newly created dynamic code
|
||||
*/
|
||||
async deleteDynamicCode(discountId, code) {
|
||||
async deleteDynamicCode(discountId: string, code: string): Promise<void> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountRepo = manager.getCustomRepository(this.discountRepository_)
|
||||
const discount = await discountRepo.findOne({
|
||||
@@ -401,77 +488,13 @@ class DiscountService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a valid product to the discount rule valid_for array.
|
||||
* @param {string} discountId - id of discount
|
||||
* @param {string} productId - id of product to add
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async addValidProduct(discountId, productId) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountRuleRepo = manager.getCustomRepository(
|
||||
this.discountRuleRepository_
|
||||
)
|
||||
|
||||
const discount = await this.retrieve(discountId, {
|
||||
relations: ["rule", "rule.valid_for"],
|
||||
})
|
||||
|
||||
const { rule } = discount
|
||||
|
||||
const exists = rule.valid_for.find((p) => p.id === productId)
|
||||
// If product is already present, we return early
|
||||
if (exists) {
|
||||
return rule
|
||||
}
|
||||
|
||||
const product = await this.productService_.retrieve(productId)
|
||||
|
||||
rule.valid_for = [...rule.valid_for, product]
|
||||
|
||||
const updated = await discountRuleRepo.save(rule)
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a product from the discount rule valid_for array
|
||||
* @param {string} discountId - id of discount
|
||||
* @param {string} productId - id of product to add
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async removeValidProduct(discountId, productId) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountRuleRepo = manager.getCustomRepository(
|
||||
this.discountRuleRepository_
|
||||
)
|
||||
|
||||
const discount = await this.retrieve(discountId, {
|
||||
relations: ["rule", "rule.valid_for"],
|
||||
})
|
||||
|
||||
const { rule } = discount
|
||||
|
||||
const exists = rule.valid_for.find((p) => p.id === productId)
|
||||
// If product is not present, we return early
|
||||
if (!exists) {
|
||||
return rule
|
||||
}
|
||||
|
||||
rule.valid_for = rule.valid_for.filter((p) => p.id !== productId)
|
||||
|
||||
const updated = await discountRuleRepo.save(rule)
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a region to the discount regions array.
|
||||
* @param {string} discountId - id of discount
|
||||
* @param {string} regionId - id of region to add
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async addRegion(discountId, regionId) {
|
||||
async addRegion(discountId: string, regionId: string): Promise<Discount> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountRepo = manager.getCustomRepository(this.discountRepository_)
|
||||
|
||||
@@ -507,7 +530,7 @@ class DiscountService extends BaseService {
|
||||
* @param {string} regionId - id of region to remove
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async removeRegion(discountId, regionId) {
|
||||
async removeRegion(discountId: string, regionId: string): Promise<Discount> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountRepo = manager.getCustomRepository(this.discountRepository_)
|
||||
|
||||
@@ -533,7 +556,7 @@ class DiscountService extends BaseService {
|
||||
* @param {string} discountId - id of discount to delete
|
||||
* @return {Promise} the result of the delete operation
|
||||
*/
|
||||
async delete(discountId) {
|
||||
async delete(discountId: string): Promise<void> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountRepo = manager.getCustomRepository(this.discountRepository_)
|
||||
|
||||
@@ -549,25 +572,191 @@ class DiscountService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorates a discount.
|
||||
* @param {string} discountId - id of discount to decorate
|
||||
* @param {string[]} fields - the fields to include.
|
||||
* @param {string[]} expandFields - fields to expand.
|
||||
* @return {Discount} return the decorated discount.
|
||||
*/
|
||||
async decorate(discountId, fields = [], expandFields = []) {
|
||||
const requiredFields = ["id", "code", "is_dynamic", "metadata"]
|
||||
resolveConditionType_(data: UpsertDiscountConditionInput):
|
||||
| {
|
||||
type: DiscountConditionType
|
||||
resource_ids: string[]
|
||||
}
|
||||
| undefined {
|
||||
switch (true) {
|
||||
case !!data.products?.length:
|
||||
return {
|
||||
type: DiscountConditionType.PRODUCTS,
|
||||
resource_ids: data.products!,
|
||||
}
|
||||
case !!data.product_collections?.length:
|
||||
return {
|
||||
type: DiscountConditionType.PRODUCT_COLLECTIONS,
|
||||
resource_ids: data.product_collections!,
|
||||
}
|
||||
case !!data.product_types?.length:
|
||||
return {
|
||||
type: DiscountConditionType.PRODUCT_TYPES,
|
||||
resource_ids: data.product_types!,
|
||||
}
|
||||
case !!data.product_tags?.length:
|
||||
return {
|
||||
type: DiscountConditionType.PRODUCT_TAGS,
|
||||
resource_ids: data.product_tags!,
|
||||
}
|
||||
case !!data.customer_groups?.length:
|
||||
return {
|
||||
type: DiscountConditionType.CUSTOMER_GROUPS,
|
||||
resource_ids: data.customer_groups!,
|
||||
}
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
fields = fields.concat(requiredFields)
|
||||
async upsertDiscountCondition_(
|
||||
discountId: string,
|
||||
data: UpsertDiscountConditionInput
|
||||
): Promise<void> {
|
||||
const resolvedConditionType = this.resolveConditionType_(data)
|
||||
|
||||
const discount = await this.retrieve(discountId, {
|
||||
select: fields,
|
||||
relations: expandFields,
|
||||
const res = this.atomicPhase_(
|
||||
async (manager) => {
|
||||
const discountConditionRepo: DiscountConditionRepository =
|
||||
manager.getCustomRepository(this.discountConditionRepository_)
|
||||
|
||||
if (!resolvedConditionType) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Missing one of products, collections, tags, types or customer groups in data`
|
||||
)
|
||||
}
|
||||
|
||||
if (data.id) {
|
||||
return await discountConditionRepo.addConditionResources(
|
||||
data.id,
|
||||
resolvedConditionType.resource_ids,
|
||||
resolvedConditionType.type,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
const discount = await this.retrieve(discountId, {
|
||||
relations: ["rule", "rule.conditions"],
|
||||
})
|
||||
|
||||
const created = discountConditionRepo.create({
|
||||
discount_rule_id: discount.rule_id,
|
||||
operator: data.operator,
|
||||
type: resolvedConditionType.type,
|
||||
})
|
||||
|
||||
const discountCondition = await discountConditionRepo.save(created)
|
||||
|
||||
return await discountConditionRepo.addConditionResources(
|
||||
discountCondition.id,
|
||||
resolvedConditionType.resource_ids,
|
||||
resolvedConditionType.type
|
||||
)
|
||||
},
|
||||
async (err: any) => {
|
||||
if (err.code === PostgresError.DUPLICATE_ERROR) {
|
||||
// A unique key constraint failed meaning the combination of
|
||||
// discount rule id, type, and operator already exists in the db.
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.DUPLICATE_ERROR,
|
||||
`Discount Condition with operator '${data.operator}' and type '${resolvedConditionType?.type}' already exist on a Discount Rule`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async validateDiscountForProduct(
|
||||
discountRuleId: string,
|
||||
productId: string | undefined
|
||||
): Promise<boolean> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountConditionRepo: DiscountConditionRepository =
|
||||
manager.getCustomRepository(this.discountConditionRepository_)
|
||||
|
||||
// In case of custom line items, we don't have a product id.
|
||||
// Instead of throwing, we simply invalidate the discount.
|
||||
if (!productId) {
|
||||
return false
|
||||
}
|
||||
|
||||
const product = await this.productService_.retrieve(productId, {
|
||||
relations: ["tags"],
|
||||
})
|
||||
|
||||
return await discountConditionRepo.isValidForProduct(
|
||||
discountRuleId,
|
||||
product.id
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// const final = await this.runDecorators_(decorated)
|
||||
return discount
|
||||
async calculateDiscountForLineItem(
|
||||
discountId: string,
|
||||
lineItem: LineItem,
|
||||
cart: Cart
|
||||
): Promise<number> {
|
||||
let adjustment = 0
|
||||
|
||||
if (!lineItem.allow_discounts) {
|
||||
return adjustment
|
||||
}
|
||||
|
||||
const discount = await this.retrieve(discountId, { relations: ["rule"] })
|
||||
|
||||
const { type, value, allocation } = discount.rule
|
||||
|
||||
const fullItemPrice = lineItem.unit_price * lineItem.quantity
|
||||
|
||||
if (type === DiscountRuleType.PERCENTAGE) {
|
||||
adjustment = Math.round((fullItemPrice / 100) * value)
|
||||
} else if (
|
||||
type === DiscountRuleType.FIXED &&
|
||||
allocation === DiscountAllocation.TOTAL
|
||||
) {
|
||||
// when a fixed discount should be applied to the total,
|
||||
// we create line adjustments for each item with an amount
|
||||
// relative to the subtotal
|
||||
const subtotal = this.totalsService_.getSubtotal(cart, {
|
||||
excludeNonDiscounts: true,
|
||||
})
|
||||
const nominator = Math.min(value, subtotal)
|
||||
const itemRelativeToSubtotal = lineItem.unit_price / subtotal
|
||||
const totalItemPercentage = itemRelativeToSubtotal * lineItem.quantity
|
||||
adjustment = Math.round(nominator * totalItemPercentage)
|
||||
} else {
|
||||
adjustment = value * lineItem.quantity
|
||||
}
|
||||
// if the amount of the discount exceeds the total price of the item,
|
||||
// we return the total item price, else the fixed amount
|
||||
return adjustment >= fullItemPrice ? fullItemPrice : adjustment
|
||||
}
|
||||
|
||||
async canApplyForCustomer(
|
||||
discountRuleId: string,
|
||||
customerId: string | undefined
|
||||
): Promise<boolean> {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const discountConditionRepo: DiscountConditionRepository =
|
||||
manager.getCustomRepository(this.discountConditionRepository_)
|
||||
|
||||
// Instead of throwing on missing customer id, we simply invalidate the discount
|
||||
if (!customerId) {
|
||||
return false
|
||||
}
|
||||
|
||||
const customer = await this.customerService_.retrieve(customerId, {
|
||||
relations: ["groups"],
|
||||
})
|
||||
|
||||
return await discountConditionRepo.canApplyForCustomer(
|
||||
discountRuleId,
|
||||
customer.id
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,8 +188,9 @@ class OrderService extends BaseService {
|
||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||
const query = this.buildQuery_(selector, config)
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals_(config)
|
||||
const { select, relations, totalsToSelect } = this.transformQueryForTotals_(
|
||||
config
|
||||
)
|
||||
|
||||
if (select && select.length) {
|
||||
query.select = select
|
||||
@@ -248,8 +249,9 @@ class OrderService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals_(config)
|
||||
const { select, relations, totalsToSelect } = this.transformQueryForTotals_(
|
||||
config
|
||||
)
|
||||
|
||||
if (select && select.length) {
|
||||
query.select = select
|
||||
@@ -306,7 +308,6 @@ class OrderService extends BaseService {
|
||||
relationSet.add("claims.additional_items.tax_lines")
|
||||
relationSet.add("discounts")
|
||||
relationSet.add("discounts.rule")
|
||||
relationSet.add("discounts.rule.valid_for")
|
||||
relationSet.add("gift_cards")
|
||||
relationSet.add("gift_card_transactions")
|
||||
relationSet.add("refunds")
|
||||
@@ -340,8 +341,9 @@ class OrderService extends BaseService {
|
||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||
const validatedId = this.validateId_(orderId)
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals_(config)
|
||||
const { select, relations, totalsToSelect } = this.transformQueryForTotals_(
|
||||
config
|
||||
)
|
||||
|
||||
const query = {
|
||||
where: { id: validatedId },
|
||||
@@ -377,8 +379,9 @@ class OrderService extends BaseService {
|
||||
async retrieveByCartId(cartId, config = {}) {
|
||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals_(config)
|
||||
const { select, relations, totalsToSelect } = this.transformQueryForTotals_(
|
||||
config
|
||||
)
|
||||
|
||||
const query = {
|
||||
where: { cart_id: cartId },
|
||||
@@ -414,8 +417,9 @@ class OrderService extends BaseService {
|
||||
async retrieveByExternalId(externalId, config = {}) {
|
||||
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
|
||||
|
||||
const { select, relations, totalsToSelect } =
|
||||
this.transformQueryForTotals_(config)
|
||||
const { select, relations, totalsToSelect } = this.transformQueryForTotals_(
|
||||
config
|
||||
)
|
||||
|
||||
const query = {
|
||||
where: { external_id: externalId },
|
||||
@@ -508,7 +512,6 @@ class OrderService extends BaseService {
|
||||
"items",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"gift_cards",
|
||||
"shipping_methods",
|
||||
],
|
||||
@@ -1180,7 +1183,6 @@ class OrderService extends BaseService {
|
||||
relations: [
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"region",
|
||||
"fulfillments",
|
||||
"shipping_address",
|
||||
|
||||
@@ -422,8 +422,9 @@ class ReturnService extends BaseService {
|
||||
}
|
||||
)
|
||||
|
||||
const calculationContext =
|
||||
this.totalsService_.getCalculationContext(order)
|
||||
const calculationContext = this.totalsService_.getCalculationContext(
|
||||
order
|
||||
)
|
||||
|
||||
const taxLines = await this.taxProviderService_
|
||||
.withTransaction(manager)
|
||||
@@ -495,8 +496,9 @@ class ReturnService extends BaseService {
|
||||
return returnOrder
|
||||
}
|
||||
|
||||
const fulfillmentData =
|
||||
await this.fulfillmentProviderService_.createReturn(returnData)
|
||||
const fulfillmentData = await this.fulfillmentProviderService_.createReturn(
|
||||
returnData
|
||||
)
|
||||
|
||||
returnOrder.shipping_data = fulfillmentData
|
||||
|
||||
@@ -558,7 +560,6 @@ class ReturnService extends BaseService {
|
||||
"payments",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"refunds",
|
||||
"shipping_methods",
|
||||
"region",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { TaxRate } from "../models/tax-rate"
|
||||
import { ShippingTaxRate } from "../models/shipping-tax-rate"
|
||||
import { ProductTaxRate } from "../models/product-tax-rate"
|
||||
import { ProductTypeTaxRate } from "../models/product-type-tax-rate"
|
||||
import { ShippingTaxRate } from "../models/shipping-tax-rate"
|
||||
import { TaxRate } from "../models/tax-rate"
|
||||
import { TaxRateRepository } from "../repositories/tax-rate"
|
||||
import ProductService from "../services/product"
|
||||
import ProductTypeService from "../services/product-type"
|
||||
@@ -12,9 +12,9 @@ import ShippingOptionService from "../services/shipping-option"
|
||||
import { FindConfig } from "../types/common"
|
||||
import {
|
||||
CreateTaxRateInput,
|
||||
UpdateTaxRateInput,
|
||||
TaxRateListByConfig,
|
||||
FilterableTaxRateProps,
|
||||
TaxRateListByConfig,
|
||||
UpdateTaxRateInput,
|
||||
} from "../types/tax-rate"
|
||||
|
||||
class TaxRateService extends BaseService {
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
import { LineItemTaxLine } from "../models/line-item-tax-line"
|
||||
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
|
||||
import { Order } from "../models/order"
|
||||
import { Cart } from "../models/cart"
|
||||
import { ShippingMethod } from "../models/shipping-method"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { Discount } from "../models/discount"
|
||||
import { DiscountRuleType } from "../models/discount-rule"
|
||||
|
||||
import TaxProviderService from "./tax-provider"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { ITaxCalculationStrategy } from "../interfaces/tax-calculation-strategy"
|
||||
import { TaxCalculationContext } from "../interfaces/tax-service"
|
||||
import { Cart } from "../models/cart"
|
||||
import { Discount } from "../models/discount"
|
||||
import { DiscountRuleType } from "../models/discount-rule"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { LineItemTaxLine } from "../models/line-item-tax-line"
|
||||
import { Order } from "../models/order"
|
||||
import { ShippingMethod } from "../models/shipping-method"
|
||||
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
|
||||
import { isCart } from "../types/cart"
|
||||
import { isOrder } from "../types/orders"
|
||||
|
||||
import {
|
||||
SubtotalOptions,
|
||||
LineDiscount,
|
||||
LineAllocationsMap,
|
||||
LineDiscount,
|
||||
LineDiscountAmount,
|
||||
SubtotalOptions,
|
||||
} from "../types/totals"
|
||||
import TaxProviderService from "./tax-provider"
|
||||
|
||||
type ShippingMethodTotals = {
|
||||
price: number
|
||||
@@ -594,23 +591,7 @@ class TotalsService extends BaseService {
|
||||
cart: Cart | Order
|
||||
): LineDiscount[] {
|
||||
const discounts: LineDiscount[] = []
|
||||
for (const item of cart.items) {
|
||||
if (discount.rule.valid_for?.length > 0) {
|
||||
discount.rule.valid_for.map(({ id }) => {
|
||||
if (item.variant.product_id === id) {
|
||||
discounts.push(
|
||||
this.calculateDiscount_(
|
||||
item,
|
||||
item.variant.id,
|
||||
item.unit_price,
|
||||
discount.rule.value,
|
||||
discount.rule.type
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// TODO: Add line item adjustments
|
||||
return discounts
|
||||
}
|
||||
|
||||
|
||||
@@ -29,20 +29,14 @@ class OrderSubscriber {
|
||||
this.eventBus_.subscribe("order.placed", this.updateDraftOrder)
|
||||
}
|
||||
|
||||
handleOrderPlaced = async data => {
|
||||
handleOrderPlaced = async (data) => {
|
||||
const order = await this.orderService_.retrieve(data.id, {
|
||||
select: ["subtotal"],
|
||||
relations: [
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"items",
|
||||
"gift_cards",
|
||||
],
|
||||
relations: ["discounts", "discounts.rule", "items", "gift_cards"],
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
order.items.map(async i => {
|
||||
order.items.map(async (i) => {
|
||||
if (i.is_giftcard) {
|
||||
for (let qty = 0; qty < i.quantity; qty++) {
|
||||
await this.giftCardService_.create({
|
||||
@@ -58,7 +52,7 @@ class OrderSubscriber {
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
order.discounts.map(async d => {
|
||||
order.discounts.map(async (d) => {
|
||||
const usageCount = d?.usage_count || 0
|
||||
return this.discountService_.update(d.id, {
|
||||
usage_count: usageCount + 1,
|
||||
@@ -67,11 +61,11 @@ class OrderSubscriber {
|
||||
)
|
||||
}
|
||||
|
||||
updateDraftOrder = async data => {
|
||||
updateDraftOrder = async (data) => {
|
||||
const order = await this.orderService_.retrieve(data.id)
|
||||
const draftOrder = await this.draftOrderService_
|
||||
.retrieveByCartId(order.cart_id)
|
||||
.catch(_ => null)
|
||||
.catch((_) => null)
|
||||
|
||||
if (draftOrder) {
|
||||
await this.draftOrderService_.registerCartCompletion(
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface FindConfig<Entity> {
|
||||
skip?: number
|
||||
take?: number
|
||||
relations?: string[]
|
||||
order?: { [k: symbol]: "ASC" | "DESC" }
|
||||
order?: Record<string, "ASC" | "DESC">
|
||||
}
|
||||
|
||||
export type PaginatedResponse = { limit: number; offset: number; count: number }
|
||||
|
||||
@@ -1,12 +1,165 @@
|
||||
import { IsEnum, IsOptional } from "class-validator"
|
||||
import { Transform, Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Validate,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { DiscountConditionOperator } from "../models/discount-condition"
|
||||
import { AllocationType, DiscountRuleType } from "../models/discount-rule"
|
||||
import { ExactlyOne } from "./validators/exactly-one"
|
||||
|
||||
export type QuerySelector = {
|
||||
q?: string
|
||||
}
|
||||
|
||||
export class ListSelector {
|
||||
export class FilterableDiscountProps {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
q?: string
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => value === "true")
|
||||
is_dynamic?: boolean
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => value === "true")
|
||||
is_disabled?: boolean
|
||||
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => AdminGetDiscountsDiscountRuleParams)
|
||||
rule?: AdminGetDiscountsDiscountRuleParams
|
||||
}
|
||||
|
||||
export class AdminGetDiscountsDiscountRuleParams {
|
||||
@IsOptional()
|
||||
@IsEnum(DiscountRuleType)
|
||||
type?: DiscountRuleType
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AllocationType)
|
||||
allocation?: AllocationType
|
||||
}
|
||||
|
||||
export class AdminUpsertConditionsReq {
|
||||
@Validate(ExactlyOne, [
|
||||
"product_collections",
|
||||
"product_types",
|
||||
"product_tags",
|
||||
"customer_groups",
|
||||
])
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
products?: string[]
|
||||
|
||||
@Validate(ExactlyOne, [
|
||||
"products",
|
||||
"product_types",
|
||||
"product_tags",
|
||||
"customer_groups",
|
||||
])
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
product_collections?: string[]
|
||||
|
||||
@Validate(ExactlyOne, [
|
||||
"product_collections",
|
||||
"products",
|
||||
"product_tags",
|
||||
"customer_groups",
|
||||
])
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
product_types?: string[]
|
||||
|
||||
@Validate(ExactlyOne, [
|
||||
"product_collections",
|
||||
"product_types",
|
||||
"products",
|
||||
"customer_groups",
|
||||
])
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
product_tags?: string[]
|
||||
|
||||
@Validate(ExactlyOne, [
|
||||
"product_collections",
|
||||
"product_types",
|
||||
"products",
|
||||
"product_tags",
|
||||
])
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
customer_groups?: string[]
|
||||
}
|
||||
|
||||
export type UpsertDiscountConditionInput = {
|
||||
id?: string
|
||||
operator?: DiscountConditionOperator
|
||||
products?: string[]
|
||||
product_collections?: string[]
|
||||
product_types?: string[]
|
||||
product_tags?: string[]
|
||||
customer_groups?: string[]
|
||||
}
|
||||
|
||||
export type CreateDiscountRuleInput = {
|
||||
description?: string
|
||||
type: string
|
||||
value: number
|
||||
allocation: string
|
||||
conditions?: UpsertDiscountConditionInput[]
|
||||
}
|
||||
|
||||
export type CreateDiscountInput = {
|
||||
code: string
|
||||
rule: CreateDiscountRuleInput
|
||||
is_dynamic: boolean
|
||||
is_disabled: boolean
|
||||
starts_at?: Date
|
||||
ends_at?: Date
|
||||
valid_duration?: string
|
||||
usage_limit?: number
|
||||
regions?: string[]
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type UpdateDiscountRuleInput = {
|
||||
id: string
|
||||
description?: string
|
||||
type: string
|
||||
value: number
|
||||
allocation: string
|
||||
conditions?: UpsertDiscountConditionInput[]
|
||||
}
|
||||
|
||||
export type UpdateDiscountInput = {
|
||||
code?: string
|
||||
rule?: UpdateDiscountRuleInput
|
||||
is_dynamic?: boolean
|
||||
is_disabled?: boolean
|
||||
starts_at?: Date
|
||||
ends_at?: Date
|
||||
valid_duration?: string
|
||||
usage_limit?: number
|
||||
regions?: string[]
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type CreateDynamicDiscountInput = {
|
||||
code: string
|
||||
ends_at?: Date
|
||||
usage_limit: number
|
||||
metadata?: object
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
isDefined,
|
||||
ValidationArguments,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from "class-validator"
|
||||
|
||||
// Defines constraint that ensures exactly one of given properties
|
||||
// It simply checks if any of the values provided is defined as
|
||||
// a property on the class along side the property which is decorated
|
||||
//
|
||||
// Inspiration: https://github.com/typestack/class-validator/issues/245
|
||||
@ValidatorConstraint({ async: false })
|
||||
export class ExactlyOne implements ValidatorConstraintInterface {
|
||||
validate(propertyValue: string, args: ValidationArguments): boolean {
|
||||
if (isDefined(propertyValue)) {
|
||||
return this.getFailedConstraints(args).length === 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
defaultMessage(args: ValidationArguments): string {
|
||||
return `Only one of ${args.property}, ${this.getFailedConstraints(
|
||||
args
|
||||
).join(", ")} is allowed`
|
||||
}
|
||||
|
||||
getFailedConstraints(args: ValidationArguments): boolean[] {
|
||||
return args.constraints.filter((prop) => isDefined(args.object[prop]))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { pick } from "lodash"
|
||||
import { FindConfig } from "../types/common"
|
||||
|
||||
type BaseEntity = {
|
||||
id: string
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
export function pickByConfig<TModel extends BaseEntity>(
|
||||
obj: TModel | TModel[],
|
||||
config: FindConfig<TModel>
|
||||
): Partial<TModel> | Partial<TModel>[] {
|
||||
const fields = [...(config.select ?? []), ...(config.relations ?? [])]
|
||||
|
||||
if (fields.length) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((o) => pick(o, fields))
|
||||
} else {
|
||||
return pick(obj, fields)
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
export function getRetrieveConfig<TModel extends BaseEntity>(
|
||||
defaultFields: (keyof TModel)[],
|
||||
defaultRelations: string[],
|
||||
fields?: (keyof TModel)[],
|
||||
expand?: string[]
|
||||
): FindConfig<TModel> {
|
||||
let includeFields: (keyof TModel)[] = []
|
||||
if (typeof fields !== "undefined") {
|
||||
const fieldSet = new Set(fields)
|
||||
fieldSet.add("id")
|
||||
includeFields = Array.from(fieldSet) as (keyof TModel)[]
|
||||
}
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (typeof expand !== "undefined") {
|
||||
expandFields = expand
|
||||
}
|
||||
|
||||
return {
|
||||
select: includeFields.length ? includeFields : defaultFields,
|
||||
relations: expandFields.length ? expandFields : defaultRelations,
|
||||
}
|
||||
}
|
||||
|
||||
export function getListConfig<TModel extends BaseEntity>(
|
||||
defaultFields: (keyof TModel)[],
|
||||
defaultRelations: string[],
|
||||
fields?: (keyof TModel)[],
|
||||
expand?: string[],
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
order?: { [k: symbol]: "DESC" | "ASC" }
|
||||
): FindConfig<TModel> {
|
||||
let includeFields: (keyof TModel)[] = []
|
||||
if (typeof fields !== "undefined") {
|
||||
const fieldSet = new Set(fields)
|
||||
// Ensure created_at is included, since we are sorting on this
|
||||
fieldSet.add("created_at")
|
||||
fieldSet.add("id")
|
||||
includeFields = Array.from(fieldSet) as (keyof TModel)[]
|
||||
}
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (typeof expand !== "undefined") {
|
||||
expandFields = expand
|
||||
}
|
||||
|
||||
const orderBy: Record<string, "DESC" | "ASC"> = order ?? {
|
||||
created_at: "DESC",
|
||||
}
|
||||
|
||||
return {
|
||||
select: includeFields.length ? includeFields : defaultFields,
|
||||
relations: expandFields.length ? expandFields : defaultRelations,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
order: orderBy,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user