feat(core-flows,medusa,types): remove rules from promotion endpoints + workflows (#6696)

This commit is contained in:
Riqwan Thamir
2024-03-14 09:19:05 +01:00
committed by GitHub
parent 640eccd5dd
commit 04a532e5ef
16 changed files with 485 additions and 53 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/core-flows": patch
"@medusajs/medusa": patch
"@medusajs/types": patch
---
feat(core-flows,medusa,types): remove rules from promotion endpoints + workflows

View File

@@ -342,6 +342,194 @@ medusaIntegrationTestRunner({
)
})
})
describe("DELETE /admin/promotions/:id/rules", () => {
it("should throw error when required params are missing", async () => {
const { response } = await api
.delete(`/admin/promotions/${standardPromotion.id}/rules`, {
...adminHeaders,
data: {},
})
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message:
"each value in rule_ids must be a string, rule_ids should not be empty",
})
})
it("should throw error when promotion does not exist", async () => {
const { response } = await api
.delete(`/admin/promotions/does-not-exist/rules`, {
...adminHeaders,
data: { rule_ids: ["test-rule-id"] },
})
.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 remove rules from a promotion successfully", async () => {
const response = await api.delete(
`/admin/promotions/${standardPromotion.id}/rules`,
{
...adminHeaders,
data: { rule_ids: [standardPromotion.rules[0].id] },
}
)
expect(response.status).toEqual(200)
expect(response.data).toEqual({
ids: [standardPromotion.rules[0].id],
object: "promotion-rule",
deleted: true,
})
const promotion = await promotionModule.retrieve(
standardPromotion.id,
{ relations: ["rules"] }
)
expect(promotion.rules!.length).toEqual(0)
})
})
describe("DELETE /admin/promotions/:id/target-rules", () => {
it("should throw error when required params are missing", async () => {
const { response } = await api
.delete(`/admin/promotions/${standardPromotion.id}/target-rules`, {
...adminHeaders,
data: {},
})
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message:
"each value in rule_ids must be a string, rule_ids should not be empty",
})
})
it("should throw error when promotion does not exist", async () => {
const { response } = await api
.delete(`/admin/promotions/does-not-exist/target-rules`, {
...adminHeaders,
data: { rule_ids: ["test-rule-id"] },
})
.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 remove target rules from a promotion successfully", async () => {
const ruleId = standardPromotion.application_method.target_rules[0].id
const response = await api.delete(
`/admin/promotions/${standardPromotion.id}/target-rules`,
{
...adminHeaders,
data: { rule_ids: [ruleId] },
}
)
expect(response.status).toEqual(200)
expect(response.data).toEqual({
ids: [ruleId],
object: "promotion-rule",
deleted: true,
})
const promotion = await promotionModule.retrieve(
standardPromotion.id,
{ relations: ["application_method.target_rules"] }
)
expect(promotion.application_method!.target_rules!.length).toEqual(0)
})
})
describe("DELETE /admin/promotions/:id/buy-rules", () => {
it("should throw error when required params are missing", async () => {
const { response } = await api
.delete(`/admin/promotions/${standardPromotion.id}/buy-rules`, {
...adminHeaders,
data: {},
})
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message:
"each value in rule_ids must be a string, rule_ids should not be empty",
})
})
it("should throw error when promotion does not exist", async () => {
const { response } = await api
.delete(`/admin/promotions/does-not-exist/buy-rules`, {
...adminHeaders,
data: { rule_ids: ["test-rule-id"] },
})
.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 remove buy rules from a 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 ruleId = buyGetPromotion!.application_method!.buy_rules![0].id
const response = await api.delete(
`/admin/promotions/${buyGetPromotion.id}/buy-rules`,
{
...adminHeaders,
data: { rule_ids: [ruleId] },
}
)
expect(response.status).toEqual(200)
expect(response.data).toEqual({
ids: [ruleId],
object: "promotion-rule",
deleted: true,
})
const promotion = await promotionModule.retrieve(buyGetPromotion.id, {
relations: ["application_method.buy_rules"],
})
expect(promotion.application_method!.buy_rules!.length).toEqual(0)
})
})
})
},
})

View File

