From e5945479e091d9560ae3e7240306a31031ef4584 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 14 Mar 2024 21:04:53 +0100 Subject: [PATCH] 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 --- .changeset/shaggy-ties-type.md | 7 ++ .../promotion/admin/promotion-rules.spec.ts | 107 ++++++++++++++++++ .../core-flows/src/promotion/steps/index.ts | 1 + .../promotion/steps/update-promotion-rules.ts | 48 ++++++++ .../src/promotion/workflows/index.ts | 1 + .../workflows/update-promotion-rules.ts | 13 +++ .../[id]/rules/batch/update/route.ts | 40 +++++++ .../api-v2/admin/promotions/middlewares.ts | 8 ++ .../src/api-v2/admin/promotions/validators.ts | 31 +++++ .../src/services/promotion-module.ts | 84 ++++++++++++++ .../src/promotion/common/promotion-rule.ts | 5 +- packages/types/src/promotion/service.ts | 13 +++ packages/types/src/promotion/workflows.ts | 10 +- 13 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 .changeset/shaggy-ties-type.md create mode 100644 packages/core-flows/src/promotion/steps/update-promotion-rules.ts create mode 100644 packages/core-flows/src/promotion/workflows/update-promotion-rules.ts create mode 100644 packages/medusa/src/api-v2/admin/promotions/[id]/rules/batch/update/route.ts diff --git a/.changeset/shaggy-ties-type.md b/.changeset/shaggy-ties-type.md new file mode 100644 index 0000000000..3534c03cd9 --- /dev/null +++ b/.changeset/shaggy-ties-type.md @@ -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 diff --git a/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts b/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts index d83488e738..e359ab3e3d 100644 --- a/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts +++ b/integration-tests/modules/__tests__/promotion/admin/promotion-rules.spec.ts @@ -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" })], + }), + ]), + }) + ) + }) + }) }) }, }) diff --git a/packages/core-flows/src/promotion/steps/index.ts b/packages/core-flows/src/promotion/steps/index.ts index a385e72d6d..f3b89f7a68 100644 --- a/packages/core-flows/src/promotion/steps/index.ts +++ b/packages/core-flows/src/promotion/steps/index.ts @@ -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" diff --git a/packages/core-flows/src/promotion/steps/update-promotion-rules.ts b/packages/core-flows/src/promotion/steps/update-promotion-rules.ts new file mode 100644 index 0000000000..29e4e65b8f --- /dev/null +++ b/packages/core-flows/src/promotion/steps/update-promotion-rules.ts @@ -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( + 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( + 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!), + })) + ) + } +) diff --git a/packages/core-flows/src/promotion/workflows/index.ts b/packages/core-flows/src/promotion/workflows/index.ts index a385e72d6d..f3b89f7a68 100644 --- a/packages/core-flows/src/promotion/workflows/index.ts +++ b/packages/core-flows/src/promotion/workflows/index.ts @@ -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" diff --git a/packages/core-flows/src/promotion/workflows/update-promotion-rules.ts b/packages/core-flows/src/promotion/workflows/update-promotion-rules.ts new file mode 100644 index 0000000000..836670fbf0 --- /dev/null +++ b/packages/core-flows/src/promotion/workflows/update-promotion-rules.ts @@ -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 + ): WorkflowData => { + updatePromotionRulesStep(input) + } +) diff --git a/packages/medusa/src/api-v2/admin/promotions/[id]/rules/batch/update/route.ts b/packages/medusa/src/api-v2/admin/promotions/[id]/rules/batch/update/route.ts new file mode 100644 index 0000000000..69c944be30 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/promotions/[id]/rules/batch/update/route.ts @@ -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, + 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 }) +} diff --git a/packages/medusa/src/api-v2/admin/promotions/middlewares.ts b/packages/medusa/src/api-v2/admin/promotions/middlewares.ts index 4e18627f08..31f8add036 100644 --- a/packages/medusa/src/api-v2/admin/promotions/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/promotions/middlewares.ts @@ -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", diff --git a/packages/medusa/src/api-v2/admin/promotions/validators.ts b/packages/medusa/src/api-v2/admin/promotions/validators.ts index b6385a625b..dd5a134889 100644 --- a/packages/medusa/src/api-v2/admin/promotions/validators.ts +++ b/packages/medusa/src/api-v2/admin/promotions/validators.ts @@ -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[] +} diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index bb675f03fb..a4c628ed21 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -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 { + 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( + 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, diff --git a/packages/types/src/promotion/common/promotion-rule.ts b/packages/types/src/promotion/common/promotion-rule.ts index b6e616b066..4f978e51cb 100644 --- a/packages/types/src/promotion/common/promotion-rule.ts +++ b/packages/types/src/promotion/common/promotion-rule.ts @@ -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 { id?: string[] - code?: string[] } export type PromotionRuleTypes = "buy_rules" | "target_rules" | "rules" diff --git a/packages/types/src/promotion/service.ts b/packages/types/src/promotion/service.ts index effcb80195..5a9cfb5025 100644 --- a/packages/types/src/promotion/service.ts +++ b/packages/types/src/promotion/service.ts @@ -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 + listPromotionRules( + filters?: FilterablePromotionRuleProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + updatePromotionRules( + data: UpdatePromotionRuleDTO[], + sharedContext?: Context + ): Promise + listCampaigns( filters?: FilterableCampaignProps, config?: FindConfig, diff --git a/packages/types/src/promotion/workflows.ts b/packages/types/src/promotion/workflows.ts index c516e45c7d..d2e90a5d3e 100644 --- a/packages/types/src/promotion/workflows.ts +++ b/packages/types/src/promotion/workflows.ts @@ -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[] +}