feat(types,utils): added promotion create with rules and application target rules (#5957)

* feat(types,utils): added promotion create with rules

* chore: add rules to promotion and application method

* chore: use common code for rule and values

* chore: address pr reviews

* chore: fix test
This commit is contained in:
Riqwan Thamir
2024-01-03 09:54:48 +01:00
committed by GitHub
parent d16d10619d
commit 42cc8ae3f8
33 changed files with 1560 additions and 122 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat(types,utils): added promotion create with rules

View File

@@ -75,15 +75,82 @@ describe("Promotion Service", () => {
},
])
const [promotion] = await service.list({
id: [createdPromotion.id],
})
const [promotion] = await service.list(
{
id: [createdPromotion.id],
},
{
relations: ["application_method"],
}
)
expect(promotion).toEqual(
expect.objectContaining({
code: "PROMOTION_TEST",
is_automatic: false,
type: "standard",
application_method: expect.objectContaining({
type: "fixed",
target_type: "order",
value: 100,
}),
})
)
})
it("should create a promotion with order application method with rules successfully", async () => {
const [createdPromotion] = await service.create([
{
code: "PROMOTION_TEST",
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "order",
value: 100,
target_rules: [
{
attribute: "product_id",
operator: "eq",
values: ["prod_tshirt"],
},
],
},
},
])
const [promotion] = await service.list(
{
id: [createdPromotion.id],
},
{
relations: [
"application_method",
"application_method.target_rules.values",
],
}
)
expect(promotion).toEqual(
expect.objectContaining({
code: "PROMOTION_TEST",
is_automatic: false,
type: "standard",
application_method: expect.objectContaining({
type: "fixed",
target_type: "order",
value: 100,
target_rules: [
expect.objectContaining({
attribute: "product_id",
operator: "eq",
values: expect.arrayContaining([
expect.objectContaining({
value: "prod_tshirt",
}),
]),
}),
],
}),
})
)
})
@@ -128,5 +195,156 @@ describe("Promotion Service", () => {
"application_method.max_quantity is required when application_method.allocation is 'each'"
)
})
it("should create a promotion with rules successfully", async () => {
const [createdPromotion] = await service.create([
{
code: "PROMOTION_TEST",
type: PromotionType.STANDARD,
rules: [
{
attribute: "customer_group_id",
operator: "in",
values: ["VIP", "top100"],
},
],
},
])
const [promotion] = await service.list(
{
id: [createdPromotion.id],
},
{
relations: ["rules", "rules.values"],
}
)
expect(promotion).toEqual(
expect.objectContaining({
code: "PROMOTION_TEST",
is_automatic: false,
type: "standard",
rules: [
expect.objectContaining({
attribute: "customer_group_id",
operator: "in",
values: expect.arrayContaining([
expect.objectContaining({
value: "VIP",
}),
expect.objectContaining({
value: "top100",
}),
]),
}),
],
})
)
})
it("should create a promotion with rules with single value successfully", async () => {
const [createdPromotion] = await service.create([
{
code: "PROMOTION_TEST",
type: PromotionType.STANDARD,
rules: [
{
attribute: "customer_group_id",
operator: "eq",
values: "VIP",
},
],
},
])
const [promotion] = await service.list(
{
id: [createdPromotion.id],
},
{
relations: ["rules", "rules.values"],
}
)
expect(promotion).toEqual(
expect.objectContaining({
code: "PROMOTION_TEST",
is_automatic: false,
type: "standard",
rules: [
expect.objectContaining({
attribute: "customer_group_id",
operator: "eq",
values: expect.arrayContaining([
expect.objectContaining({
value: "VIP",
}),
]),
}),
],
})
)
})
it("should throw an error when rule attribute is invalid", async () => {
const error = await service
.create([
{
code: "PROMOTION_TEST",
type: PromotionType.STANDARD,
rules: [
{
attribute: "",
operator: "eq",
values: "VIP",
} as any,
],
},
])
.catch((e) => e)
expect(error.message).toContain("rules[].attribute is a required field")
})
it("should throw an error when rule operator is invalid", async () => {
let error = await service
.create([
{
code: "PROMOTION_TEST",
type: PromotionType.STANDARD,
rules: [
{
attribute: "customer_group",
operator: "",
values: "VIP",
} as any,
],
},
])
.catch((e) => e)
expect(error.message).toContain("rules[].operator is a required field")
error = await service
.create([
{
code: "PROMOTION_TEST",
type: PromotionType.STANDARD,
rules: [
{
attribute: "customer_group",
operator: "doesnotexist",
values: "VIP",
} as any,
],
},
])
.catch((e) => e)
expect(error.message).toContain(
"rules[].operator (doesnotexist) is invalid. It should be one of gte, lte, gt, lt, eq, ne, in"
)
})
})
})

View File

