chore(): Reorganize modules (#7210)

**What**
Move all modules to the modules directory
This commit is contained in:
Adrien de Peretti
2024-05-02 17:33:34 +02:00
committed by GitHub
parent 7a351eef09
commit 4eae25e1ef
870 changed files with 91 additions and 62 deletions

View 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
}
}

View File

@@ -0,0 +1,3 @@
export * from "./buy-get"
export * from "./line-items"
export * from "./usage"

View File

@@ -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
)
}) || []
)
}

View File

@@ -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
}

View File

@@ -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!,
}
}
}

View File

@@ -0,0 +1,2 @@
export * as ComputeActionUtils from "./compute-actions"
export * from "./validations"

View File

@@ -0,0 +1,137 @@
import {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
ApplicationMethodType,
isDefined,
isPresent,
MedusaError,
PromotionType,
} from "@medusajs/utils"
import { Promotion } from "@models"
import { CreateApplicationMethodDTO, UpdateApplicationMethodDTO } from "@types"
export const allowedAllocationTargetTypes: string[] = [
ApplicationMethodTargetType.SHIPPING_METHODS,
ApplicationMethodTargetType.ITEMS,
]
export const allowedAllocationTypes: string[] = [
ApplicationMethodAllocation.ACROSS,
ApplicationMethodAllocation.EACH,
]
export const allowedAllocationForQuantity: string[] = [
ApplicationMethodAllocation.EACH,
]
export function validateApplicationMethodAttributes(
data: UpdateApplicationMethodDTO | CreateApplicationMethodDTO,
promotion: Promotion
) {
const applicationMethod = promotion?.application_method || {}
const buyRulesMinQuantity =
data.buy_rules_min_quantity || applicationMethod?.buy_rules_min_quantity
const applyToQuantity =
data.apply_to_quantity || applicationMethod?.apply_to_quantity
const targetType = data.target_type || applicationMethod?.target_type
const type = data.type || applicationMethod?.type
const applicationMethodType = data.type || applicationMethod?.type
const value = data.value || applicationMethod.value
const maxQuantity = data.max_quantity || applicationMethod.max_quantity
const allocation = data.allocation || applicationMethod.allocation
const allTargetTypes: string[] = Object.values(ApplicationMethodTargetType)
if (
type === ApplicationMethodType.PERCENTAGE &&
(typeof value !== "number" || value <= 0 || value > 100)
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Application Method value should be a percentage number between 0 and 100`
)
}
if (promotion?.type === PromotionType.BUYGET) {
if (!isPresent(applyToQuantity)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`apply_to_quantity is a required field for Promotion type of ${PromotionType.BUYGET}`
)
}
if (!isPresent(buyRulesMinQuantity)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`buy_rules_min_quantity is a required field for Promotion type of ${PromotionType.BUYGET}`
)
}
}
if (
allocation === ApplicationMethodAllocation.ACROSS &&
isPresent(maxQuantity)
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`application_method.max_quantity is not allowed to be set for allocation (${ApplicationMethodAllocation.ACROSS})`
)
}
if (!allTargetTypes.includes(targetType)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`application_method.target_type should be one of ${allTargetTypes.join(
", "
)}`
)
}
const allTypes: string[] = Object.values(ApplicationMethodType)
if (!allTypes.includes(applicationMethodType)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`application_method.type should be one of ${allTypes.join(", ")}`
)
}
if (
allowedAllocationTargetTypes.includes(targetType) &&
!allowedAllocationTypes.includes(allocation || "")
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`application_method.allocation should be either '${allowedAllocationTypes.join(
" OR "
)}' when application_method.target_type is either '${allowedAllocationTargetTypes.join(
" OR "
)}'`
)
}
const allAllocationTypes: string[] = Object.values(
ApplicationMethodAllocation
)
if (allocation && !allAllocationTypes.includes(allocation)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`application_method.allocation should be one of ${allAllocationTypes.join(
", "
)}`
)
}
if (
allocation &&
allowedAllocationForQuantity.includes(allocation) &&
!isDefined(maxQuantity)
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`application_method.max_quantity is required when application_method.allocation is '${allowedAllocationForQuantity.join(
" OR "
)}'`
)
}
}

View File

@@ -0,0 +1,2 @@
export * from "./application-method"
export * from "./promotion-rule"

View File

@@ -0,0 +1,104 @@
import { PromotionRuleDTO, PromotionRuleOperatorValues } from "@medusajs/types"
import {
isPresent,
isString,
MedusaError,
pickValueFromObject,
PromotionRuleOperator,
} from "@medusajs/utils"
import { CreatePromotionRuleDTO } from "@types"
export function validatePromotionRuleAttributes(
promotionRulesData: CreatePromotionRuleDTO[]
) {
const errors: string[] = []
for (const promotionRuleData of promotionRulesData) {
if (!isPresent(promotionRuleData.attribute)) {
errors.push("rules[].attribute is a required field")
}
if (!isPresent(promotionRuleData.operator)) {
errors.push("rules[].operator is a required field")
}
if (isPresent(promotionRuleData.operator)) {
const allowedOperators: PromotionRuleOperatorValues[] = Object.values(
PromotionRuleOperator
)
if (!allowedOperators.includes(promotionRuleData.operator)) {
errors.push(
`rules[].operator (${
promotionRuleData.operator
}) is invalid. It should be one of ${allowedOperators.join(", ")}`
)
}
} else {
errors.push("rules[].operator is a required field")
}
}
if (!errors.length) return
throw new MedusaError(MedusaError.Types.INVALID_DATA, errors.join(", "))
}
export function areRulesValidForContext(
rules: PromotionRuleDTO[],
context: Record<string, any>
): boolean {
return rules.every((rule) => {
const validRuleValues = rule.values?.map((ruleValue) => ruleValue.value)
if (!rule.attribute) {
return false
}
const valuesToCheck = pickValueFromObject(rule.attribute, context)
return evaluateRuleValueCondition(
validRuleValues.filter(isString),
rule.operator!,
valuesToCheck
)
})
}
export function evaluateRuleValueCondition(
ruleValues: string[],
operator: string,
ruleValuesToCheck: string[] | string
) {
if (!Array.isArray(ruleValuesToCheck)) {
ruleValuesToCheck = [ruleValuesToCheck]
}
return ruleValuesToCheck.every((ruleValueToCheck: string) => {
if (operator === "in" || operator === "eq") {
return ruleValues.some((ruleValue) => ruleValue === ruleValueToCheck)
}
if (operator === "ne") {
return ruleValues.some((ruleValue) => ruleValue !== ruleValueToCheck)
}
if (operator === "gt") {
return ruleValues.some((ruleValue) => ruleValue > ruleValueToCheck)
}
if (operator === "gte") {
return ruleValues.some((ruleValue) => ruleValue >= ruleValueToCheck)
}
if (operator === "lt") {
return ruleValues.some((ruleValue) => ruleValue < ruleValueToCheck)
}
if (operator === "lte") {
return ruleValues.some((ruleValue) => ruleValue <= ruleValueToCheck)
}
return false
})
}