1481 lines
43 KiB
TypeScript
1481 lines
43 KiB
TypeScript
import {
|
|
CampaignBudgetTypeValues,
|
|
Context,
|
|
DAL,
|
|
InternalModuleDeclaration,
|
|
ModuleJoinerConfig,
|
|
ModulesSdkTypes,
|
|
PromotionTypes,
|
|
} from "@medusajs/types"
|
|
import {
|
|
ApplicationMethodAllocation,
|
|
ApplicationMethodTargetType,
|
|
CampaignBudgetType,
|
|
ComputedActions,
|
|
InjectManager,
|
|
InjectTransactionManager,
|
|
MathBN,
|
|
MedusaContext,
|
|
MedusaError,
|
|
ModulesSdkUtils,
|
|
PromotionType,
|
|
arrayDifference,
|
|
deduplicate,
|
|
isDefined,
|
|
isPresent,
|
|
isString,
|
|
transformPropertiesToBigNumber,
|
|
} from "@medusajs/utils"
|
|
import {
|
|
ApplicationMethod,
|
|
Campaign,
|
|
CampaignBudget,
|
|
Promotion,
|
|
PromotionRule,
|
|
PromotionRuleValue,
|
|
} from "@models"
|
|
import {
|
|
ApplicationMethodRuleTypes,
|
|
CreateApplicationMethodDTO,
|
|
CreateCampaignBudgetDTO,
|
|
CreateCampaignDTO,
|
|
CreatePromotionDTO,
|
|
CreatePromotionRuleDTO,
|
|
UpdateApplicationMethodDTO,
|
|
UpdateCampaignBudgetDTO,
|
|
UpdateCampaignDTO,
|
|
UpdatePromotionDTO,
|
|
} from "@types"
|
|
import {
|
|
ComputeActionUtils,
|
|
allowedAllocationForQuantity,
|
|
areRulesValidForContext,
|
|
validateApplicationMethodAttributes,
|
|
validatePromotionRuleAttributes,
|
|
} from "@utils"
|
|
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
|
import { CreatePromotionRuleValueDTO } from "../types/promotion-rule-value"
|
|
|
|
type InjectedDependencies = {
|
|
baseRepository: DAL.RepositoryService
|
|
promotionService: ModulesSdkTypes.InternalModuleService<any>
|
|
applicationMethodService: ModulesSdkTypes.InternalModuleService<any>
|
|
promotionRuleService: ModulesSdkTypes.InternalModuleService<any>
|
|
promotionRuleValueService: ModulesSdkTypes.InternalModuleService<any>
|
|
campaignService: ModulesSdkTypes.InternalModuleService<any>
|
|
campaignBudgetService: ModulesSdkTypes.InternalModuleService<any>
|
|
}
|
|
|
|
const generateMethodForModels = [
|
|
ApplicationMethod,
|
|
Campaign,
|
|
CampaignBudget,
|
|
PromotionRule,
|
|
PromotionRuleValue,
|
|
]
|
|
|
|
export default class PromotionModuleService<
|
|
TApplicationMethod extends ApplicationMethod = ApplicationMethod,
|
|
TPromotion extends Promotion = Promotion,
|
|
TPromotionRule extends PromotionRule = PromotionRule,
|
|
TPromotionRuleValue extends PromotionRuleValue = PromotionRuleValue,
|
|
TCampaign extends Campaign = Campaign,
|
|
TCampaignBudget extends CampaignBudget = CampaignBudget
|
|
>
|
|
extends ModulesSdkUtils.abstractModuleServiceFactory<
|
|
InjectedDependencies,
|
|
PromotionTypes.PromotionDTO,
|
|
{
|
|
ApplicationMethod: { dto: PromotionTypes.ApplicationMethodDTO }
|
|
Campaign: { dto: PromotionTypes.CampaignDTO }
|
|
CampaignBudget: { dto: PromotionTypes.CampaignBudgetDTO }
|
|
PromotionRule: { dto: PromotionTypes.PromotionRuleDTO }
|
|
PromotionRuleValue: { dto: PromotionTypes.PromotionRuleValueDTO }
|
|
}
|
|
>(Promotion, generateMethodForModels, entityNameToLinkableKeysMap)
|
|
implements PromotionTypes.IPromotionModuleService
|
|
{
|
|
protected baseRepository_: DAL.RepositoryService
|
|
protected promotionService_: ModulesSdkTypes.InternalModuleService<TPromotion>
|
|
protected applicationMethodService_: ModulesSdkTypes.InternalModuleService<TApplicationMethod>
|
|
protected promotionRuleService_: ModulesSdkTypes.InternalModuleService<TPromotionRule>
|
|
protected promotionRuleValueService_: ModulesSdkTypes.InternalModuleService<TPromotionRuleValue>
|
|
protected campaignService_: ModulesSdkTypes.InternalModuleService<TCampaign>
|
|
protected campaignBudgetService_: ModulesSdkTypes.InternalModuleService<TCampaignBudget>
|
|
|
|
constructor(
|
|
{
|
|
baseRepository,
|
|
promotionService,
|
|
applicationMethodService,
|
|
promotionRuleService,
|
|
promotionRuleValueService,
|
|
campaignService,
|
|
campaignBudgetService,
|
|
}: InjectedDependencies,
|
|
protected readonly moduleDeclaration: InternalModuleDeclaration
|
|
) {
|
|
// @ts-ignore
|
|
super(...arguments)
|
|
|
|
this.baseRepository_ = baseRepository
|
|
this.promotionService_ = promotionService
|
|
this.applicationMethodService_ = applicationMethodService
|
|
this.promotionRuleService_ = promotionRuleService
|
|
this.promotionRuleValueService_ = promotionRuleValueService
|
|
this.campaignService_ = campaignService
|
|
this.campaignBudgetService_ = campaignBudgetService
|
|
}
|
|
|
|
__joinerConfig(): ModuleJoinerConfig {
|
|
return joinerConfig
|
|
}
|
|
|
|
@InjectManager("baseRepository_")
|
|
async registerUsage(
|
|
computedActions: PromotionTypes.UsageComputedActions[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<void> {
|
|
const promotionCodes = computedActions
|
|
.map((computedAction) => computedAction.code)
|
|
.filter(Boolean)
|
|
|
|
const promotionCodeCampaignBudgetMap = new Map<
|
|
string,
|
|
UpdateCampaignBudgetDTO
|
|
>()
|
|
const promotionCodeUsageMap = new Map<string, boolean>()
|
|
|
|
const existingPromotions = await this.list(
|
|
{ code: promotionCodes },
|
|
{ relations: ["application_method", "campaign", "campaign.budget"] },
|
|
sharedContext
|
|
)
|
|
|
|
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
|
|
existingPromotions.map((promotion) => [promotion.code!, promotion])
|
|
)
|
|
|
|
for (let computedAction of computedActions) {
|
|
if (!ComputeActionUtils.canRegisterUsage(computedAction)) {
|
|
continue
|
|
}
|
|
|
|
const promotion = existingPromotionsMap.get(computedAction.code)
|
|
|
|
if (!promotion) {
|
|
continue
|
|
}
|
|
|
|
const campaignBudget = promotion.campaign?.budget
|
|
|
|
if (!campaignBudget) {
|
|
continue
|
|
}
|
|
|
|
if (campaignBudget.type === CampaignBudgetType.SPEND) {
|
|
const campaignBudgetData = promotionCodeCampaignBudgetMap.get(
|
|
campaignBudget.id
|
|
) || { id: campaignBudget.id, used: campaignBudget.used ?? 0 }
|
|
|
|
campaignBudgetData.used = MathBN.add(
|
|
campaignBudgetData.used ?? 0,
|
|
computedAction.amount
|
|
)
|
|
|
|
if (
|
|
campaignBudget.limit &&
|
|
MathBN.gt(campaignBudgetData.used, campaignBudget.limit)
|
|
) {
|
|
continue
|
|
}
|
|
|
|
promotionCodeCampaignBudgetMap.set(
|
|
campaignBudget.id,
|
|
campaignBudgetData
|
|
)
|
|
}
|
|
|
|
if (campaignBudget.type === CampaignBudgetType.USAGE) {
|
|
const promotionAlreadyUsed =
|
|
promotionCodeUsageMap.get(promotion.code!) || false
|
|
|
|
if (promotionAlreadyUsed) {
|
|
continue
|
|
}
|
|
|
|
const campaignBudgetData = {
|
|
id: campaignBudget.id,
|
|
used: MathBN.add(campaignBudget.used ?? 0, 1),
|
|
}
|
|
|
|
if (
|
|
campaignBudget.limit &&
|
|
MathBN.gt(campaignBudgetData.used, campaignBudget.limit)
|
|
) {
|
|
continue
|
|
}
|
|
|
|
promotionCodeCampaignBudgetMap.set(
|
|
campaignBudget.id,
|
|
campaignBudgetData
|
|
)
|
|
|
|
promotionCodeUsageMap.set(promotion.code!, true)
|
|
}
|
|
|
|
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
|
|
|
|
for (const [_, campaignBudgetData] of promotionCodeCampaignBudgetMap) {
|
|
campaignBudgetsData.push(campaignBudgetData)
|
|
}
|
|
|
|
await this.campaignBudgetService_.update(
|
|
campaignBudgetsData,
|
|
sharedContext
|
|
)
|
|
}
|
|
}
|
|
|
|
@InjectManager("baseRepository_")
|
|
async computeActions(
|
|
promotionCodes: string[],
|
|
applicationContext: PromotionTypes.ComputeActionContext,
|
|
options: PromotionTypes.ComputeActionOptions = {},
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<PromotionTypes.ComputeActions[]> {
|
|
const { prevent_auto_promotions: preventAutoPromotions } = options
|
|
const computedActions: PromotionTypes.ComputeActions[] = []
|
|
const { items = [], shipping_methods: shippingMethods = [] } =
|
|
applicationContext
|
|
const appliedItemCodes: string[] = []
|
|
const appliedShippingCodes: string[] = []
|
|
const codeAdjustmentMap = new Map<
|
|
string,
|
|
PromotionTypes.ComputeActionAdjustmentLine[]
|
|
>()
|
|
const methodIdPromoValueMap = new Map<string, number>()
|
|
const automaticPromotions = preventAutoPromotions
|
|
? []
|
|
: await this.list(
|
|
{ is_automatic: true },
|
|
{ select: ["code"], take: null },
|
|
sharedContext
|
|
)
|
|
|
|
// Promotions we need to apply includes all the codes that are passed as an argument
|
|
// to this method, along with any automatic promotions that can be applied to the context
|
|
const automaticPromotionCodes = automaticPromotions.map((p) => p.code!)
|
|
const promotionCodesToApply = [
|
|
...promotionCodes,
|
|
...automaticPromotionCodes,
|
|
]
|
|
|
|
items.forEach((item) => {
|
|
item.adjustments?.forEach((adjustment) => {
|
|
if (isString(adjustment.code)) {
|
|
const adjustments = codeAdjustmentMap.get(adjustment.code) || []
|
|
|
|
adjustments.push(adjustment)
|
|
|
|
codeAdjustmentMap.set(adjustment.code, adjustments)
|
|
appliedItemCodes.push(adjustment.code)
|
|
}
|
|
})
|
|
})
|
|
|
|
shippingMethods.forEach((shippingMethod) => {
|
|
shippingMethod.adjustments?.forEach((adjustment) => {
|
|
if (isString(adjustment.code)) {
|
|
const adjustments = codeAdjustmentMap.get(adjustment.code) || []
|
|
|
|
adjustments.push(adjustment)
|
|
|
|
codeAdjustmentMap.set(adjustment.code, adjustments)
|
|
appliedShippingCodes.push(adjustment.code)
|
|
}
|
|
})
|
|
})
|
|
|
|
const promotions = await this.list(
|
|
{
|
|
code: [
|
|
...promotionCodesToApply,
|
|
...appliedItemCodes,
|
|
...appliedShippingCodes,
|
|
],
|
|
},
|
|
{
|
|
relations: [
|
|
"application_method",
|
|
"application_method.target_rules",
|
|
"application_method.target_rules.values",
|
|
"application_method.buy_rules",
|
|
"application_method.buy_rules.values",
|
|
"rules",
|
|
"rules.values",
|
|
"campaign",
|
|
"campaign.budget",
|
|
],
|
|
take: null,
|
|
}
|
|
)
|
|
|
|
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
|
|
promotions.map((promotion) => [promotion.code!, promotion])
|
|
)
|
|
|
|
// We look at any existing promo codes applied in the context and recommend
|
|
// them to be removed to start calculations from the beginning and refresh
|
|
// the adjustments if they are requested to be applied again
|
|
const appliedCodes = [...appliedShippingCodes, ...appliedItemCodes]
|
|
|
|
for (const appliedCode of appliedCodes) {
|
|
const promotion = existingPromotionsMap.get(appliedCode)
|
|
const adjustments = codeAdjustmentMap.get(appliedCode) || []
|
|
const action = appliedShippingCodes.includes(appliedCode)
|
|
? ComputedActions.REMOVE_SHIPPING_METHOD_ADJUSTMENT
|
|
: ComputedActions.REMOVE_ITEM_ADJUSTMENT
|
|
|
|
if (!promotion) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`Applied Promotion for code (${appliedCode}) not found`
|
|
)
|
|
}
|
|
|
|
adjustments.forEach((adjustment) =>
|
|
computedActions.push({
|
|
action,
|
|
adjustment_id: adjustment.id,
|
|
code: appliedCode,
|
|
})
|
|
)
|
|
}
|
|
|
|
// We sort the promo codes to apply with buy get type first as they
|
|
// are likely to be most valuable.
|
|
const sortedPermissionsToApply = promotions
|
|
.filter((p) => promotionCodesToApply.includes(p.code!))
|
|
.sort(ComputeActionUtils.sortByBuyGetType)
|
|
|
|
for (const promotionToApply of sortedPermissionsToApply) {
|
|
const promotion = existingPromotionsMap.get(promotionToApply.code!)!
|
|
|
|
const {
|
|
application_method: applicationMethod,
|
|
rules: promotionRules = [],
|
|
} = promotion
|
|
|
|
if (!applicationMethod) {
|
|
continue
|
|
}
|
|
|
|
const isPromotionApplicable = areRulesValidForContext(
|
|
promotionRules,
|
|
applicationContext
|
|
)
|
|
|
|
if (!isPromotionApplicable) {
|
|
continue
|
|
}
|
|
|
|
if (promotion.type === PromotionType.BUYGET) {
|
|
const computedActionsForItems =
|
|
ComputeActionUtils.getComputedActionsForBuyGet(
|
|
promotion,
|
|
applicationContext[ApplicationMethodTargetType.ITEMS],
|
|
methodIdPromoValueMap
|
|
)
|
|
|
|
computedActions.push(...computedActionsForItems)
|
|
}
|
|
|
|
if (promotion.type === PromotionType.STANDARD) {
|
|
const isTargetOrder =
|
|
applicationMethod.target_type === ApplicationMethodTargetType.ORDER
|
|
const isTargetItems =
|
|
applicationMethod.target_type === ApplicationMethodTargetType.ITEMS
|
|
const isTargetShipping =
|
|
applicationMethod.target_type ===
|
|
ApplicationMethodTargetType.SHIPPING_METHODS
|
|
const allocationOverride = isTargetOrder
|
|
? ApplicationMethodAllocation.ACROSS
|
|
: undefined
|
|
|
|
if (isTargetOrder || isTargetItems) {
|
|
const computedActionsForItems =
|
|
ComputeActionUtils.getComputedActionsForItems(
|
|
promotion,
|
|
applicationContext[ApplicationMethodTargetType.ITEMS],
|
|
methodIdPromoValueMap,
|
|
allocationOverride
|
|
)
|
|
|
|
computedActions.push(...computedActionsForItems)
|
|
}
|
|
|
|
if (isTargetShipping) {
|
|
const computedActionsForShippingMethods =
|
|
ComputeActionUtils.getComputedActionsForShippingMethods(
|
|
promotion,
|
|
applicationContext[ApplicationMethodTargetType.SHIPPING_METHODS],
|
|
methodIdPromoValueMap
|
|
)
|
|
|
|
computedActions.push(...computedActionsForShippingMethods)
|
|
}
|
|
}
|
|
}
|
|
|
|
transformPropertiesToBigNumber(computedActions, { include: ["amount"] })
|
|
|
|
return computedActions
|
|
}
|
|
|
|
async create(
|
|
data: PromotionTypes.CreatePromotionDTO,
|
|
sharedContext?: Context
|
|
): Promise<PromotionTypes.PromotionDTO>
|
|
|
|
async create(
|
|
data: PromotionTypes.CreatePromotionDTO[],
|
|
sharedContext?: Context
|
|
): Promise<PromotionTypes.PromotionDTO[]>
|
|
|
|
@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",
|
|
"application_method.target_rules",
|
|
"application_method.target_rules.values",
|
|
"application_method.buy_rules",
|
|
"application_method.buy_rules.values",
|
|
"rules",
|
|
"rules.values",
|
|
"campaign",
|
|
"campaign.budget",
|
|
],
|
|
take: null,
|
|
},
|
|
sharedContext
|
|
)
|
|
|
|
return Array.isArray(data) ? promotions : promotions[0]
|
|
}
|
|
|
|
@InjectTransactionManager("baseRepository_")
|
|
protected async create_(
|
|
data: PromotionTypes.CreatePromotionDTO[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
) {
|
|
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,
|
|
PromotionTypes.CreateApplicationMethodDTO
|
|
>()
|
|
const promotionCodeRulesDataMap = new Map<
|
|
string,
|
|
PromotionTypes.CreatePromotionRuleDTO[]
|
|
>()
|
|
const methodTargetRulesMap = new Map<
|
|
string,
|
|
PromotionTypes.CreatePromotionRuleDTO[]
|
|
>()
|
|
const methodBuyRulesMap = new Map<
|
|
string,
|
|
PromotionTypes.CreatePromotionRuleDTO[]
|
|
>()
|
|
const promotionCodeCampaignMap = new Map<
|
|
string,
|
|
PromotionTypes.CreateCampaignDTO
|
|
>()
|
|
|
|
for (const {
|
|
application_method: applicationMethodData,
|
|
rules: rulesData,
|
|
campaign: campaignData,
|
|
campaign_id: campaignId,
|
|
...promotionData
|
|
} of data) {
|
|
promotionCodeApplicationMethodDataMap.set(
|
|
promotionData.code,
|
|
applicationMethodData
|
|
)
|
|
|
|
if (rulesData) {
|
|
promotionCodeRulesDataMap.set(promotionData.code, rulesData)
|
|
}
|
|
|
|
if (campaignData && campaignId) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`Provide either the 'campaign' or 'campaign_id' parameter; both cannot be used simultaneously.`
|
|
)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
promotionsData.push({
|
|
...promotionData,
|
|
campaign_id: campaignId,
|
|
})
|
|
}
|
|
|
|
const createdPromotions = await this.promotionService_.create(
|
|
promotionsData,
|
|
sharedContext
|
|
)
|
|
|
|
for (const promotion of createdPromotions) {
|
|
const applMethodData = promotionCodeApplicationMethodDataMap.get(
|
|
promotion.code
|
|
)
|
|
|
|
const campaignData = promotionCodeCampaignMap.get(promotion.code)
|
|
|
|
if (campaignData) {
|
|
campaignsData.push({
|
|
...campaignData,
|
|
promotions: [promotion],
|
|
})
|
|
}
|
|
|
|
if (applMethodData) {
|
|
const {
|
|
target_rules: targetRulesData = [],
|
|
buy_rules: buyRulesData = [],
|
|
...applicationMethodWithoutRules
|
|
} = applMethodData
|
|
const applicationMethodData = {
|
|
...applicationMethodWithoutRules,
|
|
promotion,
|
|
}
|
|
|
|
if (
|
|
applicationMethodData.target_type ===
|
|
ApplicationMethodTargetType.ORDER &&
|
|
targetRulesData.length
|
|
) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`Target rules for application method with target type (${ApplicationMethodTargetType.ORDER}) is not allowed`
|
|
)
|
|
}
|
|
|
|
if (promotion.type === PromotionType.BUYGET && !buyRulesData.length) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`Buy rules are required for ${PromotionType.BUYGET} promotion type`
|
|
)
|
|
}
|
|
|
|
if (
|
|
promotion.type === PromotionType.BUYGET &&
|
|
!targetRulesData.length
|
|
) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`Target rules are required for ${PromotionType.BUYGET} promotion type`
|
|
)
|
|
}
|
|
|
|
validateApplicationMethodAttributes(applicationMethodData, promotion)
|
|
|
|
applicationMethodsData.push(applicationMethodData)
|
|
|
|
if (targetRulesData.length) {
|
|
methodTargetRulesMap.set(promotion.id, targetRulesData)
|
|
}
|
|
|
|
if (buyRulesData.length) {
|
|
methodBuyRulesMap.set(promotion.id, buyRulesData)
|
|
}
|
|
}
|
|
|
|
await this.createPromotionRulesAndValues_(
|
|
promotionCodeRulesDataMap.get(promotion.code) || [],
|
|
"promotions",
|
|
promotion,
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
const createdApplicationMethods =
|
|
await this.applicationMethodService_.create(
|
|
applicationMethodsData,
|
|
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) {
|
|
await this.createPromotionRulesAndValues_(
|
|
methodTargetRulesMap.get(applicationMethod.promotion.id) || [],
|
|
"method_target_rules",
|
|
applicationMethod,
|
|
sharedContext
|
|
)
|
|
|
|
await this.createPromotionRulesAndValues_(
|
|
methodBuyRulesMap.get(applicationMethod.promotion.id) || [],
|
|
"method_buy_rules",
|
|
applicationMethod,
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
return createdPromotions
|
|
}
|
|
|
|
async update(
|
|
data: PromotionTypes.UpdatePromotionDTO,
|
|
sharedContext?: Context
|
|
): Promise<PromotionTypes.PromotionDTO>
|
|
|
|
async update(
|
|
data: PromotionTypes.UpdatePromotionDTO[],
|
|
sharedContext?: Context
|
|
): Promise<PromotionTypes.PromotionDTO[]>
|
|
|
|
@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",
|
|
"application_method.target_rules",
|
|
"application_method.target_rules.values",
|
|
"rules",
|
|
"rules.values",
|
|
"campaign",
|
|
"campaign.budget",
|
|
],
|
|
take: null,
|
|
},
|
|
sharedContext
|
|
)
|
|
|
|
return Array.isArray(data) ? promotions : promotions[0]
|
|
}
|
|
|
|
@InjectTransactionManager("baseRepository_")
|
|
protected async update_(
|
|
data: PromotionTypes.UpdatePromotionDTO[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
) {
|
|
const promotionIds = data.map((d) => d.id)
|
|
const existingPromotions = await this.promotionService_.list(
|
|
{ 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])
|
|
)
|
|
|
|
const promotionsData: UpdatePromotionDTO[] = []
|
|
const applicationMethodsData: UpdateApplicationMethodDTO[] = []
|
|
|
|
for (const {
|
|
application_method: applicationMethodData,
|
|
campaign_id: campaignId,
|
|
...promotionData
|
|
} of data) {
|
|
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 || !existingApplicationMethod) {
|
|
continue
|
|
}
|
|
|
|
if (
|
|
applicationMethodData.allocation &&
|
|
!allowedAllocationForQuantity.includes(applicationMethodData.allocation)
|
|
) {
|
|
applicationMethodData.max_quantity = null
|
|
existingApplicationMethod.max_quantity = null
|
|
}
|
|
|
|
validateApplicationMethodAttributes(
|
|
applicationMethodData,
|
|
existingPromotion
|
|
)
|
|
|
|
applicationMethodsData.push({
|
|
...applicationMethodData,
|
|
id: existingApplicationMethod.id,
|
|
})
|
|
}
|
|
|
|
const updatedPromotions = this.promotionService_.update(
|
|
promotionsData,
|
|
sharedContext
|
|
)
|
|
|
|
if (applicationMethodsData.length) {
|
|
await this.applicationMethodService_.update(
|
|
applicationMethodsData,
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
return updatedPromotions
|
|
}
|
|
|
|
@InjectManager("baseRepository_")
|
|
async updatePromotionRules(
|
|
data: PromotionTypes.UpdatePromotionRuleDTO[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<PromotionTypes.PromotionRuleDTO[]> {
|
|
const updatedPromotionRules = await this.updatePromotionRules_(
|
|
data,
|
|
sharedContext
|
|
)
|
|
|
|
return this.listPromotionRules(
|
|
{ id: updatedPromotionRules.map((r) => r.id) },
|
|
{ relations: ["values"] },
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
@InjectTransactionManager("baseRepository_")
|
|
protected async updatePromotionRules_(
|
|
data: PromotionTypes.UpdatePromotionRuleDTO[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
) {
|
|
const promotionRuleIds = data.map((d) => d.id)
|
|
|
|
const promotionRules = await this.listPromotionRules(
|
|
{ id: promotionRuleIds },
|
|
{ relations: ["values"] },
|
|
sharedContext
|
|
)
|
|
|
|
const invalidRuleId = arrayDifference(
|
|
deduplicate(promotionRuleIds),
|
|
promotionRules.map((pr) => pr.id)
|
|
)
|
|
|
|
if (invalidRuleId.length) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`Promotion rules with id - ${invalidRuleId.join(", ")} not found`
|
|
)
|
|
}
|
|
|
|
const promotionRulesMap = new Map<string, PromotionTypes.PromotionRuleDTO>(
|
|
promotionRules.map((pr) => [pr.id, pr])
|
|
)
|
|
|
|
const rulesToUpdate: PromotionTypes.UpdatePromotionRuleDTO[] = []
|
|
const ruleValueIdsToDelete: string[] = []
|
|
const ruleValuesToCreate: CreatePromotionRuleValueDTO[] = []
|
|
|
|
for (const promotionRuleData of data) {
|
|
const { values, ...rest } = promotionRuleData
|
|
const normalizedValues = Array.isArray(values) ? values : [values]
|
|
rulesToUpdate.push(rest)
|
|
|
|
if (isDefined(values)) {
|
|
const promotionRule = promotionRulesMap.get(promotionRuleData.id)!
|
|
|
|
ruleValueIdsToDelete.push(...promotionRule.values.map((v) => v.id))
|
|
ruleValuesToCreate.push(
|
|
...normalizedValues.map((value) => ({
|
|
value,
|
|
promotion_rule: promotionRule,
|
|
}))
|
|
)
|
|
}
|
|
}
|
|
|
|
const [updatedRules] = await Promise.all([
|
|
this.promotionRuleService_.update(rulesToUpdate, sharedContext),
|
|
this.promotionRuleValueService_.delete(
|
|
ruleValueIdsToDelete,
|
|
sharedContext
|
|
),
|
|
this.promotionRuleValueService_.create(ruleValuesToCreate, sharedContext),
|
|
])
|
|
|
|
return updatedRules
|
|
}
|
|
|
|
@InjectManager("baseRepository_")
|
|
async addPromotionRules(
|
|
promotionId: string,
|
|
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<PromotionTypes.PromotionRuleDTO[]> {
|
|
const promotion = await this.promotionService_.retrieve(promotionId)
|
|
|
|
const createdPromotionRules = await this.createPromotionRulesAndValues_(
|
|
rulesData,
|
|
"promotions",
|
|
promotion,
|
|
sharedContext
|
|
)
|
|
|
|
return this.listPromotionRules(
|
|
{ id: createdPromotionRules.map((r) => r.id) },
|
|
{ relations: ["values"] },
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
@InjectManager("baseRepository_")
|
|
async addPromotionTargetRules(
|
|
promotionId: string,
|
|
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<PromotionTypes.PromotionRuleDTO[]> {
|
|
const promotion = await this.promotionService_.retrieve(promotionId, {
|
|
relations: ["application_method"],
|
|
})
|
|
|
|
const applicationMethod = promotion.application_method
|
|
|
|
if (!applicationMethod) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`application_method for promotion not found`
|
|
)
|
|
}
|
|
|
|
const createdPromotionRules = await this.createPromotionRulesAndValues_(
|
|
rulesData,
|
|
"method_target_rules",
|
|
applicationMethod,
|
|
sharedContext
|
|
)
|
|
|
|
return await this.listPromotionRules(
|
|
{ id: createdPromotionRules.map((pr) => pr.id) },
|
|
{ relations: ["values"] },
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
@InjectManager("baseRepository_")
|
|
async addPromotionBuyRules(
|
|
promotionId: string,
|
|
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<PromotionTypes.PromotionRuleDTO[]> {
|
|
const promotion = await this.promotionService_.retrieve(
|
|
promotionId,
|
|
{ relations: ["application_method"] },
|
|
sharedContext
|
|
)
|
|
|
|
const applicationMethod = promotion.application_method
|
|
|
|
if (!applicationMethod) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`application_method for promotion not found`
|
|
)
|
|
}
|
|
|
|
const createdPromotionRules = await this.createPromotionRulesAndValues_(
|
|
rulesData,
|
|
"method_buy_rules",
|
|
applicationMethod,
|
|
sharedContext
|
|
)
|
|
|
|
return await this.listPromotionRules(
|
|
{ id: createdPromotionRules.map((pr) => pr.id) },
|
|
{ relations: ["values"] },
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
@InjectTransactionManager("baseRepository_")
|
|
protected async createPromotionRulesAndValues_(
|
|
rulesData: PromotionTypes.CreatePromotionRuleDTO[],
|
|
relationName: "promotions" | "method_target_rules" | "method_buy_rules",
|
|
relation: Promotion | ApplicationMethod,
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<TPromotionRule[]> {
|
|
const createdPromotionRules: TPromotionRule[] = []
|
|
const promotion =
|
|
relation instanceof ApplicationMethod ? relation.promotion : relation
|
|
|
|
if (!rulesData.length) {
|
|
return []
|
|
}
|
|
|
|
if (
|
|
relationName === "method_buy_rules" &&
|
|
promotion.type === PromotionType.STANDARD
|
|
) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`Can't add buy rules to a ${PromotionType.STANDARD} promotion`
|
|
)
|
|
}
|
|
|
|
validatePromotionRuleAttributes(rulesData)
|
|
|
|
for (const ruleData of rulesData) {
|
|
const { values, ...rest } = ruleData
|
|
const promotionRuleData: CreatePromotionRuleDTO = {
|
|
...rest,
|
|
[relationName]: [relation],
|
|
}
|
|
|
|
const [createdPromotionRule] = await this.promotionRuleService_.create(
|
|
[promotionRuleData],
|
|
sharedContext
|
|
)
|
|
|
|
createdPromotionRules.push(createdPromotionRule)
|
|
|
|
const ruleValues = Array.isArray(values) ? values : [values]
|
|
const promotionRuleValuesData = ruleValues.map((ruleValue) => ({
|
|
value: ruleValue,
|
|
promotion_rule: createdPromotionRule,
|
|
}))
|
|
|
|
await this.promotionRuleValueService_.create(
|
|
promotionRuleValuesData,
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
return createdPromotionRules
|
|
}
|
|
|
|
@InjectManager("baseRepository_")
|
|
async removePromotionRules(
|
|
promotionId: string,
|
|
ruleIds: string[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<void> {
|
|
await this.removePromotionRules_(promotionId, ruleIds, sharedContext)
|
|
}
|
|
|
|
@InjectTransactionManager("baseRepository_")
|
|
protected async removePromotionRules_(
|
|
promotionId: string,
|
|
ruleIds: string[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<void> {
|
|
const promotion = await this.promotionService_.retrieve(
|
|
promotionId,
|
|
{ relations: ["rules"] },
|
|
sharedContext
|
|
)
|
|
|
|
const existingRuleIds = promotion.rules.map((rule) => rule.id)
|
|
const idsToRemove = ruleIds.filter((id) => existingRuleIds.includes(id))
|
|
|
|
await this.promotionRuleService_.delete(idsToRemove, sharedContext)
|
|
}
|
|
|
|
@InjectManager("baseRepository_")
|
|
async removePromotionTargetRules(
|
|
promotionId: string,
|
|
ruleIds: string[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<void> {
|
|
await this.removeApplicationMethodRules_(
|
|
promotionId,
|
|
ruleIds,
|
|
ApplicationMethodRuleTypes.TARGET_RULES,
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
@InjectManager("baseRepository_")
|
|
async removePromotionBuyRules(
|
|
promotionId: string,
|
|
ruleIds: string[],
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<void> {
|
|
await this.removeApplicationMethodRules_(
|
|
promotionId,
|
|
ruleIds,
|
|
ApplicationMethodRuleTypes.BUY_RULES,
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
@InjectTransactionManager("baseRepository_")
|
|
protected async removeApplicationMethodRules_(
|
|
promotionId: string,
|
|
ruleIds: string[],
|
|
relation:
|
|
| ApplicationMethodRuleTypes.TARGET_RULES
|
|
| ApplicationMethodRuleTypes.BUY_RULES,
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<void> {
|
|
const promotion = await this.promotionService_.retrieve(
|
|
promotionId,
|
|
{ relations: [`application_method.${relation}`] },
|
|
sharedContext
|
|
)
|
|
|
|
const applicationMethod = promotion.application_method
|
|
|
|
if (!applicationMethod) {
|
|
throw new MedusaError(
|
|
MedusaError.Types.INVALID_DATA,
|
|
`application_method for promotion not found`
|
|
)
|
|
}
|
|
|
|
const targetRuleIdsToRemove = applicationMethod[relation]
|
|
.filter((rule) => ruleIds.includes(rule.id))
|
|
.map((rule) => rule.id)
|
|
|
|
await this.promotionRuleService_.delete(
|
|
targetRuleIdsToRemove,
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
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", "promotions"],
|
|
take: null,
|
|
},
|
|
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) {
|
|
this.validateCampaignBudgetData(campaignBudgetData)
|
|
|
|
campaignBudgetsData.push({
|
|
...campaignBudgetData,
|
|
campaign: createdCampaign.id,
|
|
})
|
|
}
|
|
}
|
|
|
|
if (campaignBudgetsData.length) {
|
|
await this.campaignBudgetService_.create(
|
|
campaignBudgetsData,
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
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
|
|
): 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", "promotions"],
|
|
take: null,
|
|
},
|
|
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 updateBudgetData: UpdateCampaignBudgetDTO[] = []
|
|
const createBudgetData: CreateCampaignBudgetDTO[] = []
|
|
|
|
const existingCampaigns = await this.listCampaigns(
|
|
{ id: campaignIds },
|
|
{ relations: ["budget"], take: null },
|
|
sharedContext
|
|
)
|
|
|
|
const existingCampaignsMap = new Map<string, PromotionTypes.CampaignDTO>(
|
|
existingCampaigns.map((campaign) => [campaign.id, campaign])
|
|
)
|
|
|
|
for (const updateCampaignData of data) {
|
|
const { budget: budgetData, ...campaignData } = updateCampaignData
|
|
const existingCampaign = existingCampaignsMap.get(campaignData.id)!
|
|
|
|
campaignsData.push(campaignData)
|
|
|
|
// 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,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const updatedCampaigns = await this.campaignService_.update(
|
|
campaignsData,
|
|
sharedContext
|
|
)
|
|
|
|
if (updateBudgetData.length) {
|
|
await this.campaignBudgetService_.update(updateBudgetData, sharedContext)
|
|
}
|
|
|
|
if (createBudgetData.length) {
|
|
await this.campaignBudgetService_.create(createBudgetData, sharedContext)
|
|
}
|
|
|
|
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, relations: ["application_method"] },
|
|
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.`
|
|
)
|
|
}
|
|
|
|
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,
|
|
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)
|
|
}
|
|
}
|