@@ -19,6 +19,12 @@ export default async ({
container.register({
promotionService: asClass(defaultServices.PromotionService).singleton(),
promotionRuleService: asClass(
defaultServices.PromotionRuleService
).singleton(),
promotionRuleValueService: asClass(
defaultServices.PromotionRuleValueService
).singleton(),
applicationMethodService: asClass(
defaultServices.ApplicationMethodService
).singleton(),
@@ -44,5 +50,11 @@ function loadDefaultRepositories({ container }) {
promotionRepository: asClass(
defaultRepositories.PromotionRepository
).singleton(),
promotionRuleRepository: asClass(
defaultRepositories.PromotionRuleRepository
).singleton(),
promotionRuleValueRepository: asClass(
defaultRepositories.PromotionRuleValueRepository
).singleton(),
})
}

View File

@@ -1,7 +1,5 @@
{
"namespaces": [
"public"
],
"namespaces": ["public"],
"name": "public",
"tables": [
{
@@ -41,10 +39,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"enumItems": [
"standard",
"buyget"
],
"enumItems": ["standard", "buyget"],
"mappedType": "enum"
},
"created_at": {
@@ -84,18 +79,14 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"code"
],
"columnNames": ["code"],
"composite": false,
"keyName": "IDX_promotion_code",
"primary": false,
"unique": false
},
{
"columnNames": [
"type"
],
"columnNames": ["type"],
"composite": false,
"keyName": "IDX_promotion_type",
"primary": false,
@@ -103,18 +94,14 @@
},
{
"keyName": "IDX_promotion_code_unique",
"columnNames": [
"code"
],
"columnNames": ["code"],
"composite": false,
"primary": false,
"unique": true
},
{
"keyName": "promotion_pkey",
"columnNames": [
"id"
],
"columnNames": ["id"],
"composite": false,
"primary": true,
"unique": true
@@ -159,10 +146,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"enumItems": [
"fixed",
"percentage"
],
"enumItems": ["fixed", "percentage"],
"mappedType": "enum"
},
"target_type": {
@@ -172,11 +156,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"enumItems": [
"order",
"shipping",
"item"
],
"enumItems": ["order", "shipping", "item"],
"mappedType": "enum"
},
"allocation": {
@@ -186,10 +166,7 @@
"autoincrement": false,
"primary": false,
"nullable": true,
"enumItems": [
"each",
"across"
],
"enumItems": ["each", "across"],
"mappedType": "enum"
},
"promotion_id": {
@@ -238,36 +215,28 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"type"
],
"columnNames": ["type"],
"composite": false,
"keyName": "IDX_application_method_type",
"primary": false,
"unique": false
},
{
"columnNames": [
"target_type"
],
"columnNames": ["target_type"],
"composite": false,
"keyName": "IDX_application_method_target_type",
"primary": false,
"unique": false
},
{
"columnNames": [
"allocation"
],
"columnNames": ["allocation"],
"composite": false,
"keyName": "IDX_application_method_allocation",
"primary": false,
"unique": false
},
{
"columnNames": [
"promotion_id"
],
"columnNames": ["promotion_id"],
"composite": false,
"keyName": "application_method_promotion_id_unique",
"primary": false,
@@ -275,9 +244,7 @@
},
{
"keyName": "application_method_pkey",
"columnNames": [
"id"
],
"columnNames": ["id"],
"composite": false,
"primary": true,
"unique": true
@@ -287,17 +254,282 @@
"foreignKeys": {
"application_method_promotion_id_foreign": {
"constraintName": "application_method_promotion_id_foreign",
"columnNames": [
"promotion_id"
],
"columnNames": ["promotion_id"],
"localTableName": "public.application_method",
"referencedColumnNames": [
"id"
],
"referencedColumnNames": ["id"],
"referencedTableName": "public.promotion",
"updateRule": "cascade"
}
}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"description": {
"name": "description",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"attribute": {
"name": "attribute",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"operator": {
"name": "operator",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"enumItems": ["gte", "lte", "gt", "lt", "eq", "ne", "in"],
"mappedType": "enum"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "promotion_rule",
"schema": "public",
"indexes": [
{
"columnNames": ["attribute"],
"composite": false,
"keyName": "IDX_promotion_rule_attribute",
"primary": false,
"unique": false
},
{
"columnNames": ["operator"],
"composite": false,
"keyName": "IDX_promotion_rule_operator",
"primary": false,
"unique": false
},
{
"keyName": "promotion_rule_pkey",
"columnNames": ["id"],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {}
},
{
"columns": {
"promotion_id": {
"name": "promotion_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": "promotion_promotion_rule",
"schema": "public",
"indexes": [
{
"keyName": "promotion_promotion_rule_pkey",
"columnNames": ["promotion_id", "promotion_rule_id"],
"composite": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"promotion_promotion_rule_promotion_id_foreign": {
"constraintName": "promotion_promotion_rule_promotion_id_foreign",
"columnNames": ["promotion_id"],
"localTableName": "public.promotion_promotion_rule",
"referencedColumnNames": ["id"],
"referencedTableName": "public.promotion",
"deleteRule": "cascade",
"updateRule": "cascade"
},
"promotion_promotion_rule_promotion_rule_id_foreign": {
"constraintName": "promotion_promotion_rule_promotion_rule_id_foreign",
"columnNames": ["promotion_rule_id"],
"localTableName": "public.promotion_promotion_rule",
"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_promotion_rule",
"schema": "public",
"indexes": [
{
"keyName": "application_method_promotion_rule_pkey",
"columnNames": ["application_method_id", "promotion_rule_id"],
"composite": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"application_method_promotion_rule_application_method_id_foreign": {
"constraintName": "application_method_promotion_rule_application_method_id_foreign",
"columnNames": ["application_method_id"],
"localTableName": "public.application_method_promotion_rule",
"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",
"columnNames": ["promotion_rule_id"],
"localTableName": "public.application_method_promotion_rule",
"referencedColumnNames": ["id"],
"referencedTableName": "public.promotion_rule",
"deleteRule": "cascade",
"updateRule": "cascade"
}
}
},
{
"columns": {
"id": {
"name": "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"
},
"value": {
"name": "value",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
}
},
"name": "promotion_rule_value",
"schema": "public",
"indexes": [
{
"columnNames": ["promotion_rule_id"],
"composite": false,
"keyName": "IDX_promotion_rule_promotion_rule_value_id",
"primary": false,
"unique": false
},
{
"keyName": "promotion_rule_value_pkey",
"columnNames": ["id"],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"promotion_rule_value_promotion_rule_id_foreign": {
"constraintName": "promotion_rule_value_promotion_rule_id_foreign",
"columnNames": ["promotion_rule_id"],
"localTableName": "public.promotion_rule_value",
"referencedColumnNames": ["id"],
"referencedTableName": "public.promotion_rule",
"deleteRule": "cascade",
"updateRule": "cascade"
}
}
}
]
}

View File

@@ -1,34 +0,0 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20231221104256 extends Migration {
async up(): Promise<void> {
this.addSql(
'create table "promotion" ("id" text not null, "code" text not null, "is_automatic" boolean not null default false, "type" text check ("type" in (\'standard\', \'buyget\')) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_pkey" primary key ("id"));'
)
this.addSql('create index "IDX_promotion_code" on "promotion" ("code");')
this.addSql('create index "IDX_promotion_type" on "promotion" ("type");')
this.addSql(
'alter table "promotion" add constraint "IDX_promotion_code_unique" unique ("code");'
)
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\', \'item\')) 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");'
)
this.addSql(
'create index "IDX_application_method_target_type" on "application_method" ("target_type");'
)
this.addSql(
'create index "IDX_application_method_allocation" on "application_method" ("allocation");'
)
this.addSql(
'alter table "application_method" add constraint "application_method_promotion_id_unique" unique ("promotion_id");'
)
this.addSql(
'alter table "application_method" add constraint "application_method_promotion_id_foreign" foreign key ("promotion_id") references "promotion" ("id") on update cascade;'
)
}
}

View File

@@ -0,0 +1,77 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20240102130345 extends Migration {
async up(): Promise<void> {
this.addSql(
'create table "promotion" ("id" text not null, "code" text not null, "is_automatic" boolean not null default false, "type" text check ("type" in (\'standard\', \'buyget\')) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_pkey" primary key ("id"));'
)
this.addSql('create index "IDX_promotion_code" on "promotion" ("code");')
this.addSql('create index "IDX_promotion_type" on "promotion" ("type");')
this.addSql(
'alter table "promotion" add constraint "IDX_promotion_code_unique" unique ("code");'
)
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\', \'item\')) 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");'
)
this.addSql(
'create index "IDX_application_method_target_type" on "application_method" ("target_type");'
)
this.addSql(
'create index "IDX_application_method_allocation" on "application_method" ("allocation");'
)
this.addSql(
'alter table "application_method" add constraint "application_method_promotion_id_unique" unique ("promotion_id");'
)
this.addSql(
'create table "promotion_rule" ("id" text not null, "description" text null, "attribute" text not null, "operator" text check ("operator" in (\'gte\', \'lte\', \'gt\', \'lt\', \'eq\', \'ne\', \'in\')) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_rule_pkey" primary key ("id"));'
)
this.addSql(
'create index "IDX_promotion_rule_attribute" on "promotion_rule" ("attribute");'
)
this.addSql(
'create index "IDX_promotion_rule_operator" on "promotion_rule" ("operator");'
)
this.addSql(
'create table "promotion_promotion_rule" ("promotion_id" text not null, "promotion_rule_id" text not null, constraint "promotion_promotion_rule_pkey" primary key ("promotion_id", "promotion_rule_id"));'
)
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"));'
)
this.addSql(
'create table "promotion_rule_value" ("id" text not null, "promotion_rule_id" text not null, "value" text not null, constraint "promotion_rule_value_pkey" primary key ("id"));'
)
this.addSql(
'create index "IDX_promotion_rule_promotion_rule_value_id" on "promotion_rule_value" ("promotion_rule_id");'
)
this.addSql(
'alter table "application_method" add constraint "application_method_promotion_id_foreign" foreign key ("promotion_id") references "promotion" ("id") on update cascade;'
)
this.addSql(
'alter table "promotion_promotion_rule" add constraint "promotion_promotion_rule_promotion_id_foreign" foreign key ("promotion_id") references "promotion" ("id") on update cascade on delete cascade;'
)
this.addSql(
'alter table "promotion_promotion_rule" add constraint "promotion_promotion_rule_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_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;'
)
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;'
)
this.addSql(
'alter table "promotion_rule_value" add constraint "promotion_rule_value_promotion_rule_id_foreign" foreign key ("promotion_rule_id") references "promotion_rule" ("id") on update cascade on delete cascade;'
)
}
}

View File

@@ -6,9 +6,11 @@ import {
import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Collection,
Entity,
Enum,
Index,
ManyToMany,
OnInit,
OneToOne,
OptionalProps,
@@ -16,8 +18,15 @@ import {
Property,
} from "@mikro-orm/core"
import Promotion from "./promotion"
import PromotionRule from "./promotion-rule"
type OptionalFields = "value" | "max_quantity" | "allocation"
type OptionalFields =
| "value"
| "max_quantity"
| "allocation"
| "created_at"
| "updated_at"
| "deleted_at"
@Entity()
export default class ApplicationMethod {
[OptionalProps]?: OptionalFields
@@ -26,10 +35,10 @@ export default class ApplicationMethod {
id!: string
@Property({ columnType: "numeric", nullable: true, serializer: Number })
value?: number
value?: number | null
@Property({ columnType: "numeric", nullable: true, serializer: Number })
max_quantity?: number
max_quantity?: number | null
@Index({ name: "IDX_application_method_type" })
@Enum(() => PromotionUtils.ApplicationMethodType)
@@ -51,12 +60,19 @@ export default class ApplicationMethod {
})
promotion: Promotion
@ManyToMany(() => PromotionRule, "application_methods", {
owner: true,
pivotTable: "application_method_promotion_rule",
cascade: ["soft-remove"] as any,
})
target_rules = new Collection<PromotionRule>(this)
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at?: Date
created_at: Date
@Property({
onCreate: () => new Date(),
@@ -64,18 +80,18 @@ export default class ApplicationMethod {
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at?: Date
updated_at: Date
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
deleted_at: Date | null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "app_method")
this.id = generateEntityId(this.id, "proappmet")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "promo")
this.id = generateEntityId(this.id, "proappmet")
}
}

View File

@@ -1,2 +1,4 @@
export { default as ApplicationMethod } from "./application-method"
export { default as Promotion } from "./promotion"
export { default as PromotionRule } from "./promotion-rule"
export { default as PromotionRuleValue } from "./promotion-rule-value"

View File

@@ -0,0 +1,37 @@
import {
BeforeCreate,
Entity,
ManyToOne,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { generateEntityId } from "@medusajs/utils"
import PromotionRule from "./promotion-rule"
@Entity()
export default class PromotionRuleValue {
@PrimaryKey({ columnType: "text" })
id!: string
@ManyToOne(() => PromotionRule, {
onDelete: "cascade",
fieldName: "promotion_rule_id",
index: "IDX_promotion_rule_promotion_rule_value_id",
})
promotion_rule: PromotionRule
@Property({ columnType: "text" })
value: string
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "prorulval")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "prorulval")
}
}

View File

@@ -0,0 +1,83 @@
import { PromotionRuleOperatorValues } from "@medusajs/types"
import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Enum,
Index,
ManyToMany,
OnInit,
OneToMany,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import ApplicationMethod from "./application-method"
import Promotion from "./promotion"
import PromotionRuleValue from "./promotion-rule-value"
type OptionalFields = "description" | "created_at" | "updated_at" | "deleted_at"
type OptionalRelations = "values" | "promotions"
@Entity()
export default class PromotionRule {
[OptionalProps]?: OptionalFields | OptionalRelations
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text", nullable: true })
description: string | null
@Index({ name: "IDX_promotion_rule_attribute" })
@Property({ columnType: "text" })
attribute: string
@Index({ name: "IDX_promotion_rule_operator" })
@Enum(() => PromotionUtils.PromotionRuleOperator)
operator: PromotionRuleOperatorValues
@OneToMany(() => PromotionRuleValue, (prv) => prv.promotion_rule, {
cascade: [Cascade.REMOVE],
})
values = new Collection<PromotionRuleValue>(this)
@ManyToMany(() => Promotion, (promotion) => promotion.rules)
promotions = new Collection<Promotion>(this)
@ManyToMany(
() => ApplicationMethod,
(applicationMethod) => applicationMethod.target_rules
)
application_methods = new Collection<ApplicationMethod>(this)
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "prorul")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "prorul")
}
}

View File

@@ -2,9 +2,11 @@ import { PromotionType } from "@medusajs/types"
import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Collection,
Entity,
Enum,
Index,
ManyToMany,
OnInit,
OneToOne,
OptionalProps,
@@ -13,8 +15,13 @@ import {
Unique,
} from "@mikro-orm/core"
import ApplicationMethod from "./application-method"
import PromotionRule from "./promotion-rule"
type OptionalFields = "is_automatic"
type OptionalFields =
| "is_automatic"
| "created_at"
| "updated_at"
| "deleted_at"
type OptionalRelations = "application_method"
@Entity()
export default class Promotion {
@@ -44,12 +51,19 @@ export default class Promotion {
})
application_method: ApplicationMethod
@ManyToMany(() => PromotionRule, "promotions", {
owner: true,
pivotTable: "promotion_promotion_rule",
cascade: ["soft-remove"] as any,
})
rules = new Collection<PromotionRule>(this)
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at?: Date
created_at: Date
@Property({
onCreate: () => new Date(),
@@ -57,10 +71,10 @@ export default class Promotion {
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at?: Date
updated_at: Date
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
deleted_at: Date | null
@BeforeCreate()
onCreate() {

View File

@@ -1,3 +1,5 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
export { ApplicationMethodRepository } from "./application-method"
export { PromotionRepository } from "./promotion"
export { PromotionRuleRepository } from "./promotion-rule"
export { PromotionRuleValueRepository } from "./promotion-rule-value"

View File

@@ -0,0 +1,128 @@
import { Context, DAL } from "@medusajs/types"
import { DALUtils, MedusaError, arrayDifference } from "@medusajs/utils"
import {
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
} from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { PromotionRuleValue } from "@models"
import {
CreatePromotionRuleValueDTO,
UpdatePromotionRuleValueDTO,
} from "@types"
export class PromotionRuleValueRepository extends DALUtils.MikroOrmBaseRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
// @ts-ignore
// eslint-disable-next-line prefer-rest-params
super(...arguments)
this.manager_ = manager
}
async find(
findOptions: DAL.FindOptions<PromotionRuleValue> = { where: {} },
context: Context = {}
): Promise<PromotionRuleValue[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
return await manager.find(
PromotionRuleValue,
findOptions_.where as MikroFilterQuery<PromotionRuleValue>,
findOptions_.options as MikroOptions<PromotionRuleValue>
)
}
async findAndCount(
findOptions: DAL.FindOptions<PromotionRuleValue> = { where: {} },
context: Context = {}
): Promise<[PromotionRuleValue[], number]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
return await manager.findAndCount(
PromotionRuleValue,
findOptions_.where as MikroFilterQuery<PromotionRuleValue>,
findOptions_.options as MikroOptions<PromotionRuleValue>
)
}
async delete(ids: string[], context: Context = {}): Promise<void> {
const manager = this.getActiveManager<SqlEntityManager>(context)
await manager.nativeDelete(PromotionRuleValue, { id: { $in: ids } }, {})
}
async create(
data: CreatePromotionRuleValueDTO[],
context: Context = {}
): Promise<PromotionRuleValue[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const promotionRuleValue = data.map((promotionRuleValue) => {
return manager.create(PromotionRuleValue, promotionRuleValue)
})
manager.persist(promotionRuleValue)
return promotionRuleValue
}
async update(
data: UpdatePromotionRuleValueDTO[],
context: Context = {}
): Promise<PromotionRuleValue[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const promotionRuleValueIds = data.map(
(promotionRuleValue) => promotionRuleValue.id
)
const existingPromotionRuleValues = await this.find(
{
where: {
id: {
$in: promotionRuleValueIds,
},
},
},
context
)
const dataAndExistingIdDifference = arrayDifference(
data.map((d) => d.id),
existingPromotionRuleValues.map((plr) => plr.id)
)
if (dataAndExistingIdDifference.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`PromotionRuleValue with id(s) "${dataAndExistingIdDifference.join(
", "
)}" not found`
)
}
const existingPromotionRuleValueMap = new Map(
existingPromotionRuleValues.map<[string, PromotionRuleValue]>(
(promotionRuleValue) => [promotionRuleValue.id, promotionRuleValue]
)
)
const promotionRuleValue = data.map((promotionRuleValueData) => {
const existingPromotionRuleValue = existingPromotionRuleValueMap.get(
promotionRuleValueData.id
)!
return manager.assign(existingPromotionRuleValue, promotionRuleValueData)
})
manager.persist(promotionRuleValue)
return promotionRuleValue
}
}

View File

@@ -0,0 +1,124 @@
import { Context, DAL } from "@medusajs/types"
import { DALUtils, MedusaError, arrayDifference } from "@medusajs/utils"
import {
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
} from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { PromotionRule } from "@models"
import { CreatePromotionRuleDTO, UpdatePromotionRuleDTO } from "@types"
export class PromotionRuleRepository extends DALUtils.MikroOrmBaseRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
// @ts-ignore
// eslint-disable-next-line prefer-rest-params
super(...arguments)
this.manager_ = manager
}
async find(
findOptions: DAL.FindOptions<PromotionRule> = { where: {} },
context: Context = {}
): Promise<PromotionRule[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
return await manager.find(
PromotionRule,
findOptions_.where as MikroFilterQuery<PromotionRule>,
findOptions_.options as MikroOptions<PromotionRule>
)
}
async findAndCount(
findOptions: DAL.FindOptions<PromotionRule> = { where: {} },
context: Context = {}
): Promise<[PromotionRule[], number]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
return await manager.findAndCount(
PromotionRule,
findOptions_.where as MikroFilterQuery<PromotionRule>,
findOptions_.options as MikroOptions<PromotionRule>
)
}
async delete(ids: string[], context: Context = {}): Promise<void> {
const manager = this.getActiveManager<SqlEntityManager>(context)
await manager.nativeDelete(PromotionRule, { id: { $in: ids } }, {})
}
async create(
data: CreatePromotionRuleDTO[],
context: Context = {}
): Promise<PromotionRule[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const promotionRule = data.map((promotionRule) => {
return manager.create(PromotionRule, promotionRule)
})
manager.persist(promotionRule)
return promotionRule
}
async update(
data: UpdatePromotionRuleDTO[],
context: Context = {}
): Promise<PromotionRule[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const promotionRuleIds = data.map((promotionRule) => promotionRule.id)
const existingPromotionRules = await this.find(
{
where: {
id: {
$in: promotionRuleIds,
},
},
},
context
)
const dataAndExistingIdDifference = arrayDifference(
data.map((d) => d.id),
existingPromotionRules.map((plr) => plr.id)
)
if (dataAndExistingIdDifference.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`PromotionRule with id(s) "${dataAndExistingIdDifference.join(
", "
)}" not found`
)
}
const existingPromotionRuleMap = new Map(
existingPromotionRules.map<[string, PromotionRule]>((promotionRule) => [
promotionRule.id,
promotionRule,
])
)
const promotionRule = data.map((promotionRuleData) => {
const existingPromotionRule = existingPromotionRuleMap.get(
promotionRuleData.id
)!
return manager.assign(existingPromotionRule, promotionRuleData)
})
manager.persist(promotionRule)
return promotionRule
}
}

