feat(pricing): add price rule entity (#5050)

**What**
- add price-rule entity to pricing module

blocked by #4977 

Fixes CORE-1497

Co-authored-by: Riqwan Thamir <5105988+riqwan@users.noreply.github.com>
Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
Philip Korsholm
2023-09-26 14:59:55 +02:00
committed by GitHub
parent b3f1135fba
commit bc42b201ea
20 changed files with 1444 additions and 18 deletions

View File

@@ -0,0 +1,24 @@
import { CreatePriceRuleDTO } from "@medusajs/types"
export const defaultPriceRuleData = [
{
id: "price-rule-1",
price_set_id: "price-set-1",
rule_type_id: "rule-type-1",
value: "region_1",
price_list_id: "test",
price_set_money_amount: {
money_amount: { amount: 100, currency_code: "EUR" },
},
},
{
id: "price-rule-2",
price_set_id: "price-set-2",
rule_type_id: "rule-type-1",
value: "region_2",
price_list_id: "test",
price_set_money_amount: {
money_amount: { amount: 100, currency_code: "EUR" },
},
},
] as unknown as CreatePriceRuleDTO[]

View File

@@ -0,0 +1,85 @@
import { PriceRule, PriceSet, PriceSetMoneyAmount, RuleType } from "@models"
import { CreatePriceRuleDTO } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { createMoneyAmounts } from "../money-amount"
import { createRuleTypes } from "../rule-type"
import { defaultPriceRuleData } from "./data"
export async function createPriceRules(
manager: SqlEntityManager,
pricesRulesData: CreatePriceRuleDTO[] = defaultPriceRuleData
): Promise<PriceRule[]> {
const priceRules: PriceRule[] = []
for (let priceRuleData of pricesRulesData) {
const priceRuleDataClone = { ...priceRuleData }
if (priceRuleDataClone.price_set_id) {
priceRuleDataClone.price_set = manager.getReference(
PriceSet,
priceRuleDataClone.price_set_id
)
}
let dbRuleType: RuleType | undefined = await manager.findOne(RuleType, {
id: priceRuleDataClone.rule_type_id,
})
if (!dbRuleType) {
const [createdRuleType] = await createRuleTypes(manager, [
{
id: priceRuleDataClone.rule_type_id,
name: "rule 1",
rule_attribute: "region_id",
},
])
dbRuleType = createdRuleType
}
priceRuleDataClone.rule_type = manager.getReference(
RuleType,
dbRuleType!.id
)
const priceSetMoneyAmountId =
priceRuleDataClone.price_set_money_amount_id ||
priceRuleDataClone.price_set_money_amount?.id
let psma: PriceSetMoneyAmount = await manager.findOne(PriceSetMoneyAmount, {id: priceSetMoneyAmountId})
if (!psma) {
const [ma] = await createMoneyAmounts(manager, [
priceRuleDataClone.price_set_money_amount.money_amount ?? {
amount: 100,
currency_code: "EUR",
},
])
psma = manager.create(PriceSetMoneyAmount, {
price_set: manager.getReference(
PriceSet,
priceRuleDataClone.price_set.id
),
money_amount: ma.id,
title: "test",
})
await manager.persist(psma).flush()
}
priceRuleDataClone.price_set_money_amount = manager.getReference(
PriceSetMoneyAmount,
psma.id
)
const priceRule = manager.create(PriceRule, priceRuleDataClone)
priceRules.push(priceRule)
}
await manager.persistAndFlush(priceRules)
return priceRules
}

View File

@@ -1,20 +1,19 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { RuleType } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { defaultRuleTypesData } from "./data"
export async function createRuleTypes(
manager: SqlEntityManager,
ruletypesData: any[] = defaultRuleTypesData
): Promise<RuleType[]> {
const RuleTypes: RuleType[] = []
const ruleTypes: RuleType[] = []
for (let ruleTypeData of ruletypesData) {
const ruleType = manager.create(RuleType, ruleTypeData)
RuleTypes.push(ruleType)
await manager.persistAndFlush(ruleType)
ruleTypes.push(ruleType)
}
await manager.persistAndFlush(RuleTypes)
return RuleTypes
return ruleTypes
}

View File

@@ -0,0 +1,325 @@
import { PriceSet, PriceSetMoneyAmount } from "@models"
import { CreatePriceRuleDTO } from "@medusajs/types"
import { MikroOrmWrapper } from "../../../utils"
import { PriceRuleRepository } from "@repositories"
import { PriceRuleService } from "@services"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { createCurrencies } from "../../../__fixtures__/currency"
import { createMoneyAmounts } from "../../../__fixtures__/money-amount"
import { createPriceRules } from "../../../__fixtures__/price-rule"
import { createPriceSets } from "../../../__fixtures__/price-set"
import { createRuleTypes } from "../../../__fixtures__/rule-type"
jest.setTimeout(30000)
describe("PriceRule Service", () => {
let service: PriceRuleService
let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = await MikroOrmWrapper.forkManager()
testManager = await MikroOrmWrapper.forkManager()
const priceRuleRepository = new PriceRuleRepository({
manager: repositoryManager,
})
service = new PriceRuleService({
priceRuleRepository: priceRuleRepository,
})
await createCurrencies(testManager)
await createRuleTypes(testManager)
await createPriceSets(testManager)
await createPriceRules(testManager)
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("list", () => {
it("should list priceRules", async () => {
const priceRuleResult = await service.list()
const serialized = JSON.parse(JSON.stringify(priceRuleResult))
expect(serialized).toEqual([
expect.objectContaining({
id: "price-rule-1",
}),
expect.objectContaining({
id: "price-rule-2",
}),
])
})
it("should list priceRules by id", async () => {
const priceRuleResult = await service.list({
id: ["price-rule-1"],
})
expect(priceRuleResult).toEqual([
expect.objectContaining({
id: "price-rule-1",
}),
])
})
it("should list priceRules with relations and selects", async () => {
const priceRulesResult = await service.list(
{
id: ["price-rule-1"],
},
{
select: ["id", "price_set.id"],
relations: ["price_set"],
}
)
const serialized = JSON.parse(JSON.stringify(priceRulesResult))
expect(serialized).toEqual([
{
id: "price-rule-1",
price_set: {
id: "price-set-1",
},
},
])
})
describe("listAndCount", () => {
it("should return priceRules and count", async () => {
const [priceRulesResult, count] = await service.listAndCount()
expect(count).toEqual(2)
expect(priceRulesResult).toEqual([
expect.objectContaining({
id: "price-rule-1",
}),
expect.objectContaining({
id: "price-rule-2",
}),
])
})
it("should return priceRules and count when filtered", async () => {
const [priceRulesResult, count] = await service.listAndCount({
id: ["price-rule-1"],
})
expect(count).toEqual(1)
expect(priceRulesResult).toEqual([
expect.objectContaining({
id: "price-rule-1",
}),
])
})
it("should list priceRules with relations and selects", async () => {
const [priceRulesResult, count] = await service.listAndCount(
{
id: ["price-rule-1"],
},
{
select: ["id", "price_set.id"],
relations: ["price_set"],
}
)
const serialized = JSON.parse(JSON.stringify(priceRulesResult))
expect(count).toEqual(1)
expect(serialized).toEqual([
{
id: "price-rule-1",
price_set: {
id: "price-set-1",
},
},
])
})
it("should return priceRules and count when using skip and take", async () => {
const [priceRulesResult, count] = await service.listAndCount(
{},
{ skip: 1, take: 1 }
)
expect(count).toEqual(2)
expect(priceRulesResult).toEqual([
expect.objectContaining({
id: "price-rule-2",
}),
])
})
it("should return requested fields", async () => {
const [priceRulesResult, count] = await service.listAndCount(
{},
{
take: 1,
select: ["id"],
}
)
const serialized = JSON.parse(JSON.stringify(priceRulesResult))
expect(count).toEqual(2)
expect(serialized).toEqual([
{
id: "price-rule-1",
},
])
})
})
describe("retrieve", () => {
const id = "price-rule-1"
it("should return priceRule for the given id", async () => {
const priceRule = await service.retrieve(id)
expect(priceRule).toEqual(
expect.objectContaining({
id,
})
)
})
it("should throw an error when priceRule with id does not exist", async () => {
let error
try {
await service.retrieve("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual(
"PriceRule with id: does-not-exist was not found"
)
})
it("should throw an error when a id is not provided", async () => {
let error
try {
await service.retrieve(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual('"priceRuleId" must be defined')
})
it("should return priceRule based on config select param", async () => {
const priceRule = await service.retrieve(id, {
select: ["id"],
})
const serialized = JSON.parse(JSON.stringify(priceRule))
expect(serialized).toEqual({
id,
})
})
})
describe("delete", () => {
const id = "price-set-1"
it("should delete the priceRules given an id successfully", async () => {
await service.delete([id])
const priceRules = await service.list({
id: [id],
})
expect(priceRules).toHaveLength(0)
})
})
describe("update", () => {
const id = "price-set-1"
it("should throw an error when a id does not exist", async () => {
let error
try {
await service.update([
{
id: "does-not-exist",
},
])
} catch (e) {
error = e
}
expect(error.message).toEqual(
'PriceRule with id "does-not-exist" not found'
)
})
})
describe("create", () => {
it("should throw an error when a id does not exist", async () => {
let error
try {
await service.update([
{
random: "does-not-exist",
} as any,
])
} catch (e) {
error = e
}
expect(error.message).toEqual('PriceRule with id "undefined" not found')
})
it("should create a priceRule successfully", async () => {
const [ma] = await createMoneyAmounts(testManager, [
{
amount: 100,
currency_code: "EUR",
},
])
const psma: PriceSetMoneyAmount = testManager.create(PriceSetMoneyAmount, {
price_set: testManager.getReference(PriceSet, "price-set-1"),
money_amount: ma.id,
title: "test",
})
await testManager.persist(psma).flush()
await service.create([
{
id: "price-rule-new",
price_set_id: "price-set-1",
rule_type_id: "rule-type-1",
value: "region_1",
price_list_id: "test",
price_set_money_amount_id: psma.id,
} as unknown as CreatePriceRuleDTO,
])
const [pricerule] = await service.list({
id: ["price-rule-new"],
})
expect(pricerule).toEqual(
expect.objectContaining({
id: "price-rule-new",
} as unknown as CreatePriceRuleDTO)
)
})
})
})
})

View File

@@ -0,0 +1,322 @@
import { CreatePriceRuleDTO, IPricingModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { PriceSet } from "@models"
import { PriceSetMoneyAmount, initialize } from "../../../../src"
import { createCurrencies } from "../../../__fixtures__/currency"
import { createMoneyAmounts } from "../../../__fixtures__/money-amount"
import { createPriceSets } from "../../../__fixtures__/price-set"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
import { createRuleTypes } from "../../../__fixtures__/rule-type"
import { createPriceRules } from "../../../__fixtures__/price-rule"
jest.setTimeout(30000)
describe("PricingModule Service - PriceRule", () => {
let service: IPricingModuleService
let testManager: SqlEntityManager
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
testManager = MikroOrmWrapper.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PRICING_DB_SCHEMA,
},
})
await createCurrencies(testManager)
await createRuleTypes(testManager)
await createPriceSets(testManager)
await createPriceRules(testManager)
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("list", () => {
it("should list priceRules", async () => {
const PriceRulesResult = await service.listPriceRules()
const serialized = JSON.parse(JSON.stringify(PriceRulesResult))
expect(serialized).toEqual([
expect.objectContaining({
id: "price-rule-1",
}),
expect.objectContaining({
id: "price-rule-2",
}),
])
})
it("should list priceRules by id", async () => {
const priceRuleResult = await service.listPriceRules({
id: ["price-rule-1"],
})
expect(priceRuleResult).toEqual([
expect.objectContaining({
id: "price-rule-1",
}),
])
})
it("should list priceRules with relations and selects", async () => {
const priceRulesResult = await service.listPriceRules(
{
id: ["price-rule-1"],
},
{
select: ["id", "price_set.id"],
relations: ["price_set"],
}
)
const serialized = JSON.parse(JSON.stringify(priceRulesResult))
expect(serialized).toEqual([
{
id: "price-rule-1",
price_set: {
id: "price-set-1",
},
},
])
})
describe("listAndCount", () => {
it("should return priceRules and count", async () => {
const [priceRulesResult, count] = await service.listAndCountPriceRules()
expect(count).toEqual(2)
expect(priceRulesResult).toEqual([
expect.objectContaining({
id: "price-rule-1",
}),
expect.objectContaining({
id: "price-rule-2",
}),
])
})
it("should return priceRules and count when filtered", async () => {
const [priceRulesResult, count] = await service.listAndCountPriceRules({
id: ["price-rule-1"],
})
expect(count).toEqual(1)
expect(priceRulesResult).toEqual([
expect.objectContaining({
id: "price-rule-1",
}),
])
})
it("should list PriceRules with relations and selects", async () => {
const [PriceRulesResult, count] = await service.listAndCountPriceRules(
{
id: ["price-rule-1"],
},
{
select: ["id", "price_set.id"],
relations: ["price_set"],
}
)
const serialized = JSON.parse(JSON.stringify(PriceRulesResult))
expect(count).toEqual(1)
expect(serialized).toEqual([
{
id: "price-rule-1",
price_set: {
id: "price-set-1",
},
},
])
})
it("should return PriceRules and count when using skip and take", async () => {
const [PriceRulesResult, count] = await service.listAndCountPriceRules(
{},
{ skip: 1, take: 1 }
)
expect(count).toEqual(2)
expect(PriceRulesResult).toEqual([
expect.objectContaining({
id: "price-rule-2",
}),
])
})
it("should return requested fields", async () => {
const [PriceRulesResult, count] = await service.listAndCountPriceRules(
{},
{
take: 1,
select: ["id"],
}
)
const serialized = JSON.parse(JSON.stringify(PriceRulesResult))
expect(count).toEqual(2)
expect(serialized).toEqual([
{
id: "price-rule-1",
},
])
})
})
describe("retrieve", () => {
const id = "price-rule-1"
it("should return PriceRule for the given id", async () => {
const PriceRule = await service.retrievePriceRule(id)
expect(PriceRule).toEqual(
expect.objectContaining({
id,
})
)
})
it("should throw an error when PriceRule with id does not exist", async () => {
let error
try {
await service.retrievePriceRule("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual(
"PriceRule 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.retrievePriceRule(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual('"priceRuleId" must be defined')
})
it("should return PriceRule based on config select param", async () => {
const PriceRule = await service.retrievePriceRule(id, {
select: ["id"],
})
const serialized = JSON.parse(JSON.stringify(PriceRule))
expect(serialized).toEqual({
id,
})
})
})
describe("delete", () => {
const id = "price-set-1"
it("should delete the PriceRules given an id successfully", async () => {
await service.deletePriceRules([id])
const PriceRules = await service.listPriceRules({
id: [id],
})
expect(PriceRules).toHaveLength(0)
})
})
describe("update", () => {
const id = "price-set-1"
it("should throw an error when a id does not exist", async () => {
let error
try {
await service.updatePriceRules([
{
id: "does-not-exist",
},
])
} catch (e) {
error = e
}
expect(error.message).toEqual(
'PriceRule with id "does-not-exist" not found'
)
})
})
describe("create", () => {
it("should throw an error when a id does not exist", async () => {
let error
try {
await service.updatePriceRules([
{
random: "does-not-exist",
} as any,
])
} catch (e) {
error = e
}
expect(error.message).toEqual('PriceRule with id "undefined" not found')
})
it("should create a PriceRule successfully", async () => {
const [ma] = await createMoneyAmounts(testManager, [
{
amount: 100,
currency_code: "EUR",
},
])
const psma: PriceSetMoneyAmount = testManager.create(PriceSetMoneyAmount, {
price_set: testManager.getReference(PriceSet, "price-set-1"),
money_amount: ma.id,
title: "test",
})
await testManager.persist(psma).flush()
await service.createPriceRules([
{
id: "price-rule-new",
price_set_id: "price-set-1",
rule_type_id: "rule-type-1",
value: "region_1",
price_list_id: "test",
price_set_money_amount_id: psma.id,
} as unknown as CreatePriceRuleDTO,
])
const [pricerule] = await service.listPriceRules({
id: ["price-rule-new"],
})
expect(pricerule).toEqual(
expect.objectContaining({
id: "price-rule-new",
} as unknown as CreatePriceRuleDTO)
)
})
})
})
})

View File

@@ -1,3 +1,6 @@
import { DB_URL, MikroOrmWrapper } from "../../../utils"
import { Currency } from "@models"
import { IPricingModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"

View File

@@ -1,10 +1,10 @@
import { ModulesSdkTypes } from "@medusajs/types"
import * as defaultRepositories from "@repositories"
import * as defaultServices from "@services"
import { LoaderOptions } from "@medusajs/modules-sdk"
import { loadCustomRepositories } from "@medusajs/utils"
import { ModulesSdkTypes } from "@medusajs/types"
import { asClass } from "awilix"
import { loadCustomRepositories } from "@medusajs/utils"
export default async ({
container,
@@ -22,6 +22,7 @@ export default async ({
moneyAmountService: asClass(defaultServices.MoneyAmountService).singleton(),
priceSetService: asClass(defaultServices.PriceSetService).singleton(),
ruleTypeService: asClass(defaultServices.RuleTypeService).singleton(),
priceRuleService: asClass(defaultServices.PriceRuleService).singleton(),
})
if (customRepositories) {
@@ -50,5 +51,8 @@ function loadDefaultRepositories({ container }) {
ruleTypeRepository: asClass(
defaultRepositories.RuleTypeRepository
).singleton(),
priceRuleRepository: asClass(
defaultRepositories.PriceRuleRepository
).singleton(),
})
}

View File

@@ -337,6 +337,145 @@
],
"checks": [],
"foreignKeys": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"price_set_id": {
"name": "price_set_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"rule_type_id": {
"name": "rule_type_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"is_dynamic": {
"name": "is_dynamic",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "false",
"mappedType": "boolean"
},
"value": {
"name": "value",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"priority": {
"name": "priority",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "integer"
},
"price_set_money_amount_id": {
"name": "price_set_money_amount_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"price_list_id": {
"name": "price_list_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
}
},
"name": "price_rule",
"schema": "public",
"indexes": [
{
"columnNames": [
"rule_type_id"
],
"composite": false,
"keyName": "price_rule_rule_type_id_unique",
"primary": false,
"unique": true
},
{
"keyName": "price_rule_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"price_rule_price_set_id_foreign": {
"constraintName": "price_rule_price_set_id_foreign",
"columnNames": [
"price_set_id"
],
"localTableName": "public.price_rule",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.price_set",
"updateRule": "cascade"
},
"price_rule_rule_type_id_foreign": {
"constraintName": "price_rule_rule_type_id_foreign",
"columnNames": [
"rule_type_id"
],
"localTableName": "public.price_rule",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.rule_type",
"updateRule": "cascade"
},
"price_rule_price_set_money_amount_id_foreign": {
"constraintName": "price_rule_price_set_money_amount_id_foreign",
"columnNames": [
"price_set_money_amount_id"
],
"localTableName": "public.price_rule",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.price_set_money_amount",
"updateRule": "cascade"
}
}
}
]
}

View File

@@ -0,0 +1,23 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20230913055746 extends Migration {
async up(): Promise<void> {
this.addSql('create table "price_rule" ("id" text not null, "price_set_id" text not null, "rule_type_id" text not null, "is_dynamic" boolean not null default false, "value" text not null, "priority" integer not null default 0, "price_set_money_amount_id" text not null, "price_list_id" text not null, constraint "price_rule_pkey" primary key ("id"));');
this.addSql('alter table "price_rule" add constraint "price_rule_price_set_id_foreign" foreign key ("price_set_id") references "price_set" ("id") on update cascade;');
this.addSql('alter table "price_rule" add constraint "price_rule_rule_type_id_foreign" foreign key ("rule_type_id") references "rule_type" ("id") on update cascade;');
this.addSql('alter table "price_rule" add constraint "price_rule_price_set_money_amount_id_foreign" foreign key ("price_set_money_amount_id") references "price_set_money_amount" ("id") on update cascade;');
}
async down(): Promise<void> {
this.addSql('alter table "price_rule" drop constraint "price_rule_price_set_id_foreign";');
this.addSql('alter table "price_rule" drop constraint "price_rule_price_set_money_amount_id_foreign";');
this.addSql('alter table "price_rule" drop constraint "price_rule_rule_type_id_foreign";');
this.addSql('drop table if exists "price_rule" cascade;');
}g
}

View File

@@ -3,3 +3,4 @@ export { default as MoneyAmount } from "./money-amount"
export { default as PriceSet } from "./price-set"
export { default as PriceSetMoneyAmount } from "./price-set-money-amount"
export { default as RuleType } from "./rule-type"
export { default as PriceRule } from "./price-rule"

View File

@@ -0,0 +1,69 @@
import {
BeforeCreate,
Collection,
Entity,
ManyToMany,
ManyToOne,
OneToMany,
OneToOne,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import MoneyAmount from "./money-amount"
import PriceSet from "./price-set"
import PriceSetMoneyAmount from "./price-set-money-amount"
import RuleType from "./rule-type"
import { generateEntityId } from "@medusajs/utils"
type OptionalFields = "id" | "is_dynamic" | "priority"
type OptionalRelations = "price_set" | "rule_type" | "price_set_money_amount"
@Entity()
export default class PriceRule {
[OptionalProps]: OptionalFields | OptionalRelations
@PrimaryKey({ columnType: "text" })
id!: string
@ManyToOne({
entity: () => PriceSet,
fieldName: "price_set_id",
name: "price_rule_price_set_id_unique",
})
price_set: PriceSet
@ManyToOne({
entity: () => RuleType,
fieldName: "rule_type_id",
name: "price_rule_rule_type_id_unique",
})
rule_type: RuleType
@Property({ columnType: "boolean", default: false })
is_dynamic: boolean
@Property({ columnType: "text" })
value: string
@Property({ columnType: "integer", default: 0 })
priority: number
@ManyToOne({
entity: () => PriceSetMoneyAmount,
fieldName: "price_set_money_amount_id",
name: "price_set_money_amount_id_unique",
})
price_set_money_amount: PriceSetMoneyAmount
@Property({ columnType: "text" })
price_list_id!: string
// TODO: Add price list
@BeforeCreate()
beforeCreate() {
this.id = generateEntityId(this.id, "pset")
}
}

View File

@@ -3,3 +3,4 @@ export { CurrencyRepository } from "./currency"
export { MoneyAmountRepository } from "./money-amount"
export { PriceSetRepository } from "./price-set"
export { RuleTypeRepository } from "./rule-type"
export { PriceRuleRepository } from "./price-rule"

View File

@@ -0,0 +1,144 @@
import {
Context,
CreatePriceRuleDTO,
DAL,
UpdatePriceRuleDTO,
} from "@medusajs/types"
import { DALUtils, MedusaError } from "@medusajs/utils"
import {
LoadStrategy,
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
} from "@mikro-orm/core"
import { PriceRule, PriceSet, PriceSetMoneyAmount, RuleType } from "@models"
import { SqlEntityManager } from "@mikro-orm/postgresql"
export class PriceRuleRepository 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<PriceRule> = { where: {} },
context: Context = {}
): Promise<PriceRule[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN,
})
return await manager.find(
PriceRule,
findOptions_.where as MikroFilterQuery<PriceRule>,
findOptions_.options as MikroOptions<PriceRule>
)
}
async findAndCount(
findOptions: DAL.FindOptions<PriceRule> = { where: {} },
context: Context = {}
): Promise<[PriceRule[], number]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN,
})
return await manager.findAndCount(
PriceRule,
findOptions_.where as MikroFilterQuery<PriceRule>,
findOptions_.options as MikroOptions<PriceRule>
)
}
async delete(ids: string[], context: Context = {}): Promise<void> {
const manager = this.getActiveManager<SqlEntityManager>(context)
await manager.nativeDelete(PriceRule, { id: { $in: ids } }, {})
}
async create(
data: CreatePriceRuleDTO[],
context: Context = {}
): Promise<PriceRule[]> {
const manager: SqlEntityManager =
this.getActiveManager<SqlEntityManager>(context)
const toCreate = await Promise.all(data.map(async (ruleData) => {
const ruleDataClone = { ...ruleData } as CreatePriceRuleDTO & {
rule_type: string
price_set: string
price_set_money_amount: string
}
ruleDataClone.rule_type = ruleData.rule_type_id
ruleDataClone.price_set = ruleData.price_set_id
ruleDataClone.price_set_money_amount = ruleData.price_set_money_amount_id
return ruleDataClone
}))
const priceRules = toCreate.map((ruleData) => {
return manager.create(PriceRule, ruleData as CreatePriceRuleDTO)
})
manager.persist(priceRules)
return priceRules
}
async update(
data: UpdatePriceRuleDTO[],
context: Context = {}
): Promise<PriceRule[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const priceRuleIds = data.map((priceRuleData) => priceRuleData.id)
const existingPriceRules = await this.find(
{
where: {
id: {
$in: priceRuleIds,
},
},
},
context
)
const existingPriceRulesMap = new Map(
existingPriceRules.map<[string, PriceRule]>((priceRule) => [
priceRule.id,
priceRule,
])
)
const priceRules = data.map((priceRuleData) => {
const existingPriceRule = existingPriceRulesMap.get(priceRuleData.id)
if (!existingPriceRule) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`PriceRule with id "${priceRuleData.id}" not found`
)
}
return manager.assign(existingPriceRule, priceRuleData)
})
manager.persist(priceRules)
return priceRules
}
}

