feat(core-flows,types,medusa): API to add promotions to campaign (#7277)
what: - adds an API to add promotions to campaign - reworks module to perform atomic actions
This commit is contained in:
@@ -152,43 +152,6 @@ moduleIntegrationTestRunner({
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should create a basic campaign with promotions successfully", async () => {
|
||||
await createPromotions(MikroOrmWrapper.forkManager())
|
||||
|
||||
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,
|
||||
promotions: [{ id: "promotion-id-1" }, { id: "promotion-id-2" }],
|
||||
},
|
||||
])
|
||||
|
||||
const campaign = await service.retrieveCampaign(createdCampaign.id, {
|
||||
relations: ["promotions"],
|
||||
})
|
||||
|
||||
expect(campaign).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "test",
|
||||
campaign_identifier: "test",
|
||||
starts_at: startsAt,
|
||||
ends_at: endsAt,
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
id: "promotion-id-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "promotion-id-2",
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("updateCampaigns", () => {
|
||||
@@ -251,66 +214,6 @@ moduleIntegrationTestRunner({
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should update promotions of a campaign successfully", async () => {
|
||||
await createCampaigns(MikroOrmWrapper.forkManager())
|
||||
await createPromotions(MikroOrmWrapper.forkManager())
|
||||
|
||||
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"),
|
||||
promotions: [{ id: "promotion-id-1" }, { id: "promotion-id-2" }],
|
||||
},
|
||||
])
|
||||
|
||||
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"),
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
id: "promotion-id-1",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "promotion-id-2",
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should remove promotions of the campaign successfully", async () => {
|
||||
await createCampaigns(MikroOrmWrapper.forkManager())
|
||||
await createPromotions(MikroOrmWrapper.forkManager())
|
||||
|
||||
await service.updateCampaigns({
|
||||
id: "campaign-id-1",
|
||||
promotions: [{ id: "promotion-id-1" }, { id: "promotion-id-2" }],
|
||||
})
|
||||
|
||||
const updatedCampaign = await service.updateCampaigns({
|
||||
id: "campaign-id-1",
|
||||
promotions: [{ id: "promotion-id-1" }],
|
||||
})
|
||||
|
||||
expect(updatedCampaign).toEqual(
|
||||
expect.objectContaining({
|
||||
promotions: [
|
||||
expect.objectContaining({
|
||||
id: "promotion-id-1",
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveCampaign", () => {
|
||||
@@ -438,6 +341,77 @@ moduleIntegrationTestRunner({
|
||||
expect(campaigns).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("addPromotionsToCampaign", () => {
|
||||
beforeEach(async () => {
|
||||
await createCampaigns(MikroOrmWrapper.forkManager())
|
||||
await createPromotions(MikroOrmWrapper.forkManager())
|
||||
|
||||
await service.addPromotionsToCampaign({
|
||||
id,
|
||||
promotion_ids: ["promotion-id-1"],
|
||||
})
|
||||
})
|
||||
|
||||
const id = "campaign-id-1"
|
||||
|
||||
it("should add promotions to a campaign", async () => {
|
||||
await service.addPromotionsToCampaign({
|
||||
id,
|
||||
promotion_ids: ["promotion-id-2"],
|
||||
})
|
||||
|
||||
const campaign = await service.retrieveCampaign(id, {
|
||||
relations: ["promotions"],
|
||||
})
|
||||
|
||||
expect(campaign.promotions).toHaveLength(2)
|
||||
expect(campaign).toEqual(
|
||||
expect.objectContaining({
|
||||
id,
|
||||
promotions: expect.arrayContaining([
|
||||
expect.objectContaining({ id: "promotion-id-1" }),
|
||||
expect.objectContaining({ id: "promotion-id-2" }),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("removePromotionsFromCampaign", () => {
|
||||
beforeEach(async () => {
|
||||
await createCampaigns(MikroOrmWrapper.forkManager())
|
||||
await createPromotions(MikroOrmWrapper.forkManager())
|
||||
|
||||
await service.addPromotionsToCampaign({
|
||||
id,
|
||||
promotion_ids: ["promotion-id-1", "promotion-id-2"],
|
||||
})
|
||||
})
|
||||
|
||||
const id = "campaign-id-1"
|
||||
|
||||
it("should remove promotions to a campaign", async () => {
|
||||
await service.removePromotionsFromCampaign({
|
||||
id,
|
||||
promotion_ids: ["promotion-id-1"],
|
||||
})
|
||||
|
||||
const campaign = await service.retrieveCampaign(id, {
|
||||
relations: ["promotions"],
|
||||
})
|
||||
|
||||
expect(campaign.promotions).toHaveLength(1)
|
||||
expect(campaign).toEqual(
|
||||
expect.objectContaining({
|
||||
id,
|
||||
promotions: expect.arrayContaining([
|
||||
expect.objectContaining({ id: "promotion-id-2" }),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -539,7 +539,7 @@ moduleIntegrationTestRunner({
|
||||
},
|
||||
])
|
||||
|
||||
await service.updateCampaigns({
|
||||
const updated = await service.updateCampaigns({
|
||||
id: "campaign-id-2",
|
||||
budget: { used: 1000 },
|
||||
})
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
CampaignBudgetType,
|
||||
PromotionType,
|
||||
} from "@medusajs/utils"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
||||
import { createPromotions } from "../../../__fixtures__/promotion"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -918,6 +918,7 @@ moduleIntegrationTestRunner({
|
||||
{
|
||||
id: "promotion-id-1",
|
||||
code: "PROMOTION_1",
|
||||
campaign_id: null,
|
||||
campaign: null,
|
||||
is_automatic: false,
|
||||
type: "standard",
|
||||
@@ -929,6 +930,7 @@ moduleIntegrationTestRunner({
|
||||
{
|
||||
id: "promotion-id-2",
|
||||
code: "PROMOTION_2",
|
||||
campaign_id: null,
|
||||
campaign: null,
|
||||
is_automatic: false,
|
||||
type: "standard",
|
||||
|
||||
@@ -70,9 +70,7 @@ export default class Campaign {
|
||||
})
|
||||
budget: CampaignBudget | null = null
|
||||
|
||||
@OneToMany(() => Promotion, (promotion) => promotion.campaign, {
|
||||
orphanRemoval: true,
|
||||
})
|
||||
@OneToMany(() => Promotion, (promotion) => promotion.campaign)
|
||||
promotions = new Collection<Promotion>(this)
|
||||
|
||||
@Property({
|
||||
|
||||
@@ -45,13 +45,17 @@ export default class Promotion {
|
||||
})
|
||||
code: string
|
||||
|
||||
@Searchable()
|
||||
@ManyToOne(() => Campaign, {
|
||||
columnType: "text",
|
||||
fieldName: "campaign_id",
|
||||
nullable: true,
|
||||
cascade: ["soft-remove"] as any,
|
||||
mapToPk: true,
|
||||
onDelete: "set null",
|
||||
})
|
||||
campaign: Campaign | null = null
|
||||
campaign_id: string | null = null
|
||||
|
||||
@ManyToOne(() => Campaign, { persist: false })
|
||||
campaign: Campaign | null
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
is_automatic: boolean = false
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import { Context } from "@medusajs/types"
|
||||
import { DALUtils } from "@medusajs/utils"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { Campaign, Promotion } from "@models"
|
||||
import { CreateCampaignDTO, UpdateCampaignDTO } from "@types"
|
||||
|
||||
export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Campaign>(
|
||||
Campaign
|
||||
) {
|
||||
async create(
|
||||
data: CreateCampaignDTO[],
|
||||
context: Context = {}
|
||||
): Promise<Campaign[]> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
const promotionIdsToUpsert: string[] = []
|
||||
const campaignIdentifierPromotionsMap = new Map<string, string[]>()
|
||||
|
||||
data.forEach((campaignData) => {
|
||||
const campaignPromotionIds =
|
||||
campaignData.promotions?.map((p) => p.id) || []
|
||||
|
||||
promotionIdsToUpsert.push(...campaignPromotionIds)
|
||||
|
||||
campaignIdentifierPromotionsMap.set(
|
||||
campaignData.campaign_identifier,
|
||||
campaignPromotionIds
|
||||
)
|
||||
|
||||
delete campaignData.promotions
|
||||
})
|
||||
|
||||
const existingPromotions = await manager.find(Promotion, {
|
||||
id: promotionIdsToUpsert,
|
||||
})
|
||||
|
||||
const existingPromotionsMap = new Map<string, Promotion>(
|
||||
existingPromotions.map((promotion) => [promotion.id, promotion])
|
||||
)
|
||||
|
||||
const createdCampaigns = await super.create(data, context)
|
||||
|
||||
for (const createdCampaign of createdCampaigns) {
|
||||
const campaignPromotionIds =
|
||||
campaignIdentifierPromotionsMap.get(
|
||||
createdCampaign.campaign_identifier
|
||||
) || []
|
||||
|
||||
for (const campaignPromotionId of campaignPromotionIds) {
|
||||
const promotion = existingPromotionsMap.get(campaignPromotionId)
|
||||
|
||||
if (!promotion) {
|
||||
continue
|
||||
}
|
||||
|
||||
createdCampaign.promotions.add(promotion)
|
||||
}
|
||||
}
|
||||
|
||||
return createdCampaigns
|
||||
}
|
||||
|
||||
async update(
|
||||
data: { entity: Campaign; update: UpdateCampaignDTO }[],
|
||||
context: Context = {}
|
||||
): Promise<Campaign[]> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
const promotionIdsToUpsert: string[] = []
|
||||
const campaignIds: string[] = []
|
||||
const campaignPromotionIdsMap = new Map<string, string[]>()
|
||||
|
||||
data.forEach(({ update: campaignData }) => {
|
||||
const campaignPromotionIds = campaignData.promotions?.map((p) => p.id)
|
||||
|
||||
campaignIds.push(campaignData.id)
|
||||
|
||||
if (campaignPromotionIds) {
|
||||
promotionIdsToUpsert.push(...campaignPromotionIds)
|
||||
campaignPromotionIdsMap.set(campaignData.id, campaignPromotionIds)
|
||||
}
|
||||
|
||||
delete campaignData.promotions
|
||||
})
|
||||
|
||||
const existingCampaigns = await manager.find(
|
||||
Campaign,
|
||||
{ id: campaignIds },
|
||||
{ populate: ["promotions"] }
|
||||
)
|
||||
|
||||
const promotionIds = existingCampaigns
|
||||
.map((campaign) => campaign.promotions?.map((p) => p.id))
|
||||
.flat(1)
|
||||
.concat(promotionIdsToUpsert)
|
||||
|
||||
const existingPromotions = await manager.find(Promotion, {
|
||||
id: promotionIds,
|
||||
})
|
||||
|
||||
const existingCampaignsMap = new Map<string, Campaign>(
|
||||
existingCampaigns.map((campaign) => [campaign.id, campaign])
|
||||
)
|
||||
|
||||
const existingPromotionsMap = new Map<string, Promotion>(
|
||||
existingPromotions.map((promotion) => [promotion.id, promotion])
|
||||
)
|
||||
|
||||
const updatedCampaigns = await super.update(data, context)
|
||||
|
||||
for (const updatedCampaign of updatedCampaigns) {
|
||||
const upsertPromotionIds = campaignPromotionIdsMap.get(updatedCampaign.id)
|
||||
|
||||
if (!upsertPromotionIds) {
|
||||
continue
|
||||
}
|
||||
|
||||
const existingPromotionIds = (
|
||||
existingCampaignsMap.get(updatedCampaign.id)?.promotions || []
|
||||
).map((p) => p.id)
|
||||
|
||||
for (const existingPromotionId of existingPromotionIds) {
|
||||
const promotion = existingPromotionsMap.get(existingPromotionId)
|
||||
|
||||
if (!promotion) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!upsertPromotionIds.includes(existingPromotionId)) {
|
||||
updatedCampaign.promotions.remove(promotion)
|
||||
}
|
||||
}
|
||||
|
||||
for (const promotionIdToAdd of upsertPromotionIds) {
|
||||
const promotion = existingPromotionsMap.get(promotionIdToAdd)
|
||||
|
||||
if (!promotion) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (existingPromotionIds.includes(promotionIdToAdd)) {
|
||||
continue
|
||||
} else {
|
||||
updatedCampaign.promotions.add(promotion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedCampaigns
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
|
||||
export { CampaignRepository } from "./campaign"
|
||||
|
||||
@@ -528,7 +528,7 @@ export default class PromotionModuleService<
|
||||
|
||||
promotionsData.push({
|
||||
...promotionData,
|
||||
campaign: campaignId,
|
||||
campaign_id: campaignId,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -536,6 +536,7 @@ export default class PromotionModuleService<
|
||||
promotionsData,
|
||||
sharedContext
|
||||
)
|
||||
const promotionsToAdd: PromotionTypes.AddPromotionsToCampaignDTO[] = []
|
||||
|
||||
for (const promotion of createdPromotions) {
|
||||
const applMethodData = promotionCodeApplicationMethodDataMap.get(
|
||||
@@ -617,8 +618,25 @@ export default class PromotionModuleService<
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (campaignsData.length) {
|
||||
await this.createCampaigns(campaignsData, sharedContext)
|
||||
const createdCampaigns = await this.createCampaigns(
|
||||
campaignsData,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
for (const campaignData of campaignsData) {
|
||||
const promotions = campaignData.promotions
|
||||
const campaign = createdCampaigns.find(
|
||||
(c) => c.campaign_identifier === campaignData.campaign_identifier
|
||||
)
|
||||
|
||||
if (!campaign || !promotions || !promotions.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
await this.addPromotionsToCampaign(
|
||||
{ id: campaign.id, promotion_ids: promotions.map((p) => p.id) },
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
for (const applicationMethod of createdApplicationMethods) {
|
||||
@@ -704,7 +722,7 @@ export default class PromotionModuleService<
|
||||
...promotionData
|
||||
} of data) {
|
||||
if (campaignId) {
|
||||
promotionsData.push({ ...promotionData, campaign: campaignId })
|
||||
promotionsData.push({ ...promotionData, campaign_id: campaignId })
|
||||
} else {
|
||||
promotionsData.push(promotionData)
|
||||
}
|
||||
@@ -1110,19 +1128,7 @@ export default class PromotionModuleService<
|
||||
>()
|
||||
|
||||
for (const createCampaignData of data) {
|
||||
const {
|
||||
budget: campaignBudgetData,
|
||||
promotions,
|
||||
...campaignData
|
||||
} = createCampaignData
|
||||
|
||||
const promotionsToAdd = promotions
|
||||
? await this.list(
|
||||
{ id: promotions.map((p) => p.id) },
|
||||
{ take: null },
|
||||
sharedContext
|
||||
)
|
||||
: []
|
||||
const { budget: campaignBudgetData, ...campaignData } = createCampaignData
|
||||
|
||||
if (campaignBudgetData) {
|
||||
campaignIdentifierBudgetMap.set(
|
||||
@@ -1133,7 +1139,6 @@ export default class PromotionModuleService<
|
||||
|
||||
campaignsData.push({
|
||||
...campaignData,
|
||||
promotions: promotionsToAdd,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1244,4 +1249,104 @@ export default class PromotionModuleService<
|
||||
|
||||
return updatedCampaigns
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async addPromotionsToCampaign(
|
||||
data: PromotionTypes.AddPromotionsToCampaignDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<{ ids: string[] }> {
|
||||
const ids = await this.addPromotionsToCampaign_(data, sharedContext)
|
||||
|
||||
return { ids }
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - introduce currency_code to promotion
|
||||
// - allow promotions to be queried by currency code
|
||||
// - when the above is present, validate adding promotion to campaign based on currency code
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
protected async addPromotionsToCampaign_(
|
||||
data: PromotionTypes.AddPromotionsToCampaignDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const { id, promotion_ids: promotionIds = [] } = data
|
||||
|
||||
const campaign = await this.campaignService_.retrieve(id, {}, sharedContext)
|
||||
const promotionsToAdd = await this.promotionService_.list(
|
||||
{ id: promotionIds, campaign_id: null },
|
||||
{ take: null },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const diff = arrayDifference(
|
||||
promotionsToAdd.map((p) => p.id),
|
||||
promotionIds
|
||||
)
|
||||
|
||||
if (diff.length > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Cannot add promotions (${diff.join(
|
||||
","
|
||||
)}) to campaign. These promotions are either already part of a campaign or not found.`
|
||||
)
|
||||
}
|
||||
|
||||
await this.promotionService_.update(
|
||||
promotionsToAdd.map((promotion) => ({
|
||||
id: promotion.id,
|
||||
campaign_id: campaign.id,
|
||||
})),
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return promotionsToAdd.map((promo) => promo.id)
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async removePromotionsFromCampaign(
|
||||
data: PromotionTypes.AddPromotionsToCampaignDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<{ ids: string[] }> {
|
||||
const ids = await this.removePromotionsFromCampaign_(data, sharedContext)
|
||||
|
||||
return { ids }
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
protected async removePromotionsFromCampaign_(
|
||||
data: PromotionTypes.AddPromotionsToCampaignDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const { id, promotion_ids: promotionIds = [] } = data
|
||||
|
||||
await this.campaignService_.retrieve(id, {}, sharedContext)
|
||||
const promotionsToRemove = await this.promotionService_.list(
|
||||
{ id: promotionIds },
|
||||
{ take: null },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const diff = arrayDifference(
|
||||
promotionsToRemove.map((p) => p.id),
|
||||
promotionIds
|
||||
)
|
||||
|
||||
if (diff.length > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Promotions with ids (${diff.join(",")}) not found.`
|
||||
)
|
||||
}
|
||||
|
||||
await this.promotionService_.update(
|
||||
promotionsToRemove.map((promotion) => ({
|
||||
id: promotion.id,
|
||||
campaign_id: null,
|
||||
})),
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return promotionsToRemove.map((promo) => promo.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface CreatePromotionDTO {
|
||||
code: string
|
||||
type: PromotionTypeValues
|
||||
is_automatic?: boolean
|
||||
campaign?: string
|
||||
campaign_id?: string
|
||||
}
|
||||
|
||||
export interface UpdatePromotionDTO {
|
||||
@@ -12,5 +12,5 @@ export interface UpdatePromotionDTO {
|
||||
code?: string
|
||||
type?: PromotionTypeValues
|
||||
is_automatic?: boolean
|
||||
campaign?: string
|
||||
campaign_id?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user