View File

@@ -1,3 +1,5 @@
export { default as ApplicationMethodService } from "./application-method"
export { default as PromotionService } from "./promotion"
export { default as PromotionModuleService } from "./promotion-module"
export { default as PromotionRuleService } from "./promotion-rule"
export { default as PromotionRuleValueService } from "./promotion-rule-value"

View File

@@ -11,16 +11,26 @@ import {
InjectTransactionManager,
MedusaContext,
} from "@medusajs/utils"
import { Promotion } from "@models"
import { ApplicationMethodService, PromotionService } from "@services"
import { ApplicationMethod, Promotion } from "@models"
import {
ApplicationMethodService,
PromotionRuleService,
PromotionRuleValueService,
PromotionService,
} from "@services"
import { joinerConfig } from "../joiner-config"
import { CreateApplicationMethodDTO, CreatePromotionDTO } from "../types"
import { validateApplicationMethodAttributes } from "../utils"
import {
validateApplicationMethodAttributes,
validatePromotionRuleAttributes,
} from "../utils"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
promotionService: PromotionService
applicationMethodService: ApplicationMethodService
promotionRuleService: PromotionRuleService
promotionRuleValueService: PromotionRuleValueService
}
export default class PromotionModuleService<
@@ -30,18 +40,24 @@ export default class PromotionModuleService<
protected baseRepository_: DAL.RepositoryService
protected promotionService_: PromotionService
protected applicationMethodService_: ApplicationMethodService
protected promotionRuleService_: PromotionRuleService
protected promotionRuleValueService_: PromotionRuleValueService
constructor(
{
baseRepository,
promotionService,
applicationMethodService,
promotionRuleService,
promotionRuleValueService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
this.baseRepository_ = baseRepository
this.promotionService_ = promotionService
this.applicationMethodService_ = applicationMethodService
this.promotionRuleService_ = promotionRuleService
this.promotionRuleValueService_ = promotionRuleValueService
}
__joinerConfig(): ModuleJoinerConfig {
@@ -78,7 +94,7 @@ export default class PromotionModuleService<
return await this.list(
{ id: promotions.map((p) => p!.id) },
{
relations: ["application_method"],
relations: ["application_method", "rules", "rules.values"],
},
sharedContext
)
@@ -89,15 +105,26 @@ export default class PromotionModuleService<
data: PromotionTypes.CreatePromotionDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const promotionsData: CreatePromotionDTO[] = []
const applicationMethodsData: CreateApplicationMethodDTO[] = []
const promotionCodeApplicationMethodDataMap = new Map<
string,
PromotionTypes.CreateApplicationMethodDTO
>()
const promotionsData: CreatePromotionDTO[] = []
const applicationMethodsData: CreateApplicationMethodDTO[] = []
const promotionCodeRulesDataMap = new Map<
string,
PromotionTypes.CreatePromotionRuleDTO[]
>()
const applicationMethodRuleMap = new Map<
string,
PromotionTypes.CreatePromotionRuleDTO[]
>()
for (const {
application_method: applicationMethodData,
rules: rulesData,
...promotionData
} of data) {
if (applicationMethodData) {
@@ -107,33 +134,94 @@ export default class PromotionModuleService<
)
}
if (rulesData) {
promotionCodeRulesDataMap.set(promotionData.code, rulesData)
}
promotionsData.push(promotionData)
}
const createdPromotions = await this.promotionService_.create(
data,
promotionsData,
sharedContext
)
for (const promotion of createdPromotions) {
const data = promotionCodeApplicationMethodDataMap.get(promotion.code)
const applMethodData = promotionCodeApplicationMethodDataMap.get(
promotion.code
)
if (!data) continue
if (applMethodData) {
const {
target_rules: targetRulesData = [],
...applicationMethodWithoutRules
} = applMethodData
const applicationMethodData = {
...applicationMethodWithoutRules,
promotion,
}
const applicationMethodData = {
...data,
promotion,
validateApplicationMethodAttributes(applicationMethodData)
applicationMethodsData.push(applicationMethodData)
if (targetRulesData.length) {
applicationMethodRuleMap.set(promotion.id, targetRulesData)
}
}
validateApplicationMethodAttributes(applicationMethodData)
applicationMethodsData.push(applicationMethodData)
await this.createPromotionRulesAndValues(
promotionCodeRulesDataMap.get(promotion.code) || [],
"promotions",
promotion,
sharedContext
)
}
await this.applicationMethodService_.create(
applicationMethodsData,
sharedContext
)
const createdApplicationMethods =
await this.applicationMethodService_.create(
applicationMethodsData,
sharedContext
)
for (const applicationMethod of createdApplicationMethods) {
await this.createPromotionRulesAndValues(
applicationMethodRuleMap.get(applicationMethod.promotion.id) || [],
"application_methods",
applicationMethod,
sharedContext
)
}
return createdPromotions
}
protected async createPromotionRulesAndValues(
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
relationName: "promotions" | "application_methods",
relation: Promotion | ApplicationMethod,
sharedContext: Context
) {
validatePromotionRuleAttributes(rulesData)
for (const ruleData of rulesData) {
const { values, ...rest } = ruleData
const promotionRuleData = {
...rest,
[relationName]: [relation],
}
const [createdPromotionRule] = await this.promotionRuleService_.create(
[promotionRuleData],
sharedContext
)
const ruleValues = Array.isArray(values) ? values : [values]
const promotionRuleValuesData = ruleValues.map((ruleValue) => ({
value: ruleValue,
promotion_rule: createdPromotionRule,
}))
await this.promotionRuleValueService_.create(promotionRuleValuesData)
}
}
}

