feat(medusa, core-flows, fulfillment): calculate SO price endpoint (#10532)
**What** - endpoint + flow for fetching calculated price for a shipping option --- CLOSES CMRC-777
This commit is contained in:
@@ -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<IFulfillmentModuleService>(
|
||||
Modules.FULFILLMENT
|
||||
)
|
||||
|
||||
const prices = await service.calculateShippingOptionsPrices(input)
|
||||
|
||||
return new StepResponse(prices)
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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<FulfillmentWorkflow.CalculateShippingOptionsPricesWorkflowInput>
|
||||
): WorkflowResponse<FulfillmentWorkflow.CalculateShippingOptionsPricesWorkflowOutput> => {
|
||||
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)
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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<string, unknown>
|
||||
|
||||
/**
|
||||
* 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<string, unknown>
|
||||
|
||||
/**
|
||||
* The calculation context needed for the associated fulfillment provider to calculate the price of a shipping option.
|
||||
*/
|
||||
context: {
|
||||
cart: Pick<CartDTO, "id" | "items" | "shipping_address" | "email">
|
||||
} & Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export type FulfillmentOption = {
|
||||
}
|
||||
|
||||
export type CalculatedShippingOptionPrice = {
|
||||
calculated_amount: number
|
||||
calculated_price: number
|
||||
is_calculated_price_tax_inclusive: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -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<boolean[]>
|
||||
|
||||
/**
|
||||
* 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<CalculatedShippingOptionPrice[]>} 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<CalculatedShippingOptionPrice[]>
|
||||
|
||||
/**
|
||||
* This method retrieves a paginated list of fulfillment providers based on optional filters and configuration.
|
||||
*
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./entities"
|
||||
export * from "./queries"
|
||||
export * from "./responses"
|
||||
export * from "./payloads"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export type StoreCalculateShippingOptionPrice = {
|
||||
cart_id: string
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
@@ -3,3 +3,7 @@ import { StoreCartShippingOption } from "../../fulfillment"
|
||||
export interface StoreShippingOptionListResponse {
|
||||
shipping_options: StoreCartShippingOption[]
|
||||
}
|
||||
|
||||
export interface StoreShippingOptionResponse {
|
||||
shipping_option: StoreCartShippingOption
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CalculatedShippingOptionPrice } from "../../fulfillment"
|
||||
|
||||
export type CalculateShippingOptionsPricesWorkflowInput = {
|
||||
cart_id: string
|
||||
shipping_options: { id: string; data: Record<string, unknown> }[]
|
||||
}
|
||||
|
||||
export type CalculateShippingOptionsPricesWorkflowOutput =
|
||||
CalculatedShippingOptionPrice[]
|
||||
@@ -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"
|
||||
|
||||
@@ -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<number> {
|
||||
* async calculatePrice(optionData: any, data: any, context: any): Promise<number> {
|
||||
* // assuming the client can calculate the price using
|
||||
* // the third-party service
|
||||
* const price = await this.client.calculate(data)
|
||||
*
|
||||
* return price
|
||||
* }
|
||||
* }
|
||||
|
||||
@@ -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<HttpTypes.StoreCalculateShippingOptionPrice>,
|
||||
res: MedusaResponse<HttpTypes.StoreShippingOptionResponse>
|
||||
) => {
|
||||
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 } })
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -12,3 +12,8 @@ export const listTransformQueryConfig = {
|
||||
defaultLimit: 20,
|
||||
isList: true,
|
||||
}
|
||||
|
||||
export const retrieveTransformQueryConfig = {
|
||||
defaults: defaultStoreShippingOptionsFields,
|
||||
isList: false,
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
})
|
||||
|
||||
@@ -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<CalculatedShippingOptionPrice[]> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user