feat(medusa,core-flows,types): adds update promotion rule endpoint + workflow (#6702)

what:

- adds endpoint + workflow to update promotion rule
- adds method in promotion to update promotion rules
This commit is contained in:
Riqwan Thamir
2024-03-14 21:04:53 +01:00
committed by GitHub
parent 7be0a2cf6d
commit e5945479e0
13 changed files with 366 additions and 2 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/core-flows": patch
"@medusajs/medusa": patch
"@medusajs/types": patch
---
feat(medusa,core-flows,types): adds update promotion rule endpoint + workflow

View File

@@ -529,6 +529,113 @@ medusaIntegrationTestRunner({
expect(promotion.application_method!.buy_rules!.length).toEqual(0)
})
})
describe("POST /admin/promotions/:id/rules/batch/update", () => {
it("should throw error when required params are missing", async () => {
const { response } = await api
.post(
`/admin/promotions/${standardPromotion.id}/rules/batch/update`,
{
rules: [
{
attribute: "test",
operator: "eq",
values: ["new value"],
},
],
},
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message: "id must be a string, id 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/batch/update`,
{
rules: [
{
id: standardPromotion.rules[0].id,
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 error when promotion rule id does not exist", async () => {
const { response } = await api
.post(
`/admin/promotions/${standardPromotion.id}/rules/batch/update`,
{
rules: [
{
id: "does-not-exist",
attribute: "new_attr",
operator: "eq",
values: ["new value"],
},
],
},
adminHeaders
)
.catch((e) => e)
expect(response.status).toEqual(400)
expect(response.data).toEqual({
type: "invalid_data",
message: "Promotion rules with id - does-not-exist not found",
})
})
it("should add rules to a promotion successfully", async () => {
const response = await api.post(
`/admin/promotions/${standardPromotion.id}/rules/batch/update`,
{
rules: [
{
id: standardPromotion.rules[0].id,
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: "new_attr",
values: [expect.objectContaining({ value: "new value" })],
}),
]),
})
)
})
})
})
},
})

View File

@@ -5,4 +5,5 @@ export * from "./delete-campaigns"
export * from "./delete-promotions"
export * from "./remove-rules-from-promotions"
export * from "./update-campaigns"
export * from "./update-promotion-rules"
export * from "./update-promotions"

View File

@@ -0,0 +1,48 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
IPromotionModuleService,
UpdatePromotionRulesWorkflowDTO,
} from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
export const updatePromotionRulesStepId = "update-promotion-rules"
export const updatePromotionRulesStep = createStep(
updatePromotionRulesStepId,
async (input: UpdatePromotionRulesWorkflowDTO, { container }) => {
const { data } = input
const promotionModule = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)
const promotionRulesBeforeUpdate = await promotionModule.listPromotionRules(
{ id: data.map((d) => d.id) },
{ relations: ["values"] }
)
const updatedPromotionRules = await promotionModule.updatePromotionRules(
data
)
return new StepResponse(updatedPromotionRules, promotionRulesBeforeUpdate)
},
async (updatedPromotionRules, { container }) => {
if (!updatedPromotionRules?.length) {
return
}
const promotionModule = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)
await promotionModule.updatePromotionRules(
updatedPromotionRules.map((rule) => ({
id: rule.id,
description: rule.description,
attribute: rule.attribute,
operator: rule.operator,
values: rule.values.map((v) => v.value!),
}))
)
}
)

View File

@@ -5,4 +5,5 @@ export * from "./delete-campaigns"
export * from "./delete-promotions"
export * from "./remove-rules-from-promotions"
export * from "./update-campaigns"
export * from "./update-promotion-rules"
export * from "./update-promotions"

View File

@@ -0,0 +1,13 @@
import { UpdatePromotionRulesWorkflowDTO } from "@medusajs/types"
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { updatePromotionRulesStep } from "../steps"
export const updatePromotionRulesWorkflowId = "update-promotion-rules-workflow"
export const updatePromotionRulesWorkflow = createWorkflow(
updatePromotionRulesWorkflowId,
(
input: WorkflowData<UpdatePromotionRulesWorkflowDTO>
): WorkflowData<void> => {
updatePromotionRulesStep(input)
}
)

View File

