feat(dashboard,medusa): Promotion Campaign fixes (#7337)
* chore(medusa): strict zod versions in workspace * feat(dashboard): add campaign create to promotion UI * wip * fix(medusa): Missing middlewares export (#7289) * fix(docblock-generator): fix how type names created from Zod objects are inferred (#7292) * feat(api-ref): show schema of a tag (#7297) * feat: Add support for sendgrid and logger notification providers (#7290) * feat: Add support for sendgrid and logger notification providers * fix: changes based on PR review * chore: add action to automatically label docs (#7284) * chore: add action to automatically label docs * removes the paths param * docs: preparations for preview (#7267) * configured base paths + added development banner * fix typelist site url * added navbar and sidebar badges * configure algolia filters * remove AI assistant * remove unused imports * change navbar text and badge * lint fixes * fix build error * add to api reference rewrites * fix build error * fix build errors in user-guide * fix feedback component * add parent title to pagination * added breadcrumbs component * remove user-guide links * resolve todos * fix details about authentication * change documentation title * lint content * chore: fix bug with form reset * chore: address reviews * chore: fix specs * chore: loads of FE fixes + BE adds * chore: add more polishes + reorg files * chore: fixes to promotions modal * chore: cleanup * chore: cleanup * chore: fix build * chore: fkix cart spec * chore: fix module tests * chore: fix moar tests * wip * chore: templates + fixes + migrate currency * chore: fix build, add validation for max_quantity * chore: allow removing campaigns * chore: fix specs * chore: scope campaigns based on currency * remove console logs * chore: add translations + update keys * chore: move over filesfrom v2 to routes * chore(dashboard): Delete old translation files (#7423) * feat(dashboard,admin-sdk,admin-shared,admin-vite-plugin): Add support for UI extensions (#7383) * intial work * update lock * add routes and fix HMR of configs * cleanup * rm imports * rm debug from plugin * address feedback * address feedback * temp skip specs --------- Co-authored-by: Adrien de Peretti <adrien.deperetti@gmail.com> Co-authored-by: Shahed Nasser <shahednasser@gmail.com> Co-authored-by: Stevche Radevski <sradevski@live.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com>
This commit is contained in:
co-authored by
Adrien de Peretti
Shahed Nasser
Stevche Radevski
Oli Juhl
Kasper Fabricius Kristensen
parent
4a10821bfe
commit
d1d23f1e8d
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
CampaignBudgetTypeValues,
|
||||
Context,
|
||||
DAL,
|
||||
InternalModuleDeclaration,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
arrayDifference,
|
||||
deduplicate,
|
||||
isDefined,
|
||||
isPresent,
|
||||
isString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
@@ -475,6 +477,11 @@ export default class PromotionModuleService<
|
||||
const promotionsData: CreatePromotionDTO[] = []
|
||||
const applicationMethodsData: CreateApplicationMethodDTO[] = []
|
||||
const campaignsData: CreateCampaignDTO[] = []
|
||||
const existingCampaigns = await this.campaignService_.list(
|
||||
{ id: data.map((d) => d.campaign_id).filter((id) => isString(id)) },
|
||||
{ relations: ["budget"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const promotionCodeApplicationMethodDataMap = new Map<
|
||||
string,
|
||||
@@ -504,12 +511,10 @@ export default class PromotionModuleService<
|
||||
campaign_id: campaignId,
|
||||
...promotionData
|
||||
} of data) {
|
||||
if (applicationMethodData) {
|
||||
promotionCodeApplicationMethodDataMap.set(
|
||||
promotionData.code,
|
||||
applicationMethodData
|
||||
)
|
||||
}
|
||||
promotionCodeApplicationMethodDataMap.set(
|
||||
promotionData.code,
|
||||
applicationMethodData
|
||||
)
|
||||
|
||||
if (rulesData) {
|
||||
promotionCodeRulesDataMap.set(promotionData.code, rulesData)
|
||||
@@ -522,6 +527,38 @@ export default class PromotionModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
if (!campaignData && !campaignId) {
|
||||
promotionsData.push({ ...promotionData })
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
const existingCampaign = existingCampaigns.find(
|
||||
(c) => c.id === campaignId
|
||||
)
|
||||
|
||||
if (campaignId && !existingCampaign) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Could not find campaign with id - ${campaignId}`
|
||||
)
|
||||
}
|
||||
|
||||
const campaignCurrency =
|
||||
campaignData?.budget?.currency_code ||
|
||||
existingCampaigns.find((c) => c.id === campaignId)?.budget
|
||||
?.currency_code
|
||||
|
||||
if (
|
||||
campaignData?.budget?.type === CampaignBudgetType.SPEND &&
|
||||
campaignCurrency !== applicationMethodData?.currency_code
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Currency between promotion and campaigns should match`
|
||||
)
|
||||
}
|
||||
|
||||
if (campaignData) {
|
||||
promotionCodeCampaignMap.set(promotionData.code, campaignData)
|
||||
}
|
||||
@@ -536,7 +573,6 @@ export default class PromotionModuleService<
|
||||
promotionsData,
|
||||
sharedContext
|
||||
)
|
||||
const promotionsToAdd: PromotionTypes.AddPromotionsToCampaignDTO[] = []
|
||||
|
||||
for (const promotion of createdPromotions) {
|
||||
const applMethodData = promotionCodeApplicationMethodDataMap.get(
|
||||
@@ -708,6 +744,10 @@ export default class PromotionModuleService<
|
||||
{ id: promotionIds },
|
||||
{ relations: ["application_method"] }
|
||||
)
|
||||
const existingCampaigns = await this.campaignService_.list(
|
||||
{ id: data.map((d) => d.campaign_id).filter((d) => isPresent(d)) },
|
||||
{ relations: ["budget"] }
|
||||
)
|
||||
|
||||
const existingPromotionsMap = new Map<string, Promotion>(
|
||||
existingPromotions.map((promotion) => [promotion.id, promotion])
|
||||
@@ -721,20 +761,40 @@ export default class PromotionModuleService<
|
||||
campaign_id: campaignId,
|
||||
...promotionData
|
||||
} of data) {
|
||||
if (campaignId) {
|
||||
const existingCampaign = existingCampaigns.find(
|
||||
(c) => c.id === campaignId
|
||||
)
|
||||
const existingPromotion = existingPromotionsMap.get(promotionData.id)!
|
||||
const existingApplicationMethod = existingPromotion?.application_method
|
||||
const promotionCurrencyCode =
|
||||
existingApplicationMethod?.currency_code ||
|
||||
applicationMethodData?.currency_code
|
||||
|
||||
if (campaignId && !existingCampaign) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Could not find campaign with id ${campaignId}`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
campaignId &&
|
||||
existingCampaign?.budget?.type === CampaignBudgetType.SPEND &&
|
||||
existingCampaign.budget.currency_code !== promotionCurrencyCode
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Currency code doesn't match for campaign (${campaignId}) and promotion (${existingPromotion.id})`
|
||||
)
|
||||
}
|
||||
|
||||
if (isDefined(campaignId)) {
|
||||
promotionsData.push({ ...promotionData, campaign_id: campaignId })
|
||||
} else {
|
||||
promotionsData.push(promotionData)
|
||||
}
|
||||
|
||||
if (!applicationMethodData) {
|
||||
continue
|
||||
}
|
||||
|
||||
const existingPromotion = existingPromotionsMap.get(promotionData.id)
|
||||
const existingApplicationMethod = existingPromotion?.application_method
|
||||
|
||||
if (!existingApplicationMethod) {
|
||||
if (!applicationMethodData || !existingApplicationMethod) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -913,9 +973,11 @@ export default class PromotionModuleService<
|
||||
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<PromotionTypes.PromotionRuleDTO[]> {
|
||||
const promotion = await this.promotionService_.retrieve(promotionId, {
|
||||
relations: ["application_method"],
|
||||
})
|
||||
const promotion = await this.promotionService_.retrieve(
|
||||
promotionId,
|
||||
{ relations: ["application_method"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const applicationMethod = promotion.application_method
|
||||
|
||||
@@ -1153,6 +1215,8 @@ export default class PromotionModuleService<
|
||||
)
|
||||
|
||||
if (campaignBudgetData) {
|
||||
this.validateCampaignBudgetData(campaignBudgetData)
|
||||
|
||||
campaignBudgetsData.push({
|
||||
...campaignBudgetData,
|
||||
campaign: createdCampaign.id,
|
||||
@@ -1170,6 +1234,28 @@ export default class PromotionModuleService<
|
||||
return createdCampaigns
|
||||
}
|
||||
|
||||
protected validateCampaignBudgetData(data: {
|
||||
type?: CampaignBudgetTypeValues
|
||||
currency_code?: string | null
|
||||
}) {
|
||||
if (!data.type) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Campaign Budget type is a required field`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
data.type === CampaignBudgetType.SPEND &&
|
||||
!isPresent(data.currency_code)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Campaign Budget type is a required field`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async updateCampaigns(
|
||||
data: PromotionTypes.UpdateCampaignDTO,
|
||||
sharedContext?: Context
|
||||
@@ -1207,7 +1293,8 @@ export default class PromotionModuleService<
|
||||
) {
|
||||
const campaignIds = data.map((d) => d.id)
|
||||
const campaignsData: UpdateCampaignDTO[] = []
|
||||
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
|
||||
const updateBudgetData: UpdateCampaignBudgetDTO[] = []
|
||||
const createBudgetData: CreateCampaignBudgetDTO[] = []
|
||||
|
||||
const existingCampaigns = await this.listCampaigns(
|
||||
{ id: campaignIds },
|
||||
@@ -1220,18 +1307,38 @@ export default class PromotionModuleService<
|
||||
)
|
||||
|
||||
for (const updateCampaignData of data) {
|
||||
const { budget: campaignBudgetData, ...campaignData } = updateCampaignData
|
||||
|
||||
const existingCampaign = existingCampaignsMap.get(campaignData.id)
|
||||
const existingCampaignBudget = existingCampaign?.budget
|
||||
const { budget: budgetData, ...campaignData } = updateCampaignData
|
||||
const existingCampaign = existingCampaignsMap.get(campaignData.id)!
|
||||
|
||||
campaignsData.push(campaignData)
|
||||
|
||||
if (existingCampaignBudget && campaignBudgetData) {
|
||||
campaignBudgetsData.push({
|
||||
id: existingCampaignBudget.id,
|
||||
...campaignBudgetData,
|
||||
})
|
||||
// Type & currency code of the budget is immutable, we don't allow for it to be updated.
|
||||
// If an existing budget is present, we remove the type and currency from being updated
|
||||
if (
|
||||
(existingCampaign?.budget && budgetData?.type) ||
|
||||
budgetData?.currency_code
|
||||
) {
|
||||
delete budgetData?.type
|
||||
delete budgetData?.currency_code
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Campaign budget attributes (type, currency_code) are immutable`
|
||||
)
|
||||
}
|
||||
|
||||
if (budgetData) {
|
||||
if (existingCampaign?.budget) {
|
||||
updateBudgetData.push({
|
||||
id: existingCampaign.budget.id,
|
||||
...budgetData,
|
||||
})
|
||||
} else {
|
||||
createBudgetData.push({
|
||||
...budgetData,
|
||||
campaign: existingCampaign.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1240,11 +1347,12 @@ export default class PromotionModuleService<
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (campaignBudgetsData.length) {
|
||||
await this.campaignBudgetService_.update(
|
||||
campaignBudgetsData,
|
||||
sharedContext
|
||||
)
|
||||
if (updateBudgetData.length) {
|
||||
await this.campaignBudgetService_.update(updateBudgetData, sharedContext)
|
||||
}
|
||||
|
||||
if (createBudgetData.length) {
|
||||
await this.campaignBudgetService_.create(createBudgetData, sharedContext)
|
||||
}
|
||||
|
||||
return updatedCampaigns
|
||||
@@ -1274,7 +1382,7 @@ export default class PromotionModuleService<
|
||||
const campaign = await this.campaignService_.retrieve(id, {}, sharedContext)
|
||||
const promotionsToAdd = await this.promotionService_.list(
|
||||
{ id: promotionIds, campaign_id: null },
|
||||
{ take: null },
|
||||
{ take: null, relations: ["application_method"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
@@ -1292,6 +1400,20 @@ export default class PromotionModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
const promotionsWithInvalidCurrency = promotionsToAdd.filter(
|
||||
(promotion) =>
|
||||
campaign.budget?.type === CampaignBudgetType.SPEND &&
|
||||
promotion.application_method?.currency_code !==
|
||||
campaign?.budget?.currency_code
|
||||
)
|
||||
|
||||
if (promotionsWithInvalidCurrency.length > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Cannot add promotions to campaign where currency_code don't match.`
|
||||
)
|
||||
}
|
||||
|
||||
await this.promotionService_.update(
|
||||
promotionsToAdd.map((promotion) => ({
|
||||
id: promotion.id,
|
||||
|
||||
Reference in New Issue
Block a user