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:
1 parent
52b494b62d
commit
472e92e400
18 files changed
+263
-9
No files matched your search
@@ -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 "./update-shipping-profiles"
|
||||||
export * from "./upsert-shipping-options"
|
export * from "./upsert-shipping-options"
|
||||||
export * from "./validate-shipment"
|
export * from "./validate-shipment"
|
||||||
|
export * from "./calculate-shipping-options-prices"
|
||||||
+63
@@ -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-service-zones"
|
||||||
export * from "./update-shipping-options"
|
export * from "./update-shipping-options"
|
||||||
export * from "./update-shipping-profiles"
|
export * from "./update-shipping-profiles"
|
||||||
|
export * from "./calculate-shipping-options-prices"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CreateShippingOptionTypeDTO } from "./shipping-option-type"
|
import { CreateShippingOptionTypeDTO } from "./shipping-option-type"
|
||||||
import { ShippingOptionPriceType } from "../common"
|
import { ShippingOptionPriceType } from "../common"
|
||||||
import { CreateShippingOptionRuleDTO } from "./shipping-option-rule"
|
import { CreateShippingOptionRuleDTO } from "./shipping-option-rule"
|
||||||
|
import { CartDTO } from "../../cart"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The shipping option to be created.
|
* The shipping option to be created.
|
||||||
@@ -118,3 +119,39 @@ export interface UpdateShippingOptionDTO {
|
|||||||
* A shipping option to be created or updated.
|
* A shipping option to be created or updated.
|
||||||
*/
|
*/
|
||||||
export interface UpsertShippingOptionDTO extends UpdateShippingOptionDTO {}
|
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 = {
|
export type CalculatedShippingOptionPrice = {
|
||||||
calculated_amount: number
|
calculated_price: number
|
||||||
is_calculated_price_tax_inclusive: boolean
|
is_calculated_price_tax_inclusive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
ShippingProfileDTO,
|
ShippingProfileDTO,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
import {
|
import {
|
||||||
|
CalculateShippingOptionPriceDTO,
|
||||||
CreateFulfillmentSetDTO,
|
CreateFulfillmentSetDTO,
|
||||||
CreateGeoZoneDTO,
|
CreateGeoZoneDTO,
|
||||||
CreateServiceZoneDTO,
|
CreateServiceZoneDTO,
|
||||||
@@ -44,6 +45,7 @@ import {
|
|||||||
CreateShippingProfileDTO,
|
CreateShippingProfileDTO,
|
||||||
UpsertShippingProfileDTO,
|
UpsertShippingProfileDTO,
|
||||||
} from "./mutations/shipping-profile"
|
} from "./mutations/shipping-profile"
|
||||||
|
import { CalculatedShippingOptionPrice } from "./provider"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main service interface for the Fulfillment Module.
|
* The main service interface for the Fulfillment Module.
|
||||||
@@ -2647,6 +2649,33 @@ export interface IFulfillmentModuleService extends IModuleService {
|
|||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<boolean[]>
|
): 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.
|
* This method retrieves a paginated list of fulfillment providers based on optional filters and configuration.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./entities"
|
export * from "./entities"
|
||||||
export * from "./queries"
|
export * from "./queries"
|
||||||
export * from "./responses"
|
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 {
|
export interface StoreShippingOptionListResponse {
|
||||||
shipping_options: StoreCartShippingOption[]
|
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 "./shipping-profiles"
|
||||||
export * from "./update-fulfillment"
|
export * from "./update-fulfillment"
|
||||||
export * from "./update-shipping-options"
|
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.
|
* 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.
|
* If it returns `true`, Medusa uses this method to retrieve the calculated price.
|
||||||
*
|
*
|
||||||
* @param optionData - The `data` property of a shipping option.
|
* @param optionData - Shipping option data from the provider, 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 data - Additional data passed when the price is calculated.
|
||||||
* @param cart - The cart details.
|
* @param context - The context details, such as the cart or customer.
|
||||||
* @returns The calculated price
|
* @returns The calculated price
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* class MyFulfillmentProviderService extends AbstractFulfillmentProviderService {
|
* 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
|
* // assuming the client can calculate the price using
|
||||||
* // the third-party service
|
* // the third-party service
|
||||||
* const price = await this.client.calculate(data)
|
* const price = await this.client.calculate(data)
|
||||||
*
|
|
||||||
* return price
|
* 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 { validateAndTransformQuery } from "@medusajs/framework"
|
||||||
import { listTransformQueryConfig } from "./query-config"
|
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[] = [
|
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,
|
defaultLimit: 20,
|
||||||
isList: true,
|
isList: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const retrieveTransformQueryConfig = {
|
||||||
|
defaults: defaultStoreShippingOptionsFields,
|
||||||
|
isList: false,
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { applyAndAndOrOperators } from "../../utils/common-validators"
|
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
|
export const StoreGetShippingOptionsFields = z
|
||||||
.object({
|
.object({
|
||||||
@@ -18,3 +20,11 @@ export const StoreGetShippingOptions = createFindParams({
|
|||||||
})
|
})
|
||||||
.merge(StoreGetShippingOptionsFields)
|
.merge(StoreGetShippingOptionsFields)
|
||||||
.merge(applyAndAndOrOperators(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 {
|
import {
|
||||||
|
CalculatedShippingOptionPrice,
|
||||||
Context,
|
Context,
|
||||||
DAL,
|
DAL,
|
||||||
FilterableFulfillmentSetProps,
|
FilterableFulfillmentSetProps,
|
||||||
@@ -1998,6 +1999,21 @@ export default class FulfillmentModuleService
|
|||||||
return await promiseAll(promises)
|
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()
|
@InjectTransactionManager()
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
async deleteShippingProfiles(
|
async deleteShippingProfiles(
|
||||||
|
|||||||
Reference in new issue
Block a user