@@ -39,7 +39,7 @@ export const addRulesToPromotionsStep = createStep(
return new StepResponse(promotionRules, {
id: data.id,
ruleIds: createdPromotionRules.map((pr) => pr.id),
promotionRuleIds: createdPromotionRules.map((pr) => pr.id),
buyRuleIds: createdPromotionBuyRules.map((pr) => pr.id),
targetRuleIds: createdPromotionBuyRules.map((pr) => pr.id),
})
@@ -49,28 +49,24 @@ export const addRulesToPromotionsStep = createStep(
return
}
const { id, ruleIds = [], buyRuleIds = [], targetRuleIds = [] } = data
const {
id,
promotionRuleIds = [],
buyRuleIds = [],
targetRuleIds = [],
} = data
const promotionModule = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)
ruleIds.length &&
(await promotionModule.removePromotionRules(
id,
ruleIds.map((id) => ({ id }))
))
promotionRuleIds.length &&
(await promotionModule.removePromotionRules(id, promotionRuleIds))
buyRuleIds.length &&
(await promotionModule.removePromotionBuyRules(
id,
buyRuleIds.map((id) => ({ id }))
))
(await promotionModule.removePromotionBuyRules(id, buyRuleIds))
targetRuleIds.length &&
(await promotionModule.removePromotionBuyRules(
id,
targetRuleIds.map((id) => ({ id }))
))
(await promotionModule.removePromotionBuyRules(id, targetRuleIds))
}
)

View File

@@ -3,5 +3,6 @@ export * from "./create-campaigns"
export * from "./create-promotions"
export * from "./delete-campaigns"
export * from "./delete-promotions"
export * from "./remove-rules-from-promotions"
export * from "./update-campaigns"
export * from "./update-promotions"

View File

@@ -0,0 +1,95 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
CreatePromotionRuleDTO,
IPromotionModuleService,
PromotionRuleDTO,
RemovePromotionRulesWorkflowDTO,
} from "@medusajs/types"
import { RuleType } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
export const removeRulesFromPromotionsStepId = "remove-rules-from-promotions"
export const removeRulesFromPromotionsStep = createStep(
removeRulesFromPromotionsStepId,
async (input: RemovePromotionRulesWorkflowDTO, { container }) => {
const { data, rule_type: ruleType } = input
const promotionModule = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)
const promotion = await promotionModule.retrieve(data.id, {
relations: [
"rules.values",
"application_method.target_rules.values",
"application_method.buy_rules.values",
],
})
const promotionRulesToCreate: CreatePromotionRuleDTO[] = []
const buyRulesToCreate: CreatePromotionRuleDTO[] = []
const targetRulesToCreate: CreatePromotionRuleDTO[] = []
if (ruleType === RuleType.RULES) {
const rules = promotion.rules!
promotionRulesToCreate.push(...promotionRuleAttribute(rules))
await promotionModule.removePromotionRules(data.id, data.rule_ids)
}
if (ruleType === RuleType.BUY_RULES) {
const rules = promotion.application_method?.buy_rules!
buyRulesToCreate.push(...promotionRuleAttribute(rules))
await promotionModule.removePromotionBuyRules(data.id, data.rule_ids)
}
if (ruleType === RuleType.TARGET_RULES) {
const rules = promotion.application_method?.target_rules!
targetRulesToCreate.push(...promotionRuleAttribute(rules))
await promotionModule.removePromotionTargetRules(data.id, data.rule_ids)
}
return new StepResponse(null, {
id: data.id,
promotionRulesToCreate,
buyRulesToCreate,
targetRulesToCreate,
})
},
async (data, { container }) => {
if (!data) {
return
}
const {
id,
promotionRulesToCreate = [],
buyRulesToCreate = [],
targetRulesToCreate = [],
} = data
const promotionModule = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)
promotionRulesToCreate.length &&
(await promotionModule.addPromotionRules(id, promotionRulesToCreate))
buyRulesToCreate.length &&
(await promotionModule.addPromotionBuyRules(id, buyRulesToCreate))
targetRulesToCreate.length &&
(await promotionModule.addPromotionBuyRules(id, targetRulesToCreate))
}
)
function promotionRuleAttribute(rules: PromotionRuleDTO[]) {
return rules.map((rule) => ({
description: rule.description!,
attribute: rule.attribute!,
operator: rule.operator!,
values: rule.values!.map((val) => val.value!),
}))
}

View File

