feat(core-flows,medusa,types,utils): add rules to promotion endpoints + workflow (#6692)

* feat(core-flows,medusa,types,utils): add rules to promotion endpoints + workflow

* chore: fix specs

* chore: move input type to types package
This commit is contained in:
Riqwan Thamir
2024-03-13 21:19:24 +01:00
committed by GitHub
parent 02e784ce78
commit 640eccd5dd
23 changed files with 777 additions and 179 deletions

View File

@@ -0,0 +1,8 @@
---
"@medusajs/core-flows": patch
"@medusajs/medusa": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat(core-flows,medusa,types,utils): add rules to promotion endpoints + workflow

View File

@@ -1,8 +1,8 @@
import { IPromotionModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { PromotionType } from "@medusajs/utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000)
@@ -56,15 +56,13 @@ medusaIntegrationTestRunner({
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
rules: [],
application_method: expect.objectContaining({
id: expect.any(String),
value: 100,
type: "fixed",
target_type: "order",
allocation: null,
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
}),
}),
])
@@ -84,7 +82,7 @@ medusaIntegrationTestRunner({
])
const response = await api.get(
`/admin/promotions?fields=code,created_at,application_method.id`,
`/admin/promotions?fields=code,created_at,application_method.id&expand=application_method`,
adminHeaders
)

View File

