feat(utils,types): campaigns and campaign budgets + services CRUD (#6063)

* chore: added item/shipping adjustments for order/items/shipping_methods

* chore: add validation for order type and target rules

* chore: add comment for applied promotions

* chore: add shipping method and item adjustments

* chore: include applied promotions to items/shipping_method for each case

* chore: handle case for items across and order to consider existing applications

* chore: handle case for applied promo values to shipping => across

* chore: added changeset

* chore: update return of function

* chore: campaigns and campaign budgets + services CRUD

* Apply suggestions from code review

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>

* chore: minor refactor

* chore: added single/bulk interfaces

* Apply suggestions from code review

Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com>

* chore: use DAL date entity

* chore: align nullable

* Update packages/promotion/src/models/promotion-rule.ts

* chore: fix types

* chore: review changes

* Update packages/promotion/src/utils/compute-actions/shipping-methods.ts

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com>
This commit is contained in:
Riqwan Thamir
2024-01-12 14:14:34 +01:00
committed by GitHub
parent b782d3bcb7
commit fade8ea7bf
29 changed files with 1181 additions and 28 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat(utils,types): campaigns and campaign budgets + services CRUD

View File

@@ -0,0 +1,32 @@
import { CampaignBudgetType } from "@medusajs/utils"
export const defaultCampaignsData = [
{
id: "campaign-id-1",
name: "campaign 1",
description: "test description",
currency: "USD",
campaign_identifier: "test-1",
starts_at: new Date("01/01/2023"),
ends_at: new Date("01/01/2024"),
budget: {
type: CampaignBudgetType.SPEND,
limit: 1000,
used: 0,
},
},
{
id: "campaign-id-2",
name: "campaign 1",
description: "test description",
currency: "USD",
campaign_identifier: "test-2",
starts_at: new Date("01/01/2023"),
ends_at: new Date("01/01/2024"),
budget: {
type: CampaignBudgetType.USAGE,
limit: 1000,
used: 0,
},
},
]

View File

@@ -0,0 +1,23 @@
import { CreateCampaignDTO } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Campaign } from "@models"
import { defaultCampaignsData } from "./data"
export * from "./data"
export async function createCampaigns(
manager: SqlEntityManager,
campaignsData: CreateCampaignDTO[] = defaultCampaignsData
): Promise<Campaign[]> {
const campaigns: Campaign[] = []
for (let campaignData of campaignsData) {
let campaign = manager.create(Campaign, campaignData)
manager.persist(campaign)
await manager.flush()
}
return campaigns
}

View File

