diff --git a/packages/core/core-flows/src/fulfillment/steps/calculate-shipping-options-prices.ts b/packages/core/core-flows/src/fulfillment/steps/calculate-shipping-options-prices.ts new file mode 100644 index 0000000000000..f1b9f52d6bd9e --- /dev/null +++ b/packages/core/core-flows/src/fulfillment/steps/calculate-shipping-options-prices.ts @@ -0,0 +1,24 @@ +import { + CalculateShippingOptionPriceDTO, + IFulfillmentModuleService, +} from "@medusajs/framework/types" +import { Modules } from "@medusajs/framework/utils" +import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" + +export const calculateShippingOptionsPricesStepId = + "calculate-shipping-options-prices" +/** + * This step calculates the prices for one or more shipping options. + */ +export const calculateShippingOptionsPricesStep = createStep( + calculateShippingOptionsPricesStepId, + async (input: CalculateShippingOptionPriceDTO[], { container }) => { + const service = container.resolve( + Modules.FULFILLMENT + ) + + const prices = await service.calculateShippingOptionsPrices(input) + + return new StepResponse(prices) + } +) diff --git a/packages/core/core-flows/src/fulfillment/steps/index.ts b/packages/core/core-flows/src/fulfillment/steps/index.ts index 29226f6874f91..dbb9b5d33289e 100644 --- a/packages/core/core-flows/src/fulfillment/steps/index.ts +++ b/packages/core/core-flows/src/fulfillment/steps/index.ts @@ -15,3 +15,4 @@ export * from "./update-fulfillment" export * from "./update-shipping-profiles" export * from "./upsert-shipping-options" export * from "./validate-shipment" +export * from "./calculate-shipping-options-prices" diff --git a/packages/core/core-flows/src/fulfillment/workflows/calculate-shipping-options-prices.ts b/packages/core/core-flows/src/fulfillment/workflows/calculate-shipping-options-prices.ts new file mode 100644 index 0000000000000..e0ff4d36e546b --- /dev/null +++ b/packages/core/core-flows/src/fulfillment/workflows/calculate-shipping-options-prices.ts @@ -0,0 +1,63 @@ +import { FulfillmentWorkflow } from "@medusajs/framework/types" +import { + createWorkflow, + transform, + WorkflowData, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { calculateShippingOptionsPricesStep } from "../steps" +import { useQueryGraphStep } from "../../common" + +export const calculateShippingOptionsPricesWorkflowId = + "calculate-shipping-options-prices-workflow" +/** + * This workflow calculates the prices for one or more shipping options. + */ +export const calculateShippingOptionsPricesWorkflow = createWorkflow( + calculateShippingOptionsPricesWorkflowId, + ( + input: WorkflowData + ): WorkflowResponse => { + const ids = transform({ input }, ({ input }) => + input.shipping_options.map((so) => so.id) + ) + + const shippingOptionsQuery = useQueryGraphStep({ + entity: "shipping_option", + filters: { id: ids }, + fields: ["id", "provider_id", "data"], + }).config({ name: "shipping-options-query" }) + + const cartQuery = useQueryGraphStep({ + entity: "cart", + filters: { id: input.cart_id }, + fields: ["id", "items.*", "shipping_address.*"], + }).config({ name: "cart-query" }) + + const data = transform( + { shippingOptionsQuery, cartQuery, input }, + ({ shippingOptionsQuery, cartQuery, input }) => { + const shippingOptions = shippingOptionsQuery.data + const cart = cartQuery.data[0] + + const shippingOptionDataMap = new Map( + input.shipping_options.map((so) => [so.id, so.data]) + ) + + return shippingOptions.map((shippingOption) => ({ + id: shippingOption.id, + provider_id: shippingOption.provider_id, + optionData: shippingOption.data, + data: shippingOptionDataMap.get(shippingOption.id) ?? {}, + context: { + cart, + }, + })) + } + ) + + const prices = calculateShippingOptionsPricesStep(data) + + return new WorkflowResponse(prices) + } +) diff --git a/packages/core/core-flows/src/fulfillment/workflows/index.ts b/packages/core/core-flows/src/fulfillment/workflows/index.ts index fd61409d1f842..2bb231a391ff0 100644 --- a/packages/core/core-flows/src/fulfillment/workflows/index.ts +++ b/packages/core/core-flows/src/fulfillment/workflows/index.ts @@ -14,3 +14,4 @@ export * from "./update-fulfillment" export * from "./update-service-zones" export * from "./update-shipping-options" export * from "./update-shipping-profiles" +export * from "./calculate-shipping-options-prices" diff --git a/packages/core/types/src/fulfillment/mutations/shipping-option.ts b/packages/core/types/src/fulfillment/mutations/shipping-option.ts index b7432a1d0d839..df4517e86d745 100644 --- a/packages/core/types/src/fulfillment/mutations/shipping-option.ts +++ b/packages/core/types/src/fulfillment/mutations/shipping-option.ts @@ -1,6 +1,7 @@ import { CreateShippingOptionTypeDTO } from "./shipping-option-type" import { ShippingOptionPriceType } from "../common" import { CreateShippingOptionRuleDTO } from "./shipping-option-rule" +import { CartDTO } from "../../cart" /** * The shipping option to be created. @@ -118,3 +119,39 @@ export interface UpdateShippingOptionDTO { * A shipping option to be created or updated. */ export interface UpsertShippingOptionDTO extends UpdateShippingOptionDTO {} + +/** + * The data needed for the associated fulfillment provider to calculate the price of a shipping option. + */ +export interface CalculateShippingOptionPriceDTO { + /** + * The ID of the shipping option. + */ + id: string + + /** + * The ID of the fulfillment provider. + */ + provider_id: string + + /** + * The option data from the provider. + */ + optionData: Record + + /** + * Additional data passed when the price is calculated. + * + * @example + * When calculating the price for a shipping option upon creation of a shipping method additional data can be passed + * to the provider. + */ + data: Record + + /** + * The calculation context needed for the associated fulfillment provider to calculate the price of a shipping option. + */ + context: { + cart: Pick + } & Record +} diff --git a/packages/core/types/src/fulfillment/provider.ts b/packages/core/types/src/fulfillment/provider.ts index 6729bc490aece..b55141a4a40fa 100644 --- a/packages/core/types/src/fulfillment/provider.ts +++ b/packages/core/types/src/fulfillment/provider.ts @@ -13,7 +13,7 @@ export type FulfillmentOption = { } export type CalculatedShippingOptionPrice = { - calculated_amount: number + calculated_price: number is_calculated_price_tax_inclusive: boolean } diff --git a/packages/core/types/src/fulfillment/service.ts b/packages/core/types/src/fulfillment/service.ts index 46017e7b34a85..76d0671a65d41 100644 --- a/packages/core/types/src/fulfillment/service.ts +++ b/packages/core/types/src/fulfillment/service.ts @@ -24,6 +24,7 @@ import { ShippingProfileDTO, } from "./common" import { + CalculateShippingOptionPriceDTO, CreateFulfillmentSetDTO, CreateGeoZoneDTO, CreateServiceZoneDTO, @@ -44,6 +45,7 @@ import { CreateShippingProfileDTO, UpsertShippingProfileDTO, } from "./mutations/shipping-profile" +import { CalculatedShippingOptionPrice } from "./provider" /** * The main service interface for the Fulfillment Module. @@ -2647,6 +2649,33 @@ export interface IFulfillmentModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * This method calculates the prices for one or more shipping options. + * + * @param {CalculateShippingOptionPriceDTO[]} shippingOptionsData - The shipping options data to calculate the prices for. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The calculated shipping option prices. + * + * @example + * const prices = + * await fulfillmentModuleService.calculateShippingOptionsPrices( + * [ + * { + * provider_id: "webshipper", + * data: { + * cart: { + * id: "cart_123", + * }, + * }, + * }, + * ] + * ) + */ + calculateShippingOptionsPrices( + shippingOptionsData: CalculateShippingOptionPriceDTO[], + sharedContext?: Context + ): Promise + /** * This method retrieves a paginated list of fulfillment providers based on optional filters and configuration. * diff --git a/packages/core/types/src/http/shipping-option/store/index.ts b/packages/core/types/src/http/shipping-option/store/index.ts index 020c34f02c710..e236f0b40fef3 100644 --- a/packages/core/types/src/http/shipping-option/store/index.ts +++ b/packages/core/types/src/http/shipping-option/store/index.ts @@ -1,3 +1,4 @@ export * from "./entities" export * from "./queries" export * from "./responses" +export * from "./payloads" diff --git a/packages/core/types/src/http/shipping-option/store/payloads.ts b/packages/core/types/src/http/shipping-option/store/payloads.ts new file mode 100644 index 0000000000000..63815113b909f --- /dev/null +++ b/packages/core/types/src/http/shipping-option/store/payloads.ts @@ -0,0 +1,4 @@ +export type StoreCalculateShippingOptionPrice = { + cart_id: string + data: Record +} diff --git a/packages/core/types/src/http/shipping-option/store/responses.ts b/packages/core/types/src/http/shipping-option/store/responses.ts index bb7df6ad0bea4..8e64fc2ea90d1 100644 --- a/packages/core/types/src/http/shipping-option/store/responses.ts +++ b/packages/core/types/src/http/shipping-option/store/responses.ts @@ -3,3 +3,7 @@ import { StoreCartShippingOption } from "../../fulfillment" export interface StoreShippingOptionListResponse { shipping_options: StoreCartShippingOption[] } + +export interface StoreShippingOptionResponse { + shipping_option: StoreCartShippingOption +} diff --git a/packages/core/types/src/workflow/fulfillment/calculate-shipping-options-prices.ts b/packages/core/types/src/workflow/fulfillment/calculate-shipping-options-prices.ts new file mode 100644 index 0000000000000..92c5d1a5d87e0 --- /dev/null +++ b/packages/core/types/src/workflow/fulfillment/calculate-shipping-options-prices.ts @@ -0,0 +1,9 @@ +import { CalculatedShippingOptionPrice } from "../../fulfillment" + +export type CalculateShippingOptionsPricesWorkflowInput = { + cart_id: string + shipping_options: { id: string; data: Record }[] +} + +export type CalculateShippingOptionsPricesWorkflowOutput = + CalculatedShippingOptionPrice[] diff --git a/packages/core/types/src/workflow/fulfillment/index.ts b/packages/core/types/src/workflow/fulfillment/index.ts index 73eff83f3787f..54bc938766815 100644 --- a/packages/core/types/src/workflow/fulfillment/index.ts +++ b/packages/core/types/src/workflow/fulfillment/index.ts @@ -6,3 +6,4 @@ export * from "./service-zones" export * from "./shipping-profiles" export * from "./update-fulfillment" export * from "./update-shipping-options" +export * from "./calculate-shipping-options-prices" diff --git a/packages/core/utils/src/fulfillment/provider.ts b/packages/core/utils/src/fulfillment/provider.ts index bd3a1cf69faa2..12cfff438cc73 100644 --- a/packages/core/utils/src/fulfillment/provider.ts +++ b/packages/core/utils/src/fulfillment/provider.ts @@ -202,19 +202,18 @@ export class AbstractFulfillmentProviderService * The Medusa application uses the {@link canCalculate} method first to check whether the shipping option's price is calculated. * If it returns `true`, Medusa uses this method to retrieve the calculated price. * - * @param optionData - The `data` property of a shipping option. - * @param data - If the price is calculated for a shipping option, it's the `data` of the shipping option. Otherwise, it's the `data of the shipping method. - * @param cart - The cart details. + * @param optionData - Shipping option data from the provider, the `data` property of a shipping option. + * @param data - Additional data passed when the price is calculated. + * @param context - The context details, such as the cart or customer. * @returns The calculated price * * @example * class MyFulfillmentProviderService extends AbstractFulfillmentProviderService { * // ... - * async calculatePrice(optionData: any, data: any, cart: any): Promise { + * async calculatePrice(optionData: any, data: any, context: any): Promise { * // assuming the client can calculate the price using * // the third-party service * const price = await this.client.calculate(data) - * * return price * } * } diff --git a/packages/medusa/src/api/store/shipping-options/[id]/calculate/route.ts b/packages/medusa/src/api/store/shipping-options/[id]/calculate/route.ts new file mode 100644 index 0000000000000..a3ffda5ec8534 --- /dev/null +++ b/packages/medusa/src/api/store/shipping-options/[id]/calculate/route.ts @@ -0,0 +1,31 @@ +import { HttpTypes } from "@medusajs/framework/types" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { calculateShippingOptionsPricesWorkflow } from "@medusajs/core-flows" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + + const { result } = await calculateShippingOptionsPricesWorkflow( + req.scope + ).run({ + input: { + shipping_options: [{ id: req.params.id, data: req.validatedBody.data }], + cart_id: req.validatedBody.cart_id, + }, + }) + + const { data } = await query.graph({ + entity: "shipping_option", + fields: req.remoteQueryConfig.fields, + filters: { id: req.params.id }, + }) + + const shippingOption = data[0] + const priceData = result[0] + + res.status(200).json({ shipping_option: { ...shippingOption, ...priceData } }) +} diff --git a/packages/medusa/src/api/store/shipping-options/middlewares.ts b/packages/medusa/src/api/store/shipping-options/middlewares.ts index 42bb88e642363..b402dbd599df2 100644 --- a/packages/medusa/src/api/store/shipping-options/middlewares.ts +++ b/packages/medusa/src/api/store/shipping-options/middlewares.ts @@ -1,7 +1,15 @@ -import { MiddlewareRoute } from "@medusajs/framework/http" +import { + MiddlewareRoute, + validateAndTransformBody, +} from "@medusajs/framework/http" import { validateAndTransformQuery } from "@medusajs/framework" import { listTransformQueryConfig } from "./query-config" -import { StoreGetShippingOptions } from "./validators" +import { + StoreCalculateShippingOptionPrice, + StoreGetShippingOptions, + StoreGetShippingOptionsParams, +} from "./validators" +import * as QueryConfig from "./query-config" export const storeShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [ { @@ -14,4 +22,15 @@ export const storeShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/store/shipping-options/:id/calculate", + middlewares: [ + validateAndTransformQuery( + StoreGetShippingOptionsParams, + QueryConfig.retrieveTransformQueryConfig + ), + validateAndTransformBody(StoreCalculateShippingOptionPrice), + ], + }, ] diff --git a/packages/medusa/src/api/store/shipping-options/query-config.ts b/packages/medusa/src/api/store/shipping-options/query-config.ts index 07cb17ccfc3f1..5611e1110ddbb 100644 --- a/packages/medusa/src/api/store/shipping-options/query-config.ts +++ b/packages/medusa/src/api/store/shipping-options/query-config.ts @@ -12,3 +12,8 @@ export const listTransformQueryConfig = { defaultLimit: 20, isList: true, } + +export const retrieveTransformQueryConfig = { + defaults: defaultStoreShippingOptionsFields, + isList: false, +} diff --git a/packages/medusa/src/api/store/shipping-options/validators.ts b/packages/medusa/src/api/store/shipping-options/validators.ts index c44e9891a0648..3cf7a938f0b64 100644 --- a/packages/medusa/src/api/store/shipping-options/validators.ts +++ b/packages/medusa/src/api/store/shipping-options/validators.ts @@ -1,6 +1,8 @@ import { z } from "zod" import { applyAndAndOrOperators } from "../../utils/common-validators" -import { createFindParams } from "../../utils/validators" +import { createFindParams, createSelectParams } from "../../utils/validators" + +export const StoreGetShippingOptionsParams = createSelectParams() export const StoreGetShippingOptionsFields = z .object({ @@ -18,3 +20,11 @@ export const StoreGetShippingOptions = createFindParams({ }) .merge(StoreGetShippingOptionsFields) .merge(applyAndAndOrOperators(StoreGetShippingOptionsFields)) + +export type StoreCalculateShippingOptionPriceType = z.infer< + typeof StoreCalculateShippingOptionPrice +> +export const StoreCalculateShippingOptionPrice = z.object({ + cart_id: z.string(), + data: z.record(z.string(), z.unknown()), +}) diff --git a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts index dd43d15ac299b..6a8aaada8f07c 100644 --- a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts +++ b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts @@ -1,4 +1,5 @@ import { + CalculatedShippingOptionPrice, Context, DAL, FilterableFulfillmentSetProps, @@ -1998,6 +1999,21 @@ export default class FulfillmentModuleService return await promiseAll(promises) } + async calculateShippingOptionsPrices( + shippingOptionsData: FulfillmentTypes.CalculateShippingOptionPriceDTO[] + ): Promise { + const promises = shippingOptionsData.map((data) => + this.fulfillmentProviderService_.calculatePrice( + data.provider_id, + data.optionData, + data.data, + data.context + ) + ) + + return await promiseAll(promises) + } + @InjectTransactionManager() // @ts-expect-error async deleteShippingProfiles(