diff --git a/.changeset/young-forks-worry.md b/.changeset/young-forks-worry.md new file mode 100644 index 0000000000..80a0ead825 --- /dev/null +++ b/.changeset/young-forks-worry.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +feat(medusa,types): added buyget support for modules diff --git a/integration-tests/plugins/__tests__/promotion/admin/create-promotion.spec.ts b/integration-tests/plugins/__tests__/promotion/admin/create-promotion.spec.ts index 33206fd1c9..aa0ccf669c 100644 --- a/integration-tests/plugins/__tests__/promotion/admin/create-promotion.spec.ts +++ b/integration-tests/plugins/__tests__/promotion/admin/create-promotion.spec.ts @@ -62,7 +62,7 @@ describe("POST /admin/promotions", () => { ) }) - it("should create a promotion successfully", async () => { + it("should create a standard promotion successfully", async () => { const api = useApi() as any const response = await api.post( `/admin/promotions`, @@ -148,4 +148,194 @@ describe("POST /admin/promotions", () => { }) ) }) + + it("should throw an error if buy_rules params are not passed", async () => { + const api = useApi() as any + const { response } = await api + .post( + `/admin/promotions`, + { + code: "TEST", + type: PromotionType.BUYGET, + is_automatic: true, + application_method: { + target_type: "items", + type: "fixed", + allocation: "each", + value: "100", + max_quantity: 100, + target_rules: [ + { + attribute: "test.test", + operator: "eq", + values: ["test1", "test2"], + }, + ], + }, + rules: [ + { + attribute: "test.test", + operator: "eq", + values: ["test1", "test2"], + }, + ], + }, + adminHeaders + ) + .catch((e) => e) + + expect(response.status).toEqual(400) + expect(response.data.message).toEqual( + "Buy rules are required for buyget promotion type" + ) + }) + + it("should throw an error if buy_rules params are not passed", async () => { + const api = useApi() as any + const { response } = await api + .post( + `/admin/promotions`, + { + code: "TEST", + type: PromotionType.BUYGET, + is_automatic: true, + application_method: { + target_type: "items", + type: "fixed", + allocation: "each", + value: "100", + max_quantity: 100, + buy_rules: [ + { + attribute: "test.test", + operator: "eq", + values: ["test1", "test2"], + }, + ], + }, + rules: [ + { + attribute: "test.test", + operator: "eq", + values: ["test1", "test2"], + }, + ], + }, + adminHeaders + ) + .catch((e) => e) + + expect(response.status).toEqual(400) + expect(response.data.message).toEqual( + "Target rules are required for buyget promotion type" + ) + }) + + it("should create a buyget promotion successfully", async () => { + const api = useApi() as any + const response = await api.post( + `/admin/promotions`, + { + code: "TEST", + type: PromotionType.BUYGET, + is_automatic: true, + campaign: { + name: "test", + campaign_identifier: "test-1", + budget: { + type: "usage", + limit: 100, + }, + }, + application_method: { + target_type: "items", + type: "fixed", + allocation: "each", + value: "100", + max_quantity: 100, + apply_to_quantity: 1, + buy_rules_min_quantity: 1, + target_rules: [ + { + attribute: "test.test", + operator: "eq", + values: ["test1", "test2"], + }, + ], + buy_rules: [ + { + attribute: "test.test", + operator: "eq", + values: ["test1", "test2"], + }, + ], + }, + rules: [ + { + attribute: "test.test", + operator: "eq", + values: ["test1", "test2"], + }, + ], + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.promotion).toEqual( + expect.objectContaining({ + id: expect.any(String), + code: "TEST", + type: "buyget", + is_automatic: true, + campaign: expect.objectContaining({ + name: "test", + campaign_identifier: "test-1", + budget: expect.objectContaining({ + type: "usage", + limit: 100, + }), + }), + application_method: expect.objectContaining({ + value: 100, + max_quantity: 100, + type: "fixed", + target_type: "items", + allocation: "each", + apply_to_quantity: 1, + buy_rules_min_quantity: 1, + target_rules: [ + expect.objectContaining({ + operator: "eq", + attribute: "test.test", + values: expect.arrayContaining([ + expect.objectContaining({ value: "test1" }), + expect.objectContaining({ value: "test2" }), + ]), + }), + ], + buy_rules: [ + expect.objectContaining({ + operator: "eq", + attribute: "test.test", + values: expect.arrayContaining([ + expect.objectContaining({ value: "test1" }), + expect.objectContaining({ value: "test2" }), + ]), + }), + ], + }), + rules: [ + expect.objectContaining({ + operator: "eq", + attribute: "test.test", + values: expect.arrayContaining([ + expect.objectContaining({ value: "test1" }), + expect.objectContaining({ value: "test2" }), + ]), + }), + ], + }) + ) + }) }) diff --git a/integration-tests/plugins/__tests__/promotion/admin/retrieve-promotion.spec.ts b/integration-tests/plugins/__tests__/promotion/admin/retrieve-promotion.spec.ts index f0cc7301f0..95f7d56da3 100644 --- a/integration-tests/plugins/__tests__/promotion/admin/retrieve-promotion.spec.ts +++ b/integration-tests/plugins/__tests__/promotion/admin/retrieve-promotion.spec.ts @@ -74,28 +74,30 @@ describe("GET /admin/promotions", () => { ) expect(response.status).toEqual(200) - expect(response.data.promotion).toEqual({ - id: expect.any(String), - code: "TEST", - campaign: null, - is_automatic: false, - type: "standard", - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - application_method: { + expect(response.data.promotion).toEqual( + expect.objectContaining({ id: expect.any(String), - promotion: expect.any(Object), - value: 100, - type: "fixed", - target_type: "order", - max_quantity: 0, - allocation: null, + code: "TEST", + campaign: null, + is_automatic: false, + type: "standard", created_at: expect.any(String), updated_at: expect.any(String), deleted_at: null, - }, - }) + application_method: expect.objectContaining({ + id: expect.any(String), + promotion: expect.any(Object), + 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, + }), + }) + ) }) it("should get the requested promotion with filtered fields and relations", async () => { diff --git a/integration-tests/plugins/__tests__/promotion/admin/update-promotion.spec.ts b/integration-tests/plugins/__tests__/promotion/admin/update-promotion.spec.ts index 85f701a1be..4ce4652c71 100644 --- a/integration-tests/plugins/__tests__/promotion/admin/update-promotion.spec.ts +++ b/integration-tests/plugins/__tests__/promotion/admin/update-promotion.spec.ts @@ -132,4 +132,58 @@ describe("POST /admin/promotions/:id", () => { }) ) }) + + it("should update a buyget promotion successfully", async () => { + const createdPromotion = await promotionModuleService.create({ + code: "PROMOTION_TEST", + 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: [ + { + attribute: "product_collection.id", + operator: "eq", + values: ["pcol_towel"], + }, + ], + target_rules: [ + { + attribute: "product.id", + operator: "eq", + values: "prod_mat", + }, + ], + }, + }) + + const api = useApi() as any + const response = await api.post( + `/admin/promotions/${createdPromotion.id}`, + { + code: "TEST_TWO", + application_method: { + value: "200", + buy_rules_min_quantity: 6, + }, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.promotion).toEqual( + expect.objectContaining({ + id: expect.any(String), + code: "TEST_TWO", + application_method: expect.objectContaining({ + value: 200, + buy_rules_min_quantity: 6, + }), + }) + ) + }) }) diff --git a/packages/medusa/src/api-v2/admin/promotions/validators.ts b/packages/medusa/src/api-v2/admin/promotions/validators.ts index ff161dc215..cd75d04975 100644 --- a/packages/medusa/src/api-v2/admin/promotions/validators.ts +++ b/packages/medusa/src/api-v2/admin/promotions/validators.ts @@ -1,3 +1,4 @@ +import { PromotionTypeValues } from "@medusajs/types" import { ApplicationMethodAllocation, ApplicationMethodTargetType, @@ -15,6 +16,7 @@ import { IsOptional, IsString, Validate, + ValidateIf, ValidateNested, } from "class-validator" import { FindParams, extendedFindParamsMixin } from "../../../types/common" @@ -43,7 +45,7 @@ export class AdminPostPromotionsReq { @IsOptional() @IsEnum(PromotionType) - type?: PromotionType + type?: PromotionTypeValues @IsOptional() @IsString() @@ -56,8 +58,8 @@ export class AdminPostPromotionsReq { @IsNotEmpty() @ValidateNested() - @Type(() => ApplicationMethod) - application_method: ApplicationMethod + @Type(() => ApplicationMethodsPostReq) + application_method: ApplicationMethodsPostReq @IsOptional() @IsArray() @@ -83,7 +85,7 @@ export class PromotionRule { values: string[] } -export class ApplicationMethod { +export class ApplicationMethodsPostReq { @IsOptional() @IsString() description?: string @@ -113,6 +115,68 @@ export class ApplicationMethod { @ValidateNested({ each: true }) @Type(() => PromotionRule) target_rules?: PromotionRule[] + + @ValidateIf((data) => data.type === PromotionType.BUYGET) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PromotionRule) + buy_rules?: PromotionRule[] + + @ValidateIf((data) => data.type === PromotionType.BUYGET) + @IsNotEmpty() + @IsNumber() + apply_to_quantity?: number + + @ValidateIf((data) => data.type === PromotionType.BUYGET) + @IsNotEmpty() + @IsNumber() + buy_rules_min_quantity?: number +} + +export class ApplicationMethodsMethodPostReq { + @IsOptional() + @IsString() + description?: string + + @IsOptional() + @IsString() + value?: string + + @IsOptional() + @IsNumber() + max_quantity?: number + + @IsOptional() + @IsEnum(ApplicationMethodType) + type?: ApplicationMethodType + + @IsOptional() + @IsEnum(ApplicationMethodTargetType) + target_type?: ApplicationMethodTargetType + + @IsOptional() + @IsEnum(ApplicationMethodAllocation) + allocation?: ApplicationMethodAllocation + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PromotionRule) + target_rules?: PromotionRule[] + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PromotionRule) + buy_rules?: PromotionRule[] + + @IsOptional() + @IsNumber() + apply_to_quantity?: number + + @IsOptional() + @IsNumber() + buy_rules_min_quantity?: number } export class AdminPostPromotionsPromotionReq { @@ -141,8 +205,8 @@ export class AdminPostPromotionsPromotionReq { @IsOptional() @ValidateNested() - @Type(() => ApplicationMethod) - application_method?: ApplicationMethod + @Type(() => ApplicationMethodsMethodPostReq) + application_method?: ApplicationMethodsMethodPostReq @IsOptional() @IsArray() diff --git a/packages/medusa/src/api/middlewares/transform-body.ts b/packages/medusa/src/api/middlewares/transform-body.ts index 4eebb03a98..026ee29dc8 100644 --- a/packages/medusa/src/api/middlewares/transform-body.ts +++ b/packages/medusa/src/api/middlewares/transform-body.ts @@ -1,6 +1,6 @@ +import { ValidatorOptions } from "class-validator" import { NextFunction, Request, Response } from "express" import { ClassConstructor } from "../../types/global" -import { ValidatorOptions } from "class-validator" import { validator } from "../../utils/validator" export function transformBody( 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 9982b25e06..f0520ff801 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 @@ -428,6 +428,199 @@ describe("Promotion Service", () => { "rules[].operator (doesnotexist) is invalid. It should be one of gte, lte, gt, lt, eq, ne, in" ) }) + + it("should create a basic buyget promotion successfully", async () => { + const createdPromotion = await service + .create({ + code: "PROMOTION_TEST", + type: PromotionType.BUYGET, + }) + .catch((e) => e) + + const [promotion] = await service.list({ + id: [createdPromotion.id], + }) + + expect(promotion).toEqual( + expect.objectContaining({ + code: "PROMOTION_TEST", + is_automatic: false, + type: PromotionType.BUYGET, + }) + ) + }) + + it("should throw an error when target_rules are not present for buyget promotion", async () => { + const error = await service + .create({ + code: "PROMOTION_TEST", + type: PromotionType.BUYGET, + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "100", + buy_rules: [ + { + attribute: "product_collection", + operator: "eq", + values: ["pcol_towel"], + }, + ], + }, + }) + .catch((e) => e) + + expect(error.message).toContain( + "Target rules are required for buyget promotion type" + ) + }) + + it("should throw an error when buy_rules are not present for buyget promotion", async () => { + const error = await service + .create({ + code: "PROMOTION_TEST", + type: PromotionType.BUYGET, + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "100", + }, + }) + .catch((e) => e) + + expect(error.message).toContain( + "Buy rules are required for buyget promotion type" + ) + }) + + it("should throw an error when apply_to_quantity is not present for buyget promotion", async () => { + const error = await service + .create({ + code: "PROMOTION_TEST", + type: PromotionType.BUYGET, + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "100", + buy_rules_min_quantity: 1, + buy_rules: [ + { + attribute: "product_collection.id", + operator: "eq", + values: ["pcol_towel"], + }, + ], + target_rules: [ + { + attribute: "product.id", + operator: "eq", + values: ["prod_mat"], + }, + ], + }, + }) + .catch((e) => e) + + expect(error.message).toContain( + "apply_to_quantity is a required field for Promotion type of buyget" + ) + }) + + it("should throw an error when buy_rules_min_quantity is not present for buyget promotion", async () => { + const error = await service + .create({ + code: "PROMOTION_TEST", + type: PromotionType.BUYGET, + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "100", + apply_to_quantity: 1, + buy_rules: [ + { + attribute: "product_collection.id", + operator: "eq", + values: ["pcol_towel"], + }, + ], + target_rules: [ + { + attribute: "product.id", + operator: "eq", + values: ["prod_mat"], + }, + ], + }, + }) + .catch((e) => e) + + expect(error.message).toContain( + "buy_rules_min_quantity is a required field for Promotion type of buyget" + ) + }) + + it("should create a buyget promotion with rules successfully", async () => { + const createdPromotion = await service.create({ + code: "PROMOTION_TEST", + 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: [ + { + attribute: "product_collection.id", + operator: "eq", + values: ["pcol_towel"], + }, + ], + target_rules: [ + { + attribute: "product.id", + operator: "eq", + values: "prod_mat", + }, + ], + }, + }) + + expect(createdPromotion).toEqual( + expect.objectContaining({ + code: "PROMOTION_TEST", + is_automatic: false, + type: PromotionType.BUYGET, + application_method: expect.objectContaining({ + type: "fixed", + target_type: "items", + allocation: "across", + value: 100, + apply_to_quantity: 1, + buy_rules_min_quantity: 1, + target_rules: [ + expect.objectContaining({ + attribute: "product.id", + operator: "eq", + values: [expect.objectContaining({ value: "prod_mat" })], + }), + ], + buy_rules: [ + expect.objectContaining({ + attribute: "product_collection.id", + operator: "eq", + values: [expect.objectContaining({ value: "pcol_towel" })], + }), + ], + }), + }) + ) + }) }) describe("update", () => { @@ -966,6 +1159,103 @@ describe("Promotion Service", () => { }) }) + describe("addPromotionBuyRules", () => { + let promotion + + beforeEach(async () => { + ;[promotion] = await service.create([ + { + code: "TEST", + type: PromotionType.BUYGET, + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "100", + max_quantity: 500, + apply_to_quantity: 1, + buy_rules_min_quantity: 1, + target_rules: [ + { + attribute: "product.id", + operator: "in", + values: ["prod_1", "prod_2"], + }, + ], + buy_rules: [ + { + attribute: "product_collection.id", + operator: "eq", + values: ["pcol_towel"], + }, + ], + }, + }, + ]) + }) + + it("should throw an error when promotion with id does not exist", async () => { + let error + + try { + await service.addPromotionBuyRules("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.addPromotionBuyRules(undefined as unknown as string, []) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"promotionId" must be defined') + }) + + it("should successfully create buy rules for a buyget promotion", async () => { + promotion = await service.addPromotionBuyRules(promotion.id, [ + { + attribute: "product.id", + operator: "in", + values: ["prod_3", "prod_4"], + }, + ]) + + expect(promotion).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" }), + ]), + }), + ]), + }), + }) + ) + }) + }) + describe("removePromotionRules", () => { let promotion @@ -1108,4 +1398,88 @@ describe("Promotion Service", () => { ) }) }) + + describe("removePromotionBuyRules", () => { + let promotion + + beforeEach(async () => { + ;[promotion] = await service.create([ + { + code: "TEST", + type: PromotionType.BUYGET, + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "100", + max_quantity: 500, + apply_to_quantity: 1, + buy_rules_min_quantity: 1, + target_rules: [ + { + attribute: "product.id", + operator: "in", + values: ["prod_1", "prod_2"], + }, + ], + buy_rules: [ + { + attribute: "product_collection", + operator: "eq", + values: ["pcol_towel"], + }, + ], + }, + }, + ]) + }) + + it("should throw an error when promotion with id does not exist", async () => { + let error + + try { + await service.removePromotionBuyRules("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.removePromotionBuyRules( + undefined as unknown as string, + [] + ) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"promotionId" must be defined') + }) + + it("should successfully remove rules for a promotion", async () => { + const [ruleId] = promotion.application_method.buy_rules.map( + (rule) => rule.id + ) + + promotion = await service.removePromotionBuyRules(promotion.id, [ + { id: ruleId }, + ]) + + expect(promotion).toEqual( + expect.objectContaining({ + id: promotion.id, + application_method: expect.objectContaining({ + buy_rules: [], + }), + }) + ) + }) + }) }) diff --git a/packages/promotion/src/migrations/.snapshot-medusa-promotion.json b/packages/promotion/src/migrations/.snapshot-medusa-promotion.json index bdf7a42841..109ece07d1 100644 --- a/packages/promotion/src/migrations/.snapshot-medusa-promotion.json +++ b/packages/promotion/src/migrations/.snapshot-medusa-promotion.json @@ -398,6 +398,24 @@ "nullable": true, "mappedType": "decimal" }, + "apply_to_quantity": { + "name": "apply_to_quantity", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "decimal" + }, + "buy_rules_min_quantity": { + "name": "buy_rules_min_quantity", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "decimal" + }, "type": { "name": "type", "type": "text", @@ -697,11 +715,11 @@ "mappedType": "text" } }, - "name": "application_method_promotion_rule", + "name": "application_method_target_rules", "schema": "public", "indexes": [ { - "keyName": "application_method_promotion_rule_pkey", + "keyName": "application_method_target_rules_pkey", "columnNames": ["application_method_id", "promotion_rule_id"], "composite": true, "primary": true, @@ -710,19 +728,73 @@ ], "checks": [], "foreignKeys": { - "application_method_promotion_rule_application_method_id_foreign": { - "constraintName": "application_method_promotion_rule_application_method_id_foreign", + "application_method_target_rules_application_method_id_foreign": { + "constraintName": "application_method_target_rules_application_method_id_foreign", "columnNames": ["application_method_id"], - "localTableName": "public.application_method_promotion_rule", + "localTableName": "public.application_method_target_rules", "referencedColumnNames": ["id"], "referencedTableName": "public.application_method", "deleteRule": "cascade", "updateRule": "cascade" }, - "application_method_promotion_rule_promotion_rule_id_foreign": { - "constraintName": "application_method_promotion_rule_promotion_rule_id_foreign", + "application_method_target_rules_promotion_rule_id_foreign": { + "constraintName": "application_method_target_rules_promotion_rule_id_foreign", "columnNames": ["promotion_rule_id"], - "localTableName": "public.application_method_promotion_rule", + "localTableName": "public.application_method_target_rules", + "referencedColumnNames": ["id"], + "referencedTableName": "public.promotion_rule", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "application_method_id": { + "name": "application_method_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "promotion_rule_id": { + "name": "promotion_rule_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "application_method_buy_rules", + "schema": "public", + "indexes": [ + { + "keyName": "application_method_buy_rules_pkey", + "columnNames": ["application_method_id", "promotion_rule_id"], + "composite": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "application_method_buy_rules_application_method_id_foreign": { + "constraintName": "application_method_buy_rules_application_method_id_foreign", + "columnNames": ["application_method_id"], + "localTableName": "public.application_method_buy_rules", + "referencedColumnNames": ["id"], + "referencedTableName": "public.application_method", + "deleteRule": "cascade", + "updateRule": "cascade" + }, + "application_method_buy_rules_promotion_rule_id_foreign": { + "constraintName": "application_method_buy_rules_promotion_rule_id_foreign", + "columnNames": ["promotion_rule_id"], + "localTableName": "public.application_method_buy_rules", "referencedColumnNames": ["id"], "referencedTableName": "public.promotion_rule", "deleteRule": "cascade", diff --git a/packages/promotion/src/migrations/Migration20240122070028.ts b/packages/promotion/src/migrations/Migration20240122084316.ts similarity index 71% rename from packages/promotion/src/migrations/Migration20240122070028.ts rename to packages/promotion/src/migrations/Migration20240122084316.ts index aad49ce928..db3a0e841d 100644 --- a/packages/promotion/src/migrations/Migration20240122070028.ts +++ b/packages/promotion/src/migrations/Migration20240122084316.ts @@ -1,6 +1,6 @@ import { Migration } from "@mikro-orm/migrations" -export class Migration20240122070028 extends Migration { +export class Migration20240122084316 extends Migration { async up(): Promise { this.addSql( 'create table "campaign" ("id" text not null, "name" text not null, "description" text null, "currency" text null, "campaign_identifier" text not null, "starts_at" timestamptz null, "ends_at" timestamptz null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "campaign_pkey" primary key ("id"));' @@ -29,7 +29,7 @@ export class Migration20240122070028 extends Migration { ) this.addSql( - 'create table "application_method" ("id" text not null, "value" numeric null, "max_quantity" numeric null, "type" text check ("type" in (\'fixed\', \'percentage\')) not null, "target_type" text check ("target_type" in (\'order\', \'shipping_methods\', \'items\')) not null, "allocation" text check ("allocation" in (\'each\', \'across\')) null, "promotion_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "application_method_pkey" primary key ("id"));' + 'create table "application_method" ("id" text not null, "value" numeric null, "max_quantity" numeric null, "apply_to_quantity" numeric null, "buy_rules_min_quantity" numeric null, "type" text check ("type" in (\'fixed\', \'percentage\')) not null, "target_type" text check ("target_type" in (\'order\', \'shipping_methods\', \'items\')) not null, "allocation" text check ("allocation" in (\'each\', \'across\')) null, "promotion_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "application_method_pkey" primary key ("id"));' ) this.addSql( 'create index "IDX_application_method_type" on "application_method" ("type");' @@ -59,7 +59,11 @@ export class Migration20240122070028 extends Migration { ) this.addSql( - 'create table "application_method_promotion_rule" ("application_method_id" text not null, "promotion_rule_id" text not null, constraint "application_method_promotion_rule_pkey" primary key ("application_method_id", "promotion_rule_id"));' + 'create table "application_method_target_rules" ("application_method_id" text not null, "promotion_rule_id" text not null, constraint "application_method_target_rules_pkey" primary key ("application_method_id", "promotion_rule_id"));' + ) + + this.addSql( + 'create table "application_method_buy_rules" ("application_method_id" text not null, "promotion_rule_id" text not null, constraint "application_method_buy_rules_pkey" primary key ("application_method_id", "promotion_rule_id"));' ) this.addSql( @@ -89,10 +93,17 @@ export class Migration20240122070028 extends Migration { ) this.addSql( - 'alter table "application_method_promotion_rule" add constraint "application_method_promotion_rule_application_method_id_foreign" foreign key ("application_method_id") references "application_method" ("id") on update cascade on delete cascade;' + 'alter table "application_method_target_rules" add constraint "application_method_target_rules_application_method_id_foreign" foreign key ("application_method_id") references "application_method" ("id") on update cascade on delete cascade;' ) this.addSql( - 'alter table "application_method_promotion_rule" add constraint "application_method_promotion_rule_promotion_rule_id_foreign" foreign key ("promotion_rule_id") references "promotion_rule" ("id") on update cascade on delete cascade;' + 'alter table "application_method_target_rules" add constraint "application_method_target_rules_promotion_rule_id_foreign" foreign key ("promotion_rule_id") references "promotion_rule" ("id") on update cascade on delete cascade;' + ) + + this.addSql( + 'alter table "application_method_buy_rules" add constraint "application_method_buy_rules_application_method_id_foreign" foreign key ("application_method_id") references "application_method" ("id") on update cascade on delete cascade;' + ) + this.addSql( + 'alter table "application_method_buy_rules" add constraint "application_method_buy_rules_promotion_rule_id_foreign" foreign key ("promotion_rule_id") references "promotion_rule" ("id") on update cascade on delete cascade;' ) this.addSql( diff --git a/packages/promotion/src/models/application-method.ts b/packages/promotion/src/models/application-method.ts index 1f28b377d4..e4201b53f0 100644 --- a/packages/promotion/src/models/application-method.ts +++ b/packages/promotion/src/models/application-method.ts @@ -25,6 +25,8 @@ import PromotionRule from "./promotion-rule" type OptionalFields = | "value" | "max_quantity" + | "apply_to_quantity" + | "buy_rules_min_quantity" | "allocation" | DAL.SoftDeletableEntityDateColumns @@ -37,10 +39,16 @@ export default class ApplicationMethod { id!: string @Property({ columnType: "numeric", nullable: true, serializer: Number }) - value?: string | null + value?: string | null = null @Property({ columnType: "numeric", nullable: true, serializer: Number }) - max_quantity?: number | null + max_quantity?: number | null = null + + @Property({ columnType: "numeric", nullable: true, serializer: Number }) + apply_to_quantity?: number | null = null + + @Property({ columnType: "numeric", nullable: true, serializer: Number }) + buy_rules_min_quantity?: number | null = null @Index({ name: "IDX_application_method_type" }) @Enum(() => PromotionUtils.ApplicationMethodType) @@ -63,13 +71,20 @@ export default class ApplicationMethod { }) promotion: Promotion - @ManyToMany(() => PromotionRule, "application_methods", { + @ManyToMany(() => PromotionRule, "method_target_rules", { owner: true, - pivotTable: "application_method_promotion_rule", + pivotTable: "application_method_target_rules", cascade: ["soft-remove"] as any, }) target_rules = new Collection(this) + @ManyToMany(() => PromotionRule, "method_buy_rules", { + owner: true, + pivotTable: "application_method_buy_rules", + cascade: ["soft-remove"] as any, + }) + buy_rules = new Collection(this) + @Property({ onCreate: () => new Date(), columnType: "timestamptz", diff --git a/packages/promotion/src/models/campaign-budget.ts b/packages/promotion/src/models/campaign-budget.ts index 45e37d46c8..4ecf0f9272 100644 --- a/packages/promotion/src/models/campaign-budget.ts +++ b/packages/promotion/src/models/campaign-budget.ts @@ -35,7 +35,7 @@ export default class CampaignBudget { @OneToOne({ entity: () => Campaign, }) - campaign?: Campaign | null + campaign: Campaign | null = null @Property({ columnType: "numeric", @@ -43,7 +43,7 @@ export default class CampaignBudget { serializer: Number, default: null, }) - limit?: number | null + limit: number | null = null @Property({ columnType: "numeric", diff --git a/packages/promotion/src/models/campaign.ts b/packages/promotion/src/models/campaign.ts index 81cffce1e4..db01139ff9 100644 --- a/packages/promotion/src/models/campaign.ts +++ b/packages/promotion/src/models/campaign.ts @@ -36,10 +36,10 @@ export default class Campaign { name: string @Property({ columnType: "text", nullable: true }) - description?: string | null + description: string | null = null @Property({ columnType: "text", nullable: true }) - currency?: string | null + currency: string | null = null @Property({ columnType: "text" }) @Unique({ @@ -52,13 +52,13 @@ export default class Campaign { columnType: "timestamptz", nullable: true, }) - starts_at?: Date | null + starts_at: Date | null = null @Property({ columnType: "timestamptz", nullable: true, }) - ends_at?: Date | null + ends_at: Date | null = null @OneToOne({ entity: () => CampaignBudget, @@ -66,7 +66,7 @@ export default class Campaign { cascade: ["soft-remove"] as any, nullable: true, }) - budget?: CampaignBudget | null + budget: CampaignBudget | null = null @OneToMany(() => Promotion, (promotion) => promotion.campaign, { orphanRemoval: true, diff --git a/packages/promotion/src/models/promotion-rule.ts b/packages/promotion/src/models/promotion-rule.ts index c603648325..90627a9b6d 100644 --- a/packages/promotion/src/models/promotion-rule.ts +++ b/packages/promotion/src/models/promotion-rule.ts @@ -31,7 +31,7 @@ export default class PromotionRule { id!: string @Property({ columnType: "text", nullable: true }) - description?: string | null + description: string | null = null @Index({ name: "IDX_promotion_rule_attribute" }) @Property({ columnType: "text" }) @@ -53,7 +53,13 @@ export default class PromotionRule { () => ApplicationMethod, (applicationMethod) => applicationMethod.target_rules ) - application_methods = new Collection(this) + method_target_rules = new Collection(this) + + @ManyToMany( + () => ApplicationMethod, + (applicationMethod) => applicationMethod.buy_rules + ) + method_buy_rules = new Collection(this) @Property({ onCreate: () => new Date(), diff --git a/packages/promotion/src/models/promotion.ts b/packages/promotion/src/models/promotion.ts index ace7ee88ec..8a003d8977 100644 --- a/packages/promotion/src/models/promotion.ts +++ b/packages/promotion/src/models/promotion.ts @@ -1,4 +1,4 @@ -import { DAL, PromotionType } from "@medusajs/types" +import { DAL, PromotionTypeValues } from "@medusajs/types" import { DALUtils, PromotionUtils, generateEntityId } from "@medusajs/utils" import { BeforeCreate, @@ -45,14 +45,14 @@ export default class Promotion { nullable: true, cascade: ["soft-remove"] as any, }) - campaign?: Campaign | null + campaign: Campaign | null = null @Property({ columnType: "boolean", default: false }) is_automatic: boolean = false @Index({ name: "IDX_promotion_type" }) @Enum(() => PromotionUtils.PromotionType) - type: PromotionType + type: PromotionTypeValues @OneToOne({ entity: () => ApplicationMethod, diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index e73b75ef49..1756901800 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -15,6 +15,7 @@ import { InjectTransactionManager, MedusaContext, MedusaError, + PromotionType, isString, mapObjectTo, } from "@medusajs/utils" @@ -35,6 +36,7 @@ import { PromotionService, } from "@services" import { + ApplicationMethodRuleTypes, CreateApplicationMethodDTO, CreateCampaignBudgetDTO, CreateCampaignDTO, @@ -456,6 +458,8 @@ export default class PromotionModuleService< "application_method", "application_method.target_rules", "application_method.target_rules.values", + "application_method.buy_rules", + "application_method.buy_rules.values", "rules", "rules.values", "campaign", @@ -485,7 +489,11 @@ export default class PromotionModuleService< string, PromotionTypes.CreatePromotionRuleDTO[] >() - const applicationMethodRuleMap = new Map< + const methodTargetRulesMap = new Map< + string, + PromotionTypes.CreatePromotionRuleDTO[] + >() + const methodBuyRulesMap = new Map< string, PromotionTypes.CreatePromotionRuleDTO[] >() @@ -551,6 +559,7 @@ export default class PromotionModuleService< if (applMethodData) { const { target_rules: targetRulesData = [], + buy_rules: buyRulesData = [], ...applicationMethodWithoutRules } = applMethodData const applicationMethodData = { @@ -569,11 +578,33 @@ export default class PromotionModuleService< ) } - validateApplicationMethodAttributes(applicationMethodData) + if (promotion.type === PromotionType.BUYGET && !buyRulesData.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Buy rules are required for ${PromotionType.BUYGET} promotion type` + ) + } + + if ( + promotion.type === PromotionType.BUYGET && + !targetRulesData.length + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Target rules are required for ${PromotionType.BUYGET} promotion type` + ) + } + + validateApplicationMethodAttributes(applicationMethodData, promotion) + applicationMethodsData.push(applicationMethodData) if (targetRulesData.length) { - applicationMethodRuleMap.set(promotion.id, targetRulesData) + methodTargetRulesMap.set(promotion.id, targetRulesData) + } + + if (buyRulesData.length) { + methodBuyRulesMap.set(promotion.id, buyRulesData) } } @@ -597,8 +628,15 @@ export default class PromotionModuleService< for (const applicationMethod of createdApplicationMethods) { await this.createPromotionRulesAndValues_( - applicationMethodRuleMap.get(applicationMethod.promotion.id) || [], - "application_methods", + methodTargetRulesMap.get(applicationMethod.promotion.id) || [], + "method_target_rules", + applicationMethod, + sharedContext + ) + + await this.createPromotionRulesAndValues_( + methodBuyRulesMap.get(applicationMethod.promotion.id) || [], + "method_buy_rules", applicationMethod, sharedContext ) @@ -694,18 +732,10 @@ export default class PromotionModuleService< existingApplicationMethod.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, - }) + validateApplicationMethodAttributes( + applicationMethodData, + existingPromotion + ) applicationMethodsData.push({ ...applicationMethodData, @@ -771,7 +801,7 @@ export default class PromotionModuleService< await this.createPromotionRulesAndValues_( rulesData, - "application_methods", + "method_target_rules", applicationMethod, sharedContext ) @@ -791,10 +821,51 @@ export default class PromotionModuleService< ) } + @InjectManager("baseRepository_") + async addPromotionBuyRules( + 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, + "method_buy_rules", + applicationMethod, + sharedContext + ) + + return this.retrieve( + promotionId, + { + relations: [ + "rules", + "rules.values", + "application_method", + "application_method.buy_rules", + "application_method.buy_rules.values", + ], + }, + sharedContext + ) + } + @InjectTransactionManager("baseRepository_") protected async createPromotionRulesAndValues_( rulesData: PromotionTypes.CreatePromotionRuleDTO[], - relationName: "promotions" | "application_methods", + relationName: "promotions" | "method_target_rules" | "method_buy_rules", relation: Promotion | ApplicationMethod, @MedusaContext() sharedContext: Context = {} ) { @@ -952,9 +1023,10 @@ export default class PromotionModuleService< rulesData: PromotionTypes.RemovePromotionRuleDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { - await this.removePromotionTargetRules_( + await this.removeApplicationMethodRules_( promotionId, rulesData, + ApplicationMethodRuleTypes.TARGET_RULES, sharedContext ) @@ -973,16 +1045,47 @@ export default class PromotionModuleService< ) } - @InjectTransactionManager("baseRepository_") - protected async removePromotionTargetRules_( + @InjectManager("baseRepository_") + async removePromotionBuyRules( promotionId: string, rulesData: PromotionTypes.RemovePromotionRuleDTO[], @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.removeApplicationMethodRules_( + promotionId, + rulesData, + ApplicationMethodRuleTypes.BUY_RULES, + sharedContext + ) + + return this.retrieve( + promotionId, + { + relations: [ + "rules", + "rules.values", + "application_method", + "application_method.buy_rules", + "application_method.buy_rules.values", + ], + }, + sharedContext + ) + } + + @InjectTransactionManager("baseRepository_") + protected async removeApplicationMethodRules_( + promotionId: string, + rulesData: PromotionTypes.RemovePromotionRuleDTO[], + relation: + | ApplicationMethodRuleTypes.TARGET_RULES + | ApplicationMethodRuleTypes.BUY_RULES, + @MedusaContext() sharedContext: Context = {} ): Promise { const promotionRuleIds = rulesData.map((ruleData) => ruleData.id) const promotion = await this.promotionService_.retrieve( promotionId, - { relations: ["application_method.target_rules"] }, + { relations: [`application_method.${relation}`] }, sharedContext ) @@ -995,7 +1098,7 @@ export default class PromotionModuleService< ) } - const targetRuleIdsToRemove = applicationMethod.target_rules + const targetRuleIdsToRemove = applicationMethod[relation] .toArray() .filter((rule) => promotionRuleIds.includes(rule.id)) .map((rule) => rule.id) diff --git a/packages/promotion/src/types/application-method.ts b/packages/promotion/src/types/application-method.ts index f269b4de2b..3ba19ad906 100644 --- a/packages/promotion/src/types/application-method.ts +++ b/packages/promotion/src/types/application-method.ts @@ -14,6 +14,8 @@ export interface CreateApplicationMethodDTO { value?: string | null promotion: Promotion | string | PromotionDTO max_quantity?: number | null + buy_rules_min_quantity?: number | null + apply_to_quantity?: number | null } export interface UpdateApplicationMethodDTO { @@ -24,4 +26,6 @@ export interface UpdateApplicationMethodDTO { value?: string | null promotion?: Promotion | string | PromotionDTO max_quantity?: number | null + buy_rules_min_quantity?: number | null + apply_to_quantity?: number | null } diff --git a/packages/promotion/src/types/promotion-rule.ts b/packages/promotion/src/types/promotion-rule.ts index 750fe96a41..3ed12d5d37 100644 --- a/packages/promotion/src/types/promotion-rule.ts +++ b/packages/promotion/src/types/promotion-rule.ts @@ -9,3 +9,8 @@ export interface CreatePromotionRuleDTO { export interface UpdatePromotionRuleDTO { id: string } + +export enum ApplicationMethodRuleTypes { + TARGET_RULES = "target_rules", + BUY_RULES = "buy_rules", +} diff --git a/packages/promotion/src/types/promotion.ts b/packages/promotion/src/types/promotion.ts index 8e63c442cf..ae63f9f36b 100644 --- a/packages/promotion/src/types/promotion.ts +++ b/packages/promotion/src/types/promotion.ts @@ -1,8 +1,8 @@ -import { PromotionType } from "@medusajs/types" +import { PromotionTypeValues } from "@medusajs/types" export interface CreatePromotionDTO { code: string - type: PromotionType + type: PromotionTypeValues is_automatic?: boolean campaign?: string } @@ -10,8 +10,7 @@ export interface CreatePromotionDTO { export interface UpdatePromotionDTO { id: string code?: string - // TODO: add this when buyget is available - // type: PromotionType + type?: PromotionTypeValues is_automatic?: boolean campaign?: string } diff --git a/packages/promotion/src/utils/validations/application-method.ts b/packages/promotion/src/utils/validations/application-method.ts index 95d384e19f..99c6296349 100644 --- a/packages/promotion/src/utils/validations/application-method.ts +++ b/packages/promotion/src/utils/validations/application-method.ts @@ -1,16 +1,17 @@ -import { - ApplicationMethodAllocationValues, - ApplicationMethodTargetTypeValues, - ApplicationMethodTypeValues, -} from "@medusajs/types" import { ApplicationMethodAllocation, ApplicationMethodTargetType, ApplicationMethodType, MedusaError, + PromotionType, isDefined, isPresent, } from "@medusajs/utils" +import { Promotion } from "@models" +import { + CreateApplicationMethodDTO, + UpdateApplicationMethodDTO, +} from "../../types" export const allowedAllocationTargetTypes: string[] = [ ApplicationMethodTargetType.SHIPPING_METHODS, @@ -26,17 +27,40 @@ export const allowedAllocationForQuantity: string[] = [ ApplicationMethodAllocation.EACH, ] -export function validateApplicationMethodAttributes(data: { - type: ApplicationMethodTypeValues - target_type: ApplicationMethodTargetTypeValues - allocation?: ApplicationMethodAllocationValues - max_quantity?: number | null -}) { +export function validateApplicationMethodAttributes( + data: UpdateApplicationMethodDTO | CreateApplicationMethodDTO, + promotion: Promotion +) { + const applicationMethod = promotion?.application_method || {} + const buyRulesMinQuantity = + data.buy_rules_min_quantity || applicationMethod?.buy_rules_min_quantity + const applyToQuantity = + data.apply_to_quantity || applicationMethod?.apply_to_quantity + const targetType = data.target_type || applicationMethod?.target_type + const applicationMethodType = data.type || applicationMethod?.type + const maxQuantity = data.max_quantity || applicationMethod.max_quantity + const allocation = data.allocation || applicationMethod.allocation const allTargetTypes: string[] = Object.values(ApplicationMethodTargetType) + if (promotion?.type === PromotionType.BUYGET) { + if (!isPresent(applyToQuantity)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `apply_to_quantity is a required field for Promotion type of ${PromotionType.BUYGET}` + ) + } + + if (!isPresent(buyRulesMinQuantity)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `buy_rules_min_quantity is a required field for Promotion type of ${PromotionType.BUYGET}` + ) + } + } + if ( - data.allocation === ApplicationMethodAllocation.ACROSS && - isPresent(data.max_quantity) + allocation === ApplicationMethodAllocation.ACROSS && + isPresent(maxQuantity) ) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -44,7 +68,7 @@ export function validateApplicationMethodAttributes(data: { ) } - if (!allTargetTypes.includes(data.target_type)) { + if (!allTargetTypes.includes(targetType)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, `application_method.target_type should be one of ${allTargetTypes.join( @@ -55,7 +79,7 @@ export function validateApplicationMethodAttributes(data: { const allTypes: string[] = Object.values(ApplicationMethodType) - if (!allTypes.includes(data.type)) { + if (!allTypes.includes(applicationMethodType)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, `application_method.type should be one of ${allTypes.join(", ")}` @@ -63,8 +87,8 @@ export function validateApplicationMethodAttributes(data: { } if ( - allowedAllocationTargetTypes.includes(data.target_type) && - !allowedAllocationTypes.includes(data.allocation || "") + allowedAllocationTargetTypes.includes(targetType) && + !allowedAllocationTypes.includes(allocation || "") ) { throw new MedusaError( MedusaError.Types.INVALID_DATA, @@ -80,7 +104,7 @@ export function validateApplicationMethodAttributes(data: { ApplicationMethodAllocation ) - if (data.allocation && !allAllocationTypes.includes(data.allocation)) { + if (allocation && !allAllocationTypes.includes(allocation)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, `application_method.allocation should be one of ${allAllocationTypes.join( @@ -90,9 +114,9 @@ export function validateApplicationMethodAttributes(data: { } if ( - data.allocation && - allowedAllocationForQuantity.includes(data.allocation) && - !isDefined(data.max_quantity) + allocation && + allowedAllocationForQuantity.includes(allocation) && + !isDefined(maxQuantity) ) { throw new MedusaError( MedusaError.Types.INVALID_DATA, diff --git a/packages/types/src/promotion/common/application-method.ts b/packages/types/src/promotion/common/application-method.ts index 77478e14b2..4a5e469853 100644 --- a/packages/types/src/promotion/common/application-method.ts +++ b/packages/types/src/promotion/common/application-method.ts @@ -16,8 +16,11 @@ export interface ApplicationMethodDTO { allocation?: ApplicationMethodAllocationValues value?: string | null max_quantity?: number | null + buy_rules_min_quantity?: number | null + apply_to_quantity?: number | null promotion?: PromotionDTO | string target_rules?: PromotionRuleDTO[] + buy_rules?: PromotionRuleDTO[] } export interface CreateApplicationMethodDTO { @@ -26,8 +29,11 @@ export interface CreateApplicationMethodDTO { allocation?: ApplicationMethodAllocationValues value?: string | null max_quantity?: number | null + buy_rules_min_quantity?: number | null + apply_to_quantity?: number | null promotion?: PromotionDTO | string target_rules?: CreatePromotionRuleDTO[] + buy_rules?: CreatePromotionRuleDTO[] } export interface UpdateApplicationMethodDTO { @@ -37,6 +43,8 @@ export interface UpdateApplicationMethodDTO { allocation?: ApplicationMethodAllocationValues value?: string | null max_quantity?: number | null + buy_rules_min_quantity?: number | null + apply_to_quantity?: number | null promotion?: PromotionDTO | string } diff --git a/packages/types/src/promotion/common/promotion.ts b/packages/types/src/promotion/common/promotion.ts index d0cea3caad..018e97724c 100644 --- a/packages/types/src/promotion/common/promotion.ts +++ b/packages/types/src/promotion/common/promotion.ts @@ -8,12 +8,12 @@ import { import { CampaignDTO } from "./campaign" import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule" -export type PromotionType = "standard" | "buyget" +export type PromotionTypeValues = "standard" | "buyget" export interface PromotionDTO { id: string code?: string - type?: PromotionType + type?: PromotionTypeValues is_automatic?: boolean application_method?: ApplicationMethodDTO rules?: PromotionRuleDTO[] @@ -22,7 +22,7 @@ export interface PromotionDTO { export interface CreatePromotionDTO { code: string - type: PromotionType + type: PromotionTypeValues is_automatic?: boolean application_method?: CreateApplicationMethodDTO rules?: CreatePromotionRuleDTO[] @@ -34,7 +34,7 @@ export interface UpdatePromotionDTO { id: string is_automatic?: boolean code?: string - type?: PromotionType + type?: PromotionTypeValues application_method?: UpdateApplicationMethodDTO campaign_id?: string } @@ -44,6 +44,6 @@ export interface FilterablePromotionProps id?: string[] code?: string[] is_automatic?: boolean - type?: PromotionType[] + type?: PromotionTypeValues[] budget_id?: string[] } diff --git a/packages/types/src/promotion/service.ts b/packages/types/src/promotion/service.ts index d968133f15..fd9721ef54 100644 --- a/packages/types/src/promotion/service.ts +++ b/packages/types/src/promotion/service.ts @@ -90,6 +90,12 @@ export interface IPromotionModuleService extends IModuleService { sharedContext?: Context ): Promise + addPromotionBuyRules( + promotionId: string, + rulesData: CreatePromotionRuleDTO[], + sharedContext?: Context + ): Promise + removePromotionRules( promotionId: string, rulesData: RemovePromotionRuleDTO[], @@ -102,6 +108,12 @@ export interface IPromotionModuleService extends IModuleService { sharedContext?: Context ): Promise + removePromotionBuyRules( + promotionId: string, + rulesData: RemovePromotionRuleDTO[], + sharedContext?: Context + ): Promise + createCampaigns( data: CreateCampaignDTO, sharedContext?: Context