@@ -3,5 +3,6 @@ export * from "./create-campaigns"
export * from "./create-promotions"
export * from "./delete-campaigns"
export * from "./delete-promotions"
export * from "./remove-rules-from-promotions"
export * from "./update-campaigns"
export * from "./update-promotions"

View File

@@ -0,0 +1,14 @@
import { RemovePromotionRulesWorkflowDTO } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { removeRulesFromPromotionsStep } from "../steps"
export const removeRulesFromPromotionsWorkflowId =
"remove-rules-from-promotions-workflow"
export const removeRulesFromPromotionsWorkflow = createWorkflow(
removeRulesFromPromotionsWorkflowId,
(
input: WorkflowData<RemovePromotionRulesWorkflowDTO>
): WorkflowData<void> => {
removeRulesFromPromotionsStep(input)
}
)

View File

@@ -1,4 +1,7 @@
import { addRulesToPromotionsWorkflow } from "@medusajs/core-flows"
import {
addRulesToPromotionsWorkflow,
removeRulesFromPromotionsWorkflow,
} from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { RuleType } from "@medusajs/utils"
@@ -10,7 +13,10 @@ import {
defaultAdminPromotionFields,
defaultAdminPromotionRelations,
} from "../../query-config"
import { AdminPostPromotionsPromotionRulesReq } from "../../validators"
import {
AdminDeletePromotionsPromotionRulesReq,
AdminPostPromotionsPromotionRulesReq,
} from "../../validators"
export const POST = async (
req: AuthenticatedMedusaRequest<AdminPostPromotionsPromotionRulesReq>,
@@ -22,7 +28,7 @@ export const POST = async (
input: {
rule_type: RuleType.BUY_RULES,
data: {
id: req.params.id,
id,
...req.validatedBody,
},
},
@@ -44,3 +50,33 @@ export const POST = async (
res.status(200).json({ promotion })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest<AdminDeletePromotionsPromotionRulesReq>,
res: MedusaResponse
) => {
const id = req.params.id
const workflow = removeRulesFromPromotionsWorkflow(req.scope)
const validatedBody = req.validatedBody
const { errors } = await workflow.run({
input: {
rule_type: RuleType.BUY_RULES,
data: {
id,
...validatedBody,
},
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({
ids: validatedBody.rule_ids,
object: "promotion-rule",
deleted: true,
})
}

View File

@@ -1,4 +1,7 @@
import { addRulesToPromotionsWorkflow } from "@medusajs/core-flows"
import {
addRulesToPromotionsWorkflow,
removeRulesFromPromotionsWorkflow,
} from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { RuleType } from "@medusajs/utils"
@@ -10,7 +13,10 @@ import {
defaultAdminPromotionFields,
defaultAdminPromotionRelations,
} from "../../query-config"
import { AdminPostPromotionsPromotionRulesReq } from "../../validators"
import {
AdminDeletePromotionsPromotionRulesReq,
AdminPostPromotionsPromotionRulesReq,
} from "../../validators"
export const POST = async (
req: AuthenticatedMedusaRequest<AdminPostPromotionsPromotionRulesReq>,
@@ -23,7 +29,7 @@ export const POST = async (
input: {
rule_type: RuleType.RULES,
data: {
id: req.params.id,
id,
...req.validatedBody,
},
},
@@ -45,3 +51,33 @@ export const POST = async (
res.status(200).json({ promotion })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest<AdminDeletePromotionsPromotionRulesReq>,
res: MedusaResponse
) => {
const id = req.params.id
const workflow = removeRulesFromPromotionsWorkflow(req.scope)
const validatedBody = req.validatedBody
const { errors } = await workflow.run({
input: {
rule_type: RuleType.RULES,
data: {
id,
...validatedBody,
},
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({
ids: validatedBody.rule_ids,
object: "promotion-rule",
deleted: true,
})
}

View File

@@ -1,4 +1,7 @@
import { addRulesToPromotionsWorkflow } from "@medusajs/core-flows"
import {
addRulesToPromotionsWorkflow,
removeRulesFromPromotionsWorkflow,
} from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { RuleType } from "@medusajs/utils"
@@ -10,7 +13,10 @@ import {
defaultAdminPromotionFields,
defaultAdminPromotionRelations,
} from "../../query-config"
import { AdminPostPromotionsPromotionRulesReq } from "../../validators"
import {
AdminDeletePromotionsPromotionRulesReq,
AdminPostPromotionsPromotionRulesReq,
} from "../../validators"
export const POST = async (
req: AuthenticatedMedusaRequest<AdminPostPromotionsPromotionRulesReq>,
@@ -22,7 +28,7 @@ export const POST = async (
input: {
rule_type: RuleType.TARGET_RULES,
data: {
id: req.params.id,
id,
...req.validatedBody,
},
},
@@ -44,3 +50,33 @@ export const POST = async (
res.status(200).json({ promotion })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest<AdminDeletePromotionsPromotionRulesReq>,
res: MedusaResponse
) => {
const id = req.params.id
const workflow = removeRulesFromPromotionsWorkflow(req.scope)
const validatedBody = req.validatedBody
const { errors } = await workflow.run({
input: {
rule_type: RuleType.TARGET_RULES,
data: {
id,
...validatedBody,
},
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res.status(200).json({
ids: validatedBody.rule_ids,
object: "promotion-rule",
deleted: true,
})
}

View File

@@ -2,6 +2,7 @@ import * as QueryConfig from "./query-config"
import { transformBody, transformQuery } from "../../../api/middlewares"
import {
AdminDeletePromotionsPromotionRulesReq,
AdminGetPromotionsParams,
AdminGetPromotionsPromotionParams,
AdminPostPromotionsPromotionReq,
@@ -62,4 +63,19 @@ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/promotions/:id/buy-rules",
middlewares: [transformBody(AdminPostPromotionsPromotionRulesReq)],
},
{
method: ["DELETE"],
matcher: "/admin/promotions/:id/rules",
middlewares: [transformBody(AdminDeletePromotionsPromotionRulesReq)],
},
{
method: ["DELETE"],
matcher: "/admin/promotions/:id/target-rules",
middlewares: [transformBody(AdminDeletePromotionsPromotionRulesReq)],
},
{
method: ["DELETE"],
matcher: "/admin/promotions/:id/buy-rules",
middlewares: [transformBody(AdminDeletePromotionsPromotionRulesReq)],
},
]

View File

@@ -8,6 +8,7 @@ import {
} from "@medusajs/utils"
import { Type } from "class-transformer"
import {
ArrayNotEmpty,
IsArray,
IsBoolean,
IsEnum,
@@ -221,3 +222,9 @@ export class AdminPostPromotionsPromotionRulesReq {
@Type(() => PromotionRule)
rules: PromotionRule[]
}
export class AdminDeletePromotionsPromotionRulesReq {
@ArrayNotEmpty()
@IsString({ each: true })
rule_ids: string[]
}

View File

@@ -1318,9 +1318,9 @@ describe("Promotion Service", () => {
})
it("should successfully remove rules for a promotion", async () => {
const [ruleId] = promotion.rules.map((rule) => rule.id)
const ruleIds = promotion.rules.map((rule) => rule.id)
await service.removePromotionRules(promotion.id, [{ id: ruleId }])
await service.removePromotionRules(promotion.id, ruleIds)
const updatedPromotion = await service.retrieve(promotion.id, {
relations: ["rules", "rules.values"],
@@ -1391,11 +1391,11 @@ describe("Promotion Service", () => {
})
it("should successfully create rules for a promotion", async () => {
const [ruleId] = promotion.application_method.target_rules.map(
const ruleIds = promotion.application_method.target_rules.map(
(rule) => rule.id
)
await service.removePromotionTargetRules(promotion.id, [{ id: ruleId }])
await service.removePromotionTargetRules(promotion.id, ruleIds)
const updatedPromotion = await service.retrieve(promotion.id, {
relations: ["application_method.target_rules.values"],
@@ -1477,11 +1477,11 @@ describe("Promotion Service", () => {
})
it("should successfully remove rules for a promotion", async () => {
const [ruleId] = promotion.application_method.buy_rules.map(
const ruleIds = promotion.application_method.buy_rules.map(
(rule) => rule.id
)
await service.removePromotionBuyRules(promotion.id, [{ id: ruleId }])
await service.removePromotionBuyRules(promotion.id, ruleIds)
const updatedPromotion = await service.retrieve(promotion.id, {
relations: ["application_method.buy_rules.values"],

View File

@@ -897,32 +897,26 @@ export default class PromotionModuleService<
@InjectManager("baseRepository_")
async removePromotionRules(
promotionId: string,
rulesData: PromotionTypes.RemovePromotionRuleDTO[],
ruleIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.removePromotionRules_(promotionId, rulesData, sharedContext)
await this.removePromotionRules_(promotionId, ruleIds, sharedContext)
}
@InjectTransactionManager("baseRepository_")
protected async removePromotionRules_(
promotionId: string,
rulesData: PromotionTypes.RemovePromotionRuleDTO[],
ruleIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
const promotionRuleIdsToRemove = rulesData.map((ruleData) => ruleData.id)
const promotion = await this.promotionService_.retrieve(
promotionId,
{ relations: ["rules"] },
sharedContext
)
const existingPromotionRuleIds = promotion.rules
.toArray()
.map((rule) => rule.id)
const idsToRemove = promotionRuleIdsToRemove.filter((ruleId) =>
existingPromotionRuleIds.includes(ruleId)
)
const existingRuleIds = promotion.rules.map((rule) => rule.id)
const idsToRemove = ruleIds.filter((id) => existingRuleIds.includes(id))
await this.promotionRuleService_.delete(idsToRemove, sharedContext)
}
@@ -930,12 +924,12 @@ export default class PromotionModuleService<
@InjectManager("baseRepository_")
async removePromotionTargetRules(
promotionId: string,
rulesData: PromotionTypes.RemovePromotionRuleDTO[],
ruleIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.removeApplicationMethodRules_(
promotionId,
rulesData,
ruleIds,
ApplicationMethodRuleTypes.TARGET_RULES,
sharedContext
)
@@ -944,12 +938,12 @@ export default class PromotionModuleService<
@InjectManager("baseRepository_")
async removePromotionBuyRules(
promotionId: string,
rulesData: PromotionTypes.RemovePromotionRuleDTO[],
ruleIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.removeApplicationMethodRules_(
promotionId,
rulesData,
ruleIds,
ApplicationMethodRuleTypes.BUY_RULES,
sharedContext
)
@@ -958,13 +952,12 @@ export default class PromotionModuleService<
@InjectTransactionManager("baseRepository_")
protected async removeApplicationMethodRules_(
promotionId: string,
rulesData: PromotionTypes.RemovePromotionRuleDTO[],
ruleIds: string[],
relation:
| ApplicationMethodRuleTypes.TARGET_RULES
| ApplicationMethodRuleTypes.BUY_RULES,
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
const promotionRuleIds = rulesData.map((ruleData) => ruleData.id)
const promotion = await this.promotionService_.retrieve(
promotionId,
{ relations: [`application_method.${relation}`] },
@@ -981,8 +974,7 @@ export default class PromotionModuleService<
}
const targetRuleIdsToRemove = applicationMethod[relation]
.toArray()
.filter((rule) => promotionRuleIds.includes(rule.id))
.filter((rule) => ruleIds.includes(rule.id))
.map((rule) => rule.id)
await this.promotionRuleService_.delete(

View File

@@ -12,7 +12,6 @@ import {
FilterablePromotionProps,
PromotionDTO,
PromotionRuleDTO,
RemovePromotionRuleDTO,
UpdatePromotionDTO,
} from "./common"
import { CreateCampaignDTO, UpdateCampaignDTO } from "./mutations"
@@ -99,19 +98,19 @@ export interface IPromotionModuleService extends IModuleService {
removePromotionRules(
promotionId: string,
rulesData: RemovePromotionRuleDTO[],
ruleIds: string[],
sharedContext?: Context
): Promise<void>
removePromotionTargetRules(
promotionId: string,
rulesData: RemovePromotionRuleDTO[],
ruleIds: string[],
sharedContext?: Context
): Promise<void>
removePromotionBuyRules(
promotionId: string,
rulesData: RemovePromotionRuleDTO[],
ruleIds: string[],
sharedContext?: Context
): Promise<void>

View File

@@ -7,3 +7,11 @@ export type AddPromotionRulesWorkflowDTO = {
rules: CreatePromotionRuleDTO[]
}
}
export type RemovePromotionRulesWorkflowDTO = {
rule_type: PromotionRuleTypes
data: {
id: string
rule_ids: string[]
}
}