@@ -0,0 +1,347 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { PromotionType } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000)
const env = { MEDUSA_FF_MEDUSA_V2: true }
const adminHeaders = { headers: { "x-medusa-access-token": "test_token" } }
medusaIntegrationTestRunner({
env,
testSuite: ({ dbConnection, getContainer, api }) => {
describe("Admin: Promotion Rules API", () => {
let appContainer
let standardPromotion
let promotionModule: IPromotionModuleService
const promotionRule = {
operator: "eq",
attribute: "old_attr",
values: ["old value"],
}
beforeAll(async () => {
appContainer = getContainer()
promotionModule = appContainer.resolve(ModuleRegistrationName.PROMOTION)
})
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, appContainer)
standardPromotion = await promotionModule.create({
code: "TEST_ACROSS",
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
allocation: "across",
target_type: "items",
value: 100,
target_rules: [promotionRule],
},
rules: [promotionRule],
})
})
describe("POST /admin/promotions/:id/rules", () => {
it("should throw error when required params are missing", async () => {
const { response } = await api
.post(
`/admin/promotions/${standardPromotion.id}/rules`,
{
rules: [
{
operator: "eq",
values: ["new value"],
},
],
},
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message:
"attribute must be a string, attribute should not be empty",
})
})
it("should throw error when promotion does not exist", async () => {
const { response } = await api
.post(
`/admin/promotions/does-not-exist/rules`,
{
rules: [
{
attribute: "new_attr",
operator: "eq",
values: ["new value"],
},
],
},
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(404)
expect(response.data).toEqual({
type: "not_found",
message: "Promotion with id: does-not-exist was not found",
})
})
it("should add rules to a promotion successfully", async () => {
const response = await api.post(
`/admin/promotions/${standardPromotion.id}/rules`,
{
rules: [
{
operator: "eq",
attribute: "new_attr",
values: ["new value"],
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.promotion).toEqual(
expect.objectContaining({
id: standardPromotion.id,
rules: expect.arrayContaining([
expect.objectContaining({
operator: "eq",
attribute: "old_attr",
values: [expect.objectContaining({ value: "old value" })],
}),
expect.objectContaining({
operator: "eq",
attribute: "new_attr",
values: [expect.objectContaining({ value: "new value" })],
}),
]),
})
)
})
})
describe("POST /admin/promotions/:id/target-rules", () => {
it("should throw error when required params are missing", async () => {
const { response } = await api
.post(
`/admin/promotions/${standardPromotion.id}/target-rules`,
{
rules: [
{
operator: "eq",
values: ["new value"],
},
],
},
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message:
"attribute must be a string, attribute should not be empty",
})
})
it("should throw error when promotion does not exist", async () => {
const { response } = await api
.post(
`/admin/promotions/does-not-exist/target-rules`,
{
rules: [
{
attribute: "new_attr",
operator: "eq",
values: ["new value"],
},
],
},
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(404)
expect(response.data).toEqual({
type: "not_found",
message: "Promotion with id: does-not-exist was not found",
})
})
it("should add target rules to a promotion successfully", async () => {
const response = await api.post(
`/admin/promotions/${standardPromotion.id}/target-rules`,
{
rules: [
{
operator: "eq",
attribute: "new_attr",
values: ["new value"],
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.promotion).toEqual(
expect.objectContaining({
id: standardPromotion.id,
application_method: expect.objectContaining({
target_rules: expect.arrayContaining([
expect.objectContaining({
operator: "eq",
attribute: "old_attr",
values: [expect.objectContaining({ value: "old value" })],
}),
expect.objectContaining({
operator: "eq",
attribute: "new_attr",
values: [expect.objectContaining({ value: "new value" })],
}),
]),
}),
})
)
})
})
describe("POST /admin/promotions/:id/buy-rules", () => {
it("should throw error when required params are missing", async () => {
const { response } = await api
.post(
`/admin/promotions/${standardPromotion.id}/buy-rules`,
{
rules: [
{
operator: "eq",
values: ["new value"],
},
],
},
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message:
"attribute must be a string, attribute should not be empty",
})
})
it("should throw error when promotion does not exist", async () => {
const { response } = await api
.post(
`/admin/promotions/does-not-exist/buy-rules`,
{
rules: [
{
attribute: "new_attr",
operator: "eq",
values: ["new value"],
},
],
},
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(404)
expect(response.data).toEqual({
type: "not_found",
message: "Promotion with id: does-not-exist was not found",
})
})
it("should throw an error when trying to add buy rules to a standard promotion", async () => {
const { response } = await api
.post(
`/admin/promotions/${standardPromotion.id}/buy-rules`,
{
rules: [
{
operator: "eq",
attribute: "new_attr",
values: ["new value"],
},
],
},
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message: "Can't add buy rules to a standard promotion",
})
})
it("should add buy rules to a buyget promotion successfully", async () => {
const buyGetPromotion = await promotionModule.create({
code: "TEST_BUYGET",
type: PromotionType.BUYGET,
application_method: {
type: "fixed",
target_type: "items",
allocation: "across",
value: 100,
apply_to_quantity: 1,
buy_rules_min_quantity: 1,
buy_rules: [promotionRule],
target_rules: [promotionRule],
},
rules: [promotionRule],
})
const response = await api.post(
`/admin/promotions/${buyGetPromotion.id}/buy-rules`,
{
rules: [
{
operator: "eq",
attribute: "new_attr",
values: ["new value"],
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.promotion).toEqual(
expect.objectContaining({
id: buyGetPromotion.id,
application_method: expect.objectContaining({
buy_rules: expect.arrayContaining([
expect.objectContaining({
operator: "eq",
attribute: "old_attr",
values: [expect.objectContaining({ value: "old value" })],
}),
expect.objectContaining({
operator: "eq",
attribute: "new_attr",
values: [expect.objectContaining({ value: "new value" })],
}),
]),
}),
})
)
})
})
})
},
})

View File

@@ -1,8 +1,8 @@
import { IPromotionModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { PromotionType } from "@medusajs/utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { createAdminUser } from "../../../../helpers/create-admin-user"
jest.setTimeout(50000)
@@ -73,11 +73,7 @@ medusaIntegrationTestRunner({
value: 100,
type: "fixed",
target_type: "order",
max_quantity: 0,
allocation: null,
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
}),
})
)

View File

@@ -0,0 +1,76 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
AddPromotionRulesWorkflowDTO,
IPromotionModuleService,
} from "@medusajs/types"
import { RuleType } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
export const addRulesToPromotionsStepId = "add-rules-to-promotions"
export const addRulesToPromotionsStep = createStep(
addRulesToPromotionsStepId,
async (input: AddPromotionRulesWorkflowDTO, { container }) => {
const { data, rule_type: ruleType } = input
const promotionModule = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)
const createdPromotionRules =
ruleType === RuleType.RULES
? await promotionModule.addPromotionRules(data.id, data.rules)
: []
const createdPromotionBuyRules =
ruleType === RuleType.BUY_RULES
? await promotionModule.addPromotionBuyRules(data.id, data.rules)
: []
const createdPromotionTargetRules =
ruleType === RuleType.TARGET_RULES
? await promotionModule.addPromotionTargetRules(data.id, data.rules)
: []
const promotionRules = [
...createdPromotionRules,
...createdPromotionBuyRules,
...createdPromotionTargetRules,
]
return new StepResponse(promotionRules, {
id: data.id,
ruleIds: createdPromotionRules.map((pr) => pr.id),
buyRuleIds: createdPromotionBuyRules.map((pr) => pr.id),
targetRuleIds: createdPromotionBuyRules.map((pr) => pr.id),
})
},
async (data, { container }) => {
if (!data) {
return
}
const { id, ruleIds = [], buyRuleIds = [], targetRuleIds = [] } = data
const promotionModule = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)
ruleIds.length &&
(await promotionModule.removePromotionRules(
id,
ruleIds.map((id) => ({ id }))
))
buyRuleIds.length &&
(await promotionModule.removePromotionBuyRules(
id,
buyRuleIds.map((id) => ({ id }))
))
targetRuleIds.length &&
(await promotionModule.removePromotionBuyRules(
id,
targetRuleIds.map((id) => ({ id }))
))
}
)

