feat: Add support for shipping options prices update (#7028)

This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-04-11 18:34:55 +02:00
1 parent 51acd1da5b
commit c78915c7c5
13 files changed
+450 -77

No files matched your search

@@ -7,3 +7,4 @@ export * from "./delete-service-zones"
export * from "./delete-shipping-options"
export * from "./create-shipping-profiles"
export * from "./remove-rules-from-fulfillment-shipping-option"
export * from "./set-shipping-options-prices"
@@ -0,0 +1,193 @@
import {
CreatePriceDTO,
CreatePricesDTO,
FulfillmentWorkflow,
IPricingModuleService,
IRegionModuleService,
PriceDTO,
PriceSetDTO,
RemoteQueryFunction,
} from "@medusajs/types"
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
import {
ContainerRegistrationKeys,
isDefined,
LINKS,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
interface PriceRegionId {
region_id: string
amount: number
}
type SetShippingOptionsPricesStepInput = {
id: string
prices?: FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput["prices"]
}[]
async function getCurrentShippingOptionPrices(
shippingOptionIds: string[],
{ remoteQuery }: { remoteQuery: RemoteQueryFunction }
): Promise<
{ shipping_option_id: string; price_set_id: string; prices: PriceDTO[] }[]
> {
const query = remoteQueryObjectFromString({
service: LINKS.ShippingOptionPriceSet,
variables: {
filters: { shipping_option_id: shippingOptionIds },
take: null,
},
fields: ["shipping_option_id", "price_set_id", "price_set.prices.*"],
})
const shippingOptionPrices = (await remoteQuery(query)) as {
shipping_option_id: string
price_set_id: string
price_set: PriceSetDTO
}[]
return shippingOptionPrices.map((shippingOption) => {
const prices = shippingOption.price_set?.prices ?? []
const price_set_id = shippingOption.price_set_id
return {
shipping_option_id: shippingOption.shipping_option_id,
price_set_id,
prices,
}
})
}
function buildPrices(
prices: SetShippingOptionsPricesStepInput[0]["prices"],
regionToCurrencyMap: Map<string, string>
): CreatePriceDTO[] {
if (!prices) {
return []
}
const shippingOptionPrices = prices.map((price) => {
if ("region_id" in price) {
const currency_code = regionToCurrencyMap.get(price.region_id!)!
const regionId = price.region_id
delete price.region_id
return {
...price,
currency_code: currency_code,
amount: price.amount,
rules: {
region_id: regionId,
},
}
}
return price
})
return shippingOptionPrices as CreatePriceDTO[]
}
export const setShippingOptionsPricesStepId = "set-shipping-options-prices-step"
export const setShippingOptionsPricesStep = createStep(
setShippingOptionsPricesStepId,
async (data: SetShippingOptionsPricesStepInput, { container }) => {
if (!data.length) {
return
}
const regionIds = data
.map((input) => input.prices)
.flat()
.filter((price): price is PriceRegionId => "region_id" in (price ?? {}))
.map((price) => price.region_id)
let regionToCurrencyMap: Map<string, string> = new Map()
if (regionIds.length) {
const regionService = container.resolve<IRegionModuleService>(
ModuleRegistrationName.REGION
)
const regions = await regionService.list(
{
id: [...new Set(regionIds)],
},
{
select: ["id", "currency_code"],
}
)
regionToCurrencyMap = new Map(
regions.map((region) => [region.id, region.currency_code])
)
}
const remoteQuery = container.resolve<RemoteQueryFunction>(
ContainerRegistrationKeys.REMOTE_QUERY
)
const currentShippingOptionPricesData =
await getCurrentShippingOptionPrices(
data.map((d) => d.id),
{ remoteQuery }
)
const shippingOptionPricesMap = new Map(
currentShippingOptionPricesData.map((currentShippingOptionDataItem) => {
const shippingOptionData = data.find(
(d) => d.id === currentShippingOptionDataItem.shipping_option_id
)!
const pricesData = shippingOptionData?.prices?.map((priceData) => {
return {
...priceData,
price_set_id: currentShippingOptionDataItem.price_set_id,
}
})
const buildPricesData =
pricesData && buildPrices(pricesData, regionToCurrencyMap)
return [
currentShippingOptionDataItem.shipping_option_id,
{
price_set_id: currentShippingOptionDataItem.price_set_id,
prices: buildPricesData,
},
]
})
)
const pricingService = container.resolve<IPricingModuleService>(
ModuleRegistrationName.PRICING
)
for (const data_ of data) {
const shippingOptionData = shippingOptionPricesMap.get(data_.id)!
if (!isDefined(shippingOptionData.prices)) {
continue
}
await pricingService.update(shippingOptionData.price_set_id, {
prices: shippingOptionData.prices,
})
}
return new StepResponse(void 0, currentShippingOptionPricesData)
},
async (rollbackData, { container }) => {
if (!rollbackData?.length) {
return
}
const pricingService = container.resolve<IPricingModuleService>(
ModuleRegistrationName.PRICING
)
for (const data_ of rollbackData) {
const prices = data_.prices as CreatePricesDTO[]
if (!isDefined(prices)) {
continue
}
await pricingService.update(data_.price_set_id, { prices })
}
}
)
@@ -22,9 +22,10 @@ export const createShippingOptionsWorkflow = createWorkflow(
): WorkflowData<FulfillmentWorkflow.CreateShippingOptionsWorkflowOutput> => {
const data = transform(input, (data) => {
const shippingOptionsIndexToPrices = data.map((option, index) => {
const prices = option.prices
return {
shipping_option_index: index,
prices: option.prices,
prices,
}
})
@@ -1,10 +1,18 @@
import { FulfillmentWorkflow } from "@medusajs/types"
import {
CreateRuleTypeDTO,
FulfillmentWorkflow,
RuleTypeDTO,
} from "@medusajs/types"
import {
createWorkflow,
transform,
WorkflowData,
} from "@medusajs/workflows-sdk"
import { upsertShippingOptionsStep } from "../steps"
import {
setShippingOptionsPricesStep,
upsertShippingOptionsStep,
} from "../steps"
import { createPricingRuleTypesStep } from "../../pricing"
export const updateShippingOptionsWorkflowId =
"update-shipping-options-workflow"
@@ -17,9 +25,11 @@ export const updateShippingOptionsWorkflow = createWorkflow(
): WorkflowData<FulfillmentWorkflow.UpdateShippingOptionsWorkflowOutput> => {
const data = transform(input, (data) => {
const shippingOptionsIndexToPrices = data.map((option, index) => {
const prices = option.prices
delete option.prices
return {
shipping_option_index: index,
prices: option.prices,
prices,
}
})
@@ -33,7 +43,7 @@ export const updateShippingOptionsWorkflow = createWorkflow(
data.shippingOptions
)
/*const normalizedShippingOptionsPrices = transform(
const normalizedShippingOptionsPrices = transform(
{
shippingOptions: updatedShippingOptions,
shippingOptionsIndexToPrices: data.shippingOptionsIndexToPrices,
@@ -60,32 +70,16 @@ export const updateShippingOptionsWorkflow = createWorkflow(
return {
shippingOptionsPrices,
ruleTypes: Array.from(ruleTypes) as UpdateRuleTypeDTO[],
ruleTypes: Array.from(ruleTypes) as CreateRuleTypeDTO[],
}
}
)*/
/*updatePricingRuleTypesStep(normalizedShippingOptionsPrices.ruleTypes)*/
/*const shippingOptionsPriceSetsLinkData = updateShippingOptionsPriceSetsStep(
normalizedShippingOptionsPrices.shippingOptionsPrices
)
const normalizedLinkData = transform(
{
shippingOptionsPriceSetsLinkData,
},
(data) => {
return data.shippingOptionsPriceSetsLinkData.map((item) => {
return {
id: item.id,
price_sets: [item.priceSetId],
}
})
}
)*/
createPricingRuleTypesStep(normalizedShippingOptionsPrices.ruleTypes)
/*setShippingOptionsPriceSetsStep(normalizedLinkData)*/
setShippingOptionsPricesStep(
normalizedShippingOptionsPrices.shippingOptionsPrices
)
return updatedShippingOptions
}
@@ -70,6 +70,23 @@ export const AdminCreateShippingOptionPriceWithRegion = z
})
.strict()
export const AdminUpdateShippingOptionPriceWithCurrency =z
.object({
id: z.string().optional(),
currency_code: z.string().optional(),
amount: z.number().optional(),
})
.strict()
export const AdminUpdateShippingOptionPriceWithRegion =
z
.object({
id: z.string().optional(),
region_id: z.string().optional(),
amount: z.number().optional(),
})
.strict()
export const AdminCreateShippingOption = z
.object({
name: z.string(),
@@ -98,6 +115,11 @@ export const AdminUpdateShippingOption = z
price_type: z.nativeEnum(ShippingOptionPriceTypeEnum).optional(),
provider_id: z.string().optional(),
type: AdminCreateShippingOptionTypeObject.optional(),
prices: AdminUpdateShippingOptionPriceWithCurrency.or(
AdminUpdateShippingOptionPriceWithRegion
)
.array()
.optional(),
})
.strict()
+12 -5
View File
@@ -1,8 +1,14 @@
import { getSetDifference, stringToSelectRelationObject } from "@medusajs/utils"
import {
getSetDifference,
isPresent,
stringToSelectRelationObject,
} from "@medusajs/utils"
import { pick } from "lodash"
import { MedusaError, isDefined } from "medusa-core-utils"
import { isDefined, MedusaError } from "medusa-core-utils"
import { BaseEntity } from "../interfaces"
import { FindConfig, QueryConfig, RequestQueryFields } from "../types/common"
import { featureFlagRouter } from "../loaders/feature-flags"
import MedusaV2 from "../loaders/feature-flags/medusa-v2"
export function pickByConfig<TModel extends BaseEntity>(
obj: TModel | TModel[],
@@ -24,7 +30,7 @@ export function prepareListQuery<
T extends RequestQueryFields,
TEntity extends BaseEntity
>(validated: T, queryConfig: QueryConfig<TEntity> = {}) {
const isMedusaV2 = process.env.MEDUSA_FF_MEDUSA_V2 == "true"
const isMedusaV2 = featureFlagRouter.isFeatureEnabled(MedusaV2.key)
// TODO: this function will be simplified a lot once we drop support for the old api
const { order, fields, limit = 50, expand, offset = 0 } = validated
@@ -189,13 +195,14 @@ export function prepareListQuery<
}
}
const finalOrder = isPresent(orderBy) ? orderBy : undefined
return {
listConfig: {
select: select.length ? select : undefined,
relations: Array.from(allRelations),
skip: offset,
take: limit ?? defaultLimit,
order: orderBy,
order: finalOrder,
},
remoteQueryConfig: {
// Add starFields that are relations only on which we want all properties with a dedicated format to the remote query
@@ -207,7 +214,7 @@ export function prepareListQuery<
? {
skip: offset,
take: limit ?? defaultLimit,
order: orderBy,
order: finalOrder,
}
: {},
},
+83 -10
View File
@@ -2,8 +2,8 @@ import {
AddPricesDTO,
Context,
CreatePriceListRuleDTO,
CreatePriceSetDTO,
CreatePricesDTO,
CreatePriceSetDTO,
DAL,
InternalModuleDeclaration,
ModuleJoinerConfig,
@@ -23,7 +23,7 @@ import {
groupBy,
InjectManager,
InjectTransactionManager,
isDefined,
isDefined, isPresent,
isString,
MedusaContext,
MedusaError,
@@ -44,12 +44,12 @@ import {
RuleType,
} from "@models"
import { PriceListService, RuleTypeService } from "@services"
import { validatePriceListDates } from "@utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { PriceSetIdPrefix } from "../models/price-set"
import { PriceListIdPrefix } from "../models/price-list"
import { UpdatePriceSetInput } from "src/types/services"
import {PriceListService, RuleTypeService} from "@services"
import {validatePriceListDates} from "@utils"
import {entityNameToLinkableKeysMap, joinerConfig} from "../joiner-config"
import {PriceSetIdPrefix} from "../models/price-set"
import {PriceListIdPrefix} from "../models/price-list"
import {UpdatePriceSetInput} from "src/types/services"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
@@ -336,6 +336,54 @@ export default class PricingModuleService<
return isString(idOrSelector) ? priceSets[0] : priceSets
}
private async normalizeUpdateData(
data: UpdatePriceSetInput[],
sharedContext
) {
const ruleAttributes = data
.map((d) => d.prices?.map((p) => Object.keys(p.rules ?? [])) ?? [])
.flat(Infinity)
.filter(Boolean)
const ruleTypes = await this.ruleTypeService_.list(
{ rule_attribute: ruleAttributes },
{ take: null },
sharedContext
)
const ruleTypeMap = ruleTypes.reduce((acc, curr) => {
acc.set(curr.rule_attribute, curr)
return acc
}, new Map())
return data.map((priceSet) => {
const prices = priceSet.prices?.map((price) => {
const rules = Object.entries(price.rules ?? {}).map(
([attribute, value]) => {
return {
price_set_id: priceSet.id,
rule_type_id: ruleTypeMap.get(attribute)!.id,
value,
}
}
)
const hasRulesInput = isPresent(price.rules)
delete price.rules
return {
...price,
price_set_id: priceSet.id,
price_rules: hasRulesInput ? rules : undefined,
rules_count: hasRulesInput ? rules.length : undefined
}
})
return {
...priceSet,
prices,
}
})
}
@InjectTransactionManager("baseRepository_")
protected async update_(
data: UpdatePriceSetInput[],
@@ -344,8 +392,33 @@ export default class PricingModuleService<
// TODO: We are not handling rule types, rules, etc. here, add support after data models are finalized
// TODO: Since money IDs are rarely passed, this will delete all previous data and insert new entries.
// We can make the `insert` inside upsertWithReplace do an `upsert` instead to avoid this
return this.priceSetService_.upsertWithReplace(
data,
const normalizedData = await this.normalizeUpdateData(data, sharedContext)
const prices = normalizedData.flatMap((priceSet) => priceSet.prices || [])
const upsertedPrices = await this.priceService_.upsertWithReplace(
prices,
{
relations: ["price_rules"],
},
sharedContext
)
const priceSetsToUpsert = normalizedData.map((priceSet) => {
const { prices, ...rest } = priceSet
return {
...rest,
prices: upsertedPrices
.filter((p) => p.price_set_id === priceSet.id)
.map((price) => {
// @ts-ignore
delete price.price_rules
return price
}),
}
})
return await this.priceSetService_.upsertWithReplace(
priceSetsToUpsert,
{ relations: ["prices"] },
sharedContext
)
@@ -16,12 +16,14 @@ export interface UpdateShippingOptionsWorkflowInput {
}
prices?: (
| {
currency_code: string
amount: number
id?: string
currency_code?: string
amount?: number
}
| {
region_id: string
amount: number
id?: string
region_id?: string
amount?: number
}
)[]
rules?: {
@@ -13,6 +13,7 @@ import {
doNotForceTransaction,
isDefined,
isObject,
isPresent,
isString,
lowerCaseFirst,
MedusaError,
@@ -85,7 +86,7 @@ export function internalModuleServiceFactory<
* @param config
*/
static applyDefaultOrdering(config: FindConfig<any>) {
if (config.order) {
if (isPresent(config.order)) {
return
}