View File

@@ -2,4 +2,5 @@ export { default as CurrencyService } from "./currency"
export { default as MoneyAmountService } from "./money-amount"
export { default as PriceSetService } from "./price-set"
export { default as PricingModuleService } from "./pricing-module"
export { default as RuleTypeService } from "./rule-type"
export { default as RuleTypeService } from "./rule-type"
export { default as PriceRuleService } from "./price-rule"

View File

@@ -0,0 +1,97 @@
import { Context, DAL, FindConfig, PricingTypes } from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
doNotForceTransaction,
retrieveEntity,
shouldForceTransaction,
} from "@medusajs/utils"
import { PriceRule } from "@models"
import { PriceRuleRepository } from "@repositories"
type InjectedDependencies = {
priceRuleRepository: DAL.RepositoryService
}
export default class PriceRuleService<TEntity extends PriceRule = PriceRule> {
protected readonly priceRuleRepository_: DAL.RepositoryService
constructor({ priceRuleRepository }: InjectedDependencies) {
this.priceRuleRepository_ = priceRuleRepository
}
@InjectManager("priceRuleRepository_")
async retrieve(
priceRuleId: string,
config: FindConfig<PricingTypes.PriceRuleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<PriceRule, PricingTypes.PriceRuleDTO>({
id: priceRuleId,
entityName: PriceRule.name,
repository: this.priceRuleRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("priceRuleRepository_")
async list(
filters: PricingTypes.FilterablePriceRuleProps = {},
config: FindConfig<PricingTypes.PriceRuleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryConfig = ModulesSdkUtils.buildQuery<PriceRule>(filters, config)
return (await this.priceRuleRepository_.find(
queryConfig,
sharedContext
)) as TEntity[]
}
@InjectManager("priceRuleRepository_")
async listAndCount(
filters: PricingTypes.FilterablePriceRuleProps = {},
config: FindConfig<PricingTypes.PriceRuleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const queryConfig = ModulesSdkUtils.buildQuery<PriceRule>(filters, config)
return (await this.priceRuleRepository_.findAndCount(
queryConfig,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager(shouldForceTransaction, "priceRuleRepository_")
async create(
data: PricingTypes.CreatePriceRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.priceRuleRepository_ as PriceRuleRepository).create(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(shouldForceTransaction, "priceRuleRepository_")
async update(
data: PricingTypes.UpdatePriceRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.priceRuleRepository_ as PriceRuleRepository).update(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(doNotForceTransaction, "priceRuleRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.priceRuleRepository_.delete(ids, sharedContext)
}
}

View File

@@ -8,13 +8,14 @@ import {
PricingFilters,
PricingTypes,
} from "@medusajs/types"
import { Currency, MoneyAmount, PriceSet, RuleType } from "@models"
import {
CurrencyService,
MoneyAmountService,
PriceSetService,
RuleTypeService,
PriceRuleService
} from "@services"
import { Currency, MoneyAmount, PriceRule, PriceSet, RuleType } from "@models"
import {
InjectManager,
@@ -29,15 +30,17 @@ type InjectedDependencies = {
baseRepository: DAL.RepositoryService
currencyService: CurrencyService<any>
moneyAmountService: MoneyAmountService<any>
ruleTypeService: RuleTypeService<any>
priceSetService: PriceSetService<any>
ruleTypeService: RuleTypeService<any>
priceRuleService: PriceRuleService<any>
}
export default class PricingModuleService<
TPriceSet extends PriceSet = PriceSet,
TMoneyAmount extends MoneyAmount = MoneyAmount,
TCurrency extends Currency = Currency,
TRuleType extends RuleType = RuleType
TRuleType extends RuleType = RuleType,
TPriceRule extends PriceRule = PriceRule,
> implements PricingTypes.IPricingModuleService
{
protected baseRepository_: DAL.RepositoryService
@@ -45,6 +48,7 @@ export default class PricingModuleService<
protected readonly moneyAmountService_: MoneyAmountService<TMoneyAmount>
protected readonly ruleTypeService_: RuleTypeService<TRuleType>
protected readonly priceSetService_: PriceSetService<TPriceSet>
protected readonly priceRuleService_: PriceRuleService<TPriceRule>
constructor(
{
@@ -53,6 +57,7 @@ export default class PricingModuleService<
currencyService,
ruleTypeService,
priceSetService,
priceRuleService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
@@ -61,6 +66,7 @@ export default class PricingModuleService<
this.moneyAmountService_ = moneyAmountService
this.ruleTypeService_ = ruleTypeService
this.priceSetService_ = priceSetService
this.priceRuleService_ = priceRuleService
}
__joinerConfig(): ModuleJoinerConfig {
@@ -520,4 +526,102 @@ export default class PricingModuleService<
): Promise<void> {
await this.ruleTypeService_.delete(ruleTypes, sharedContext)
}
@InjectManager("baseRepository_")
async retrievePriceRule(
id: string,
config: FindConfig<PricingTypes.PriceRuleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceRuleDTO> {
const priceRule = await this.priceRuleService_.retrieve(
id,
config,
sharedContext
)
return this.baseRepository_.serialize<PricingTypes.PriceRuleDTO>(priceRule, {
populate: true,
})
}
@InjectManager("baseRepository_")
async listPriceRules(
filters: PricingTypes.FilterablePriceRuleProps = {},
config: FindConfig<PricingTypes.PriceRuleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceRuleDTO[]> {
const priceRules = await this.priceRuleService_.list(
filters,
config,
sharedContext
)
return this.baseRepository_.serialize<PricingTypes.PriceRuleDTO[]>(
priceRules,
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listAndCountPriceRules(
filters: PricingTypes.FilterablePriceRuleProps = {},
config: FindConfig<PricingTypes.PriceRuleDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[PricingTypes.PriceRuleDTO[], number]> {
const [priceRules, count] = await this.priceRuleService_.listAndCount(
filters,
config,
sharedContext
)
return [
await this.baseRepository_.serialize<PricingTypes.PriceRuleDTO[]>(
priceRules,
{
populate: true,
}
),
count,
]
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async createPriceRules(
data: PricingTypes.CreatePriceRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceRuleDTO[]> {
const priceRules = await this.priceRuleService_.create(data, sharedContext)
return this.baseRepository_.serialize<PricingTypes.PriceRuleDTO[]>(
priceRules,
{
populate: true,
}
)
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async updatePriceRules(
data: PricingTypes.UpdatePriceRuleDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceRuleDTO[]> {
const priceRules = await this.priceRuleService_.update(data, sharedContext)
return this.baseRepository_.serialize<PricingTypes.PriceRuleDTO[]>(
priceRules,
{
populate: true,
}
)
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async deletePriceRules(
priceRuleIds: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.priceRuleService_.delete(priceRuleIds, sharedContext)
}
}

View File

@@ -2,3 +2,4 @@ export * from "./currency"
export * from "./money-amount"
export * from "./price-set"
export * from "./rule-type"
export * from './price-rule'

View File

@@ -0,0 +1,44 @@
import { BaseFilterable } from "../../dal"
import { PriceSetDTO } from "./price-set"
import { RuleTypeDTO } from "./rule-type"
export interface PriceRuleDTO {
id: string
price_set_id: string
price_set: PriceSetDTO
rule_type_id: string
rule_type: RuleTypeDTO
is_dynamic: boolean
value: string
priority: number
price_set_money_amount_id: string
price_list_id: string
}
export interface CreatePriceRuleDTO {
id: string
price_set_id: string
rule_type_id: string
is_dynamic?: boolean
value: string
priority?: number
price_set_money_amount_id: string
price_list_id: string
}
export interface UpdatePriceRuleDTO {
id: string
price_set_id?: string
rule_type_id?: string
is_dynamic?: boolean
value?: string
priority?: number
price_set_money_amount_id?: string
price_list_id?: string
}
export interface FilterablePriceRuleProps
extends BaseFilterable<FilterablePriceRuleProps> {
id?: string[]
name?: string[]
}

View File

@@ -1,2 +1,2 @@
export * from "./common"
export * from "./service"
export * from "./service"

View File

@@ -1,28 +1,33 @@
import { FindConfig } from "../common"
import { ModuleJoinerConfig } from "../modules-sdk"
import { Context } from "../shared-context"
import {
CalculatedPriceSetDTO,
CreateCurrencyDTO,
CreateMoneyAmountDTO,
CreatePriceRuleDTO,
CreatePriceSetDTO,
CreateRuleTypeDTO,
CurrencyDTO,
FilterableCurrencyProps,
FilterableMoneyAmountProps,
FilterablePriceRuleProps,
FilterablePriceSetProps,
FilterableRuleTypeProps,
MoneyAmountDTO,
PriceRuleDTO,
PriceSetDTO,
PricingContext,
PricingFilters,
RuleTypeDTO,
UpdateCurrencyDTO,
UpdateMoneyAmountDTO,
UpdatePriceRuleDTO,
UpdatePriceSetDTO,
UpdateRuleTypeDTO,
} from "./common"
import { Context } from "../shared-context"
import { FindConfig } from "../common"
import { ModuleJoinerConfig } from "../modules-sdk"
export interface IPricingModuleService {
__joinerConfig(): ModuleJoinerConfig
@@ -124,7 +129,6 @@ export interface IPricingModuleService {
currencyCodes: string[],
sharedContext?: Context
): Promise<void>
retrieveRuleType(
code: string,
config?: FindConfig<RuleTypeDTO>,
@@ -153,5 +157,41 @@ export interface IPricingModuleService {
sharedContext?: Context
): Promise<RuleTypeDTO[]>
deleteRuleTypes(ruleTypes: string[], sharedContext?: Context): Promise<void>
deleteRuleTypes(
ruleTypeIds: string[],
sharedContext?: Context
): Promise<void>
retrievePriceRule(
id: string,
config?: FindConfig<PriceRuleDTO>,
sharedContext?: Context
): Promise<PriceRuleDTO>
listPriceRules(
filters?: FilterablePriceRuleProps,
config?: FindConfig<PriceRuleDTO>,
sharedContext?: Context
): Promise<PriceRuleDTO[]>
listAndCountPriceRules(
filters?: FilterablePriceRuleProps,
config?: FindConfig<PriceRuleDTO>,
sharedContext?: Context
): Promise<[PriceRuleDTO[], number]>
createPriceRules(
data: CreatePriceRuleDTO[],
sharedContext?: Context
): Promise<PriceRuleDTO[]>
updatePriceRules(
data: UpdatePriceRuleDTO[],
sharedContext?: Context
): Promise<PriceRuleDTO[]>
deletePriceRules(
priceRuleIds: string[],
sharedContext?: Context
): Promise<void>
}