@@ -0,0 +1,241 @@
import { IPromotionModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { initialize } from "../../../../src"
import { createCampaigns } from "../../../__fixtures__/campaigns"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
jest.setTimeout(30000)
describe("Promotion Module Service: Campaigns", () => {
let service: IPromotionModuleService
let repositoryManager: SqlEntityManager
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = MikroOrmWrapper.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PROMOTION_DB_SCHEMA,
},
})
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("createCampaigns", () => {
it("should throw an error when required params are not passed", async () => {
const error = await service
.createCampaigns([
{
name: "test",
} as any,
])
.catch((e) => e)
expect(error.message).toContain(
"Value for Campaign.campaign_identifier is required, 'undefined' found"
)
})
it("should create a basic campaign successfully", async () => {
const startsAt = new Date("01/01/2024")
const endsAt = new Date("01/01/2025")
const [createdCampaign] = await service.createCampaigns([
{
name: "test",
campaign_identifier: "test",
starts_at: startsAt,
ends_at: endsAt,
},
])
const campaign = await service.retrieveCampaign(createdCampaign.id)
expect(campaign).toEqual(
expect.objectContaining({
name: "test",
campaign_identifier: "test",
starts_at: startsAt,
ends_at: endsAt,
})
)
})
it("should create a campaign with campaign budget successfully", async () => {
const startsAt = new Date("01/01/2024")
const endsAt = new Date("01/01/2025")
const [createdPromotion] = await service.createCampaigns([
{
name: "test",
campaign_identifier: "test",
starts_at: startsAt,
ends_at: endsAt,
budget: {
limit: 1000,
type: "usage",
used: 10,
},
},
])
const campaign = await service.retrieveCampaign(createdPromotion.id, {
relations: ["budget"],
})
expect(campaign).toEqual(
expect.objectContaining({
name: "test",
campaign_identifier: "test",
starts_at: startsAt,
ends_at: endsAt,
budget: expect.objectContaining({
limit: 1000,
type: "usage",
used: 10,
}),
})
)
})
})
describe("updateCampaigns", () => {
it("should throw an error when required params are not passed", async () => {
const error = await service
.updateCampaigns([
{
name: "test",
} as any,
])
.catch((e) => e)
expect(error.message).toContain('Campaign with id "" not found')
})
it("should update the attributes of a campaign successfully", async () => {
await createCampaigns(repositoryManager)
const [updatedCampaign] = await service.updateCampaigns([
{
id: "campaign-id-1",
description: "test description 1",
currency: "EUR",
campaign_identifier: "new",
starts_at: new Date("01/01/2024"),
ends_at: new Date("01/01/2025"),
},
])
expect(updatedCampaign).toEqual(
expect.objectContaining({
description: "test description 1",
currency: "EUR",
campaign_identifier: "new",
starts_at: new Date("01/01/2024"),
ends_at: new Date("01/01/2025"),
})
)
})
it("should update the attributes of a campaign budget successfully", async () => {
await createCampaigns(repositoryManager)
const [updatedCampaign] = await service.updateCampaigns([
{
id: "campaign-id-1",
budget: {
limit: 100,
used: 100,
},
},
])
expect(updatedCampaign).toEqual(
expect.objectContaining({
budget: expect.objectContaining({
limit: 100,
used: 100,
}),
})
)
})
})
describe("retrieveCampaign", () => {
beforeEach(async () => {
await createCampaigns(repositoryManager)
})
const id = "campaign-id-1"
it("should return campaign for the given id", async () => {
const campaign = await service.retrieveCampaign(id)
expect(campaign).toEqual(
expect.objectContaining({
id,
})
)
})
it("should throw an error when campaign with id does not exist", async () => {
let error
try {
await service.retrieveCampaign("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual(
"Campaign 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.retrieveCampaign(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual('"campaignId" must be defined')
})
it("should return campaign based on config select param", async () => {
const campaign = await service.retrieveCampaign(id, {
select: ["id"],
})
const serialized = JSON.parse(JSON.stringify(campaign))
expect(serialized).toEqual({
id,
})
})
})
describe("deleteCampaigns", () => {
beforeEach(async () => {
await createCampaigns(repositoryManager)
})
const id = "campaign-id-1"
it("should delete the campaigns given an id successfully", async () => {
await service.deleteCampaigns([id])
const campaigns = await service.list({
id: [id],
})
expect(campaigns).toHaveLength(0)
})
})
})

View File

@@ -3,6 +3,8 @@ module.exports = {
"^@models": "<rootDir>/src/models",
"^@services": "<rootDir>/src/services",
"^@repositories": "<rootDir>/src/repositories",
"^@utils": "<rootDir>/src/utils",
"^@types": "<rootDir>/src/types",
},
transform: {
"^.+\\.[jt]s?$": [

View File

@@ -28,6 +28,10 @@ export default async ({
applicationMethodService: asClass(
defaultServices.ApplicationMethodService
).singleton(),
campaignService: asClass(defaultServices.CampaignService).singleton(),
campaignBudgetService: asClass(
defaultServices.CampaignBudgetService
).singleton(),
})
if (customRepositories) {
@@ -56,5 +60,11 @@ function loadDefaultRepositories({ container }) {
promotionRuleValueRepository: asClass(
defaultRepositories.PromotionRuleValueRepository
).singleton(),
campaignRepository: asClass(
defaultRepositories.CampaignRepository
).singleton(),
campaignBudgetRepository: asClass(
defaultRepositories.CampaignBudgetRepository
).singleton(),
})
}

View File

@@ -2,6 +2,7 @@ import {
ApplicationMethodAllocationValues,
ApplicationMethodTargetTypeValues,
ApplicationMethodTypeValues,
DAL,
} from "@medusajs/types"
import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import {
@@ -24,9 +25,8 @@ type OptionalFields =
| "value"
| "max_quantity"
| "allocation"
| "created_at"
| "updated_at"
| "deleted_at"
| DAL.EntityDateColumns
@Entity()
export default class ApplicationMethod {

View File

@@ -0,0 +1,81 @@
import { CampaignBudgetTypeValues, DAL } from "@medusajs/types"
import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Entity,
Enum,
Index,
OnInit,
OneToOne,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Campaign from "./campaign"
type OptionalFields =
| "description"
| "limit"
| "used"
| "deleted_at"
| DAL.EntityDateColumns
@Entity()
export default class CampaignBudget {
[OptionalProps]?: OptionalFields
@PrimaryKey({ columnType: "text" })
id!: string
@Index({ name: "IDX_campaign_budget_type" })
@Enum(() => PromotionUtils.CampaignBudgetType)
type: CampaignBudgetTypeValues
@OneToOne({
entity: () => Campaign,
})
campaign?: Campaign
@Property({
columnType: "numeric",
nullable: true,
serializer: Number,
default: null,
})
limit: number | null
@Property({
columnType: "numeric",
serializer: Number,
default: 0,
})
used: number
@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, "probudg")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "probudg")
}
}