@@ -0,0 +1,40 @@
import { updatePromotionRulesWorkflow } from "@medusajs/core-flows"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../../../types/routing"
import {
defaultAdminPromotionFields,
defaultAdminPromotionRelations,
} from "../../../../query-config"
import { AdminPostPromotionsPromotionRulesBatchUpdateReq } from "../../../../validators"
export const POST = async (
req: AuthenticatedMedusaRequest<AdminPostPromotionsPromotionRulesBatchUpdateReq>,
res: MedusaResponse
) => {
const id = req.params.id
const workflow = updatePromotionRulesWorkflow(req.scope)
const { errors } = await workflow.run({
input: { data: req.validatedBody.rules },
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

@@ -7,6 +7,7 @@ import {
AdminPostPromotionsPromotionReq,
AdminPostPromotionsPromotionRulesBatchAddReq,
AdminPostPromotionsPromotionRulesBatchRemoveReq,
AdminPostPromotionsPromotionRulesBatchUpdateReq,
AdminPostPromotionsReq,
} from "./validators"
@@ -63,6 +64,13 @@ export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/promotions/:id/buy-rules/batch/add",
middlewares: [transformBody(AdminPostPromotionsPromotionRulesBatchAddReq)],
},
{
method: ["POST"],
matcher: "/admin/promotions/:id/rules/batch/update",
middlewares: [
transformBody(AdminPostPromotionsPromotionRulesBatchUpdateReq),
],
},
{
method: ["POST"],
matcher: "/admin/promotions/:id/rules/batch/remove",

View File

@@ -228,3 +228,34 @@ export class AdminPostPromotionsPromotionRulesBatchRemoveReq {
@IsString({ each: true })
rule_ids: string[]
}
export class AdminPostPromotionsPromotionRulesBatchUpdateReq {
@IsArray()
@ValidateNested({ each: true })
@Type(() => UpdatePromotionRule)
rules: UpdatePromotionRule[]
}
export class UpdatePromotionRule {
@IsNotEmpty()
@IsString()
id: string
@IsOptional()
@IsEnum(PromotionRuleOperator)
operator?: PromotionRuleOperator
@IsOptional()
@IsString()
description?: string | null
@IsOptional()
@IsNotEmpty()
@IsString()
attribute: string
@IsOptional()
@IsArray()
@Type(() => String)
values: string[]
}

View File

@@ -17,6 +17,9 @@ import {
MedusaError,
ModulesSdkUtils,
PromotionType,
arrayDifference,
deduplicate,
isDefined,
isString,
} from "@medusajs/utils"
import {
@@ -47,6 +50,7 @@ import {
validatePromotionRuleAttributes,
} from "@utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { CreatePromotionRuleValueDTO } from "../types/promotion-rule-value"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
@@ -750,6 +754,86 @@ export default class PromotionModuleService<
return updatedPromotions
}
@InjectManager("baseRepository_")
async updatePromotionRules(
data: PromotionTypes.UpdatePromotionRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionRuleDTO[]> {
const updatedPromotionRules = await this.updatePromotionRules_(
data,
sharedContext
)
return this.listPromotionRules(
{ id: updatedPromotionRules.map((r) => r.id) },
{ relations: ["values"] },
sharedContext
)
}
@InjectTransactionManager("baseRepository_")
protected async updatePromotionRules_(
data: PromotionTypes.UpdatePromotionRuleDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const promotionRuleIds = data.map((d) => d.id)
const promotionRules = await this.listPromotionRules(
{ id: promotionRuleIds },
{ relations: ["values"] },
sharedContext
)
const invalidRuleId = arrayDifference(
deduplicate(promotionRuleIds),
promotionRules.map((pr) => pr.id)
)
if (invalidRuleId.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Promotion rules with id - ${invalidRuleId.join(", ")} not found`
)
}
const promotionRulesMap = new Map<string, PromotionTypes.PromotionRuleDTO>(
promotionRules.map((pr) => [pr.id, pr])
)
const rulesToUpdate: PromotionTypes.UpdatePromotionRuleDTO[] = []
const ruleValueIdsToDelete: string[] = []
const ruleValuesToCreate: CreatePromotionRuleValueDTO[] = []
for (const promotionRuleData of data) {
const { values, ...rest } = promotionRuleData
const normalizedValues = Array.isArray(values) ? values : [values]
rulesToUpdate.push(rest)
if (isDefined(values)) {
const promotionRule = promotionRulesMap.get(promotionRuleData.id)!
ruleValueIdsToDelete.push(...promotionRule.values.map((v) => v.id))
ruleValuesToCreate.push(
...normalizedValues.map((value) => ({
value,
promotion_rule: promotionRule,
}))
)
}
}
const [updatedRules] = await Promise.all([
this.promotionRuleService_.update(rulesToUpdate, sharedContext),
this.promotionRuleValueService_.delete(
ruleValueIdsToDelete,
sharedContext
),
this.promotionRuleValueService_.create(ruleValuesToCreate, sharedContext),
])
return updatedRules
}
@InjectManager("baseRepository_")
async addPromotionRules(
promotionId: string,

View File

@@ -27,6 +27,10 @@ export interface CreatePromotionRuleDTO {
export interface UpdatePromotionRuleDTO {
id: string
description?: string | null
attribute?: string
operator?: PromotionRuleOperatorValues
values?: string[] | string
}
export interface RemovePromotionRuleDTO {
@@ -36,7 +40,6 @@ export interface RemovePromotionRuleDTO {
export interface FilterablePromotionRuleProps
extends BaseFilterable<FilterablePromotionRuleProps> {
id?: string[]
code?: string[]
}
export type PromotionRuleTypes = "buy_rules" | "target_rules" | "rules"

View File

@@ -10,9 +10,11 @@ import {
CreatePromotionRuleDTO,
FilterableCampaignProps,
FilterablePromotionProps,
FilterablePromotionRuleProps,
PromotionDTO,
PromotionRuleDTO,
UpdatePromotionDTO,
UpdatePromotionRuleDTO,
} from "./common"
import { CreateCampaignDTO, UpdateCampaignDTO } from "./mutations"
@@ -134,6 +136,17 @@ export interface IPromotionModuleService extends IModuleService {
sharedContext?: Context
): Promise<CampaignDTO>
listPromotionRules(
filters?: FilterablePromotionRuleProps,
config?: FindConfig<PromotionRuleDTO>,
sharedContext?: Context
): Promise<PromotionRuleDTO[]>
updatePromotionRules(
data: UpdatePromotionRuleDTO[],
sharedContext?: Context
): Promise<PromotionRuleDTO[]>
listCampaigns(
filters?: FilterableCampaignProps,
config?: FindConfig<CampaignDTO>,

View File

@@ -1,4 +1,8 @@
import { CreatePromotionRuleDTO, PromotionRuleTypes } from "./common"
import {
CreatePromotionRuleDTO,
PromotionRuleTypes,
UpdatePromotionRuleDTO,
} from "./common"
export type AddPromotionRulesWorkflowDTO = {
rule_type: PromotionRuleTypes
@@ -15,3 +19,7 @@ export type RemovePromotionRulesWorkflowDTO = {
rule_ids: string[]
}
}
export type UpdatePromotionRulesWorkflowDTO = {
data: UpdatePromotionRuleDTO[]
}