chore(): Reorganize modules (#7210)
**What** Move all modules to the modules directory
This commit is contained in:
committed by
GitHub
parent
7a351eef09
commit
4eae25e1ef
107
packages/modules/promotion/src/utils/compute-actions/buy-get.ts
Normal file
107
packages/modules/promotion/src/utils/compute-actions/buy-get.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { PromotionTypes } from "@medusajs/types"
|
||||
import {
|
||||
ApplicationMethodTargetType,
|
||||
ComputedActions,
|
||||
MedusaError,
|
||||
PromotionType,
|
||||
isPresent,
|
||||
} from "@medusajs/utils"
|
||||
import { areRulesValidForContext } from "../validations"
|
||||
import { computeActionForBudgetExceeded } from "./usage"
|
||||
|
||||
// TODO: calculations should eventually move to a totals util outside of the module
|
||||
export function getComputedActionsForBuyGet(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
itemsContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS],
|
||||
methodIdPromoValueMap: Map<string, number>
|
||||
): PromotionTypes.ComputeActions[] {
|
||||
const buyRulesMinQuantity =
|
||||
promotion.application_method?.buy_rules_min_quantity
|
||||
const applyToQuantity = promotion.application_method?.apply_to_quantity
|
||||
const buyRules = promotion.application_method?.buy_rules
|
||||
const targetRules = promotion.application_method?.target_rules
|
||||
const computedActions: PromotionTypes.ComputeActions[] = []
|
||||
|
||||
if (!itemsContext) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`"items" should be present as an array in the context to compute actions`
|
||||
)
|
||||
}
|
||||
|
||||
if (!Array.isArray(buyRules) || !Array.isArray(targetRules)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const validQuantity = itemsContext
|
||||
.filter((item) => areRulesValidForContext(buyRules, item))
|
||||
.reduce((acc, next) => acc + next.quantity, 0)
|
||||
|
||||
if (
|
||||
!buyRulesMinQuantity ||
|
||||
!applyToQuantity ||
|
||||
buyRulesMinQuantity > validQuantity
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
const validItemsForTargetRules = itemsContext
|
||||
.filter((item) => areRulesValidForContext(targetRules, item))
|
||||
.filter((item) => isPresent(item.subtotal) && isPresent(item.quantity))
|
||||
.sort((a, b) => {
|
||||
const aPrice = a.subtotal / a.quantity
|
||||
const bPrice = b.subtotal / b.quantity
|
||||
|
||||
return bPrice - aPrice
|
||||
})
|
||||
|
||||
let remainingQtyToApply = applyToQuantity
|
||||
|
||||
for (const method of validItemsForTargetRules) {
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
||||
const multiplier = Math.min(method.quantity, remainingQtyToApply)
|
||||
const amount = (method.subtotal / method.quantity) * multiplier
|
||||
const newRemainingQtyToApply = remainingQtyToApply - multiplier
|
||||
|
||||
if (newRemainingQtyToApply < 0 || amount <= 0) {
|
||||
break
|
||||
} else {
|
||||
remainingQtyToApply = newRemainingQtyToApply
|
||||
}
|
||||
|
||||
const budgetExceededAction = computeActionForBudgetExceeded(
|
||||
promotion,
|
||||
amount
|
||||
)
|
||||
|
||||
if (budgetExceededAction) {
|
||||
computedActions.push(budgetExceededAction)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
||||
|
||||
computedActions.push({
|
||||
action: ComputedActions.ADD_ITEM_ADJUSTMENT,
|
||||
item_id: method.id,
|
||||
amount,
|
||||
code: promotion.code!,
|
||||
})
|
||||
}
|
||||
|
||||
return computedActions
|
||||
}
|
||||
|
||||
export function sortByBuyGetType(a, b) {
|
||||
if (a.type === PromotionType.BUYGET && b.type !== PromotionType.BUYGET) {
|
||||
return -1
|
||||
} else if (
|
||||
a.type !== PromotionType.BUYGET &&
|
||||
b.type === PromotionType.BUYGET
|
||||
) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./buy-get"
|
||||
export * from "./line-items"
|
||||
export * from "./usage"
|
||||
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
ApplicationMethodAllocationValues,
|
||||
PromotionTypes,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ApplicationMethodAllocation,
|
||||
ComputedActions,
|
||||
MedusaError,
|
||||
ApplicationMethodTargetType as TargetType,
|
||||
calculateAdjustmentAmountFromPromotion,
|
||||
} from "@medusajs/utils"
|
||||
import { areRulesValidForContext } from "../validations"
|
||||
import { computeActionForBudgetExceeded } from "./usage"
|
||||
|
||||
function validateContext(
|
||||
contextKey: string,
|
||||
context: PromotionTypes.ComputeActionContext[TargetType]
|
||||
) {
|
||||
if (!context) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`"${contextKey}" should be present as an array in the context for computeActions`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function getComputedActionsForItems(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
items: PromotionTypes.ComputeActionContext[TargetType.ITEMS],
|
||||
appliedPromotionsMap: Map<string, number>,
|
||||
allocationOverride?: ApplicationMethodAllocationValues
|
||||
): PromotionTypes.ComputeActions[] {
|
||||
validateContext("items", items)
|
||||
|
||||
return applyPromotionToItems(
|
||||
promotion,
|
||||
items,
|
||||
appliedPromotionsMap,
|
||||
allocationOverride
|
||||
)
|
||||
}
|
||||
|
||||
export function getComputedActionsForShippingMethods(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
shippingMethods: PromotionTypes.ComputeActionContext[TargetType.SHIPPING_METHODS],
|
||||
appliedPromotionsMap: Map<string, number>
|
||||
): PromotionTypes.ComputeActions[] {
|
||||
validateContext("shipping_methods", shippingMethods)
|
||||
|
||||
return applyPromotionToItems(promotion, shippingMethods, appliedPromotionsMap)
|
||||
}
|
||||
|
||||
export function getComputedActionsForOrder(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
itemApplicationContext: PromotionTypes.ComputeActionContext,
|
||||
methodIdPromoValueMap: Map<string, number>
|
||||
): PromotionTypes.ComputeActions[] {
|
||||
return getComputedActionsForItems(
|
||||
promotion,
|
||||
itemApplicationContext[TargetType.ITEMS],
|
||||
methodIdPromoValueMap,
|
||||
ApplicationMethodAllocation.ACROSS
|
||||
)
|
||||
}
|
||||
|
||||
function applyPromotionToItems(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
items:
|
||||
| PromotionTypes.ComputeActionContext[TargetType.ITEMS]
|
||||
| PromotionTypes.ComputeActionContext[TargetType.SHIPPING_METHODS],
|
||||
appliedPromotionsMap: Map<string, number>,
|
||||
allocationOverride?: ApplicationMethodAllocationValues
|
||||
): PromotionTypes.ComputeActions[] {
|
||||
const { application_method: applicationMethod } = promotion
|
||||
const allocation = applicationMethod?.allocation! || allocationOverride
|
||||
const computedActions: PromotionTypes.ComputeActions[] = []
|
||||
const applicableItems = getValidItemsForPromotion(items, promotion)
|
||||
const target = applicationMethod?.target_type
|
||||
|
||||
const isTargetShippingMethod = target === TargetType.SHIPPING_METHODS
|
||||
const isTargetLineItems = target === TargetType.ITEMS
|
||||
const isTargetOrder = target === TargetType.ORDER
|
||||
|
||||
let lineItemsTotal = 0
|
||||
|
||||
if (allocation === ApplicationMethodAllocation.ACROSS) {
|
||||
lineItemsTotal = applicableItems.reduce(
|
||||
(acc, item) =>
|
||||
acc + item.subtotal - (appliedPromotionsMap.get(item.id) ?? 0),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
for (const item of applicableItems!) {
|
||||
const appliedPromoValue = appliedPromotionsMap.get(item.id) ?? 0
|
||||
const maxQuantity = isTargetShippingMethod
|
||||
? 1
|
||||
: applicationMethod?.max_quantity!
|
||||
|
||||
if (isTargetShippingMethod) {
|
||||
item.quantity = 1
|
||||
}
|
||||
|
||||
const amount = calculateAdjustmentAmountFromPromotion(
|
||||
item,
|
||||
{
|
||||
value: applicationMethod?.value ?? 0,
|
||||
applied_value: appliedPromoValue,
|
||||
max_quantity: maxQuantity,
|
||||
type: applicationMethod?.type!,
|
||||
allocation,
|
||||
},
|
||||
lineItemsTotal
|
||||
)
|
||||
|
||||
if (amount <= 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const budgetExceededAction = computeActionForBudgetExceeded(
|
||||
promotion,
|
||||
amount
|
||||
)
|
||||
|
||||
if (budgetExceededAction) {
|
||||
computedActions.push(budgetExceededAction)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
appliedPromotionsMap.set(item.id, appliedPromoValue + amount)
|
||||
|
||||
if (isTargetLineItems || isTargetOrder) {
|
||||
computedActions.push({
|
||||
action: ComputedActions.ADD_ITEM_ADJUSTMENT,
|
||||
item_id: item.id,
|
||||
amount,
|
||||
code: promotion.code!,
|
||||
})
|
||||
}
|
||||
|
||||
if (isTargetShippingMethod) {
|
||||
computedActions.push({
|
||||
action: ComputedActions.ADD_SHIPPING_METHOD_ADJUSTMENT,
|
||||
shipping_method_id: item.id,
|
||||
amount,
|
||||
code: promotion.code!,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return computedActions
|
||||
}
|
||||
|
||||
function getValidItemsForPromotion(
|
||||
items:
|
||||
| PromotionTypes.ComputeActionContext[TargetType.ITEMS]
|
||||
| PromotionTypes.ComputeActionContext[TargetType.SHIPPING_METHODS],
|
||||
promotion: PromotionTypes.PromotionDTO
|
||||
) {
|
||||
const isTargetShippingMethod =
|
||||
promotion.application_method?.target_type === TargetType.SHIPPING_METHODS
|
||||
|
||||
return (
|
||||
items?.filter((item) => {
|
||||
const isSubtotalPresent = "subtotal" in item
|
||||
const isQuantityPresent = "quantity" in item
|
||||
const isPromotionApplicableToItem = areRulesValidForContext(
|
||||
promotion?.application_method?.target_rules!,
|
||||
item
|
||||
)
|
||||
|
||||
return (
|
||||
isPromotionApplicableToItem &&
|
||||
(isQuantityPresent || isTargetShippingMethod) &&
|
||||
isSubtotalPresent
|
||||
)
|
||||
}) || []
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { PromotionTypes } from "@medusajs/types"
|
||||
import {
|
||||
ApplicationMethodAllocation,
|
||||
ApplicationMethodTargetType,
|
||||
ApplicationMethodType,
|
||||
ComputedActions,
|
||||
MedusaError,
|
||||
} from "@medusajs/utils"
|
||||
import { areRulesValidForContext } from "../validations"
|
||||
import { computeActionForBudgetExceeded } from "./usage"
|
||||
|
||||
export function getComputedActionsForShippingMethods(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
shippingMethodApplicationContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS],
|
||||
methodIdPromoValueMap: Map<string, number>
|
||||
): PromotionTypes.ComputeActions[] {
|
||||
const applicableShippingItems: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS] =
|
||||
[]
|
||||
|
||||
if (!shippingMethodApplicationContext) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`"shipping_methods" should be present as an array in the context for computeActions`
|
||||
)
|
||||
}
|
||||
|
||||
for (const shippingMethodContext of shippingMethodApplicationContext) {
|
||||
const isPromotionApplicableToItem = areRulesValidForContext(
|
||||
promotion.application_method?.target_rules!,
|
||||
shippingMethodContext
|
||||
)
|
||||
|
||||
if (!isPromotionApplicableToItem) {
|
||||
continue
|
||||
}
|
||||
|
||||
applicableShippingItems.push(shippingMethodContext)
|
||||
}
|
||||
|
||||
return applyPromotionToShippingMethods(
|
||||
promotion,
|
||||
applicableShippingItems,
|
||||
methodIdPromoValueMap
|
||||
)
|
||||
}
|
||||
|
||||
export function applyPromotionToShippingMethods(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
shippingMethods: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS],
|
||||
methodIdPromoValueMap: Map<string, number>
|
||||
): PromotionTypes.ComputeActions[] {
|
||||
const { application_method: applicationMethod } = promotion
|
||||
const allocation = applicationMethod?.allocation!
|
||||
const computedActions: PromotionTypes.ComputeActions[] = []
|
||||
|
||||
if (allocation === ApplicationMethodAllocation.EACH) {
|
||||
for (const method of shippingMethods!) {
|
||||
if (!method.subtotal) {
|
||||
continue
|
||||
}
|
||||
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
||||
let promotionValue = applicationMethod?.value ?? 0
|
||||
const applicableTotal = method.subtotal - appliedPromoValue
|
||||
|
||||
if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) {
|
||||
promotionValue = (promotionValue / 100) * applicableTotal
|
||||
}
|
||||
|
||||
const amount = Math.min(promotionValue, applicableTotal)
|
||||
|
||||
if (amount <= 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const budgetExceededAction = computeActionForBudgetExceeded(
|
||||
promotion,
|
||||
amount
|
||||
)
|
||||
|
||||
if (budgetExceededAction) {
|
||||
computedActions.push(budgetExceededAction)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
||||
|
||||
computedActions.push({
|
||||
action: ComputedActions.ADD_SHIPPING_METHOD_ADJUSTMENT,
|
||||
shipping_method_id: method.id,
|
||||
amount,
|
||||
code: promotion.code!,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (allocation === ApplicationMethodAllocation.ACROSS) {
|
||||
const totalApplicableValue = shippingMethods!.reduce((acc, method) => {
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
||||
|
||||
return acc + (method.subtotal ?? 0) - appliedPromoValue
|
||||
}, 0)
|
||||
|
||||
if (totalApplicableValue <= 0) {
|
||||
return computedActions
|
||||
}
|
||||
|
||||
for (const method of shippingMethods!) {
|
||||
if (!method.subtotal) {
|
||||
continue
|
||||
}
|
||||
|
||||
const promotionValue = applicationMethod?.value ?? 0
|
||||
const applicableTotal = method.subtotal
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
||||
|
||||
// TODO: should we worry about precision here?
|
||||
let applicablePromotionValue =
|
||||
(applicableTotal / totalApplicableValue) * promotionValue -
|
||||
appliedPromoValue
|
||||
|
||||
if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) {
|
||||
applicablePromotionValue =
|
||||
(promotionValue / 100) * (applicableTotal - appliedPromoValue)
|
||||
}
|
||||
|
||||
const amount = Math.min(applicablePromotionValue, applicableTotal)
|
||||
|
||||
if (amount <= 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const budgetExceededAction = computeActionForBudgetExceeded(
|
||||
promotion,
|
||||
amount
|
||||
)
|
||||
|
||||
if (budgetExceededAction) {
|
||||
computedActions.push(budgetExceededAction)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
||||
|
||||
computedActions.push({
|
||||
action: ComputedActions.ADD_SHIPPING_METHOD_ADJUSTMENT,
|
||||
shipping_method_id: method.id,
|
||||
amount,
|
||||
code: promotion.code!,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return computedActions
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
CampaignBudgetExceededAction,
|
||||
ComputeActions,
|
||||
PromotionDTO,
|
||||
} from "@medusajs/types"
|
||||
import { CampaignBudgetType, ComputedActions } 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(
|
||||
promotion: PromotionDTO,
|
||||
amount: number
|
||||
): CampaignBudgetExceededAction | void {
|
||||
const campaignBudget = promotion.campaign?.budget
|
||||
|
||||
if (!campaignBudget) {
|
||||
return
|
||||
}
|
||||
|
||||
const campaignBudgetUsed = campaignBudget.used ?? 0
|
||||
const totalUsed =
|
||||
campaignBudget.type === CampaignBudgetType.SPEND
|
||||
? campaignBudgetUsed + amount
|
||||
: campaignBudgetUsed + 1
|
||||
|
||||
if (campaignBudget.limit && totalUsed > campaignBudget.limit) {
|
||||
return {
|
||||
action: ComputedActions.CAMPAIGN_BUDGET_EXCEEDED,
|
||||
code: promotion.code!,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user