View File

@@ -0,0 +1,103 @@
import { DAL } from "@medusajs/types"
import { generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Collection,
Entity,
OnInit,
OneToMany,
OneToOne,
OptionalProps,
PrimaryKey,
Property,
Unique,
} from "@mikro-orm/core"
import CampaignBudget from "./campaign-budget"
import Promotion from "./promotion"
type OptionalFields =
| "description"
| "currency"
| "starts_at"
| "ends_at"
| "deleted_at"
| DAL.EntityDateColumns
type OptionalRelations = "budget"
@Entity()
export default class Campaign {
[OptionalProps]?: OptionalFields | OptionalRelations
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
name: string
@Property({ columnType: "text", nullable: true })
description: string | null
@Property({ columnType: "text", nullable: true })
currency: string | null
@Property({ columnType: "text" })
@Unique({
name: "IDX_campaign_identifier_unique",
properties: ["campaign_identifier"],
})
campaign_identifier: string
@Property({
columnType: "timestamptz",
nullable: true,
})
starts_at: Date | null
@Property({
columnType: "timestamptz",
nullable: true,
})
ends_at: Date | null
@OneToOne({
entity: () => CampaignBudget,
mappedBy: (cb) => cb.campaign,
cascade: ["soft-remove"] as any,
nullable: true,
})
budget: CampaignBudget | null
@OneToMany(() => Promotion, (promotion) => promotion.campaign, {
orphanRemoval: true,
})
promotions = new Collection<Promotion>(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, "procamp")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "procamp")
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { PromotionRuleOperatorValues } from "@medusajs/types"
import { DAL, PromotionRuleOperatorValues } from "@medusajs/types"
import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
@@ -18,7 +18,7 @@ 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 OptionalFields = "description" | "deleted_at" | DAL.EntityDateColumns
type OptionalRelations = "values" | "promotions"
@Entity()

View File

