Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(medusa, core-flows, fulfillment): calculate SO price endpoint #10532

Merged
merged 10 commits into from
Dec 12, 2024
Original file line number Diff line number Diff line change
@@ -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)
}
)
1 change: 1 addition & 0 deletions packages/core/core-flows/src/fulfillment/steps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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 optionIds = transform({ input }, ({ input }) =>
input.map(({ shipping_option_id }) => shipping_option_id)
)

const shippingOptionsQuery = useQueryGraphStep({
entity: "shipping_option",
filters: { id: optionIds },
fields: ["id", "provider_id", "data"],
})

const data = transform(
{ shippingOptionsQuery },
({ shippingOptionsQuery }) => {
const shippingOptions = shippingOptionsQuery.data

return shippingOptions.map((shippingOption) => ({
id: shippingOption.id,
provider_id: shippingOption.provider_id,
data: shippingOption.data,
context: {
cart: {
id: shippingOption.cart_id,
},
},
}))
}
)

const prices = calculateShippingOptionsPricesStep(data)

return new WorkflowResponse(prices)
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,32 @@ 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.
*/
data: Record<string, unknown>

/**
* The calculation context needed for the associated fulfillment provider to calculate the price of a shipping option.
*/
context: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: is it enough to pass the cart id and the rest of what is needed can be fetched by the provider or should we pass specific customer info, address info etc. (if, for example, a calculated option is used in an RMA flow)

Copy link
Contributor

@olivermrbl olivermrbl Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we will need more, such as items to calculate prices on product/inventory item dimensions. To be consistent with other providers, we don't want to delegate the responsibility of fetching data from core modules to the provider (yet). This should be a preliminary step and passed to the provider.

Just looping in @srindom – he might have an idea about what we could start with

Copy link
Contributor

@olivermrbl olivermrbl Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fPolic to unblock the PR, here's what I think we will definitely need:

  • Items, e.g. package dimensions and weight
  • Shipping address, e.g. distance from origin to destination

We can add these now and expand in the future in case we need more.

