feat(core-flows,types,promotion): register promotion campaign usage upon cart completion (#8970)
* feat(core-flows,types,promotion): register promotion campaign usage upon cart completion * Apply suggestions from code review Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
1 parent
2a055b71ef
commit
5d7179b7d0
8 files changed
+421
-56
No files matched your search
@@ -1812,6 +1812,7 @@ medusaIntegrationTestRunner({
|
|||||||
let paymentSession
|
let paymentSession
|
||||||
let stockLocation
|
let stockLocation
|
||||||
let inventoryItem
|
let inventoryItem
|
||||||
|
let promotion
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await setupTaxStructure(taxModule)
|
await setupTaxStructure(taxModule)
|
||||||
@@ -1925,6 +1926,43 @@ medusaIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
promotion = (
|
||||||
|
await api.post(
|
||||||
|
`/admin/promotions`,
|
||||||
|
{
|
||||||
|
code: "TEST",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
is_automatic: true,
|
||||||
|
campaign: {
|
||||||
|
campaign_identifier: "test",
|
||||||
|
name: "test",
|
||||||
|
budget: {
|
||||||
|
type: "spend",
|
||||||
|
limit: 1000,
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
application_method: {
|
||||||
|
target_type: "items",
|
||||||
|
type: "fixed",
|
||||||
|
allocation: "each",
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
value: 10,
|
||||||
|
max_quantity: 1,
|
||||||
|
target_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product_id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product.id],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
rules: [],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/stock-locations/${stockLocation.id}/fulfillment-providers`,
|
`/admin/stock-locations/${stockLocation.id}/fulfillment-providers`,
|
||||||
{ add: ["manual_test-provider"] },
|
{ add: ["manual_test-provider"] },
|
||||||
@@ -2015,16 +2053,16 @@ medusaIntegrationTestRunner({
|
|||||||
type: "order",
|
type: "order",
|
||||||
order: expect.objectContaining({
|
order: expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
total: 106,
|
total: 94.764,
|
||||||
subtotal: 100,
|
subtotal: 100,
|
||||||
tax_total: 6,
|
tax_total: 5.364,
|
||||||
discount_total: 0,
|
discount_total: 10.6,
|
||||||
discount_tax_total: 0,
|
discount_tax_total: 0.636,
|
||||||
original_total: 106,
|
original_total: 95.4,
|
||||||
original_tax_total: 6,
|
original_tax_total: 6,
|
||||||
item_total: 106,
|
item_total: 94.764,
|
||||||
item_subtotal: 100,
|
item_subtotal: 100,
|
||||||
item_tax_total: 6,
|
item_tax_total: 5.364,
|
||||||
original_item_total: 106,
|
original_item_total: 106,
|
||||||
original_item_subtotal: 100,
|
original_item_subtotal: 100,
|
||||||
original_item_tax_total: 6,
|
original_item_tax_total: 6,
|
||||||
@@ -2040,17 +2078,27 @@ medusaIntegrationTestRunner({
|
|||||||
product_id: product.id,
|
product_id: product.id,
|
||||||
unit_price: 100,
|
unit_price: 100,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
tax_total: 6,
|
tax_total: 5.364,
|
||||||
|
total: 94.764,
|
||||||
subtotal: 100,
|
subtotal: 100,
|
||||||
total: 106,
|
|
||||||
original_total: 106,
|
original_total: 106,
|
||||||
discount_total: 0,
|
discount_total: 10.6,
|
||||||
|
discount_tax_total: 0.636,
|
||||||
|
original_tax_total: 6,
|
||||||
tax_lines: [
|
tax_lines: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
rate: 6,
|
rate: 6,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
adjustments: [],
|
adjustments: [
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: 10,
|
||||||
|
promotion_id: promotion.id,
|
||||||
|
code: promotion.code,
|
||||||
|
subtotal: 10,
|
||||||
|
total: 10.6,
|
||||||
|
}),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
shipping_address: expect.objectContaining({
|
shipping_address: expect.objectContaining({
|
||||||
@@ -2062,6 +2110,12 @@ medusaIntegrationTestRunner({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
promotion = (
|
||||||
|
await api.get(`/admin/promotions/${promotion.id}`, adminHeaders)
|
||||||
|
).data.promotion
|
||||||
|
|
||||||
|
expect(promotion.campaign.budget.used).toEqual(10)
|
||||||
|
|
||||||
const reservation = await api.get(`/admin/reservations`, adminHeaders)
|
const reservation = await api.get(`/admin/reservations`, adminHeaders)
|
||||||
const reservationItem = reservation.data.reservations[0]
|
const reservationItem = reservation.data.reservations[0]
|
||||||
|
|
||||||
@@ -2086,7 +2140,7 @@ medusaIntegrationTestRunner({
|
|||||||
payment_collections: [
|
payment_collections: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
amount: 106,
|
amount: 94.764,
|
||||||
status: "authorized",
|
status: "authorized",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { OrderDTO } from "@medusajs/types"
|
import {
|
||||||
|
CartWorkflowDTO,
|
||||||
|
OrderDTO,
|
||||||
|
UsageComputedActions,
|
||||||
|
} from "@medusajs/types"
|
||||||
import { Modules, OrderStatus, OrderWorkflowEvents } from "@medusajs/utils"
|
import { Modules, OrderStatus, OrderWorkflowEvents } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
WorkflowData,
|
|
||||||
WorkflowResponse,
|
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
parallelize,
|
parallelize,
|
||||||
transform,
|
transform,
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
} from "@medusajs/workflows-sdk"
|
} from "@medusajs/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
createRemoteLinkStep,
|
createRemoteLinkStep,
|
||||||
@@ -14,6 +18,7 @@ import {
|
|||||||
} from "../../common"
|
} from "../../common"
|
||||||
import { createOrdersStep } from "../../order/steps/create-orders"
|
import { createOrdersStep } from "../../order/steps/create-orders"
|
||||||
import { authorizePaymentSessionStep } from "../../payment/steps/authorize-payment-session"
|
import { authorizePaymentSessionStep } from "../../payment/steps/authorize-payment-session"
|
||||||
|
import { registerUsageStep } from "../../promotion/steps/register-usage"
|
||||||
import { updateCartsStep, validateCartPaymentsStep } from "../steps"
|
import { updateCartsStep, validateCartPaymentsStep } from "../steps"
|
||||||
import { reserveInventoryStep } from "../steps/reserve-inventory"
|
import { reserveInventoryStep } from "../steps/reserve-inventory"
|
||||||
import { validateCartStep } from "../steps/validate-cart"
|
import { validateCartStep } from "../steps/validate-cart"
|
||||||
@@ -61,7 +66,8 @@ export const completeCartWorkflow = createWorkflow(
|
|||||||
(data) => {
|
(data) => {
|
||||||
const allItems: any[] = []
|
const allItems: any[] = []
|
||||||
const allVariants: any[] = []
|
const allVariants: any[] = []
|
||||||
data.cart.items.forEach((item) => {
|
|
||||||
|
data.cart?.items?.forEach((item) => {
|
||||||
allItems.push({
|
allItems.push({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
variant_id: item.variant_id,
|
variant_id: item.variant_id,
|
||||||
@@ -188,6 +194,39 @@ export const completeCartWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const promotionUsage = transform(
|
||||||
|
{ cart },
|
||||||
|
({ cart }: { cart: CartWorkflowDTO }) => {
|
||||||
|
const promotionUsage: UsageComputedActions[] = []
|
||||||
|
|
||||||
|
const itemAdjustments = (cart.items ?? [])
|
||||||
|
.map((item) => item.adjustments ?? [])
|
||||||
|
.flat(1)
|
||||||
|
|
||||||
|
const shippingAdjustments = (cart.shipping_methods ?? [])
|
||||||
|
.map((item) => item.adjustments ?? [])
|
||||||
|
.flat(1)
|
||||||
|
|
||||||
|
for (const adjustment of itemAdjustments) {
|
||||||
|
promotionUsage.push({
|
||||||
|
amount: adjustment.amount,
|
||||||
|
code: adjustment.code!,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const adjustment of shippingAdjustments) {
|
||||||
|
promotionUsage.push({
|
||||||
|
amount: adjustment.amount,
|
||||||
|
code: adjustment.code!,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return promotionUsage
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
registerUsageStep(promotionUsage)
|
||||||
|
|
||||||
return new WorkflowResponse(order)
|
return new WorkflowResponse(order)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { IPromotionModuleService, UsageComputedActions } from "@medusajs/types"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
export const registerUsageStepId = "register-usage"
|
||||||
|
/**
|
||||||
|
* This step registers usage for promotion campaigns
|
||||||
|
*/
|
||||||
|
export const registerUsageStep = createStep(
|
||||||
|
registerUsageStepId,
|
||||||
|
async (data: UsageComputedActions[], { container }) => {
|
||||||
|
if (!data.length) {
|
||||||
|
return new StepResponse(null, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const promotionModule = container.resolve<IPromotionModuleService>(
|
||||||
|
ModuleRegistrationName.PROMOTION
|
||||||
|
)
|
||||||
|
|
||||||
|
await promotionModule.registerUsage(data)
|
||||||
|
|
||||||
|
return new StepResponse(null, data)
|
||||||
|
},
|
||||||
|
async (revertData, { container }) => {
|
||||||
|
if (!revertData?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const promotionModule = container.resolve<IPromotionModuleService>(
|
||||||
|
ModuleRegistrationName.PROMOTION
|
||||||
|
)
|
||||||
|
|
||||||
|
await promotionModule.revertUsage(revertData)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -13,9 +13,17 @@ export type ComputeActions =
|
|||||||
/**
|
/**
|
||||||
* These computed action types can affect a campaign's budget.
|
* These computed action types can affect a campaign's budget.
|
||||||
*/
|
*/
|
||||||
export type UsageComputedActions =
|
export type UsageComputedActions = {
|
||||||
| AddShippingMethodAdjustment
|
/**
|
||||||
| AddItemAdjustmentAction
|
* The amount to remove off the shipping method's total.
|
||||||
|
*/
|
||||||
|
amount: BigNumberInput
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The promotion's code.
|
||||||
|
*/
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This action indicates that the promotions within a campaign can no longer be used
|
* This action indicates that the promotions within a campaign can no longer be used
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
PromotionRuleDTO,
|
PromotionRuleDTO,
|
||||||
UpdatePromotionDTO,
|
UpdatePromotionDTO,
|
||||||
UpdatePromotionRuleDTO,
|
UpdatePromotionRuleDTO,
|
||||||
|
UsageComputedActions,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
import {
|
import {
|
||||||
AddPromotionsToCampaignDTO,
|
AddPromotionsToCampaignDTO,
|
||||||
@@ -32,26 +33,42 @@ export interface IPromotionModuleService extends IModuleService {
|
|||||||
* It adjusts the `used` property of a `CampaignBudget` to account for the adjustment amounts in the specified associated
|
* It adjusts the `used` property of a `CampaignBudget` to account for the adjustment amounts in the specified associated
|
||||||
* computed actions.
|
* computed actions.
|
||||||
*
|
*
|
||||||
* @param {ComputeActions[]} computedActions - The computed actions to adjust their promotion's campaign budget.
|
* @param {UsageComputedActions[]} computedActions - The computed actions to adjust their promotion's campaign budget.
|
||||||
* @returns {Promise<void>} Resolves when the campaign budgets have been adjusted successfully.
|
* @returns {Promise<void>} Resolves when the campaign budgets have been adjusted successfully.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await promotionModuleService.registerUsage([
|
* await promotionModuleService.registerUsage([
|
||||||
* {
|
* {
|
||||||
* action: "addItemAdjustment",
|
|
||||||
* item_id: "cali_123",
|
|
||||||
* amount: 50,
|
* amount: 50,
|
||||||
* code: "50OFF",
|
* code: "50OFF",
|
||||||
* },
|
* },
|
||||||
* {
|
* {
|
||||||
* action: "addShippingMethodAdjustment",
|
|
||||||
* shipping_method_id: "casm_123",
|
|
||||||
* amount: 5000,
|
* amount: 5000,
|
||||||
* code: "FREESHIPPING",
|
* code: "FREESHIPPING",
|
||||||
* },
|
* },
|
||||||
* ])
|
* ])
|
||||||
*/
|
*/
|
||||||
registerUsage(computedActions: ComputeActions[]): Promise<void>
|
registerUsage(computedActions: UsageComputedActions[]): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is used to revert the changes made by registerUsage action
|
||||||
|
*
|
||||||
|
* @param {UsageComputedActions[]} computedActions - The computed actions to adjust their promotion's campaign budget.
|
||||||
|
* @returns {Promise<void>} Resolves when the campaign budgets have been adjusted successfully.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* await promotionModuleService.revertUsage([
|
||||||
|
* {
|
||||||
|
* amount: 50,
|
||||||
|
* code: "50OFF",
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* amount: 5000,
|
||||||
|
* code: "FREESHIPPING",
|
||||||
|
* },
|
||||||
|
* ])
|
||||||
|
*/
|
||||||
|
revertUsage(computedActions: UsageComputedActions[]): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method provides the actions to perform on a cart based on the specified promotions
|
* This method provides the actions to perform on a cart based on the specified promotions
|
||||||
|
|||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
import { IPromotionModuleService } from "@medusajs/types"
|
||||||
|
import { Modules } from "@medusajs/utils"
|
||||||
|
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||||
|
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
||||||
|
import { createDefaultPromotion } from "../../../__fixtures__/promotion"
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
moduleIntegrationTestRunner({
|
||||||
|
moduleName: Modules.PROMOTION,
|
||||||
|
testSuite: ({
|
||||||
|
MikroOrmWrapper,
|
||||||
|
service,
|
||||||
|
}: SuiteOptions<IPromotionModuleService>) => {
|
||||||
|
describe("Promotion Service: campaign usage", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createCampaigns(MikroOrmWrapper.forkManager())
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("revertUsage", () => {
|
||||||
|
it("should revert usage for type spend", async () => {
|
||||||
|
const createdPromotion = await createDefaultPromotion(service, {})
|
||||||
|
const createdPromotion2 = await createDefaultPromotion(service, {
|
||||||
|
code: "PROMO_2",
|
||||||
|
campaign_id: createdPromotion.campaign?.id,
|
||||||
|
})
|
||||||
|
const createdPromotion3 = await createDefaultPromotion(service, {
|
||||||
|
code: "PROMO_3",
|
||||||
|
campaign_id: createdPromotion.campaign?.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
await service.registerUsage([
|
||||||
|
{ amount: 200, code: createdPromotion.code! },
|
||||||
|
{ amount: 100, code: createdPromotion.code! },
|
||||||
|
])
|
||||||
|
|
||||||
|
await service.registerUsage([
|
||||||
|
{ amount: 100, code: createdPromotion2.code! },
|
||||||
|
{ amount: 200, code: createdPromotion2.code! },
|
||||||
|
])
|
||||||
|
|
||||||
|
await service.registerUsage([
|
||||||
|
{ amount: 50, code: createdPromotion3.code! },
|
||||||
|
{ amount: 250, code: createdPromotion3.code! },
|
||||||
|
])
|
||||||
|
|
||||||
|
await service.revertUsage([
|
||||||
|
{ amount: 200, code: createdPromotion.code! },
|
||||||
|
{ amount: 100, code: createdPromotion.code! },
|
||||||
|
])
|
||||||
|
|
||||||
|
await service.revertUsage([
|
||||||
|
{ amount: 50, code: createdPromotion3.code! },
|
||||||
|
{ amount: 250, code: createdPromotion3.code! },
|
||||||
|
])
|
||||||
|
|
||||||
|
const campaign = await service.retrieveCampaign(
|
||||||
|
createdPromotion.campaign?.id!,
|
||||||
|
{ relations: ["budget"] }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(campaign.budget).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "spend",
|
||||||
|
limit: 1000,
|
||||||
|
used: 300,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should revert usage for type usage", async () => {
|
||||||
|
const campaignId = "campaign-id-2"
|
||||||
|
const createdPromotion = await createDefaultPromotion(service, {
|
||||||
|
code: "PROMO_1",
|
||||||
|
campaign_id: campaignId,
|
||||||
|
})
|
||||||
|
const createdPromotion2 = await createDefaultPromotion(service, {
|
||||||
|
code: "PROMO_2",
|
||||||
|
campaign_id: campaignId,
|
||||||
|
})
|
||||||
|
const createdPromotion3 = await createDefaultPromotion(service, {
|
||||||
|
code: "PROMO_3",
|
||||||
|
campaign_id: campaignId,
|
||||||
|
})
|
||||||
|
|
||||||
|
await service.registerUsage([
|
||||||
|
{ amount: 200, code: createdPromotion.code! },
|
||||||
|
{ amount: 500, code: createdPromotion.code! },
|
||||||
|
])
|
||||||
|
|
||||||
|
await service.registerUsage([
|
||||||
|
{ amount: 200, code: createdPromotion2.code! },
|
||||||
|
{ amount: 500, code: createdPromotion3.code! },
|
||||||
|
])
|
||||||
|
|
||||||
|
await service.revertUsage([
|
||||||
|
{ amount: 200, code: createdPromotion.code! },
|
||||||
|
{ amount: 500, code: createdPromotion.code! },
|
||||||
|
])
|
||||||
|
|
||||||
|
await service.revertUsage([
|
||||||
|
{ amount: 200, code: createdPromotion2.code! },
|
||||||
|
{ amount: 500, code: createdPromotion3.code! },
|
||||||
|
])
|
||||||
|
|
||||||
|
const campaign = await service.retrieveCampaign(campaignId, {
|
||||||
|
relations: ["budget"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(campaign.budget).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "usage",
|
||||||
|
limit: 1000,
|
||||||
|
used: 0,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not throw an error when compute action with code does not exist", async () => {
|
||||||
|
const response = await service
|
||||||
|
.revertUsage([{ amount: 200, code: "DOESNOTEXIST" }])
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(response).toEqual(undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -123,33 +123,39 @@ export default class PromotionModuleService
|
|||||||
@InjectManager("baseRepository_")
|
@InjectManager("baseRepository_")
|
||||||
async registerUsage(
|
async registerUsage(
|
||||||
computedActions: PromotionTypes.UsageComputedActions[],
|
computedActions: PromotionTypes.UsageComputedActions[],
|
||||||
|
@MedusaContext()
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const promotionCodes = computedActions
|
const promotionCodes = computedActions
|
||||||
.map((computedAction) => computedAction.code)
|
.map((computedAction) => computedAction.code)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
const promotionCodeCampaignBudgetMap = new Map<
|
const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>()
|
||||||
string,
|
|
||||||
UpdateCampaignBudgetDTO
|
|
||||||
>()
|
|
||||||
const promotionCodeUsageMap = new Map<string, boolean>()
|
const promotionCodeUsageMap = new Map<string, boolean>()
|
||||||
|
|
||||||
const existingPromotions = await this.listPromotions(
|
const existingPromotions = await this.listPromotions(
|
||||||
{ code: promotionCodes },
|
{ code: promotionCodes },
|
||||||
{ relations: ["application_method", "campaign", "campaign.budget"] },
|
{
|
||||||
|
relations: ["campaign", "campaign.budget"],
|
||||||
|
take: null,
|
||||||
|
},
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for (const promotion of existingPromotions) {
|
||||||
|
if (promotion.campaign?.budget) {
|
||||||
|
campaignBudgetMap.set(
|
||||||
|
promotion.campaign?.budget.id,
|
||||||
|
promotion.campaign?.budget
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
|
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
|
||||||
existingPromotions.map((promotion) => [promotion.code!, promotion])
|
existingPromotions.map((promotion) => [promotion.code!, promotion])
|
||||||
)
|
)
|
||||||
|
|
||||||
for (let computedAction of computedActions) {
|
for (let computedAction of computedActions) {
|
||||||
if (!ComputeActionUtils.canRegisterUsage(computedAction)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const promotion = existingPromotionsMap.get(computedAction.code)
|
const promotion = existingPromotionsMap.get(computedAction.code)
|
||||||
|
|
||||||
if (!promotion) {
|
if (!promotion) {
|
||||||
@@ -163,9 +169,11 @@ export default class PromotionModuleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (campaignBudget.type === CampaignBudgetType.SPEND) {
|
if (campaignBudget.type === CampaignBudgetType.SPEND) {
|
||||||
const campaignBudgetData = promotionCodeCampaignBudgetMap.get(
|
const campaignBudgetData = campaignBudgetMap.get(campaignBudget.id)
|
||||||
campaignBudget.id
|
|
||||||
) || { id: campaignBudget.id, used: campaignBudget.used ?? 0 }
|
if (!campaignBudgetData) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
campaignBudgetData.used = MathBN.add(
|
campaignBudgetData.used = MathBN.add(
|
||||||
campaignBudgetData.used ?? 0,
|
campaignBudgetData.used ?? 0,
|
||||||
@@ -179,10 +187,7 @@ export default class PromotionModuleService
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
promotionCodeCampaignBudgetMap.set(
|
campaignBudgetMap.set(campaignBudget.id, campaignBudgetData)
|
||||||
campaignBudget.id,
|
|
||||||
campaignBudgetData
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (campaignBudget.type === CampaignBudgetType.USAGE) {
|
if (campaignBudget.type === CampaignBudgetType.USAGE) {
|
||||||
@@ -205,17 +210,105 @@ export default class PromotionModuleService
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
promotionCodeCampaignBudgetMap.set(
|
campaignBudgetMap.set(campaignBudget.id, campaignBudgetData)
|
||||||
campaignBudget.id,
|
|
||||||
campaignBudgetData
|
|
||||||
)
|
|
||||||
|
|
||||||
promotionCodeUsageMap.set(promotion.code!, true)
|
promotionCodeUsageMap.set(promotion.code!, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
|
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
|
||||||
|
|
||||||
for (const [_, campaignBudgetData] of promotionCodeCampaignBudgetMap) {
|
for (const [_, campaignBudgetData] of campaignBudgetMap) {
|
||||||
|
campaignBudgetsData.push(campaignBudgetData)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.campaignBudgetService_.update(
|
||||||
|
campaignBudgetsData,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InjectManager("baseRepository_")
|
||||||
|
async revertUsage(
|
||||||
|
computedActions: PromotionTypes.UsageComputedActions[],
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const promotionCodeUsageMap = new Map<string, boolean>()
|
||||||
|
const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>()
|
||||||
|
|
||||||
|
const existingPromotions = await this.listPromotions(
|
||||||
|
{
|
||||||
|
code: computedActions
|
||||||
|
.map((computedAction) => computedAction.code)
|
||||||
|
.filter(Boolean),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relations: ["campaign", "campaign.budget"],
|
||||||
|
take: null,
|
||||||
|
},
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const promotion of existingPromotions) {
|
||||||
|
if (promotion.campaign?.budget) {
|
||||||
|
campaignBudgetMap.set(
|
||||||
|
promotion.campaign?.budget.id,
|
||||||
|
promotion.campaign?.budget
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
|
||||||
|
existingPromotions.map((promotion) => [promotion.code!, promotion])
|
||||||
|
)
|
||||||
|
|
||||||
|
for (let computedAction of computedActions) {
|
||||||
|
const promotion = existingPromotionsMap.get(computedAction.code)
|
||||||
|
|
||||||
|
if (!promotion) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const campaignBudget = promotion.campaign?.budget
|
||||||
|
|
||||||
|
if (!campaignBudget) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campaignBudget.type === CampaignBudgetType.SPEND) {
|
||||||
|
const campaignBudgetData = campaignBudgetMap.get(campaignBudget.id)
|
||||||
|
|
||||||
|
if (!campaignBudgetData) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
campaignBudgetData.used = MathBN.sub(
|
||||||
|
campaignBudgetData.used ?? 0,
|
||||||
|
computedAction.amount
|
||||||
|
)
|
||||||
|
|
||||||
|
campaignBudgetMap.set(campaignBudget.id, campaignBudgetData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campaignBudget.type === CampaignBudgetType.USAGE) {
|
||||||
|
const promotionAlreadyUsed =
|
||||||
|
promotionCodeUsageMap.get(promotion.code!) || false
|
||||||
|
|
||||||
|
if (promotionAlreadyUsed) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
campaignBudgetMap.set(campaignBudget.id, {
|
||||||
|
id: campaignBudget.id,
|
||||||
|
used: MathBN.sub(campaignBudget.used ?? 0, 1),
|
||||||
|
})
|
||||||
|
|
||||||
|
promotionCodeUsageMap.set(promotion.code!, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
|
||||||
|
|
||||||
|
for (const [_, campaignBudgetData] of campaignBudgetMap) {
|
||||||
campaignBudgetsData.push(campaignBudgetData)
|
campaignBudgetsData.push(campaignBudgetData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
BigNumberInput,
|
BigNumberInput,
|
||||||
CampaignBudgetExceededAction,
|
CampaignBudgetExceededAction,
|
||||||
ComputeActions,
|
|
||||||
PromotionDTO,
|
PromotionDTO,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import { CampaignBudgetType, ComputedActions, MathBN } from "@medusajs/utils"
|
import { CampaignBudgetType, ComputedActions, MathBN } from "@medusajs/utils"
|
||||||
|
|
||||||
export function canRegisterUsage(computedAction: ComputeActions): boolean {
|
|
||||||
return (
|
|
||||||
[
|
|
||||||
ComputedActions.ADD_ITEM_ADJUSTMENT,
|
|
||||||
ComputedActions.ADD_SHIPPING_METHOD_ADJUSTMENT,
|
|
||||||
] as string[]
|
|
||||||
).includes(computedAction.action)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeActionForBudgetExceeded(
|
export function computeActionForBudgetExceeded(
|
||||||
promotion: PromotionDTO,
|
promotion: PromotionDTO,
|
||||||
amount: BigNumberInput
|
amount: BigNumberInput
|
||||||
|
|||||||
Reference in new issue
Block a user