feat(core-flows, dashboard, medusa, types): optional shipping profile (#11434)

* feat: create product flow changes

* feat: allow unsetting SP on product update

* feat: update prepare line item helper

* test: add testcase

* wip: fix tests

* fix: update module tests

* fix: cart module test
This commit is contained in:
Frane Polić
2025-02-17 19:08:59 +01:00
committed by GitHub
parent 3b7856e8f5
commit ee848bf0f4
19 changed files with 191 additions and 112 deletions

View File

@@ -147,6 +147,7 @@ export const productVariantsFields = [
"product.collection.title",
"product.handle",
"product.discountable",
"product.shipping_profile.id",
"calculated_price.*",
"inventory_items.inventory_item_id",
"inventory_items.required_quantity",

View File

@@ -5,6 +5,7 @@ import {
InventoryItemDTO,
LineItemAdjustmentDTO,
LineItemTaxLineDTO,
ProductDTO,
ProductVariantDTO,
} from "@medusajs/framework/types"
import {
@@ -50,6 +51,10 @@ interface PrepareItemLineItemInput {
metadata?: Record<string, unknown> | null
}
type AddItemProductDTO = ProductDTO & {
shipping_profile: { id: string }
}
export interface PrepareVariantLineItemInput extends ProductVariantDTO {
inventory_items: { inventory: InventoryItemDTO }[]
calculated_price: {
@@ -106,17 +111,19 @@ export function prepareLineItemData(data: PrepareLineItemDataInput) {
compareAtUnitPrice = variant.calculated_price.original_amount
}
// Note: If any of the items require shipping, we enable fulfillment
// unless explicitly set to not require shipping by the item in the request
const someInventoryRequiresShipping = variant?.inventory_items?.length
? variant.inventory_items.some(
(inventoryItem) => !!inventoryItem.inventory.requires_shipping
)
: true
const hasShippingProfile = isDefined(
(variant?.product as AddItemProductDTO)?.shipping_profile?.id
)
const someInventoryRequiresShipping = !!variant?.inventory_items?.some(
(inventoryItem) => !!inventoryItem.inventory.requires_shipping
)
// Note: If any of the items require shipping or product has a shipping profile set,
// we enable fulfillment unless explicitly set to not require shipping by the item in the request
const requiresShipping = isDefined(item?.requires_shipping)
? item.requires_shipping
: someInventoryRequiresShipping
: hasShippingProfile || someInventoryRequiresShipping
let lineItem: any = {
quantity: item?.quantity,

View File

@@ -35,12 +35,12 @@ const cartFields = ["completed_at"].concat(cartFieldsForPricingContext)
export const addToCartWorkflowId = "add-to-cart"
/**
* This workflow adds a product variant to a cart as a line item. It's executed by the
* This workflow adds a product variant to a cart as a line item. It's executed by the
* [Add Line Item Store API Route](https://docs.medusajs.com/api/store#carts_postcartsidlineitems).
*
*
* You can use this workflow within your own customizations or custom workflows, allowing you to wrap custom logic around adding an item to the cart.
* For example, you can use this workflow to add a line item to the cart with a custom price.
*
*
* @example
* const { result } = await addToCartWorkflow(container)
* .run({
@@ -59,11 +59,11 @@ export const addToCartWorkflowId = "add-to-cart"
* ]
* }
* })
*
*
* @summary
*
*
* Add a line item to a cart.
*
*
* @property hooks.validate - This hook is executed before all operations. You can consume this hook to perform any custom validation. If validation fails, you can throw an error to stop the workflow execution.
*/
export const addToCartWorkflow = createWorkflow(

View File

@@ -19,11 +19,7 @@ import {
transform,
createStep,
} from "@medusajs/framework/workflows-sdk"
import {
createRemoteLinkStep,
emitEventStep,
useQueryGraphStep,
} from "../../common"
import { createRemoteLinkStep, emitEventStep } from "../../common"
import { associateProductsWithSalesChannelsStep } from "../../sales-channel"
import { createProductsStep } from "../steps/create-products"
import { createProductVariantsWorkflow } from "./create-product-variants"
@@ -36,16 +32,6 @@ export interface ValidateProductInputStepInput {
* The products to validate.
*/
products: Omit<CreateProductWorkflowInputDTO, "sales_channels">[]
/**
* The shipping profiles to validate.
*/
shippingProfiles: {
/**
* The shipping profile's ID.
*/
id: string
}[]
}
const validateProductInputStepId = "validate-product-input"
@@ -87,7 +73,7 @@ const validateProductInputStepId = "validate-product-input"
export const validateProductInputStep = createStep(
validateProductInputStepId,
async (data: ValidateProductInputStepInput) => {
const { products, shippingProfiles } = data
const { products } = data
const missingOptionsProductTitles = products
.filter((product) => !product.options?.length)
@@ -101,25 +87,6 @@ export const validateProductInputStep = createStep(
)}].`
)
}
const existingProfileIds = new Set(shippingProfiles.map((p) => p.id))
const missingShippingProfileProductTitles = products
.filter(
(product) =>
!product.shipping_profile_id ||
!existingProfileIds.has(product.shipping_profile_id)
)
.map((product) => product.title)
if (missingShippingProfileProductTitles.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Shipping profile is not provided for: [${missingShippingProfileProductTitles.join(
", "
)}].`
)
}
}
)
@@ -190,14 +157,10 @@ export const createProductsWorkflow = createWorkflow(
createProductsWorkflowId,
(input: WorkflowData<CreateProductsWorkflowInput>) => {
// Passing prices to the product module will fail, we want to keep them for after the product is created.
const { products: productWithoutExternalRelations, shippingPorfileIds } =
transform({ input }, (data) => {
const shippingPorfileIds: string[] = []
const { products: productWithoutExternalRelations } = transform(
{ input },
(data) => {
const productsData = data.input.products.map((p) => {
if (p.shipping_profile_id) {
shippingPorfileIds.push(p.shipping_profile_id)
}
return {
...p,
sales_channels: undefined,
@@ -206,18 +169,11 @@ export const createProductsWorkflow = createWorkflow(
}
})
return { products: productsData, shippingPorfileIds }
})
return { products: productsData }
}
)
const { data: shippingProfiles } = useQueryGraphStep({
entity: "shipping_profile",
fields: ["id"],
filters: {
id: shippingPorfileIds,
},
})
validateProductInputStep({ products: input.products, shippingProfiles })
validateProductInputStep({ products: input.products })
const createdProducts = createProductsStep(productWithoutExternalRelations)
@@ -240,16 +196,18 @@ export const createProductsWorkflow = createWorkflow(
const shippingProfileLinks = transform(
{ input, createdProducts },
(data) => {
return data.createdProducts.map((createdProduct, i) => {
return {
[Modules.PRODUCT]: {
product_id: createdProduct.id,
},
[Modules.FULFILLMENT]: {
shipping_profile_id: data.input.products[i].shipping_profile_id,
},
}
})
return data.createdProducts
.map((createdProduct, i) => {
return {
[Modules.PRODUCT]: {
product_id: createdProduct.id,
},
[Modules.FULFILLMENT]: {
shipping_profile_id: data.input.products[i].shipping_profile_id,
},
}
})
.filter((link) => !!link[Modules.FULFILLMENT].shipping_profile_id)
}
)

View File

@@ -10,6 +10,7 @@ import {
Modules,
ProductWorkflowEvents,
arrayDifference,
isDefined,
} from "@medusajs/framework/utils"
import {
WorkflowData,
@@ -50,7 +51,7 @@ export type UpdateProductsWorkflowInputSelector = {
/**
* The shipping profile to set.
*/
shipping_profile_id?: string
shipping_profile_id?: string | null
}
} & AdditionalData
@@ -73,7 +74,7 @@ export type UpdateProductsWorkflowInputProducts = {
/**
* The shipping profile to set.
*/
shipping_profile_id?: string
shipping_profile_id?: string | null
})[]
} & AdditionalData
@@ -152,12 +153,12 @@ function findProductsWithShippingProfiles({
if ("products" in input) {
const discardedProductIds: string[] = input.products
.filter((p) => !p.shipping_profile_id)
.filter((p) => !isDefined(p.shipping_profile_id))
.map((p) => p.id as string)
return arrayDifference(productIds, discardedProductIds)
}
return !input.update?.shipping_profile_id ? [] : productIds
return !isDefined(input.update?.shipping_profile_id) ? [] : productIds
}
function prepareSalesChannelLinks({
@@ -215,7 +216,7 @@ function prepareShippingProfileLinks({
}
return input.products
.filter((p) => p.shipping_profile_id)
.filter((p) => typeof p.shipping_profile_id === "string")
.map((p) => ({
[Modules.PRODUCT]: {
product_id: p.id,
@@ -226,7 +227,7 @@ function prepareShippingProfileLinks({
}))
}
if (input.selector && input.update?.shipping_profile_id) {
if (input.selector && typeof input.update?.shipping_profile_id === "string") {
return updatedProducts.map((p) => ({
[Modules.PRODUCT]: {
product_id: p.id,