cart: {
id: string
}
} & Record<string, unknown>
}
1 change: 0 additions & 1 deletion packages/core/types/src/fulfillment/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export interface IFulfillmentProvider {
*/
calculatePrice(
optionData: Record<string, unknown>,
Copy link
Contributor

@olivermrbl olivermrbl Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: think we still need data – I believe the current naming is cause for confusion.

optionData – data that was populated when creating the shipping option, e.g. requires_drop_point.
data – data that is populated when you create the shipping method, e.g. drop_point_id: 1

For requests that are made prior to adding the shipping method to a cart, the data might be empty.

For example:

POST /store/shipping-options/:id/calculate

However, when adding the shipping method to cart and the price is calculated, you might need the data for the calculation.

For example:

POST /store/carts/:id/shipping-methods
{ option_id: "so_1234", data: { drop_point_id: 1 } }

In your implementation of calculatePrice, the calculated price might depend on the drop point ID (maybe a farfetched example):

// pseudo code
calculatePrice() {
  if (drop_point_id) {
    const acceptPackageSizes = await this.fetchDropPointPackageSizes(drop_point_id)

    const price = // one of those package sizes
    return price
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh ok, thanks for clarifying - will revert the changes

data: Record<string, unknown>,
context: Record<string, unknown>
): Promise<CalculatedShippingOptionPrice>
/**
Expand Down
29 changes: 29 additions & 0 deletions packages/core/types/src/fulfillment/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ShippingProfileDTO,
} from "./common"
import {
CalculateShippingOptionPriceDTO,
CreateFulfillmentSetDTO,
CreateGeoZoneDTO,
CreateServiceZoneDTO,
Expand All @@ -44,6 +45,7 @@ import {
CreateShippingProfileDTO,
UpsertShippingProfileDTO,
} from "./mutations/shipping-profile"
import { CalculatedShippingOptionPrice } from "./provider"

/**
* The main service interface for the Fulfillment Module.
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ import { StoreCartShippingOption } from "../../fulfillment"
export interface StoreShippingOptionListResponse {
shipping_options: StoreCartShippingOption[]
}

export interface StoreCalculateShippingOptionPriceResponse {
calculated_amount: number
is_calculated_price_tax_inclusive: boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { CalculatedShippingOptionPrice } from "../../fulfillment"

export type CalculateShippingOptionsPricesWorkflowInput = {
cart_id: string
shipping_option_id: string
}[]

export type CalculateShippingOptionsPricesWorkflowOutput =
CalculatedShippingOptionPrice[]
1 change: 1 addition & 0 deletions packages/core/types/src/workflow/fulfillment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
1 change: 0 additions & 1 deletion packages/core/utils/src/fulfillment/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ export class AbstractFulfillmentProviderService
* }
*/
async calculatePrice(
optionData: Record<string, unknown>,
fPolic marked this conversation as resolved.
Show resolved Hide resolved
data: Record<string, unknown>,
context: Record<string, unknown>
): Promise<CalculatedShippingOptionPrice> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { HttpTypes } from "@medusajs/framework/types"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { calculateShippingOptionsPricesWorkflow } from "@medusajs/core-flows"

import { StoreCalculateShippingOptionPriceType } from "../../validators"

export const POST = async (
req: MedusaRequest<StoreCalculateShippingOptionPriceType>,
fPolic marked this conversation as resolved.
Show resolved Hide resolved
res: MedusaResponse<HttpTypes.StoreCalculateShippingOptionPriceResponse>
) => {
const { result } = await calculateShippingOptionsPricesWorkflow(
req.scope
).run({
input: [
{
shipping_option_id: req.params.id,
cart_id: req.validatedBody.cart_id,
},
],
})

res.status(200).json(result[0])
}
15 changes: 13 additions & 2 deletions packages/medusa/src/api/store/shipping-options/middlewares.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
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,
} from "./validators"

export const storeShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [
{
Expand All @@ -14,4 +20,9 @@ export const storeShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [
),
],
},
{
method: ["POST"],
matcher: "/store/shipping-options/:id/calculate",
middlewares: [validateAndTransformBody(StoreCalculateShippingOptionPrice)],
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ 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(),
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
CalculatedShippingOptionPrice,
Context,
DAL,
FilterableFulfillmentSetProps,
Expand Down Expand Up @@ -1998,6 +1999,20 @@ export default class FulfillmentModuleService
return await promiseAll(promises)
}

async calculateShippingOptionsPrices(
shippingOptionsData: FulfillmentTypes.CalculateShippingOptionPriceDTO[]
): Promise<CalculatedShippingOptionPrice[]> {
const promises = shippingOptionsData.map((option) =>
this.fulfillmentProviderService_.calculatePrice(
option.provider_id,
option.data,
option.context
)
)

return await promiseAll(promises)
}

@InjectTransactionManager()
// @ts-expect-error
async deleteShippingProfiles(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,10 @@ export default class FulfillmentProviderService extends ModulesSdkUtils.MedusaIn
async calculatePrice(
providerId: string,
optionData: Record<string, unknown>,
data: Record<string, unknown>,
context: Record<string, unknown>
) {
const provider = this.retrieveProviderRegistration(providerId)
return await provider.calculatePrice(optionData, data, context)
return await provider.calculatePrice(optionData, context)
}

async createFulfillment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export class ManualFulfillmentService extends AbstractFulfillmentProviderService
}

async calculatePrice(
fPolic marked this conversation as resolved.
Show resolved Hide resolved
optionData: Record<string, unknown>,
fPolic marked this conversation as resolved.
Show resolved Hide resolved
data: Record<string, unknown>,
context: Record<string, unknown>
): Promise<CalculatedShippingOptionPrice> {
Expand Down
Loading