diff --git a/.changeset/great-cameras-stare.md b/.changeset/great-cameras-stare.md new file mode 100644 index 0000000000..08eb7fab32 --- /dev/null +++ b/.changeset/great-cameras-stare.md @@ -0,0 +1,5 @@ +--- +"@medusajs/types": patch +--- + +feat(types): promotion delete / update / retrieve diff --git a/packages/promotion/integration-tests/__fixtures__/promotion/index.ts b/packages/promotion/integration-tests/__fixtures__/promotion/index.ts index 80ef2f7e66..2339d9a583 100644 --- a/packages/promotion/integration-tests/__fixtures__/promotion/index.ts +++ b/packages/promotion/integration-tests/__fixtures__/promotion/index.ts @@ -1,3 +1,4 @@ +import { CreatePromotionDTO } from "@medusajs/types" import { SqlEntityManager } from "@mikro-orm/postgresql" import { Promotion } from "@models" import { defaultPromotionsData } from "./data" @@ -6,9 +7,9 @@ export * from "./data" export async function createPromotions( manager: SqlEntityManager, - promotionsData = defaultPromotionsData + promotionsData: CreatePromotionDTO[] = defaultPromotionsData ): Promise { - const promotion: Promotion[] = [] + const promotions: Promotion[] = [] for (let promotionData of promotionsData) { let promotion = manager.create(Promotion, promotionData) @@ -18,5 +19,5 @@ export async function createPromotions( await manager.flush() } - return promotion + return promotions } diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts index 5c5e8742ef..ddcff28851 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts @@ -2,6 +2,7 @@ import { IPromotionModuleService } from "@medusajs/types" import { PromotionType } from "@medusajs/utils" import { SqlEntityManager } from "@mikro-orm/postgresql" import { initialize } from "../../../../src" +import { createPromotions } from "../../../__fixtures__/promotion" import { DB_URL, MikroOrmWrapper } from "../../../utils" jest.setTimeout(30000) @@ -70,7 +71,7 @@ describe("Promotion Service", () => { application_method: { type: "fixed", target_type: "order", - value: 100, + value: "100", }, }, ]) @@ -106,7 +107,7 @@ describe("Promotion Service", () => { application_method: { type: "fixed", target_type: "order", - value: 100, + value: "100", target_rules: [ { attribute: "product_id", @@ -164,7 +165,7 @@ describe("Promotion Service", () => { application_method: { type: "fixed", target_type: "item", - value: 100, + value: "100", }, }, ]) @@ -185,7 +186,7 @@ describe("Promotion Service", () => { type: "fixed", allocation: "each", target_type: "shipping", - value: 100, + value: "100", }, }, ]) @@ -347,4 +348,541 @@ describe("Promotion Service", () => { ) }) }) + + describe("update", () => { + it("should throw an error when required params are not passed", async () => { + const error = await service + .update([ + { + type: PromotionType.STANDARD, + } as any, + ]) + .catch((e) => e) + + expect(error.message).toContain('Promotion with id "undefined" not found') + }) + + it("should update the attributes of a promotion successfully", async () => { + await createPromotions(repositoryManager) + + const [updatedPromotion] = await service.update([ + { + id: "promotion-id-1", + is_automatic: true, + code: "TEST", + type: PromotionType.BUYGET, + }, + ]) + + expect(updatedPromotion).toEqual( + expect.objectContaining({ + is_automatic: true, + code: "TEST", + type: PromotionType.BUYGET, + }) + ) + }) + + it("should update the attributes of a application method successfully", async () => { + const [createdPromotion] = await service.create([ + { + code: "TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "item", + allocation: "across", + value: "100", + }, + }, + ]) + const applicationMethod = createdPromotion.application_method + + const [updatedPromotion] = await service.update([ + { + id: createdPromotion.id, + application_method: { + id: applicationMethod?.id as string, + value: "200", + }, + }, + ]) + + expect(updatedPromotion).toEqual( + expect.objectContaining({ + application_method: expect.objectContaining({ + value: 200, + }), + }) + ) + }) + + it("should change max_quantity to 0 when target_type is changed to order", async () => { + const [createdPromotion] = await service.create([ + { + code: "TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "item", + allocation: "each", + value: "100", + max_quantity: 500, + }, + }, + ]) + const applicationMethod = createdPromotion.application_method + + const [updatedPromotion] = await service.update([ + { + id: createdPromotion.id, + application_method: { + id: applicationMethod?.id as string, + target_type: "order", + allocation: "across", + }, + }, + ]) + + expect(updatedPromotion).toEqual( + expect.objectContaining({ + application_method: expect.objectContaining({ + target_type: "order", + allocation: "across", + max_quantity: 0, + }), + }) + ) + }) + + it("should validate the attributes of a application method successfully", async () => { + const [createdPromotion] = await service.create([ + { + code: "TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "order", + allocation: "across", + value: "100", + }, + }, + ]) + const applicationMethod = createdPromotion.application_method + + let error = await service + .update([ + { + id: createdPromotion.id, + application_method: { + id: applicationMethod?.id as string, + target_type: "should-error", + } as any, + }, + ]) + .catch((e) => e) + + expect(error.message).toContain( + `application_method.target_type should be one of order, shipping, item` + ) + + error = await service + .update([ + { + id: createdPromotion.id, + application_method: { + id: applicationMethod?.id as string, + allocation: "should-error", + } as any, + }, + ]) + .catch((e) => e) + + expect(error.message).toContain( + `application_method.allocation should be one of each, across` + ) + + error = await service + .update([ + { + id: createdPromotion.id, + application_method: { + id: applicationMethod?.id as string, + type: "should-error", + } as any, + }, + ]) + .catch((e) => e) + + expect(error.message).toContain( + `application_method.type should be one of fixed, percentage` + ) + }) + }) + + describe("retrieve", () => { + beforeEach(async () => { + await createPromotions(repositoryManager) + }) + + const id = "promotion-id-1" + + it("should return promotion for the given id", async () => { + const promotion = await service.retrieve(id) + + expect(promotion).toEqual( + expect.objectContaining({ + id, + }) + ) + }) + + it("should throw an error when promotion with id does not exist", async () => { + let error + + try { + await service.retrieve("does-not-exist") + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "Promotion with id: does-not-exist was not found" + ) + }) + + it("should throw an error when a id is not provided", async () => { + let error + + try { + await service.retrieve(undefined as unknown as string) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"promotionId" must be defined') + }) + + it("should return promotion based on config select param", async () => { + const promotion = await service.retrieve(id, { + select: ["id"], + }) + + const serialized = JSON.parse(JSON.stringify(promotion)) + + expect(serialized).toEqual({ + id, + }) + }) + }) + + describe("delete", () => { + beforeEach(async () => { + await createPromotions(repositoryManager) + }) + + const id = "promotion-id-1" + + it("should delete the promotions given an id successfully", async () => { + await service.delete([id]) + + const promotions = await service.list({ + id: [id], + }) + + expect(promotions).toHaveLength(0) + }) + }) + + describe("addPromotionRules", () => { + let promotion + + beforeEach(async () => { + ;[promotion] = await service.create([ + { + code: "TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "item", + allocation: "each", + value: "100", + max_quantity: 500, + }, + }, + ]) + }) + + it("should throw an error when promotion with id does not exist", async () => { + let error + + try { + await service.addPromotionRules("does-not-exist", []) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "Promotion with id: does-not-exist was not found" + ) + }) + + it("should throw an error when a id is not provided", async () => { + let error + + try { + await service.addPromotionRules(undefined as unknown as string, []) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"promotionId" must be defined') + }) + + it("should successfully create rules for a promotion", async () => { + promotion = await service.addPromotionRules(promotion.id, [ + { + attribute: "customer_group_id", + operator: "in", + values: ["VIP", "top100"], + }, + ]) + + expect(promotion).toEqual( + expect.objectContaining({ + id: promotion.id, + rules: [ + expect.objectContaining({ + attribute: "customer_group_id", + operator: "in", + values: [ + expect.objectContaining({ value: "VIP" }), + expect.objectContaining({ value: "top100" }), + ], + }), + ], + }) + ) + }) + }) + + describe("addPromotionTargetRules", () => { + let promotion + + beforeEach(async () => { + ;[promotion] = await service.create([ + { + code: "TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "item", + allocation: "each", + value: "100", + max_quantity: 500, + }, + }, + ]) + }) + + it("should throw an error when promotion with id does not exist", async () => { + let error + + try { + await service.addPromotionTargetRules("does-not-exist", []) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "Promotion with id: does-not-exist was not found" + ) + }) + + it("should throw an error when a id is not provided", async () => { + let error + + try { + await service.addPromotionTargetRules( + undefined as unknown as string, + [] + ) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"promotionId" must be defined') + }) + + it("should successfully create target rules for a promotion", async () => { + promotion = await service.addPromotionTargetRules(promotion.id, [ + { + attribute: "customer_group_id", + operator: "in", + values: ["VIP", "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" }), + ], + }), + ], + }), + }) + ) + }) + }) + + describe("removePromotionRules", () => { + let promotion + + beforeEach(async () => { + ;[promotion] = await service.create([ + { + code: "TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer_group_id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "item", + allocation: "each", + value: "100", + max_quantity: 500, + }, + }, + ]) + }) + + it("should throw an error when promotion with id does not exist", async () => { + let error + + try { + await service.removePromotionRules("does-not-exist", []) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "Promotion with id: does-not-exist was not found" + ) + }) + + it("should throw an error when a id is not provided", async () => { + let error + + try { + await service.removePromotionRules(undefined as unknown as string, []) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"promotionId" must be defined') + }) + + it("should successfully create rules for a promotion", async () => { + const [ruleId] = promotion.rules.map((rule) => rule.id) + + promotion = await service.removePromotionRules(promotion.id, [ + { id: ruleId }, + ]) + + expect(promotion).toEqual( + expect.objectContaining({ + id: promotion.id, + rules: [], + }) + ) + }) + }) + + describe("removePromotionTargetRules", () => { + let promotion + + beforeEach(async () => { + ;[promotion] = await service.create([ + { + code: "TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "item", + allocation: "each", + value: "100", + max_quantity: 500, + target_rules: [ + { + attribute: "customer_group_id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + }, + }, + ]) + }) + + it("should throw an error when promotion with id does not exist", async () => { + let error + + try { + await service.removePromotionTargetRules("does-not-exist", []) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "Promotion with id: does-not-exist was not found" + ) + }) + + it("should throw an error when a id is not provided", async () => { + let error + + try { + await service.removePromotionTargetRules( + undefined as unknown as string, + [] + ) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"promotionId" must be defined') + }) + + it("should successfully create rules for a promotion", async () => { + const [ruleId] = promotion.application_method.target_rules.map( + (rule) => rule.id + ) + + promotion = await service.removePromotionTargetRules(promotion.id, [ + { id: ruleId }, + ]) + + expect(promotion).toEqual( + expect.objectContaining({ + id: promotion.id, + application_method: expect.objectContaining({ + target_rules: [], + }), + }) + ) + }) + }) }) diff --git a/packages/promotion/src/models/application-method.ts b/packages/promotion/src/models/application-method.ts index 24a0dc4b09..6eab16e0e6 100644 --- a/packages/promotion/src/models/application-method.ts +++ b/packages/promotion/src/models/application-method.ts @@ -1,7 +1,7 @@ import { - ApplicationMethodAllocation, - ApplicationMethodTargetType, - ApplicationMethodType, + ApplicationMethodAllocationValues, + ApplicationMethodTargetTypeValues, + ApplicationMethodTypeValues, } from "@medusajs/types" import { PromotionUtils, generateEntityId } from "@medusajs/utils" import { @@ -27,6 +27,7 @@ type OptionalFields = | "created_at" | "updated_at" | "deleted_at" + @Entity() export default class ApplicationMethod { [OptionalProps]?: OptionalFields @@ -35,25 +36,25 @@ export default class ApplicationMethod { id!: string @Property({ columnType: "numeric", nullable: true, serializer: Number }) - value?: number | null + value?: string | null @Property({ columnType: "numeric", nullable: true, serializer: Number }) max_quantity?: number | null @Index({ name: "IDX_application_method_type" }) @Enum(() => PromotionUtils.ApplicationMethodType) - type: ApplicationMethodType + type: ApplicationMethodTypeValues @Index({ name: "IDX_application_method_target_type" }) @Enum(() => PromotionUtils.ApplicationMethodTargetType) - target_type: ApplicationMethodTargetType + target_type: ApplicationMethodTargetTypeValues @Index({ name: "IDX_application_method_allocation" }) @Enum({ items: () => PromotionUtils.ApplicationMethodAllocation, nullable: true, }) - allocation?: ApplicationMethodAllocation + allocation?: ApplicationMethodAllocationValues @OneToOne({ entity: () => Promotion, diff --git a/packages/promotion/src/models/promotion.ts b/packages/promotion/src/models/promotion.ts index 6e58568480..94af940a8e 100644 --- a/packages/promotion/src/models/promotion.ts +++ b/packages/promotion/src/models/promotion.ts @@ -48,6 +48,7 @@ export default class Promotion { @OneToOne({ entity: () => ApplicationMethod, mappedBy: (am) => am.promotion, + cascade: ["soft-remove"] as any, }) application_method: ApplicationMethod diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index dcf4b54818..fbe52aa512 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -10,6 +10,7 @@ import { InjectManager, InjectTransactionManager, MedusaContext, + MedusaError, } from "@medusajs/utils" import { ApplicationMethod, Promotion } from "@models" import { @@ -19,8 +20,14 @@ import { PromotionService, } from "@services" import { joinerConfig } from "../joiner-config" -import { CreateApplicationMethodDTO, CreatePromotionDTO } from "../types" import { + CreateApplicationMethodDTO, + CreatePromotionDTO, + UpdateApplicationMethodDTO, + UpdatePromotionDTO, +} from "../types" +import { + allowedAllocationForQuantity, validateApplicationMethodAttributes, validatePromotionRuleAttributes, } from "../utils" @@ -64,6 +71,26 @@ export default class PromotionModuleService< return joinerConfig } + @InjectManager("baseRepository_") + async retrieve( + id: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const promotion = await this.promotionService_.retrieve( + id, + config, + sharedContext + ) + + return await this.baseRepository_.serialize( + promotion, + { + populate: true, + } + ) + } + @InjectManager("baseRepository_") async list( filters: PromotionTypes.FilterablePromotionProps = {}, @@ -76,7 +103,7 @@ export default class PromotionModuleService< sharedContext ) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( promotions, { populate: true, @@ -94,7 +121,13 @@ export default class PromotionModuleService< return await this.list( { id: promotions.map((p) => p!.id) }, { - relations: ["application_method", "rules", "rules.values"], + relations: [ + "application_method", + "application_method.target_rules", + "application_method.target_rules.values", + "rules", + "rules.values", + ], }, sharedContext ) @@ -195,6 +228,162 @@ export default class PromotionModuleService< return createdPromotions } + @InjectManager("baseRepository_") + async update( + data: PromotionTypes.UpdatePromotionDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const promotions = await this.update_(data, sharedContext) + + return await this.list( + { id: promotions.map((p) => p!.id) }, + { + relations: [ + "application_method", + "application_method.target_rules", + "rules", + "rules.values", + ], + }, + sharedContext + ) + } + + @InjectTransactionManager("baseRepository_") + protected async update_( + data: PromotionTypes.UpdatePromotionDTO[], + @MedusaContext() sharedContext: Context = {} + ) { + const promotionIds = data.map((d) => d.id) + const existingPromotions = await this.promotionService_.list( + { + id: promotionIds, + }, + { + relations: ["application_method"], + } + ) + const existingPromotionsMap = new Map( + existingPromotions.map((promotion) => [promotion.id, promotion]) + ) + + const promotionsData: UpdatePromotionDTO[] = [] + const applicationMethodsData: UpdateApplicationMethodDTO[] = [] + + for (const { + application_method: applicationMethodData, + ...promotionData + } of data) { + promotionsData.push(promotionData) + + if (!applicationMethodData) { + continue + } + + const existingPromotion = existingPromotionsMap.get(promotionData.id) + const existingApplicationMethod = existingPromotion?.application_method + + if (!existingApplicationMethod) { + continue + } + + if ( + applicationMethodData.allocation && + !allowedAllocationForQuantity.includes(applicationMethodData.allocation) + ) { + applicationMethodData.max_quantity = null + } + + validateApplicationMethodAttributes({ + type: applicationMethodData.type || existingApplicationMethod.type, + target_type: + applicationMethodData.target_type || + existingApplicationMethod.target_type, + allocation: + applicationMethodData.allocation || + existingApplicationMethod.allocation, + max_quantity: + applicationMethodData.max_quantity || + existingApplicationMethod.max_quantity, + }) + + applicationMethodsData.push(applicationMethodData) + } + + const updatedPromotions = this.promotionService_.update( + promotionsData, + sharedContext + ) + + if (applicationMethodsData.length) { + await this.applicationMethodService_.update( + applicationMethodsData, + sharedContext + ) + } + + return updatedPromotions + } + + @InjectManager("baseRepository_") + @InjectTransactionManager("baseRepository_") + async addPromotionRules( + promotionId: string, + rulesData: PromotionTypes.CreatePromotionRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const promotion = await this.promotionService_.retrieve(promotionId) + + await this.createPromotionRulesAndValues( + rulesData, + "promotions", + promotion, + sharedContext + ) + + return this.retrieve(promotionId, { + relations: ["rules", "rules.values"], + }) + } + + @InjectManager("baseRepository_") + @InjectTransactionManager("baseRepository_") + async addPromotionTargetRules( + promotionId: string, + rulesData: PromotionTypes.CreatePromotionRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const promotion = await this.promotionService_.retrieve(promotionId, { + relations: ["application_method"], + }) + + const applicationMethod = promotion.application_method + + if (!applicationMethod) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `application_method for promotion not found` + ) + } + + await this.createPromotionRulesAndValues( + rulesData, + "application_methods", + applicationMethod, + sharedContext + ) + + return this.retrieve(promotionId, { + relations: [ + "rules", + "rules.values", + "application_method", + "application_method.target_rules", + "application_method.target_rules.values", + ], + }) + } + protected async createPromotionRulesAndValues( rulesData: PromotionTypes.CreatePromotionRuleDTO[], relationName: "promotions" | "application_methods", @@ -224,4 +413,111 @@ export default class PromotionModuleService< await this.promotionRuleValueService_.create(promotionRuleValuesData) } } + + @InjectTransactionManager("baseRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.promotionService_.delete(ids, sharedContext) + } + + @InjectManager("baseRepository_") + async removePromotionRules( + promotionId: string, + rulesData: PromotionTypes.RemovePromotionRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.removePromotionRules_(promotionId, rulesData, sharedContext) + + return this.retrieve( + promotionId, + { relations: ["rules", "rules.values"] }, + sharedContext + ) + } + + @InjectTransactionManager("baseRepository_") + protected async removePromotionRules_( + promotionId: string, + rulesData: PromotionTypes.RemovePromotionRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + 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) + ) + + await this.promotionRuleService_.delete(idsToRemove, sharedContext) + } + + @InjectManager("baseRepository_") + async removePromotionTargetRules( + promotionId: string, + rulesData: PromotionTypes.RemovePromotionRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.removePromotionTargetRules_( + promotionId, + rulesData, + sharedContext + ) + + return this.retrieve( + promotionId, + { + relations: [ + "rules", + "rules.values", + "application_method", + "application_method.target_rules", + "application_method.target_rules.values", + ], + }, + sharedContext + ) + } + + @InjectTransactionManager("baseRepository_") + protected async removePromotionTargetRules_( + promotionId: string, + rulesData: PromotionTypes.RemovePromotionRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const promotionRuleIds = rulesData.map((ruleData) => ruleData.id) + const promotion = await this.promotionService_.retrieve( + promotionId, + { relations: ["application_method.target_rules"] }, + sharedContext + ) + + const applicationMethod = promotion.application_method + + if (!applicationMethod) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `application_method for promotion not found` + ) + } + + const targetRuleIdsToRemove = applicationMethod.target_rules + .toArray() + .filter((rule) => promotionRuleIds.includes(rule.id)) + .map((rule) => rule.id) + + await this.promotionRuleService_.delete( + targetRuleIdsToRemove, + sharedContext + ) + } } diff --git a/packages/promotion/src/types/application-method.ts b/packages/promotion/src/types/application-method.ts index b683958f03..f269b4de2b 100644 --- a/packages/promotion/src/types/application-method.ts +++ b/packages/promotion/src/types/application-method.ts @@ -1,19 +1,27 @@ import { - ApplicationMethodAllocation, - ApplicationMethodTargetType, - ApplicationMethodType, + ApplicationMethodAllocationValues, + ApplicationMethodTargetTypeValues, + ApplicationMethodTypeValues, PromotionDTO, } from "@medusajs/types" +import { Promotion } from "@models" + export interface CreateApplicationMethodDTO { - type: ApplicationMethodType - target_type: ApplicationMethodTargetType - allocation?: ApplicationMethodAllocation - value?: number - promotion: PromotionDTO | string - max_quantity?: number + type: ApplicationMethodTypeValues + target_type: ApplicationMethodTargetTypeValues + allocation?: ApplicationMethodAllocationValues + value?: string | null + promotion: Promotion | string | PromotionDTO + max_quantity?: number | null } export interface UpdateApplicationMethodDTO { id: string + type?: ApplicationMethodTypeValues + target_type?: ApplicationMethodTargetTypeValues + allocation?: ApplicationMethodAllocationValues + value?: string | null + promotion?: Promotion | string | PromotionDTO + max_quantity?: number | null } diff --git a/packages/promotion/src/types/promotion.ts b/packages/promotion/src/types/promotion.ts index 835f82df1b..502e4fdd60 100644 --- a/packages/promotion/src/types/promotion.ts +++ b/packages/promotion/src/types/promotion.ts @@ -8,4 +8,8 @@ export interface CreatePromotionDTO { export interface UpdatePromotionDTO { id: string + code?: string + // TODO: add this when buyget is available + // type: PromotionType + is_automatic?: boolean } diff --git a/packages/promotion/src/utils/validations/application-method.ts b/packages/promotion/src/utils/validations/application-method.ts index 99e193a387..75dc0275a0 100644 --- a/packages/promotion/src/utils/validations/application-method.ts +++ b/packages/promotion/src/utils/validations/application-method.ts @@ -1,44 +1,86 @@ +import { + ApplicationMethodAllocationValues, + ApplicationMethodTargetTypeValues, + ApplicationMethodTypeValues, +} from "@medusajs/types" import { ApplicationMethodAllocation, ApplicationMethodTargetType, + ApplicationMethodType, MedusaError, isDefined, } from "@medusajs/utils" -import { CreateApplicationMethodDTO } from "../../types" -const allowedTargetTypes: string[] = [ +export const allowedAllocationTargetTypes: string[] = [ ApplicationMethodTargetType.SHIPPING, ApplicationMethodTargetType.ITEM, ] -const allowedAllocationTypes: string[] = [ +export const allowedAllocationTypes: string[] = [ ApplicationMethodAllocation.ACROSS, ApplicationMethodAllocation.EACH, ] -const allowedAllocationForQuantity: string[] = [ +export const allowedAllocationForQuantity: string[] = [ ApplicationMethodAllocation.EACH, ] -export function validateApplicationMethodAttributes( - data: CreateApplicationMethodDTO -) { +export function validateApplicationMethodAttributes(data: { + type: ApplicationMethodTypeValues + target_type: ApplicationMethodTargetTypeValues + allocation?: ApplicationMethodAllocationValues + max_quantity?: number | null +}) { + const allTargetTypes: string[] = Object.values(ApplicationMethodTargetType) + + if (!allTargetTypes.includes(data.target_type)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `application_method.target_type should be one of ${allTargetTypes.join( + ", " + )}` + ) + } + + const allTypes: string[] = Object.values(ApplicationMethodType) + + if (!allTypes.includes(data.type)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `application_method.type should be one of ${allTypes.join(", ")}` + ) + } + if ( - allowedTargetTypes.includes(data.target_type) && + allowedAllocationTargetTypes.includes(data.target_type) && !allowedAllocationTypes.includes(data.allocation || "") ) { throw new MedusaError( MedusaError.Types.INVALID_DATA, `application_method.allocation should be either '${allowedAllocationTypes.join( " OR " - )}' when application_method.target_type is either '${allowedTargetTypes.join( + )}' when application_method.target_type is either '${allowedAllocationTargetTypes.join( " OR " )}'` ) } + const allAllocationTypes: string[] = Object.values( + ApplicationMethodAllocation + ) + + if (data.allocation && !allAllocationTypes.includes(data.allocation)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `application_method.allocation should be one of ${allAllocationTypes.join( + ", " + )}` + ) + } + if ( - allowedAllocationForQuantity.includes(data.allocation || "") && + data.allocation && + allowedAllocationForQuantity.includes(data.allocation) && !isDefined(data.max_quantity) ) { throw new MedusaError( diff --git a/packages/types/src/promotion/common/application-method.ts b/packages/types/src/promotion/common/application-method.ts index a288e2fa6b..c5ce5a2d2a 100644 --- a/packages/types/src/promotion/common/application-method.ts +++ b/packages/types/src/promotion/common/application-method.ts @@ -1,33 +1,46 @@ import { BaseFilterable } from "../../dal" import { PromotionDTO } from "./promotion" -import { CreatePromotionRuleDTO } from "./promotion-rule" +import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule" -export type ApplicationMethodType = "fixed" | "percentage" -export type ApplicationMethodTargetType = "order" | "shipping" | "item" -export type ApplicationMethodAllocation = "each" | "across" +export type ApplicationMethodTypeValues = "fixed" | "percentage" +export type ApplicationMethodTargetTypeValues = "order" | "shipping" | "item" +export type ApplicationMethodAllocationValues = "each" | "across" export interface ApplicationMethodDTO { id: string + type?: ApplicationMethodTypeValues + target_type?: ApplicationMethodTargetTypeValues + allocation?: ApplicationMethodAllocationValues + value?: string | null + max_quantity?: number | null + promotion?: PromotionDTO | string + target_rules?: PromotionRuleDTO[] } export interface CreateApplicationMethodDTO { - type: ApplicationMethodType - target_type: ApplicationMethodTargetType - allocation?: ApplicationMethodAllocation - value?: number - max_quantity?: number + type: ApplicationMethodTypeValues + target_type: ApplicationMethodTargetTypeValues + allocation?: ApplicationMethodAllocationValues + value?: string | null + max_quantity?: number | null promotion?: PromotionDTO | string target_rules?: CreatePromotionRuleDTO[] } export interface UpdateApplicationMethodDTO { id: string + type?: ApplicationMethodTypeValues + target_type?: ApplicationMethodTargetTypeValues + allocation?: ApplicationMethodAllocationValues + value?: string | null + max_quantity?: number | null + promotion?: PromotionDTO | string } export interface FilterableApplicationMethodProps extends BaseFilterable { id?: string[] - type?: ApplicationMethodType[] - target_type?: ApplicationMethodTargetType[] - allocation?: ApplicationMethodAllocation[] + type?: ApplicationMethodTypeValues[] + target_type?: ApplicationMethodTargetTypeValues[] + allocation?: ApplicationMethodAllocationValues[] } diff --git a/packages/types/src/promotion/common/promotion-rule.ts b/packages/types/src/promotion/common/promotion-rule.ts index 4b5b5fb4bc..98adedc37f 100644 --- a/packages/types/src/promotion/common/promotion-rule.ts +++ b/packages/types/src/promotion/common/promotion-rule.ts @@ -24,6 +24,10 @@ export interface UpdatePromotionRuleDTO { id: string } +export interface RemovePromotionRuleDTO { + id: string +} + export interface FilterablePromotionRuleProps extends BaseFilterable { id?: string[] diff --git a/packages/types/src/promotion/common/promotion.ts b/packages/types/src/promotion/common/promotion.ts index e2dd6834f5..7fbbef439e 100644 --- a/packages/types/src/promotion/common/promotion.ts +++ b/packages/types/src/promotion/common/promotion.ts @@ -1,11 +1,19 @@ import { BaseFilterable } from "../../dal" -import { CreateApplicationMethodDTO } from "./application-method" +import { + ApplicationMethodDTO, + CreateApplicationMethodDTO, + UpdateApplicationMethodDTO, +} from "./application-method" import { CreatePromotionRuleDTO } from "./promotion-rule" export type PromotionType = "standard" | "buyget" export interface PromotionDTO { id: string + code?: string + type?: PromotionType + is_automatic?: boolean + application_method?: ApplicationMethodDTO } export interface CreatePromotionDTO { @@ -18,6 +26,10 @@ export interface CreatePromotionDTO { export interface UpdatePromotionDTO { id: string + is_automatic?: boolean + code?: string + type?: PromotionType + application_method?: UpdateApplicationMethodDTO } export interface FilterablePromotionProps diff --git a/packages/types/src/promotion/service.ts b/packages/types/src/promotion/service.ts index dbbba24ffa..cacda1faa5 100644 --- a/packages/types/src/promotion/service.ts +++ b/packages/types/src/promotion/service.ts @@ -3,8 +3,11 @@ import { IModuleService } from "../modules-sdk" import { Context } from "../shared-context" import { CreatePromotionDTO, + CreatePromotionRuleDTO, FilterablePromotionProps, PromotionDTO, + RemovePromotionRuleDTO, + UpdatePromotionDTO, } from "./common" export interface IPromotionModuleService extends IModuleService { @@ -13,9 +16,46 @@ export interface IPromotionModuleService extends IModuleService { sharedContext?: Context ): Promise + update( + data: UpdatePromotionDTO[], + sharedContext?: Context + ): Promise + list( filters?: FilterablePromotionProps, config?: FindConfig, sharedContext?: Context ): Promise + + retrieve( + id: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + delete(ids: string[], sharedContext?: Context): Promise + + addPromotionRules( + promotionId: string, + rulesData: CreatePromotionRuleDTO[], + sharedContext?: Context + ): Promise + + addPromotionTargetRules( + promotionId: string, + rulesData: CreatePromotionRuleDTO[], + sharedContext?: Context + ): Promise + + removePromotionRules( + promotionId: string, + rulesData: RemovePromotionRuleDTO[], + sharedContext?: Context + ): Promise + + removePromotionTargetRules( + promotionId: string, + rulesData: RemovePromotionRuleDTO[], + sharedContext?: Context + ): Promise }