feat(utils,types): added item/shipping adjustments for order/items/shipping_methods (#6050)

what:

- adds compute actions for the following cases:
  - items => each & across
  - shipping_method => each & across
  - order
- adds a remove compute actions when code is no longer present in adjustments array

RESOLVES CORE-1625
RESOLVES CORE-1626
RESOLVES CORE-1627
RESOLVES CORE-1628
RESOLVES CORE-1585
This commit is contained in:
Riqwan Thamir
2024-01-12 14:00:06 +01:00
committed by GitHub
parent 192bc336cc
commit b782d3bcb7
24 changed files with 2638 additions and 81 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat(utils,types): added item/shipping adjustments for order/items/shipping_methods

View File

@@ -99,63 +99,6 @@ describe("Promotion Service", () => {
)
})
it("should create a promotion with order application method with rules successfully", async () => {
const [createdPromotion] = await service.create([
{
code: "PROMOTION_TEST",
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "order",
value: "100",
target_rules: [
{
attribute: "product_id",
operator: "eq",
values: ["prod_tshirt"],
},
],
},
},
])
const [promotion] = await service.list(
{
id: [createdPromotion.id],
},
{
relations: [
"application_method",
"application_method.target_rules.values",
],
}
)
expect(promotion).toEqual(
expect.objectContaining({
code: "PROMOTION_TEST",
is_automatic: false,
type: "standard",
application_method: expect.objectContaining({
type: "fixed",
target_type: "order",
value: 100,
target_rules: [
expect.objectContaining({
attribute: "product_id",
operator: "eq",
values: expect.arrayContaining([
expect.objectContaining({
value: "prod_tshirt",
}),
]),
}),
],
}),
})
)
})
it("should throw error when creating an item application method without allocation", async () => {
const error = await service
.create([
@@ -164,7 +107,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "item",
target_type: "items",
value: "100",
},
},
@@ -172,7 +115,7 @@ describe("Promotion Service", () => {
.catch((e) => e)
expect(error.message).toContain(
"application_method.allocation should be either 'across OR each' when application_method.target_type is either 'shipping OR item'"
"application_method.allocation should be either 'across OR each' when application_method.target_type is either 'shipping_methods OR items'"
)
})
@@ -185,7 +128,7 @@ describe("Promotion Service", () => {
application_method: {
type: "fixed",
allocation: "each",
target_type: "shipping",
target_type: "shipping_methods",
value: "100",
},
},
@@ -197,6 +140,33 @@ describe("Promotion Service", () => {
)
})
it("should throw error when creating an order application method with rules", async () => {
const error = await service
.create([
{
code: "PROMOTION_TEST",
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "order",
value: "100",
target_rules: [
{
attribute: "product_id",
operator: "eq",
values: ["prod_tshirt"],
},
],
},
},
])
.catch((e) => e)
expect(error.message).toContain(
"Target rules for application method with target type (order) is not allowed"
)
})
it("should create a promotion with rules successfully", async () => {
const [createdPromotion] = await service.create([
{
@@ -390,7 +360,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "item",
target_type: "items",
allocation: "across",
value: "100",
},
@@ -424,7 +394,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "item",
target_type: "items",
allocation: "each",
value: "100",
max_quantity: 500,
@@ -483,7 +453,7 @@ describe("Promotion Service", () => {
.catch((e) => e)
expect(error.message).toContain(
`application_method.target_type should be one of order, shipping, item`
`application_method.target_type should be one of order, shipping_methods, items`
)
error = await service
@@ -604,7 +574,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "item",
target_type: "items",
allocation: "each",
value: "100",
max_quantity: 500,
@@ -676,7 +646,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "item",
target_type: "items",
allocation: "each",
value: "100",
max_quantity: 500,
@@ -760,7 +730,7 @@ describe("Promotion Service", () => {
],
application_method: {
type: "fixed",
target_type: "item",
target_type: "items",
allocation: "each",
value: "100",
max_quantity: 500,
@@ -821,7 +791,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "item",
target_type: "items",
allocation: "each",
value: "100",
max_quantity: 500,

View File

@@ -7,10 +7,12 @@ import {
PromotionTypes,
} from "@medusajs/types"
import {
ApplicationMethodTargetType,
InjectManager,
InjectTransactionManager,
MedusaContext,
MedusaError,
isString,
} from "@medusajs/utils"
import { ApplicationMethod, Promotion } from "@models"
import {
@@ -23,11 +25,14 @@ import { joinerConfig } from "../joiner-config"
import {
CreateApplicationMethodDTO,
CreatePromotionDTO,
CreatePromotionRuleDTO,
UpdateApplicationMethodDTO,
UpdatePromotionDTO,
} from "../types"
import {
ComputeActionUtils,
allowedAllocationForQuantity,
areRulesValidForContext,
validateApplicationMethodAttributes,
validatePromotionRuleAttributes,
} from "../utils"
@@ -71,6 +76,161 @@ export default class PromotionModuleService<
return joinerConfig
}
async computeActions(
promotionCodesToApply: string[],
applicationContext: PromotionTypes.ComputeActionContext,
// TODO: specify correct type with options
options: Record<string, any> = {}
): Promise<PromotionTypes.ComputeActions[]> {
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>()
items.forEach((item) => {
item.adjustments?.forEach((adjustment) => {
if (isString(adjustment.code)) {
codeAdjustmentMap.set(adjustment.code, adjustment)
appliedItemCodes.push(adjustment.code)
}
})
})
shippingMethods.forEach((shippingMethod) => {
shippingMethod.adjustments?.forEach((adjustment) => {
if (isString(adjustment.code)) {
codeAdjustmentMap.set(adjustment.code, adjustment)
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",
"rules",
"rules.values",
],
}
)
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
promotions.map((promotion) => [promotion.code!, promotion])
)
for (const appliedCode of [...appliedShippingCodes, ...appliedItemCodes]) {
const promotion = existingPromotionsMap.get(appliedCode)
if (!promotion) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Applied Promotion for code (${appliedCode}) not found`
)
}
if (promotionCodesToApply.includes(appliedCode)) {
continue
}
if (appliedItemCodes.includes(appliedCode)) {
computedActions.push({
action: "removeItemAdjustment",
adjustment_id: codeAdjustmentMap.get(appliedCode)!.id,
})
}
if (appliedShippingCodes.includes(appliedCode)) {
computedActions.push({
action: "removeShippingMethodAdjustment",
adjustment_id: codeAdjustmentMap.get(appliedCode)!.id,
})
}
}
for (const promotionCode of promotionCodesToApply) {
const promotion = existingPromotionsMap.get(promotionCode)
if (!promotion) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Promotion for code (${promotionCode}) not found`
)
}
const {
application_method: applicationMethod,
rules: promotionRules = [],
} = promotion
if (!applicationMethod) {
continue
}
const isPromotionApplicable = areRulesValidForContext(
promotionRules,
applicationContext
)
if (!isPromotionApplicable) {
continue
}
if (applicationMethod.target_type === ApplicationMethodTargetType.ORDER) {
const computedActionsForItems =
ComputeActionUtils.getComputedActionsForOrder(
promotion,
applicationContext,
methodIdPromoValueMap
)
computedActions.push(...computedActionsForItems)
}
if (applicationMethod.target_type === ApplicationMethodTargetType.ITEMS) {
const computedActionsForItems =
ComputeActionUtils.getComputedActionsForItems(
promotion,
applicationContext[ApplicationMethodTargetType.ITEMS],
methodIdPromoValueMap
)
computedActions.push(...computedActionsForItems)
}
if (
applicationMethod.target_type ===
ApplicationMethodTargetType.SHIPPING_METHODS
) {
const computedActionsForShippingMethods =
ComputeActionUtils.getComputedActionsForShippingMethods(
promotion,
applicationContext[ApplicationMethodTargetType.SHIPPING_METHODS],
methodIdPromoValueMap
)
computedActions.push(...computedActionsForShippingMethods)
}
}
return computedActions
}
@InjectManager("baseRepository_")
async retrieve(
id: string,
@@ -194,6 +354,17 @@ export default class PromotionModuleService<
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`
)
}
validateApplicationMethodAttributes(applicationMethodData)
applicationMethodsData.push(applicationMethodData)
@@ -394,7 +565,7 @@ export default class PromotionModuleService<
for (const ruleData of rulesData) {
const { values, ...rest } = ruleData
const promotionRuleData = {
const promotionRuleData: CreatePromotionRuleDTO = {
...rest,
[relationName]: [relation],
}

View File

@@ -1,12 +1,13 @@
import { PromotionRuleDTO } from "@medusajs/types"
import { PromotionRule } from "@models"
export interface CreatePromotionRuleValueDTO {
value: any
promotion_rule: string | PromotionRuleDTO
promotion_rule: string | PromotionRuleDTO | PromotionRule
}
export interface UpdatePromotionRuleValueDTO {
id: string
value: any
promotion_rule: string | PromotionRuleDTO
promotion_rule: string | PromotionRuleDTO | PromotionRule
}

View File

@@ -1,7 +1,7 @@
import { PromotionRuleOperatorValues } from "@medusajs/types"
export interface CreatePromotionRuleDTO {
description?: string
description?: string | null
attribute: string
operator: PromotionRuleOperatorValues
}

View File

@@ -0,0 +1,3 @@
export * from "./items"
export * from "./order"
export * from "./shipping-methods"

View File

@@ -0,0 +1,131 @@
import {
ApplicationMethodAllocationValues,
PromotionTypes,
} from "@medusajs/types"
import {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
MedusaError,
} from "@medusajs/utils"
import { areRulesValidForContext } from "../validations"
export function getComputedActionsForItems(
promotion: PromotionTypes.PromotionDTO,
itemApplicationContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS],
methodIdPromoValueMap: Map<string, number>,
allocationOverride?: ApplicationMethodAllocationValues
): PromotionTypes.ComputeActions[] {
const applicableItems: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS] =
[]
if (!itemApplicationContext) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`"items" should be present as an array in the context for computeActions`
)
}
for (const itemContext of itemApplicationContext) {
const isPromotionApplicableToItem = areRulesValidForContext(
promotion?.application_method?.target_rules!,
itemContext
)
if (!isPromotionApplicableToItem) {
continue
}
applicableItems.push(itemContext)
}
return applyPromotionToItems(
promotion,
applicableItems,
methodIdPromoValueMap,
allocationOverride
)
}
export function applyPromotionToItems(
promotion: PromotionTypes.PromotionDTO,
items: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS],
methodIdPromoValueMap: Map<string, number>,
allocationOverride?: ApplicationMethodAllocationValues
): PromotionTypes.ComputeActions[] {
const { application_method: applicationMethod } = promotion
const allocation = applicationMethod?.allocation!
const computedActions: PromotionTypes.ComputeActions[] = []
if (
[allocation, allocationOverride].includes(ApplicationMethodAllocation.EACH)
) {
for (const method of items!) {
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
const promotionValue = parseFloat(applicationMethod!.value!)
const applicableTotal =
method.unit_price *
Math.min(method.quantity, applicationMethod?.max_quantity!) -
appliedPromoValue
const amount = Math.min(promotionValue, applicableTotal)
if (amount <= 0) {
continue
}
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
computedActions.push({
action: "addItemAdjustment",
item_id: method.id,
amount,
code: promotion.code!,
})
}
}
if (
[allocation, allocationOverride].includes(
ApplicationMethodAllocation.ACROSS
)
) {
const totalApplicableValue = items!.reduce((acc, method) => {
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
return (
acc +
method.unit_price *
Math.min(method.quantity, applicationMethod?.max_quantity!) -
appliedPromoValue
)
}, 0)
for (const method of items!) {
const promotionValue = parseFloat(applicationMethod!.value!)
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
const applicableTotal =
method.unit_price *
Math.min(method.quantity, applicationMethod?.max_quantity!) -
appliedPromoValue
// TODO: should we worry about precision here?
const applicablePromotionValue =
(applicableTotal / totalApplicableValue) * promotionValue
const amount = Math.min(applicablePromotionValue, applicableTotal)
if (amount <= 0) {
continue
}
computedActions.push({
action: "addItemAdjustment",
item_id: method.id,
amount,
code: promotion.code!,
})
}
}
return computedActions
}

View File

@@ -0,0 +1,19 @@
import { PromotionTypes } from "@medusajs/types"
import {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
} from "@medusajs/utils"
import { getComputedActionsForItems } from "./items"
export function getComputedActionsForOrder(
promotion: PromotionTypes.PromotionDTO,
itemApplicationContext: PromotionTypes.ComputeActionContext,
methodIdPromoValueMap: Map<string, number>
): PromotionTypes.ComputeActions[] {
return getComputedActionsForItems(
promotion,
itemApplicationContext[ApplicationMethodTargetType.ITEMS],
methodIdPromoValueMap,
ApplicationMethodAllocation.ACROSS
)
}

View File

@@ -0,0 +1,114 @@
import { PromotionTypes } from "@medusajs/types"
import {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
MedusaError,
} from "@medusajs/utils"
import { areRulesValidForContext } from "../validations"
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!) {
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
const promotionValue = parseFloat(applicationMethod!.value!)
const applicableTotal = method.unit_price - appliedPromoValue
const amount = Math.min(promotionValue, applicableTotal)
if (amount <= 0) {
continue
}
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
computedActions.push({
action: "addShippingMethodAdjustment",
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.unit_price - appliedPromoValue
}, 0)
if (totalApplicableValue <= 0) {
return computedActions
}
for (const method of shippingMethods!) {
const promotionValue = parseFloat(applicationMethod!.value!)
const applicableTotal = method.unit_price
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
// TODO: should we worry about precision here?
const applicablePromotionValue =
(applicableTotal / totalApplicableValue) * promotionValue -
appliedPromoValue
const amount = Math.min(applicablePromotionValue, applicableTotal)
if (amount <= 0) {
continue
}
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
computedActions.push({
action: "addShippingMethodAdjustment",
shipping_method_id: method.id,
amount,
code: promotion.code!,
})
}
}
return computedActions
}

View File

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

View File

@@ -12,8 +12,8 @@ import {
} from "@medusajs/utils"
export const allowedAllocationTargetTypes: string[] = [
ApplicationMethodTargetType.SHIPPING,
ApplicationMethodTargetType.ITEM,
ApplicationMethodTargetType.SHIPPING_METHODS,
ApplicationMethodTargetType.ITEMS,
]
export const allowedAllocationTypes: string[] = [

View File

@@ -1,5 +1,11 @@
import { PromotionRuleOperatorValues } from "@medusajs/types"
import { MedusaError, PromotionRuleOperator, isPresent } from "@medusajs/utils"
import { PromotionRuleDTO, PromotionRuleOperatorValues } from "@medusajs/types"
import {
MedusaError,
PromotionRuleOperator,
isPresent,
isString,
pickValueFromObject,
} from "@medusajs/utils"
import { CreatePromotionRuleDTO } from "../../types"
export function validatePromotionRuleAttributes(
@@ -37,3 +43,62 @@ export function validatePromotionRuleAttributes(
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
})
}

View File

@@ -3,7 +3,10 @@ import { PromotionDTO } from "./promotion"
import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule"
export type ApplicationMethodTypeValues = "fixed" | "percentage"
export type ApplicationMethodTargetTypeValues = "order" | "shipping" | "item"
export type ApplicationMethodTargetTypeValues =
| "order"
| "shipping_methods"
| "items"
export type ApplicationMethodAllocationValues = "each" | "across"
export interface ApplicationMethodDTO {

View File

@@ -0,0 +1,55 @@
export type ComputeActions =
| AddItemAdjustmentAction
| RemoveItemAdjustmentAction
| AddShippingMethodAdjustment
| RemoveShippingMethodAdjustment
export interface AddItemAdjustmentAction {
action: "addItemAdjustment"
item_id: string
amount: number
code: string
description?: string
}
export interface RemoveItemAdjustmentAction {
action: "removeItemAdjustment"
adjustment_id: string
description?: string
}
export interface AddShippingMethodAdjustment {
action: "addShippingMethodAdjustment"
shipping_method_id: string
amount: number
code: string
description?: string
}
export interface RemoveShippingMethodAdjustment {
action: "removeShippingMethodAdjustment"
adjustment_id: string
}
export interface ComputeActionAdjustmentLine {
id: string
code: string
}
export interface ComputeActionItemLine {
id: string
quantity: number
unit_price: number
adjustments?: ComputeActionAdjustmentLine[]
}
export interface ComputeActionShippingLine {
id: string
unit_price: number
adjustments?: ComputeActionAdjustmentLine[]
}
export interface ComputeActionContext {
items?: ComputeActionItemLine[]
shipping_methods?: ComputeActionShippingLine[]
}

View File

@@ -1,4 +1,5 @@
export * from "./application-method"
export * from "./compute-actions"
export * from "./promotion"
export * from "./promotion-rule"
export * from "./promotion-rule-value"

View File

@@ -3,10 +3,11 @@ import { PromotionRuleDTO } from "./promotion-rule"
export interface PromotionRuleValueDTO {
id: string
value?: string
}
export interface CreatePromotionRuleValueDTO {
value: any
value: string
promotion_rule: PromotionRuleDTO
}

View File

@@ -1,4 +1,5 @@
import { BaseFilterable } from "../../dal"
import { PromotionRuleValueDTO } from "./promotion-rule-value"
export type PromotionRuleOperatorValues =
| "gt"
@@ -11,10 +12,14 @@ export type PromotionRuleOperatorValues =
export interface PromotionRuleDTO {
id: string
description?: string | null
attribute?: string
operator?: PromotionRuleOperatorValues
values: PromotionRuleValueDTO[]
}
export interface CreatePromotionRuleDTO {
description?: string
description?: string | null
attribute: string
operator: PromotionRuleOperatorValues
values: string[] | string
@@ -31,4 +36,5 @@ export interface RemovePromotionRuleDTO {
export interface FilterablePromotionRuleProps
extends BaseFilterable<FilterablePromotionRuleProps> {
id?: string[]
code?: string[]
}

View File

@@ -4,7 +4,7 @@ import {
CreateApplicationMethodDTO,
UpdateApplicationMethodDTO,
} from "./application-method"
import { CreatePromotionRuleDTO } from "./promotion-rule"
import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule"
export type PromotionType = "standard" | "buyget"
@@ -14,6 +14,7 @@ export interface PromotionDTO {
type?: PromotionType
is_automatic?: boolean
application_method?: ApplicationMethodDTO
rules?: PromotionRuleDTO[]
}
export interface CreatePromotionDTO {

View File

@@ -2,6 +2,8 @@ import { FindConfig } from "../common"
import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context"
import {
ComputeActionContext,
ComputeActions,
CreatePromotionDTO,
CreatePromotionRuleDTO,
FilterablePromotionProps,
@@ -11,6 +13,12 @@ import {
} from "./common"
export interface IPromotionModuleService extends IModuleService {
computeActions(
promotionCodesToApply: string[],
applicationContext: ComputeActionContext,
options?: Record<string, any>
): Promise<ComputeActions[]>
create(
data: CreatePromotionDTO[],
sharedContext?: Context

View File

@@ -0,0 +1,111 @@
import { pickValueFromObject } from "../pick-value-from-object"
describe("pickValueFromObject", function () {
it("should return true or false for different types of data", function () {
const expectations = [
{
input: {
1: "attribute.another_attribute",
2: {
attribute: {
another_attribute: "test",
},
},
},
output: "test",
},
{
input: {
1: "attribute.another_attribute.array_attribute",
2: {
attribute: {
another_attribute: [
{
array_attribute: "test 1",
},
{
array_attribute: "test 2",
},
],
},
},
},
output: ["test 1", "test 2"],
},
{
input: {
1: "attribute.another_attribute.array_attribute.deep_array_attribute",
2: {
attribute: {
another_attribute: [
{
array_attribute: [
{
deep_array_attribute: "test 1",
},
{
deep_array_attribute: "test 2",
},
],
},
{
array_attribute: [],
},
],
},
},
},
output: ["test 1", "test 2"],
},
{
input: {
1: "attribute.another_attribute.array_attribute",
2: {
attribute: {
another_attribute: [
{
array_attribute: [
{
deep_array_attribute: "test 1",
},
{
deep_array_attribute: "test 2",
},
],
},
{
array_attribute: [],
},
],
},
},
},
output: [
{
deep_array_attribute: "test 1",
},
{
deep_array_attribute: "test 2",
},
],
},
{
input: {
1: "attribute.missing_attribute",
2: {
attribute: {
another_attribute: "test",
},
},
},
output: undefined,
},
]
expectations.forEach((expectation) => {
expect(
pickValueFromObject(expectation.input["1"], expectation.input["2"])
).toEqual(expectation.output)
})
})
})

View File

@@ -23,6 +23,7 @@ export * from "./medusa-container"
export * from "./object-from-string-path"
export * from "./object-to-string-path"
export * from "./optional-numeric-serializer"
export * from "./pick-value-from-object"
export * from "./promise-all"
export * from "./remote-query-object-from-string"
export * from "./remote-query-object-to-string"

View File

@@ -0,0 +1,37 @@
import { isObject } from "./is-object"
export function pickValueFromObject(
path: string,
object: Record<any, any>
): any {
const segments = path.split(".")
let result: any = undefined
for (const segment of segments) {
const segmentsLeft = [...segments].splice(1, segments.length - 1)
const segmentOutput = object[segment]
if (segmentsLeft.length === 0) {
result = segmentOutput
break
}
if (isObject(segmentOutput)) {
result = pickValueFromObject(segmentsLeft.join("."), segmentOutput)
break
}
if (Array.isArray(segmentOutput)) {
result = segmentOutput
.map((segmentOutput_) =>
pickValueFromObject(segmentsLeft.join("."), segmentOutput_)
)
.flat()
break
}
result = segmentOutput
}
return result
}

View File

@@ -10,8 +10,8 @@ export enum ApplicationMethodType {
export enum ApplicationMethodTargetType {
ORDER = "order",
SHIPPING = "shipping",
ITEM = "item",
SHIPPING_METHODS = "shipping_methods",
ITEMS = "items",
}
export enum ApplicationMethodAllocation {