View File

@@ -0,0 +1,108 @@
import { Context, DAL, FindConfig, PromotionTypes } from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { PromotionRuleValue } from "@models"
import { PromotionRuleValueRepository } from "@repositories"
import {
CreatePromotionRuleValueDTO,
UpdatePromotionRuleValueDTO,
} from "../types"
type InjectedDependencies = {
promotionRuleValueRepository: DAL.RepositoryService
}
export default class PromotionRuleValueService<
TEntity extends PromotionRuleValue = PromotionRuleValue
> {
protected readonly promotionRuleValueRepository_: DAL.RepositoryService
constructor({ promotionRuleValueRepository }: InjectedDependencies) {
this.promotionRuleValueRepository_ = promotionRuleValueRepository
}
@InjectManager("promotionRuleValueRepository_")
async retrieve(
promotionRuleValueId: string,
config: FindConfig<PromotionTypes.PromotionRuleValueDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<
PromotionRuleValue,
PromotionTypes.PromotionRuleValueDTO
>({
id: promotionRuleValueId,
entityName: PromotionRuleValue.name,
repository: this.promotionRuleValueRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("promotionRuleValueRepository_")
async list(
filters: PromotionTypes.FilterablePromotionRuleValueProps = {},
config: FindConfig<PromotionTypes.PromotionRuleValueDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryOptions = ModulesSdkUtils.buildQuery<PromotionRuleValue>(
filters,
config
)
return (await this.promotionRuleValueRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
}
@InjectManager("promotionRuleValueRepository_")
async listAndCount(
filters: PromotionTypes.FilterablePromotionRuleValueProps = {},
config: FindConfig<PromotionTypes.PromotionRuleValueDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<PromotionRuleValue>(
filters,
config
)
return (await this.promotionRuleValueRepository_.findAndCount(
queryOptions,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager("promotionRuleValueRepository_")
async create(
data: CreatePromotionRuleValueDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (
this.promotionRuleValueRepository_ as PromotionRuleValueRepository
).create(data, sharedContext)) as TEntity[]
}
@InjectTransactionManager("promotionRuleValueRepository_")
async update(
data: UpdatePromotionRuleValueDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (
this.promotionRuleValueRepository_ as PromotionRuleValueRepository
).update(data, sharedContext)) as TEntity[]
}
@InjectTransactionManager("promotionRuleValueRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.promotionRuleValueRepository_.delete(ids, sharedContext)
}
}

View File

@@ -0,0 +1,105 @@
import { Context, DAL, FindConfig, PromotionTypes } from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { PromotionRule } from "@models"
import { PromotionRuleRepository } from "@repositories"
import { CreatePromotionRuleDTO, UpdatePromotionRuleDTO } from "../types"
type InjectedDependencies = {
promotionRuleRepository: DAL.RepositoryService
}
export default class PromotionRuleService<
TEntity extends PromotionRule = PromotionRule
> {
protected readonly promotionRuleRepository_: DAL.RepositoryService
constructor({ promotionRuleRepository }: InjectedDependencies) {
this.promotionRuleRepository_ = promotionRuleRepository
}
@InjectManager("promotionRuleRepository_")
async retrieve(
promotionRuleId: string,
config: FindConfig<PromotionTypes.PromotionRuleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<
PromotionRule,
PromotionTypes.PromotionRuleDTO
>({
id: promotionRuleId,
entityName: PromotionRule.name,
repository: this.promotionRuleRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("promotionRuleRepository_")
async list(
filters: PromotionTypes.FilterablePromotionRuleProps = {},
config: FindConfig<PromotionTypes.PromotionRuleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryOptions = ModulesSdkUtils.buildQuery<PromotionRule>(
filters,
config
)
return (await this.promotionRuleRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
}
@InjectManager("promotionRuleRepository_")
async listAndCount(
filters: PromotionTypes.FilterablePromotionRuleProps = {},
config: FindConfig<PromotionTypes.PromotionRuleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<PromotionRule>(
filters,
config
)
return (await this.promotionRuleRepository_.findAndCount(
queryOptions,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager("promotionRuleRepository_")
async create(
data: CreatePromotionRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (
this.promotionRuleRepository_ as PromotionRuleRepository
).create(data, sharedContext)) as TEntity[]
}
@InjectTransactionManager("promotionRuleRepository_")
async update(
data: UpdatePromotionRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (
this.promotionRuleRepository_ as PromotionRuleRepository
).update(data, sharedContext)) as TEntity[]
}
@InjectTransactionManager("promotionRuleRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.promotionRuleRepository_.delete(ids, sharedContext)
}
}

View File

@@ -6,3 +6,5 @@ export type InitializeModuleInjectableDependencies = {
export * from "./application-method"
export * from "./promotion"
export * from "./promotion-rule"
export * from "./promotion-rule-value"

View File

@@ -0,0 +1,12 @@
import { PromotionRuleDTO } from "@medusajs/types"
export interface CreatePromotionRuleValueDTO {
value: any
promotion_rule: string | PromotionRuleDTO
}
export interface UpdatePromotionRuleValueDTO {
id: string
value: any
promotion_rule: string | PromotionRuleDTO
}

View File

@@ -0,0 +1,11 @@
import { PromotionRuleOperatorValues } from "@medusajs/types"
export interface CreatePromotionRuleDTO {
description?: string
attribute: string
operator: PromotionRuleOperatorValues
}
export interface UpdatePromotionRuleDTO {
id: string
}

View File

@@ -1 +1,2 @@
export * from "./application-method"
export * from "./promotion-rule"

View File

@@ -0,0 +1,39 @@
import { PromotionRuleOperatorValues } from "@medusajs/types"
import { MedusaError, PromotionRuleOperator, isPresent } from "@medusajs/utils"
import { CreatePromotionRuleDTO } from "../../types"
export function validatePromotionRuleAttributes(
promotionRulesData: CreatePromotionRuleDTO[]
) {
const errors: string[] = []
for (const promotionRuleData of promotionRulesData) {
if (!isPresent(promotionRuleData.attribute)) {
errors.push("rules[].attribute is a required field")
}
if (!isPresent(promotionRuleData.operator)) {
errors.push("rules[].operator is a required field")
}
if (isPresent(promotionRuleData.operator)) {
const allowedOperators: PromotionRuleOperatorValues[] = Object.values(
PromotionRuleOperator
)
if (!allowedOperators.includes(promotionRuleData.operator)) {
errors.push(
`rules[].operator (${
promotionRuleData.operator
}) is invalid. It should be one of ${allowedOperators.join(", ")}`
)
}
} else {
errors.push("rules[].operator is a required field")
}
}
if (!errors.length) return
throw new MedusaError(MedusaError.Types.INVALID_DATA, errors.join(", "))
}

View File

@@ -23,7 +23,9 @@
"paths": {
"@models": ["./src/models"],
"@services": ["./src/services"],
"@repositories": ["./src/repositories"]
"@repositories": ["./src/repositories"],
"@types": ["./src/types"],
"@utils": ["./src/utils"]
}
},
"include": ["src"],

View File

@@ -1,5 +1,6 @@
import { BaseFilterable } from "../../dal"
import { PromotionDTO } from "./promotion"
import { CreatePromotionRuleDTO } from "./promotion-rule"
export type ApplicationMethodType = "fixed" | "percentage"
export type ApplicationMethodTargetType = "order" | "shipping" | "item"
@@ -16,6 +17,7 @@ export interface CreateApplicationMethodDTO {
value?: number
max_quantity?: number
promotion?: PromotionDTO | string
target_rules?: CreatePromotionRuleDTO[]
}
export interface UpdateApplicationMethodDTO {

View File

@@ -1,2 +1,4 @@
export * from "./application-method"
export * from "./promotion"
export * from "./promotion-rule"
export * from "./promotion-rule-value"

View File

@@ -0,0 +1,20 @@
import { BaseFilterable } from "../../dal"
import { PromotionRuleDTO } from "./promotion-rule"
export interface PromotionRuleValueDTO {
id: string
}
export interface CreatePromotionRuleValueDTO {
value: any
promotion_rule: PromotionRuleDTO
}
export interface UpdatePromotionRuleValueDTO {
id: string
}
export interface FilterablePromotionRuleValueProps
extends BaseFilterable<FilterablePromotionRuleValueProps> {
id?: string[]
}

View File

@@ -0,0 +1,30 @@
import { BaseFilterable } from "../../dal"
export type PromotionRuleOperatorValues =
| "gt"
| "lt"
| "eq"
| "ne"
| "in"
| "lte"
| "gte"
export interface PromotionRuleDTO {
id: string
}
export interface CreatePromotionRuleDTO {
description?: string
attribute: string
operator: PromotionRuleOperatorValues
values: string[] | string
}
export interface UpdatePromotionRuleDTO {
id: string
}
export interface FilterablePromotionRuleProps
extends BaseFilterable<FilterablePromotionRuleProps> {
id?: string[]
}

View File

@@ -1,5 +1,6 @@
import { BaseFilterable } from "../../dal"
import { CreateApplicationMethodDTO } from "./application-method"
import { CreatePromotionRuleDTO } from "./promotion-rule"
export type PromotionType = "standard" | "buyget"
@@ -12,6 +13,7 @@ export interface CreatePromotionDTO {
type: PromotionType
is_automatic?: boolean
application_method?: CreateApplicationMethodDTO
rules?: CreatePromotionRuleDTO[]
}
export interface UpdatePromotionDTO {

View File

@@ -0,0 +1,61 @@
import { isPresent } from "../is-present"
describe("isPresent", function () {
it("should return true or false for different types of data", function () {
const expectations = [
{
input: null,
output: false,
},
{
input: undefined,
output: false,
},
{
input: "Testing",
output: true,
},
{
input: "",
output: false,
},
{
input: {},
output: false,
},
{
input: { test: 1 },
output: true,
},
{
input: [],
output: false,
},
{
input: [{ test: 1 }],
output: true,
},
{
input: new Map([["test", "test"]]),
output: true,
},
{
input: new Map([]),
output: false,
},
{
input: new Set(["test"]),
output: true,
},
{
input: new Set([]),
output: false,
},
]
expectations.forEach((expectation) => {
expect(isPresent(expectation.input)).toEqual(expectation.output)
})
})
})

View File

@@ -8,14 +8,15 @@ export * from "./deep-equal-obj"
export * from "./errors"
export * from "./generate-entity-id"
export * from "./get-config-file"
export * from "./get-iso-string-from-date"
export * from "./group-by"
export * from "./handle-postgres-database-error"
export * from "./is-date"
export * from "./is-defined"
export * from "./is-email"
export * from "./is-object"
export * from "./is-present"
export * from "./is-string"
export * from "./get-iso-string-from-date"
export * from "./lower-case-first"
export * from "./map-object-to"
export * from "./medusa-container"

View File

@@ -0,0 +1,23 @@
import { isDefined } from "./is-defined"
import { isObject } from "./is-object"
import { isString } from "./is-string"
export function isPresent(value: any): boolean {
if (!isDefined(value) || value === null) {
return false
}
if (isString(value) || Array.isArray(value)) {
return value.length > 0
}
if (value instanceof Map || value instanceof Set) {
return value.size > 0
}
if (isObject(value)) {
return Object.keys(value).length > 0
}
return true
}

View File

@@ -18,3 +18,13 @@ export enum ApplicationMethodAllocation {
EACH = "each",
ACROSS = "across",
}
export enum PromotionRuleOperator {
GTE = "gte",
LTE = "lte",
GT = "gt",
LT = "lt",
EQ = "eq",
NE = "ne",
IN = "in",
}