feat(core-flows,dashboard,js-sdk,promotion,medusa,types,utils): limit promotion usage per customer (#13451)

**What**
- implement promotion usage limits per customer/email
- fix registering spend usage over the limit
- fix type errors in promotion module tests

**How**
- introduce a new type of campaign budget that can be defined by an attribute such as customer id or email
- add `CampaignBudgetUsage` entity to keep track of the number of uses per attribute value
- update `registerUsage` and `computeActions` in the promotion module to work with the new type
- update `core-flows` to pass context needed for usage calculation to the promotion module

**Breaking**
- registering promotion usage now throws (and cart complete fails) if the budget limit is exceeded or if the cart completion would result in a breached limit

---

CLOSES CORE-1172
CLOSES CORE-1173
CLOSES CORE-1174
CLOSES CORE-1175


Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
Frane Polić
2025-10-09 14:35:54 +02:00
committed by GitHub
parent 924564bee5
commit 7dc3b0c5ff
36 changed files with 2390 additions and 190 deletions

View File

@@ -2,6 +2,7 @@
// Always ensure that cartFieldsForCalculateShippingOptionsPrices is present in cartFieldsForRefreshSteps
export const cartFieldsForRefreshSteps = [
"id",
"email",
"currency_code",
"quantity",
"subtotal",

View File

@@ -338,7 +338,13 @@ export const completeCartWorkflow = createWorkflow(
})
}
return promotionUsage
return {
computedActions: promotionUsage,
registrationContext: {
customer_id: cart.customer?.id || null,
customer_email: cart.email || null,
},
}
}
)

View File

@@ -1,4 +1,5 @@
import {
CampaignBudgetUsageContext,
IPromotionModuleService,
UsageComputedActions,
} from "@medusajs/framework/types"
@@ -6,26 +7,37 @@ import { Modules } from "@medusajs/framework/utils"
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
export const registerUsageStepId = "register-usage"
type RegisterUsageStepInput = {
computedActions: UsageComputedActions[]
registrationContext: CampaignBudgetUsageContext
}
/**
* This step registers usage for a promotion.
*/
export const registerUsageStep = createStep(
registerUsageStepId,
async (data: UsageComputedActions[], { container }) => {
if (!data.length) {
return new StepResponse(null, [])
async (data: RegisterUsageStepInput, { container }) => {
if (!data.computedActions.length) {
return new StepResponse(null, {
computedActions: [],
registrationContext: data.registrationContext,
})
}
const promotionModule = container.resolve<IPromotionModuleService>(
Modules.PROMOTION
)
await promotionModule.registerUsage(data)
await promotionModule.registerUsage(
data.computedActions,
data.registrationContext
)
return new StepResponse(null, data)
},
async (revertData, { container }) => {
if (!revertData?.length) {
if (!revertData?.computedActions.length) {
return
}
@@ -33,6 +45,9 @@ export const registerUsageStep = createStep(
Modules.PROMOTION
)
await promotionModule.revertUsage(revertData)
await promotionModule.revertUsage(
revertData.computedActions,
revertData.registrationContext
)
}
)