feat(medusa,types): added buyget support for modules (#6159)
This commit is contained in:
6
.changeset/young-forks-worry.md
Normal file
6
.changeset/young-forks-worry.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
feat(medusa,types): added buyget support for modules
|
||||
@@ -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" }),
|
||||
]),
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<T>(
|
||||
|
||||
@@ -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: [],
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20240122070028 extends Migration {
|
||||
export class Migration20240122084316 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
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(
|
||||
@@ -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<PromotionRule>(this)
|
||||
|
||||
@ManyToMany(() => PromotionRule, "method_buy_rules", {
|
||||
owner: true,
|
||||
pivotTable: "application_method_buy_rules",
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
buy_rules = new Collection<PromotionRule>(this)
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
columnType: "timestamptz",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ApplicationMethod>(this)
|
||||
method_target_rules = new Collection<ApplicationMethod>(this)
|
||||
|
||||
@ManyToMany(
|
||||
() => ApplicationMethod,
|
||||
(applicationMethod) => applicationMethod.buy_rules
|
||||
)
|
||||
method_buy_rules = new Collection<ApplicationMethod>(this)
|
||||
|
||||
@Property({
|
||||
onCreate: () => new Date(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<PromotionTypes.PromotionDTO> {
|
||||
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<PromotionTypes.PromotionDTO> {
|
||||
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<PromotionTypes.PromotionDTO> {
|
||||
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<void> {
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -9,3 +9,8 @@ export interface CreatePromotionRuleDTO {
|
||||
export interface UpdatePromotionRuleDTO {
|
||||
id: string
|
||||
}
|
||||
|
||||
export enum ApplicationMethodRuleTypes {
|
||||
TARGET_RULES = "target_rules",
|
||||
BUY_RULES = "buy_rules",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -90,6 +90,12 @@ export interface IPromotionModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<PromotionDTO>
|
||||
|
||||
addPromotionBuyRules(
|
||||
promotionId: string,
|
||||
rulesData: CreatePromotionRuleDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<PromotionDTO>
|
||||
|
||||
removePromotionRules(
|
||||
promotionId: string,
|
||||
rulesData: RemovePromotionRuleDTO[],
|
||||
@@ -102,6 +108,12 @@ export interface IPromotionModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<PromotionDTO>
|
||||
|
||||
removePromotionBuyRules(
|
||||
promotionId: string,
|
||||
rulesData: RemovePromotionRuleDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<PromotionDTO>
|
||||
|
||||
createCampaigns(
|
||||
data: CreateCampaignDTO,
|
||||
sharedContext?: Context
|
||||
|
||||
Reference in New Issue
Block a user