@@ -1,4 +1,4 @@
import { PromotionType } from "@medusajs/types"
import { DAL, PromotionType } from "@medusajs/types"
import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
@@ -7,6 +7,7 @@ import {
Enum,
Index,
ManyToMany,
ManyToOne,
OnInit,
OneToOne,
OptionalProps,
@@ -15,14 +16,12 @@ import {
Unique,
} from "@mikro-orm/core"
import ApplicationMethod from "./application-method"
import Campaign from "./campaign"
import PromotionRule from "./promotion-rule"
type OptionalFields =
| "is_automatic"
| "created_at"
| "updated_at"
| "deleted_at"
type OptionalRelations = "application_method"
type OptionalFields = "is_automatic" | "deleted_at" | DAL.EntityDateColumns
type OptionalRelations = "application_method" | "campaign"
@Entity()
export default class Promotion {
[OptionalProps]?: OptionalFields | OptionalRelations
@@ -38,6 +37,13 @@ export default class Promotion {
})
code: string
@ManyToOne(() => Campaign, {
joinColumn: "campaign",
fieldName: "campaign_id",
nullable: true,
})
campaign: Campaign
@Property({ columnType: "boolean", default: false })
is_automatic?: boolean = false

View File

@@ -0,0 +1,11 @@
import { DALUtils } from "@medusajs/utils"
import { CampaignBudget } from "@models"
import { CreateCampaignBudgetDTO, UpdateCampaignBudgetDTO } from "@types"
export class CampaignBudgetRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
CampaignBudget,
{
create: CreateCampaignBudgetDTO
update: UpdateCampaignBudgetDTO
}
>(CampaignBudget) {}

View File

@@ -0,0 +1,11 @@
import { DALUtils } from "@medusajs/utils"
import { Campaign } from "@models"
import { CreateCampaignDTO, UpdateCampaignDTO } from "@types"
export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
Campaign,
{
create: CreateCampaignDTO
update: UpdateCampaignDTO
}
>(Campaign) {}

View File

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

View File

@@ -0,0 +1,105 @@
import { Context, DAL, FindConfig, PromotionTypes } from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { CampaignBudget } from "@models"
import { CampaignBudgetRepository } from "@repositories"
import { CreateCampaignBudgetDTO, UpdateCampaignBudgetDTO } from "../types"
type InjectedDependencies = {
campaignBudgetRepository: DAL.RepositoryService
}
export default class CampaignBudgetService<
TEntity extends CampaignBudget = CampaignBudget
> {
protected readonly campaignBudgetRepository_: DAL.RepositoryService
constructor({ campaignBudgetRepository }: InjectedDependencies) {
this.campaignBudgetRepository_ = campaignBudgetRepository
}
@InjectManager("campaignBudgetRepository_")
async retrieve(
campaignBudgetId: string,
config: FindConfig<PromotionTypes.CampaignBudgetDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<
CampaignBudget,
PromotionTypes.CampaignBudgetDTO
>({
id: campaignBudgetId,
entityName: CampaignBudget.name,
repository: this.campaignBudgetRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("campaignBudgetRepository_")
async list(
filters: PromotionTypes.FilterableCampaignBudgetProps = {},
config: FindConfig<PromotionTypes.CampaignBudgetDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryOptions = ModulesSdkUtils.buildQuery<CampaignBudget>(
filters,
config
)
return (await this.campaignBudgetRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
}
@InjectManager("campaignBudgetRepository_")
async listAndCount(
filters: PromotionTypes.FilterableCampaignBudgetProps = {},
config: FindConfig<PromotionTypes.CampaignBudgetDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<CampaignBudget>(
filters,
config
)
return (await this.campaignBudgetRepository_.findAndCount(
queryOptions,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager("campaignBudgetRepository_")
async create(
data: CreateCampaignBudgetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (
this.campaignBudgetRepository_ as CampaignBudgetRepository
).create(data, sharedContext)) as TEntity[]
}
@InjectTransactionManager("campaignBudgetRepository_")
async update(
data: UpdateCampaignBudgetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (
this.campaignBudgetRepository_ as CampaignBudgetRepository
).update(data, sharedContext)) as TEntity[]
}
@InjectTransactionManager("campaignBudgetRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.campaignBudgetRepository_.delete(ids, sharedContext)
}
}

View File

@@ -0,0 +1,96 @@
import { Context, DAL, FindConfig, PromotionTypes } from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { Campaign } from "@models"
import { CampaignRepository } from "@repositories"
import { CreateCampaignDTO, UpdateCampaignDTO } from "../types"
type InjectedDependencies = {
campaignRepository: DAL.RepositoryService
}
export default class CampaignService<TEntity extends Campaign = Campaign> {
protected readonly campaignRepository_: DAL.RepositoryService
constructor({ campaignRepository }: InjectedDependencies) {
this.campaignRepository_ = campaignRepository
}
@InjectManager("campaignRepository_")
async retrieve(
campaignId: string,
config: FindConfig<PromotionTypes.CampaignDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<Campaign, PromotionTypes.CampaignDTO>({
id: campaignId,
entityName: Campaign.name,
repository: this.campaignRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("campaignRepository_")
async list(
filters: PromotionTypes.FilterableCampaignProps = {},
config: FindConfig<PromotionTypes.CampaignDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryOptions = ModulesSdkUtils.buildQuery<Campaign>(filters, config)
return (await this.campaignRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
}
@InjectManager("campaignRepository_")
async listAndCount(
filters: PromotionTypes.FilterableCampaignProps = {},
config: FindConfig<PromotionTypes.CampaignDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<Campaign>(filters, config)
return (await this.campaignRepository_.findAndCount(
queryOptions,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager("campaignRepository_")
async create(
data: CreateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.campaignRepository_ as CampaignRepository).create(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager("campaignRepository_")
async update(
data: UpdateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.campaignRepository_ as CampaignRepository).update(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager("campaignRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.campaignRepository_.delete(ids, sharedContext)
}
}

View File

@@ -1,4 +1,6 @@
export { default as ApplicationMethodService } from "./application-method"
export { default as CampaignService } from "./campaign"
export { default as CampaignBudgetService } from "./campaign-budget"
export { default as PromotionService } from "./promotion"
export { default as PromotionModuleService } from "./promotion-module"
export { default as PromotionRuleService } from "./promotion-rule"

View File

@@ -17,25 +17,31 @@ import {
import { ApplicationMethod, Promotion } from "@models"
import {
ApplicationMethodService,
CampaignBudgetService,
CampaignService,
PromotionRuleService,
PromotionRuleValueService,
PromotionService,
} from "@services"
import { joinerConfig } from "../joiner-config"
import {
CreateApplicationMethodDTO,
CreateCampaignBudgetDTO,
CreateCampaignDTO,
CreatePromotionDTO,
CreatePromotionRuleDTO,
UpdateApplicationMethodDTO,
UpdateCampaignBudgetDTO,
UpdateCampaignDTO,
UpdatePromotionDTO,
} from "../types"
} from "@types"
import {
ComputeActionUtils,
allowedAllocationForQuantity,
areRulesValidForContext,
validateApplicationMethodAttributes,
validatePromotionRuleAttributes,
} from "../utils"
} from "@utils"
import { joinerConfig } from "../joiner-config"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
@@ -43,6 +49,8 @@ type InjectedDependencies = {
applicationMethodService: ApplicationMethodService
promotionRuleService: PromotionRuleService
promotionRuleValueService: PromotionRuleValueService
campaignService: CampaignService
campaignBudgetService: CampaignBudgetService
}
export default class PromotionModuleService<
@@ -54,6 +62,8 @@ export default class PromotionModuleService<
protected applicationMethodService_: ApplicationMethodService
protected promotionRuleService_: PromotionRuleService
protected promotionRuleValueService_: PromotionRuleValueService
protected campaignService_: CampaignService
protected campaignBudgetService_: CampaignBudgetService
constructor(
{
@@ -62,6 +72,8 @@ export default class PromotionModuleService<
applicationMethodService,
promotionRuleService,
promotionRuleValueService,
campaignService,
campaignBudgetService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
@@ -70,6 +82,8 @@ export default class PromotionModuleService<
this.applicationMethodService_ = applicationMethodService
this.promotionRuleService_ = promotionRuleService
this.promotionRuleValueService_ = promotionRuleValueService
this.campaignService_ = campaignService
this.campaignBudgetService_ = campaignBudgetService
}
__joinerConfig(): ModuleJoinerConfig {
@@ -271,15 +285,28 @@ export default class PromotionModuleService<
)
}
@InjectManager("baseRepository_")
async create(
data: PromotionTypes.CreatePromotionDTO,
sharedContext?: Context
): Promise<PromotionTypes.PromotionDTO>
async create(
data: PromotionTypes.CreatePromotionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO[]> {
const promotions = await this.create_(data, sharedContext)
sharedContext?: Context
): Promise<PromotionTypes.PromotionDTO[]>
return await this.list(
{ id: promotions.map((p) => p!.id) },
@InjectManager("baseRepository_")
async create(
data:
| PromotionTypes.CreatePromotionDTO
| PromotionTypes.CreatePromotionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO | PromotionTypes.PromotionDTO[]> {
const input = Array.isArray(data) ? data : [data]
const createdPromotions = await this.create_(input, sharedContext)
const promotions = await this.list(
{ id: createdPromotions.map((p) => p!.id) },
{
relations: [
"application_method",
@@ -291,6 +318,8 @@ export default class PromotionModuleService<
},
sharedContext
)
return Array.isArray(data) ? promotions : promotions[0]
}
@InjectTransactionManager("baseRepository_")
@@ -399,15 +428,28 @@ export default class PromotionModuleService<
return createdPromotions
}
@InjectManager("baseRepository_")
async update(
data: PromotionTypes.UpdatePromotionDTO,
sharedContext?: Context
): Promise<PromotionTypes.PromotionDTO>
async update(
data: PromotionTypes.UpdatePromotionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO[]> {
const promotions = await this.update_(data, sharedContext)
sharedContext?: Context
): Promise<PromotionTypes.PromotionDTO[]>
return await this.list(
{ id: promotions.map((p) => p!.id) },
@InjectManager("baseRepository_")
async update(
data:
| PromotionTypes.UpdatePromotionDTO
| PromotionTypes.UpdatePromotionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO | PromotionTypes.PromotionDTO[]> {
const input = Array.isArray(data) ? data : [data]
const updatedPromotions = await this.update_(input, sharedContext)
const promotions = await this.list(
{ id: updatedPromotions.map((p) => p!.id) },
{
relations: [
"application_method",
@@ -418,6 +460,8 @@ export default class PromotionModuleService<
},
sharedContext
)
return Array.isArray(data) ? promotions : promotions[0]
}
@InjectTransactionManager("baseRepository_")
@@ -587,10 +631,12 @@ export default class PromotionModuleService<
@InjectTransactionManager("baseRepository_")
async delete(
ids: string[],
ids: string | string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.promotionService_.delete(ids, sharedContext)
const promotionIds = Array.isArray(ids) ? ids : [ids]
await this.promotionService_.delete(promotionIds, sharedContext)
}
@InjectManager("baseRepository_")
@@ -691,4 +737,214 @@ export default class PromotionModuleService<
sharedContext
)
}
@InjectManager("baseRepository_")
async retrieveCampaign(
id: string,
config: FindConfig<PromotionTypes.CampaignDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.CampaignDTO> {
const campaign = await this.campaignService_.retrieve(
id,
config,
sharedContext
)
return await this.baseRepository_.serialize<PromotionTypes.CampaignDTO>(
campaign,
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listCampaigns(
filters: PromotionTypes.FilterableCampaignProps = {},
config: FindConfig<PromotionTypes.CampaignDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.CampaignDTO[]> {
const campaigns = await this.campaignService_.list(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<PromotionTypes.CampaignDTO[]>(
campaigns,
{
populate: true,
}
)
}
async createCampaigns(
data: PromotionTypes.CreateCampaignDTO,
sharedContext?: Context
): Promise<PromotionTypes.CampaignDTO>
async createCampaigns(
data: PromotionTypes.CreateCampaignDTO[],
sharedContext?: Context
): Promise<PromotionTypes.CampaignDTO[]>
@InjectManager("baseRepository_")
async createCampaigns(
data: PromotionTypes.CreateCampaignDTO | PromotionTypes.CreateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.CampaignDTO | PromotionTypes.CampaignDTO[]> {
const input = Array.isArray(data) ? data : [data]
const createdCampaigns = await this.createCampaigns_(input, sharedContext)
const campaigns = await this.listCampaigns(
{ id: createdCampaigns.map((p) => p!.id) },
{
relations: ["budget"],
},
sharedContext
)
return Array.isArray(data) ? campaigns : campaigns[0]
}
@InjectTransactionManager("baseRepository_")
protected async createCampaigns_(
data: PromotionTypes.CreateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const campaignsData: CreateCampaignDTO[] = []
const campaignBudgetsData: CreateCampaignBudgetDTO[] = []
const campaignIdentifierBudgetMap = new Map<
string,
CreateCampaignBudgetDTO
>()
for (const createCampaignData of data) {
const { budget: campaignBudgetData, ...campaignData } = createCampaignData
if (campaignBudgetData) {
campaignIdentifierBudgetMap.set(
campaignData.campaign_identifier,
campaignBudgetData
)
}
campaignsData.push(campaignData)
}
const createdCampaigns = await this.campaignService_.create(
campaignsData,
sharedContext
)
for (const createdCampaign of createdCampaigns) {
const campaignBudgetData = campaignIdentifierBudgetMap.get(
createdCampaign.campaign_identifier
)
if (campaignBudgetData) {
campaignBudgetsData.push({
...campaignBudgetData,
campaign: createdCampaign.id,
})
}
}
if (campaignBudgetsData.length) {
await this.campaignBudgetService_.create(
campaignBudgetsData,
sharedContext
)
}
return createdCampaigns
}
async updateCampaigns(
data: PromotionTypes.UpdateCampaignDTO,
sharedContext?: Context
): Promise<PromotionTypes.CampaignDTO>
async updateCampaigns(
data: PromotionTypes.UpdateCampaignDTO[],
sharedContext?: Context
): Promise<PromotionTypes.CampaignDTO[]>
@InjectManager("baseRepository_")
async updateCampaigns(
data: PromotionTypes.UpdateCampaignDTO | PromotionTypes.UpdateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.CampaignDTO | PromotionTypes.CampaignDTO[]> {
const input = Array.isArray(data) ? data : [data]
const updatedCampaigns = await this.updateCampaigns_(input, sharedContext)
const campaigns = await this.listCampaigns(
{ id: updatedCampaigns.map((p) => p!.id) },
{
relations: ["budget"],
},
sharedContext
)
return Array.isArray(data) ? campaigns : campaigns[0]
}
@InjectTransactionManager("baseRepository_")
protected async updateCampaigns_(
data: PromotionTypes.UpdateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const campaignIds = data.map((d) => d.id)
const campaignsData: UpdateCampaignDTO[] = []
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
const existingCampaigns = await this.listCampaigns(
{
id: campaignIds,
},
{
relations: ["budget"],
},
sharedContext
)
const existingCampaignsMap = new Map<string, PromotionTypes.CampaignDTO>(
existingCampaigns.map((campaign) => [campaign.id, campaign])
)
for (const updateCampaignData of data) {
const { budget: campaignBudgetData, ...campaignData } = updateCampaignData
const existingCampaign = existingCampaignsMap.get(campaignData.id)
const existingCampaignBudget = existingCampaign?.budget
campaignsData.push(campaignData)
if (existingCampaignBudget && campaignBudgetData) {
campaignBudgetsData.push({
id: existingCampaignBudget.id,
...campaignBudgetData,
})
}
}
const updatedCampaigns = await this.campaignService_.update(campaignsData)
if (campaignBudgetsData.length) {
await this.campaignBudgetService_.update(campaignBudgetsData)
}
return updatedCampaigns
}
@InjectTransactionManager("baseRepository_")
async deleteCampaigns(
ids: string | string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
const campaignIds = Array.isArray(ids) ? ids : [ids]
await this.promotionService_.delete(campaignIds, sharedContext)
}
}

View File

@@ -0,0 +1,16 @@
import { CampaignBudgetTypeValues } from "@medusajs/types"
import { Campaign } from "@models"
export interface CreateCampaignBudgetDTO {
type: CampaignBudgetTypeValues
limit: number | null
used?: number
campaign?: Campaign | string
}
export interface UpdateCampaignBudgetDTO {
id: string
type?: CampaignBudgetTypeValues
limit?: number | null
used?: number
}

View File

@@ -0,0 +1,18 @@
export interface CreateCampaignDTO {
name: string
description?: string
currency?: string
campaign_identifier: string
starts_at: Date
ends_at: Date
}
export interface UpdateCampaignDTO {
id: string
name?: string
description?: string
currency?: string
campaign_identifier?: string
starts_at?: Date
ends_at?: Date
}

View File

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

View File

@@ -0,0 +1,16 @@
import { BaseFilterable } from "../../dal"
export type CampaignBudgetTypeValues = "spend" | "usage"
export interface CampaignBudgetDTO {
id: string
type?: CampaignBudgetTypeValues
limit?: string | null
used?: string
}
export interface FilterableCampaignBudgetProps
extends BaseFilterable<FilterableCampaignBudgetProps> {
id?: string[]
type?: string[]
}

View File

@@ -0,0 +1,19 @@
import { BaseFilterable } from "../../dal"
import { CampaignBudgetDTO } from "./campaign-budget"
export interface CampaignDTO {
id: string
name?: string
description?: string
currency?: string
campaign_identifier?: string
starts_at?: Date
ends_at?: Date
budget?: CampaignBudgetDTO
}
export interface FilterableCampaignProps
extends BaseFilterable<FilterableCampaignProps> {
id?: string[]
campaign_identifier?: string[]
}

View File

@@ -1,4 +1,6 @@
export * from "./application-method"
export * from "./campaign"
export * from "./campaign-budget"
export * from "./compute-actions"
export * from "./promotion"
export * from "./promotion-rule"

View File

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

View File

@@ -0,0 +1,35 @@
import { CampaignBudgetTypeValues } from "./common"
export interface CreateCampaignBudgetDTO {
type: CampaignBudgetTypeValues
limit: number | null
used?: number
}
export interface UpdateCampaignBudgetDTO {
id: string
type?: CampaignBudgetTypeValues
limit?: number | null
used?: number
}
export interface CreateCampaignDTO {
name: string
description?: string
currency?: string
campaign_identifier: string
starts_at: Date
ends_at: Date
budget?: CreateCampaignBudgetDTO
}
export interface UpdateCampaignDTO {
id: string
name?: string
description?: string
currency?: string
campaign_identifier?: string
starts_at?: Date
ends_at?: Date
budget?: Omit<UpdateCampaignBudgetDTO, "id">
}

View File

@@ -2,16 +2,20 @@ import { FindConfig } from "../common"
import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context"
import {
CampaignDTO,
ComputeActionContext,
ComputeActions,
CreatePromotionDTO,
CreatePromotionRuleDTO,
FilterableCampaignProps,
FilterablePromotionProps,
PromotionDTO,
RemovePromotionRuleDTO,
UpdatePromotionDTO,
} from "./common"
import { CreateCampaignDTO, UpdateCampaignDTO } from "./mutations"
export interface IPromotionModuleService extends IModuleService {
computeActions(
promotionCodesToApply: string[],
@@ -24,11 +28,21 @@ export interface IPromotionModuleService extends IModuleService {
sharedContext?: Context
): Promise<PromotionDTO[]>
create(
data: CreatePromotionDTO,
sharedContext?: Context
): Promise<PromotionDTO>
update(
data: UpdatePromotionDTO[],
sharedContext?: Context
): Promise<PromotionDTO[]>
update(
data: UpdatePromotionDTO,
sharedContext?: Context
): Promise<PromotionDTO>
list(
filters?: FilterablePromotionProps,
config?: FindConfig<PromotionDTO>,
@@ -42,6 +56,7 @@ export interface IPromotionModuleService extends IModuleService {
): Promise<PromotionDTO>
delete(ids: string[], sharedContext?: Context): Promise<void>
delete(ids: string, sharedContext?: Context): Promise<void>
addPromotionRules(
promotionId: string,
@@ -66,4 +81,39 @@ export interface IPromotionModuleService extends IModuleService {
rulesData: RemovePromotionRuleDTO[],
sharedContext?: Context
): Promise<PromotionDTO>
createCampaigns(
data: CreateCampaignDTO,
sharedContext?: Context
): Promise<CampaignDTO>
createCampaigns(
data: CreateCampaignDTO[],
sharedContext?: Context
): Promise<CampaignDTO[]>
updateCampaigns(
data: UpdateCampaignDTO[],
sharedContext?: Context
): Promise<CampaignDTO[]>
updateCampaigns(
data: UpdateCampaignDTO,
sharedContext?: Context
): Promise<CampaignDTO>
listCampaigns(
filters?: FilterableCampaignProps,
config?: FindConfig<CampaignDTO>,
sharedContext?: Context
): Promise<CampaignDTO[]>
retrieveCampaign(
id: string,
config?: FindConfig<CampaignDTO>,
sharedContext?: Context
): Promise<CampaignDTO>
deleteCampaigns(ids: string[], sharedContext?: Context): Promise<void>
deleteCampaigns(ids: string, sharedContext?: Context): Promise<void>
}

View File

@@ -28,3 +28,8 @@ export enum PromotionRuleOperator {
NE = "ne",
IN = "in",
}
export enum CampaignBudgetType {
SPEND = "spend",
USAGE = "usage",
}