View File

@@ -1,3 +1,4 @@
export * from "./add-rules-to-promotions"
export * from "./create-campaigns"
export * from "./create-promotions"
export * from "./delete-campaigns"

View File

@@ -0,0 +1,11 @@
import { AddPromotionRulesWorkflowDTO } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { addRulesToPromotionsStep } from "../steps"
export const addRulesToPromotionsWorkflowId = "add-rules-to-promotions-workflow"
export const addRulesToPromotionsWorkflow = createWorkflow(
addRulesToPromotionsWorkflowId,
(input: WorkflowData<AddPromotionRulesWorkflowDTO>): WorkflowData<void> => {
addRulesToPromotionsStep(input)
}
)

View File

@@ -1,3 +1,4 @@
export * from "./add-rules-to-promotions"
export * from "./create-campaigns"
export * from "./create-promotions"
export * from "./delete-campaigns"

View File

@@ -0,0 +1,46 @@
import { addRulesToPromotionsWorkflow } from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { RuleType } from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import {
defaultAdminPromotionFields,
defaultAdminPromotionRelations,
} from "../../query-config"
import { AdminPostPromotionsPromotionRulesReq } from "../../validators"
export const POST = async (
req: AuthenticatedMedusaRequest<AdminPostPromotionsPromotionRulesReq>,
res: MedusaResponse
) => {
const id = req.params.id
const workflow = addRulesToPromotionsWorkflow(req.scope)
const { errors } = await workflow.run({
input: {
rule_type: RuleType.BUY_RULES,
data: {
id: req.params.id,
...req.validatedBody,
},
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const promotionModuleService: IPromotionModuleService = req.scope.resolve(
ModuleRegistrationName.PROMOTION
)
const promotion = await promotionModuleService.retrieve(id, {
select: defaultAdminPromotionFields,
relations: defaultAdminPromotionRelations,
})
res.status(200).json({ promotion })
}

View File

@@ -1,16 +1,15 @@
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../types/routing"
import {
deletePromotionsWorkflow,
updatePromotionsWorkflow,
} from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../types/routing"
import { AdminPostPromotionsPromotionReq } from "../validators"
import { IPromotionModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { UpdatePromotionDTO } from "@medusajs/types"
import { IPromotionModuleService, UpdatePromotionDTO } from "@medusajs/types"
import { AdminPostPromotionsPromotionReq } from "../validators"
export const GET = async (
req: AuthenticatedMedusaRequest,

View File

@@ -0,0 +1,47 @@
import { addRulesToPromotionsWorkflow } from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { RuleType } from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import {
defaultAdminPromotionFields,
defaultAdminPromotionRelations,
} from "../../query-config"
import { AdminPostPromotionsPromotionRulesReq } from "../../validators"
export const POST = async (
req: AuthenticatedMedusaRequest<AdminPostPromotionsPromotionRulesReq>,
res: MedusaResponse
) => {
const id = req.params.id
const workflow = addRulesToPromotionsWorkflow(req.scope)
const { errors } = await workflow.run({
input: {
rule_type: RuleType.RULES,
data: {
id: req.params.id,
...req.validatedBody,
},
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const promotionModuleService: IPromotionModuleService = req.scope.resolve(
ModuleRegistrationName.PROMOTION
)
const promotion = await promotionModuleService.retrieve(id, {
select: defaultAdminPromotionFields,
relations: defaultAdminPromotionRelations,
})
res.status(200).json({ promotion })
}

View File

@@ -0,0 +1,46 @@
import { addRulesToPromotionsWorkflow } from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { RuleType } from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import {
defaultAdminPromotionFields,
defaultAdminPromotionRelations,
} from "../../query-config"
import { AdminPostPromotionsPromotionRulesReq } from "../../validators"
export const POST = async (
req: AuthenticatedMedusaRequest<AdminPostPromotionsPromotionRulesReq>,
res: MedusaResponse
) => {
const id = req.params.id
const workflow = addRulesToPromotionsWorkflow(req.scope)
const { errors } = await workflow.run({
input: {
rule_type: RuleType.TARGET_RULES,
data: {
id: req.params.id,
...req.validatedBody,
},
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const promotionModuleService: IPromotionModuleService = req.scope.resolve(
ModuleRegistrationName.PROMOTION
)
const promotion = await promotionModuleService.retrieve(id, {
select: defaultAdminPromotionFields,
relations: defaultAdminPromotionRelations,
})
res.status(200).json({ promotion })
}

View File

@@ -1,18 +1,14 @@
import * as QueryConfig from "./query-config"
import { transformBody, transformQuery } from "../../../api/middlewares"
import {
AdminGetPromotionsParams,
AdminGetPromotionsPromotionParams,
AdminPostPromotionsPromotionReq,
AdminPostPromotionsPromotionRulesReq,
AdminPostPromotionsReq,
} from "./validators"
import {
isFeatureFlagEnabled,
transformBody,
transformQuery,
} from "../../../api/middlewares"
import { MedusaV2Flag } from "@medusajs/utils"
import { MiddlewareRoute } from "../../../loaders/helpers/routing/types"
import { authenticate } from "../../../utils/authenticate-middleware"
@@ -51,4 +47,19 @@ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/promotions/:id",
middlewares: [transformBody(AdminPostPromotionsPromotionReq)],
},
{
method: ["POST"],
matcher: "/admin/promotions/:id/rules",
middlewares: [transformBody(AdminPostPromotionsPromotionRulesReq)],
},
{
method: ["POST"],
matcher: "/admin/promotions/:id/target-rules",
middlewares: [transformBody(AdminPostPromotionsPromotionRulesReq)],
},
{
method: ["POST"],
matcher: "/admin/promotions/:id/buy-rules",
middlewares: [transformBody(AdminPostPromotionsPromotionRulesReq)],
},
]

View File

@@ -1,16 +1,43 @@
export const defaultAdminPromotionRelations = ["campaign", "application_method"]
export const defaultAdminPromotionRelations = [
"campaign",
"rules",
"rules.values",
"application_method",
"application_method.buy_rules",
"application_method.buy_rules.values",
"application_method.target_rules",
"application_method.target_rules.values",
]
export const allowedAdminPromotionRelations = [
...defaultAdminPromotionRelations,
]
export const defaultAdminPromotionFields = [
"id",
"code",
"campaign",
"is_automatic",
"type",
"created_at",
"updated_at",
"deleted_at",
"campaign.id",
"campaign.name",
"application_method.value",
"application_method.type",
"application_method.max_quantity",
"application_method.target_type",
"application_method.allocation",
"application_method.created_at",
"application_method.updated_at",
"application_method.deleted_at",
"application_method.buy_rules.attribute",
"application_method.buy_rules.operator",
"application_method.buy_rules.values.value",
"application_method.target_rules.attribute",
"application_method.target_rules.operator",
"application_method.target_rules.values.value",
"rules.attribute",
"rules.operator",
"rules.values.value",
]
export const retrieveTransformQueryConfig = {

View File

@@ -1,11 +1,11 @@
import { CreatePromotionDTO, IPromotionModuleService } from "@medusajs/types"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import { CreatePromotionDTO, IPromotionModuleService } from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { createPromotionsWorkflow } from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
export const GET = async (
req: AuthenticatedMedusaRequest,

View File

@@ -214,3 +214,10 @@ export class AdminPostPromotionsPromotionReq {
@Type(() => PromotionRule)
rules?: PromotionRule[]
}
export class AdminPostPromotionsPromotionRulesReq {
@IsArray()
@ValidateNested({ each: true })
@Type(() => PromotionRule)
rules: PromotionRule[]
}

View File

@@ -1084,8 +1084,8 @@ describe("Promotion Service", () => {
expect(error.message).toEqual("promotion - id must be defined")
})
it("should successfully create rules for a promotion", async () => {
promotion = await service.addPromotionRules(promotion.id, [
it("should successfully add rules to a promotion", async () => {
const promotionRules = await service.addPromotionRules(promotion.id, [
{
attribute: "customer_group_id",
operator: "in",
@@ -1093,21 +1093,17 @@ describe("Promotion Service", () => {
},
])
expect(promotion).toEqual(
expect(promotionRules).toEqual([
expect.objectContaining({
id: promotion.id,
rules: [
expect.objectContaining({
attribute: "customer_group_id",
operator: "in",
values: [
expect.objectContaining({ value: "VIP" }),
expect.objectContaining({ value: "top100" }),
],
}),
],
})
)
id: promotionRules[0].id,
attribute: "customer_group_id",
operator: "in",
values: expect.arrayContaining([
expect.objectContaining({ value: "VIP" }),
expect.objectContaining({ value: "top100" }),
]),
}),
])
})
})
@@ -1160,31 +1156,28 @@ describe("Promotion Service", () => {
})
it("should successfully create target rules for a promotion", async () => {
promotion = await service.addPromotionTargetRules(promotion.id, [
{
const promotionRules = await service.addPromotionTargetRules(
promotion.id,
[
{
attribute: "customer_group_id",
operator: "in",
values: ["VIP", "top100"],
},
]
)
expect(promotionRules).toEqual([
expect.objectContaining({
id: promotionRules[0].id,
attribute: "customer_group_id",
operator: "in",
values: ["VIP", "top100"],
},
values: expect.arrayContaining([
expect.objectContaining({ value: "VIP" }),
expect.objectContaining({ value: "top100" }),
]),
}),
])
expect(promotion).toEqual(
expect.objectContaining({
id: promotion.id,
application_method: expect.objectContaining({
target_rules: [
expect.objectContaining({
attribute: "customer_group_id",
operator: "in",
values: [
expect.objectContaining({ value: "VIP" }),
expect.objectContaining({ value: "top100" }),
],
}),
],
}),
})
)
})
})
@@ -1250,7 +1243,7 @@ describe("Promotion Service", () => {
})
it("should successfully create buy rules for a buyget promotion", async () => {
promotion = await service.addPromotionBuyRules(promotion.id, [
const promotionRules = await service.addPromotionBuyRules(promotion.id, [
{
attribute: "product.id",
operator: "in",
@@ -1258,30 +1251,17 @@ describe("Promotion Service", () => {
},
])
expect(promotion).toEqual(
expect(promotionRules).toEqual([
expect.objectContaining({
id: promotion.id,
application_method: expect.objectContaining({
buy_rules: expect.arrayContaining([
expect.objectContaining({
attribute: "product_collection.id",
operator: "eq",
values: expect.arrayContaining([
expect.objectContaining({ value: "pcol_towel" }),
]),
}),
expect.objectContaining({
attribute: "product.id",
operator: "in",
values: expect.arrayContaining([
expect.objectContaining({ value: "prod_3" }),
expect.objectContaining({ value: "prod_4" }),
]),
}),
]),
}),
})
)
id: promotionRules[0].id,
attribute: "product.id",
operator: "in",
values: expect.arrayContaining([
expect.objectContaining({ value: "prod_3" }),
expect.objectContaining({ value: "prod_4" }),
]),
}),
])
})
})
@@ -1337,14 +1317,16 @@ describe("Promotion Service", () => {
expect(error.message).toEqual("promotion - id must be defined")
})
it("should successfully create rules for a promotion", async () => {
it("should successfully remove rules for a promotion", async () => {
const [ruleId] = promotion.rules.map((rule) => rule.id)
promotion = await service.removePromotionRules(promotion.id, [
{ id: ruleId },
])
await service.removePromotionRules(promotion.id, [{ id: ruleId }])
expect(promotion).toEqual(
const updatedPromotion = await service.retrieve(promotion.id, {
relations: ["rules", "rules.values"],
})
expect(updatedPromotion).toEqual(
expect.objectContaining({
id: promotion.id,
rules: [],
@@ -1413,11 +1395,13 @@ describe("Promotion Service", () => {
(rule) => rule.id
)
promotion = await service.removePromotionTargetRules(promotion.id, [
{ id: ruleId },
])
await service.removePromotionTargetRules(promotion.id, [{ id: ruleId }])
expect(promotion).toEqual(
const updatedPromotion = await service.retrieve(promotion.id, {
relations: ["application_method.target_rules.values"],
})
expect(updatedPromotion).toEqual(
expect.objectContaining({
id: promotion.id,
application_method: expect.objectContaining({
@@ -1497,11 +1481,13 @@ describe("Promotion Service", () => {
(rule) => rule.id
)
promotion = await service.removePromotionBuyRules(promotion.id, [
{ id: ruleId },
])
await service.removePromotionBuyRules(promotion.id, [{ id: ruleId }])
expect(promotion).toEqual(
const updatedPromotion = await service.retrieve(promotion.id, {
relations: ["application_method.buy_rules.values"],
})
expect(updatedPromotion).toEqual(
expect.objectContaining({
id: promotion.id,
application_method: expect.objectContaining({

View File

@@ -755,19 +755,19 @@ export default class PromotionModuleService<
promotionId: string,
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO> {
): Promise<PromotionTypes.PromotionRuleDTO[]> {
const promotion = await this.promotionService_.retrieve(promotionId)
await this.createPromotionRulesAndValues_(
const createdPromotionRules = await this.createPromotionRulesAndValues_(
rulesData,
"promotions",
promotion,
sharedContext
)
return await this.retrieve(
promotionId,
{ relations: ["rules", "rules.values"] },
return this.listPromotionRules(
{ id: createdPromotionRules.map((r) => r.id) },
{ relations: ["values"] },
sharedContext
)
}
@@ -777,7 +777,7 @@ export default class PromotionModuleService<
promotionId: string,
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO> {
): Promise<PromotionTypes.PromotionRuleDTO[]> {
const promotion = await this.promotionService_.retrieve(promotionId, {
relations: ["application_method"],
})
@@ -791,24 +791,16 @@ export default class PromotionModuleService<
)
}
await this.createPromotionRulesAndValues_(
const createdPromotionRules = await this.createPromotionRulesAndValues_(
rulesData,
"method_target_rules",
applicationMethod,
sharedContext
)
return await this.retrieve(
promotionId,
{
relations: [
"rules",
"rules.values",
"application_method",
"application_method.target_rules",
"application_method.target_rules.values",
],
},
return await this.listPromotionRules(
{ id: createdPromotionRules.map((pr) => pr.id) },
{ relations: ["values"] },
sharedContext
)
}
@@ -818,7 +810,7 @@ export default class PromotionModuleService<
promotionId: string,
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO> {
): Promise<PromotionTypes.PromotionRuleDTO[]> {
const promotion = await this.promotionService_.retrieve(promotionId, {
relations: ["application_method"],
})
@@ -832,24 +824,16 @@ export default class PromotionModuleService<
)
}
await this.createPromotionRulesAndValues_(
const createdPromotionRules = await this.createPromotionRulesAndValues_(
rulesData,
"method_buy_rules",
applicationMethod,
sharedContext
)
return await this.retrieve(
promotionId,
{
relations: [
"rules",
"rules.values",
"application_method",
"application_method.buy_rules",
"application_method.buy_rules.values",
],
},
return await this.listPromotionRules(
{ id: createdPromotionRules.map((pr) => pr.id) },
{ relations: ["values"] },
sharedContext
)
}
@@ -860,7 +844,25 @@ export default class PromotionModuleService<
relationName: "promotions" | "method_target_rules" | "method_buy_rules",
relation: Promotion | ApplicationMethod,
@MedusaContext() sharedContext: Context = {}
) {
): Promise<TPromotionRule[]> {
const createdPromotionRules: TPromotionRule[] = []
const promotion =
relation instanceof ApplicationMethod ? relation.promotion : relation
if (!rulesData.length) {
return []
}
if (
relationName === "method_buy_rules" &&
promotion.type === PromotionType.STANDARD
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Can't add buy rules to a ${PromotionType.STANDARD} promotion`
)
}
validatePromotionRuleAttributes(rulesData)
for (const ruleData of rulesData) {
@@ -875,6 +877,8 @@ export default class PromotionModuleService<
sharedContext
)
createdPromotionRules.push(createdPromotionRule)
const ruleValues = Array.isArray(values) ? values : [values]
const promotionRuleValuesData = ruleValues.map((ruleValue) => ({
value: ruleValue,
@@ -886,6 +890,8 @@ export default class PromotionModuleService<
sharedContext
)
}
return createdPromotionRules
}
@InjectManager("baseRepository_")
@@ -893,14 +899,8 @@ export default class PromotionModuleService<
promotionId: string,
rulesData: PromotionTypes.RemovePromotionRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO> {
): Promise<void> {
await this.removePromotionRules_(promotionId, rulesData, sharedContext)
return await this.retrieve(
promotionId,
{ relations: ["rules", "rules.values"] },
sharedContext
)
}
@InjectTransactionManager("baseRepository_")
@@ -932,27 +932,13 @@ export default class PromotionModuleService<
promotionId: string,
rulesData: PromotionTypes.RemovePromotionRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO> {
): Promise<void> {
await this.removeApplicationMethodRules_(
promotionId,
rulesData,
ApplicationMethodRuleTypes.TARGET_RULES,
sharedContext
)
return await this.retrieve(
promotionId,
{
relations: [
"rules",
"rules.values",
"application_method",
"application_method.target_rules",
"application_method.target_rules.values",
],
},
sharedContext
)
}
@InjectManager("baseRepository_")
@@ -960,27 +946,13 @@ export default class PromotionModuleService<
promotionId: string,
rulesData: PromotionTypes.RemovePromotionRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO> {
): Promise<void> {
await this.removeApplicationMethodRules_(
promotionId,
rulesData,
ApplicationMethodRuleTypes.BUY_RULES,
sharedContext
)
return await this.retrieve(
promotionId,
{
relations: [
"rules",
"rules.values",
"application_method",
"application_method.buy_rules",
"application_method.buy_rules.values",
],
},
sharedContext
)
}
@InjectTransactionManager("baseRepository_")

View File

@@ -38,3 +38,5 @@ export interface FilterablePromotionRuleProps
id?: string[]
code?: string[]
}
export type PromotionRuleTypes = "buy_rules" | "target_rules" | "rules"

View File

@@ -1,3 +1,4 @@
export * from "./common"
export * from "./mutations"
export * from "./service"
export * from "./workflows"

View File

@@ -11,6 +11,7 @@ import {
FilterableCampaignProps,
FilterablePromotionProps,
PromotionDTO,
PromotionRuleDTO,
RemovePromotionRuleDTO,
UpdatePromotionDTO,
} from "./common"
@@ -82,37 +83,37 @@ export interface IPromotionModuleService extends IModuleService {
promotionId: string,
rulesData: CreatePromotionRuleDTO[],
sharedContext?: Context
): Promise<PromotionDTO>
): Promise<PromotionRuleDTO[]>
addPromotionTargetRules(
promotionId: string,
rulesData: CreatePromotionRuleDTO[],
sharedContext?: Context
): Promise<PromotionDTO>
): Promise<PromotionRuleDTO[]>
addPromotionBuyRules(
promotionId: string,
rulesData: CreatePromotionRuleDTO[],
sharedContext?: Context
): Promise<PromotionDTO>
): Promise<PromotionRuleDTO[]>
removePromotionRules(
promotionId: string,
rulesData: RemovePromotionRuleDTO[],
sharedContext?: Context
): Promise<PromotionDTO>
): Promise<void>
removePromotionTargetRules(
promotionId: string,
rulesData: RemovePromotionRuleDTO[],
sharedContext?: Context
): Promise<PromotionDTO>
): Promise<void>
removePromotionBuyRules(
promotionId: string,
rulesData: RemovePromotionRuleDTO[],
sharedContext?: Context
): Promise<PromotionDTO>
): Promise<void>
createCampaigns(
data: CreateCampaignDTO,

View File

@@ -0,0 +1,9 @@
import { CreatePromotionRuleDTO, PromotionRuleTypes } from "./common"
export type AddPromotionRulesWorkflowDTO = {
rule_type: PromotionRuleTypes
data: {
id: string
rules: CreatePromotionRuleDTO[]
}
}

View File

@@ -47,3 +47,9 @@ export enum PromotionActions {
REMOVE = "remove",
REPLACE = "replace",
}
export enum RuleType {
RULES = "rules",
TARGET_RULES = "target_rules",
BUY_RULES = "buy_rules",
}