diff --git a/.changeset/six-rats-pretend.md b/.changeset/six-rats-pretend.md new file mode 100644 index 0000000000..71beadc1c9 --- /dev/null +++ b/.changeset/six-rats-pretend.md @@ -0,0 +1,10 @@ +--- +"@medusajs/link-modules": patch +"@medusajs/modules-sdk": patch +"@medusajs/core-flows": patch +"@medusajs/pricing": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +Remote query supporting context diff --git a/integration-tests/modules/__tests__/link-modules/shipping-option-price-set.spec.ts b/integration-tests/modules/__tests__/link-modules/shipping-option-price-set.spec.ts index c3a5223a34..383606f700 100644 --- a/integration-tests/modules/__tests__/link-modules/shipping-option-price-set.spec.ts +++ b/integration-tests/modules/__tests__/link-modules/shipping-option-price-set.spec.ts @@ -71,6 +71,10 @@ medusaIntegrationTestRunner({ amount: 3000, currency_code: "usd", }, + { + amount: 5000, + currency_code: "eur", + }, ], }) @@ -91,6 +95,17 @@ medusaIntegrationTestRunner({ price_set_link: { fields: ["id", "price_set_id", "shipping_option_id"], }, + prices: { + fields: ["amount", "currency_code"], + }, + calculated_price: { + fields: ["calculated_amount", "currency_code"], + __args: { + context: { + currency_code: "eur", + }, + }, + }, }, }) @@ -103,6 +118,20 @@ medusaIntegrationTestRunner({ price_set_id: priceSet.id, shipping_option_id: shippingOption.id, }), + prices: [ + expect.objectContaining({ + amount: 5000, + currency_code: "eur", + }), + expect.objectContaining({ + amount: 3000, + currency_code: "usd", + }), + ], + calculated_price: expect.objectContaining({ + calculated_amount: 5000, + currency_code: "eur", + }), }), ]) ) diff --git a/packages/core-flows/src/definition/cart/workflows/add-shipping-method-to-cart.ts b/packages/core-flows/src/definition/cart/workflows/add-shipping-method-to-cart.ts index e64fb965a0..2da7c0b111 100644 --- a/packages/core-flows/src/definition/cart/workflows/add-shipping-method-to-cart.ts +++ b/packages/core-flows/src/definition/cart/workflows/add-shipping-method-to-cart.ts @@ -5,7 +5,6 @@ import { } from "@medusajs/workflows-sdk" import { useRemoteQueryStep } from "../../../common/steps/use-remote-query" import { addShippingMethodToCartStep } from "../steps" -import { getShippingOptionPriceSetsStep } from "../steps/get-shipping-option-price-sets" import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions" import { updateTaxLinesStep } from "../steps/update-tax-lines" @@ -28,32 +27,30 @@ export const addShippingMethodToWorkflow = createWorkflow( return (data.input.options ?? []).map((i) => i.id) }) - const priceSets = getShippingOptionPriceSetsStep({ - optionIds: optionIds, - context: { currency_code: input.currency_code }, - }) - const shippingOptions = useRemoteQueryStep({ entry_point: "shipping_option", - fields: ["id", "name"], + fields: ["id", "name", "calculated_price.calculated_amount"], variables: { id: optionIds, + calculated_price: { + context: { + currency_code: input.currency_code, + }, + }, }, }) const shippingMethodInput = transform( - { priceSets, input, shippingOptions }, + { input, shippingOptions }, (data) => { const options = (data.input.options ?? []).map((option) => { const shippingOption = data.shippingOptions.find( (so) => so.id === option.id )! - const price = data.priceSets[option.id].calculated_amount - return { shipping_option_id: shippingOption.id, - amount: price, + amount: shippingOption.calculated_price.calculated_amount, data: option.data ?? {}, name: shippingOption.name, cart_id: data.input.cart_id, diff --git a/packages/link-modules/src/definitions/product-variant-price-set.ts b/packages/link-modules/src/definitions/product-variant-price-set.ts index 2daf117205..c571704625 100644 --- a/packages/link-modules/src/definitions/product-variant-price-set.ts +++ b/packages/link-modules/src/definitions/product-variant-price-set.ts @@ -43,6 +43,10 @@ export const ProductVariantPriceSet: ModuleJoinerConfig = { serviceName: Modules.PRODUCT, fieldAlias: { price_set: "price_set_link.price_set", + calculated_price: { + path: "price_set_link.price_set.calculated_price", + forwardArgumentsOnPath: ["price_set_link.price_set"], + }, }, relationship: { serviceName: LINKS.ProductVariantPriceSet, diff --git a/packages/link-modules/src/definitions/shipping-option-price-set.ts b/packages/link-modules/src/definitions/shipping-option-price-set.ts index b586d9a0b0..475400366e 100644 --- a/packages/link-modules/src/definitions/shipping-option-price-set.ts +++ b/packages/link-modules/src/definitions/shipping-option-price-set.ts @@ -44,6 +44,10 @@ export const ShippingOptionPriceSet: ModuleJoinerConfig = { path: "price_set_link.price_set.prices", isList: true, }, + calculated_price: { + path: "price_set_link.price_set.calculated_price", + forwardArgumentsOnPath: ["price_set_link.price_set"], + }, }, relationship: { serviceName: LINKS.ShippingOptionPriceSet, diff --git a/packages/modules-sdk/src/remote-query.ts b/packages/modules-sdk/src/remote-query.ts index ed1a036ace..c7649d0af1 100644 --- a/packages/modules-sdk/src/remote-query.ts +++ b/packages/modules-sdk/src/remote-query.ts @@ -191,7 +191,9 @@ export class RemoteQuery { for (const arg of expand.args || []) { if (arg.name === "filters" && arg.value) { - filters = { ...arg.value } + filters = { ...filters, ...arg.value } + } else if (arg.name === "context" && arg.value) { + filters["context"] = arg.value } else if (availableOptions.includes(arg.name)) { const argName = availableOptionsAlias.has(arg.name) ? availableOptionsAlias.get(arg.name)! diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index 6c865c558d..9acbe70fb1 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -6,6 +6,7 @@ import { CreatePricesDTO, CreatePriceSetDTO, DAL, + FindConfig, InternalModuleDeclaration, ModuleJoinerConfig, ModulesSdkTypes, @@ -152,6 +153,95 @@ export default class PricingModuleService< return joinerConfig } + private setupCalculatedPriceConfig_( + filters, + config + ): PricingContext["context"] | undefined { + const fieldIdx = config.relations?.indexOf("calculated_price") + const shouldCalculatePrice = fieldIdx > -1 + if (!shouldCalculatePrice) { + return + } + + let pricingContext = filters.context ?? {} + + // cleanup virtual field "calculated_price" + config.relations?.splice(fieldIdx, 1) + delete filters.context + + return pricingContext + } + + @InjectManager("baseRepository_") + async list( + filters: PricingTypes.FilterablePriceSetProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const pricingContext = this.setupCalculatedPriceConfig_(filters, config) + + const priceSets = await super.list(filters, config, sharedContext) + + if (pricingContext && priceSets.length) { + const priceSetIds: string[] = [] + const priceSetMap = new Map() + for (const priceSet of priceSets) { + priceSetIds.push(priceSet.id) + priceSetMap.set(priceSet.id, priceSet) + } + + const calculatedPrices = await this.calculatePrices( + { id: priceSets.map((p) => p.id) }, + { context: pricingContext }, + sharedContext + ) + + for (const calculatedPrice of calculatedPrices) { + const priceSet = priceSetMap.get(calculatedPrice.id) + priceSet.calculated_price = calculatedPrice + } + } + + return priceSets + } + + @InjectManager("baseRepository_") + async listAndCount( + filters: PricingTypes.FilterablePriceSetProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[PriceSetDTO[], number]> { + const pricingContext = this.setupCalculatedPriceConfig_(filters, config) + + const [priceSets, count] = await super.listAndCount( + filters, + config, + sharedContext + ) + + if (pricingContext && priceSets.length) { + const priceSetIds: string[] = [] + const priceSetMap = new Map() + for (const priceSet of priceSets) { + priceSetIds.push(priceSet.id) + priceSetMap.set(priceSet.id, priceSet) + } + + const calculatedPrices = await this.calculatePrices( + { id: priceSets.map((p) => p.id) }, + { context: pricingContext }, + sharedContext + ) + + for (const calculatedPrice of calculatedPrices) { + const priceSet = priceSetMap.get(calculatedPrice.id) + priceSet.calculated_price = calculatedPrice + } + } + + return [priceSets, count] + } + @InjectManager("baseRepository_") async calculatePrices( pricingFilters: PricingFilters, @@ -171,7 +261,9 @@ export default class PricingModuleService< (priceSetId: string): PricingTypes.CalculatedPriceSet => { // This is where we select prices, for now we just do a first match based on the database results // which is prioritized by rules_count first for exact match and then deafult_priority of the rule_type - // inject custom price selection here + + // TODO: inject custom price selection here + const prices = pricesSetPricesMap.get(priceSetId) || [] const priceListPrice = prices.find((p) => p.price_list_id) @@ -749,7 +841,7 @@ export default class PricingModuleService< return { ...rest, - title: "test", // TODO: accept title + title: "", // TODO: accept title rules_count: numberOfRules, price_rules: Array.from(rulesDataMap.values()), } diff --git a/packages/types/src/pricing/common/price-set.ts b/packages/types/src/pricing/common/price-set.ts index f93ebe8b76..c337a7ace2 100644 --- a/packages/types/src/pricing/common/price-set.ts +++ b/packages/types/src/pricing/common/price-set.ts @@ -58,6 +58,12 @@ export interface PriceSetDTO { * The prices that belong to this price set. */ prices?: MoneyAmountDTO[] + + /** + * The calculated price based on the context. + */ + calculated_price?: CalculatedPriceSet + /** * The rule types applied on this price set. */ @@ -321,7 +327,8 @@ export interface UpdatePriceSetDTO { * Filters to apply on price sets. */ export interface FilterablePriceSetProps - extends BaseFilterable { + extends BaseFilterable, + PricingContext { /** * IDs to filter price sets by. */ diff --git a/packages/utils/src/common/__tests__/remote-query-object-from-string.spec.ts b/packages/utils/src/common/__tests__/remote-query-object-from-string.spec.ts index d39acc4f2b..f910642c11 100644 --- a/packages/utils/src/common/__tests__/remote-query-object-from-string.spec.ts +++ b/packages/utils/src/common/__tests__/remote-query-object-from-string.spec.ts @@ -33,13 +33,23 @@ describe("remoteQueryObjectFromString", function () { it("should return a remote query object", function () { const output = remoteQueryObjectFromString({ entryPoint: "product", - variables: {}, + variables: { + q: "name", + options: { + name: "option_name", + }, + "options.values": { + value: 123, + }, + }, fields, }) expect(output).toEqual({ product: { - __args: {}, + __args: { + q: "name", + }, fields: [ "id", "created_at", @@ -54,6 +64,9 @@ describe("remoteQueryObjectFromString", function () { }, options: { + __args: { + name: "option_name", + }, fields: [ "id", "created_at", @@ -64,6 +77,9 @@ describe("remoteQueryObjectFromString", function () { "metadata", ], values: { + __args: { + value: 123, + }, fields: [ "id", "created_at", diff --git a/packages/utils/src/common/remote-query-object-from-string.ts b/packages/utils/src/common/remote-query-object-from-string.ts index 000848333a..4fa135e26f 100644 --- a/packages/utils/src/common/remote-query-object-from-string.ts +++ b/packages/utils/src/common/remote-query-object-from-string.ts @@ -1,3 +1,5 @@ +import { isObject } from "./is-object" + /** * Convert a string fields array to a remote query object * @param config - The configuration object @@ -109,9 +111,7 @@ export function remoteQueryObjectFromString( }, } - if (variables) { - remoteJoinerConfig[entryKey]["__args"] = variables - } + const usedVariables = new Set() for (const field of fields) { if (!field.includes(".")) { @@ -122,8 +122,19 @@ export function remoteQueryObjectFromString( const fieldSegments = field.split(".") const fieldProperty = fieldSegments.pop() + let combinedPath = "" + const deepConfigRef = fieldSegments.reduce((acc, curr) => { - acc[curr] ??= {} + combinedPath = combinedPath ? combinedPath + "." + curr : curr + + if (isObject(variables) && combinedPath in variables) { + acc[curr] ??= {} + acc[curr]["__args"] = variables[combinedPath] + usedVariables.add(combinedPath) + } else { + acc[curr] ??= {} + } + return acc[curr] }, remoteJoinerConfig[entryKey]) @@ -131,5 +142,14 @@ export function remoteQueryObjectFromString( deepConfigRef["fields"].push(fieldProperty) } + const topLevelArgs = {} + for (const key of Object.keys(variables ?? {})) { + if (!usedVariables.has(key)) { + topLevelArgs[key] = variables[key] + } + } + + remoteJoinerConfig[entryKey]["__args"] = topLevelArgs ?? {} + return remoteJoinerConfig }