feat(core-flows, dashboard, link-modules,medusa, types, utils): fulfillment shipping changes (#10902)
**What** - product <> shipping profile link - create and update product workflows/endpoints accepts shipping profile - pass shipping option id when creating fulfillment to allow overriding customer selected SO - validate shipping profile delete - dashboard - set shipping profile on product create - manage shipping profile for a product - **update the create fulfillment form** - other - fix create product form infinite rerenders --- CLOSES CMRC-831 CMRC-834 CMRC-836 CMRC-837 CMRC-838 CMRC-857 TRI-761
This commit is contained in:
@@ -58,14 +58,14 @@ export type CreateFulfillmentValidateOrderStepInput = {
|
||||
* This step validates that a fulfillment can be created for an order. If the order
|
||||
* is canceled, the items don't exist in the order, or the items aren't grouped by
|
||||
* shipping requirement, the step throws an error.
|
||||
*
|
||||
*
|
||||
* :::note
|
||||
*
|
||||
*
|
||||
* You can retrieve an order's details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query),
|
||||
* or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep).
|
||||
*
|
||||
*
|
||||
* :::
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const data = createFulfillmentValidateOrder({
|
||||
* order: {
|
||||
@@ -82,10 +82,7 @@ export type CreateFulfillmentValidateOrderStepInput = {
|
||||
*/
|
||||
export const createFulfillmentValidateOrder = createStep(
|
||||
"create-fulfillment-validate-order",
|
||||
({
|
||||
order,
|
||||
inputItems,
|
||||
}: CreateFulfillmentValidateOrderStepInput) => {
|
||||
({ order, inputItems }: CreateFulfillmentValidateOrderStepInput) => {
|
||||
throwIfOrderIsCancelled({ order })
|
||||
throwIfItemsDoesNotExistsInOrder({ order, inputItems })
|
||||
throwIfItemsAreNotGroupedByShippingRequirement({ order, inputItems })
|
||||
@@ -128,6 +125,7 @@ function prepareFulfillmentData({
|
||||
id: string
|
||||
provider_id: string
|
||||
service_zone: { fulfillment_set: { location?: { id: string } } }
|
||||
shipping_profile_id: string
|
||||
}
|
||||
shippingMethod: { data?: Record<string, unknown> | null }
|
||||
reservations: ReservationItemDTO[]
|
||||
@@ -156,6 +154,18 @@ function prepareFulfillmentData({
|
||||
const orderItem = orderItemsMap.get(i.id)!
|
||||
const reservation = reservationItemMap.get(i.id)!
|
||||
|
||||
if (
|
||||
orderItem.requires_shipping &&
|
||||
(orderItem as any).variant?.product &&
|
||||
(orderItem as any).variant?.product.shipping_profile?.id !==
|
||||
shippingOption.shipping_profile_id
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Shipping profile ${shippingOption.shipping_profile_id} does not match the shipping profile of the order item ${orderItem.id}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
line_item_id: i.id,
|
||||
inventory_item_id: reservation?.inventory_item_id,
|
||||
@@ -273,17 +283,18 @@ function prepareInventoryUpdate({
|
||||
/**
|
||||
* The details of the fulfillment to create, along with custom data that's passed to the workflow's hooks.
|
||||
*/
|
||||
export type CreateOrderFulfillmentWorkflowInput = OrderWorkflow.CreateOrderFulfillmentWorkflowInput & AdditionalData
|
||||
export type CreateOrderFulfillmentWorkflowInput =
|
||||
OrderWorkflow.CreateOrderFulfillmentWorkflowInput & AdditionalData
|
||||
|
||||
export const createOrderFulfillmentWorkflowId = "create-order-fulfillment"
|
||||
/**
|
||||
* This workflow creates a fulfillment for an order. It's used by the [Create Order Fulfillment Admin API Route](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments).
|
||||
*
|
||||
* This workflow has a hook that allows you to perform custom actions on the created fulfillment. For example, you can pass under `additional_data` custom data that
|
||||
*
|
||||
* This workflow has a hook that allows you to perform custom actions on the created fulfillment. For example, you can pass under `additional_data` custom data that
|
||||
* allows you to create custom data models linked to the fulfillment.
|
||||
*
|
||||
*
|
||||
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around creating a fulfillment.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const { result } = await createOrderFulfillmentWorkflow(container)
|
||||
* .run({
|
||||
@@ -300,18 +311,16 @@ export const createOrderFulfillmentWorkflowId = "create-order-fulfillment"
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
*
|
||||
* Creates a fulfillment for an order.
|
||||
*
|
||||
*
|
||||
* @property hooks.fulfillmentCreated - This hook is executed after the fulfillment is created. You can consume this hook to perform custom actions on the created fulfillment.
|
||||
*/
|
||||
export const createOrderFulfillmentWorkflow = createWorkflow(
|
||||
createOrderFulfillmentWorkflowId,
|
||||
(
|
||||
input: WorkflowData<CreateOrderFulfillmentWorkflowInput>
|
||||
) => {
|
||||
(input: WorkflowData<CreateOrderFulfillmentWorkflowInput>) => {
|
||||
const order: OrderDTO = useRemoteQueryStep({
|
||||
entry_point: "orders",
|
||||
fields: [
|
||||
@@ -323,12 +332,14 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
|
||||
"items.variant.manage_inventory",
|
||||
"items.variant.allow_backorder",
|
||||
"items.variant.product.id",
|
||||
"items.variant.product.shipping_profile.id",
|
||||
"items.variant.weight",
|
||||
"items.variant.length",
|
||||
"items.variant.height",
|
||||
"items.variant.width",
|
||||
"items.variant.material",
|
||||
"shipping_address.*",
|
||||
"shipping_methods.id",
|
||||
"shipping_methods.shipping_option_id",
|
||||
"shipping_methods.data",
|
||||
],
|
||||
@@ -346,17 +357,29 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
|
||||
}, {})
|
||||
})
|
||||
|
||||
const shippingMethod = transform(order, (data) => {
|
||||
return { data: data.shipping_methods?.[0]?.data }
|
||||
const shippingOptionId = transform({ order, input }, (data) => {
|
||||
return (
|
||||
data.input.shipping_option_id ??
|
||||
data.order.shipping_methods?.[0]?.shipping_option_id
|
||||
)
|
||||
})
|
||||
|
||||
const shippingOptionId = transform(order, (data) => {
|
||||
return data.shipping_methods?.[0]?.shipping_option_id
|
||||
const shippingMethod = transform({ order, shippingOptionId }, (data) => {
|
||||
return {
|
||||
data: data.order.shipping_methods?.find(
|
||||
(sm) => sm.shipping_option_id === data.shippingOptionId
|
||||
)?.data,
|
||||
}
|
||||
})
|
||||
|
||||
const shippingOption = useRemoteQueryStep({
|
||||
entry_point: "shipping_options",
|
||||
fields: ["id", "provider_id", "service_zone.fulfillment_set.location.id"],
|
||||
fields: [
|
||||
"id",
|
||||
"provider_id",
|
||||
"service_zone.fulfillment_set.location.id",
|
||||
"shipping_profile_id",
|
||||
],
|
||||
variables: {
|
||||
id: shippingOptionId,
|
||||
},
|
||||
|
||||
@@ -136,6 +136,11 @@ const normalizeProductForImport = (
|
||||
return
|
||||
}
|
||||
|
||||
if (normalizedKey.startsWith("shipping_profile_id")) {
|
||||
response["shipping_profile_id"] = normalizedValue
|
||||
return
|
||||
}
|
||||
|
||||
if (normalizedKey.startsWith("product_category_")) {
|
||||
response["categories"] = [
|
||||
...(response["categories"] || []),
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { ProductTypes, SalesChannelTypes } from "@medusajs/framework/types"
|
||||
import {
|
||||
ProductTypes,
|
||||
SalesChannelTypes,
|
||||
ShippingProfileDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import { MedusaError } from "@medusajs/framework/utils"
|
||||
|
||||
const basicFieldsToOmit = [
|
||||
@@ -32,6 +36,7 @@ export const normalizeV1Products = (
|
||||
productTypes: ProductTypes.ProductTypeDTO[]
|
||||
productCollections: ProductTypes.ProductCollectionDTO[]
|
||||
salesChannels: SalesChannelTypes.SalesChannelDTO[]
|
||||
shippingProfiles: ShippingProfileDTO[]
|
||||
}
|
||||
): object[] => {
|
||||
const productTypesMap = new Map(
|
||||
@@ -43,6 +48,9 @@ export const normalizeV1Products = (
|
||||
const salesChannelsMap = new Map(
|
||||
supportingData.salesChannels.map((sc) => [sc.name, sc.id])
|
||||
)
|
||||
const shippingProfilesIds = new Set(
|
||||
supportingData.shippingProfiles.map((sp) => sp.id)
|
||||
)
|
||||
|
||||
return rawProducts.map((product) => {
|
||||
let finalRes = {
|
||||
@@ -140,6 +148,21 @@ export const normalizeV1Products = (
|
||||
}
|
||||
}
|
||||
|
||||
if (key.startsWith("Shipping Profile Id")) {
|
||||
if (!value) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Shipping Profile Id is required when importing products"
|
||||
)
|
||||
}
|
||||
if (!shippingProfilesIds.has(value)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Shipping profile: '${value}' does not exist`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
key.startsWith("Product Category") &&
|
||||
(key.endsWith("Handle") ||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
IFulfillmentModuleService,
|
||||
IProductModuleService,
|
||||
IRegionModuleService,
|
||||
ISalesChannelModuleService,
|
||||
@@ -16,9 +17,9 @@ export type ParseProductCsvStepInput = string
|
||||
|
||||
export const parseProductCsvStepId = "parse-product-csv"
|
||||
/**
|
||||
* This step parses a CSV file holding products to import, returning the products as
|
||||
* This step parses a CSV file holding products to import, returning the products as
|
||||
* objects that can be imported.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const data = parseProductCsvStep("products.csv")
|
||||
*/
|
||||
@@ -35,20 +36,25 @@ export const parseProductCsvStep = createStep(
|
||||
Modules.SALES_CHANNEL
|
||||
)
|
||||
|
||||
const fulfillmentService = container.resolve<IFulfillmentModuleService>(
|
||||
Modules.FULFILLMENT
|
||||
)
|
||||
|
||||
const csvProducts = convertCsvToJson(fileContent)
|
||||
|
||||
const [productTypes, productCollections, salesChannels] = await Promise.all(
|
||||
[
|
||||
const [productTypes, productCollections, salesChannels, shippingProfiles] =
|
||||
await Promise.all([
|
||||
productService.listProductTypes({}, {}),
|
||||
productService.listProductCollections({}, {}),
|
||||
salesChannelService.listSalesChannels({}, {}),
|
||||
]
|
||||
)
|
||||
fulfillmentService.listShippingProfiles({}, {}),
|
||||
])
|
||||
|
||||
const v1Normalized = normalizeV1Products(csvProducts, {
|
||||
productTypes,
|
||||
productCollections,
|
||||
salesChannels,
|
||||
shippingProfiles,
|
||||
})
|
||||
|
||||
// We use the handle to group products and variants correctly.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AdditionalData,
|
||||
CreateProductWorkflowInputDTO,
|
||||
LinkDefinition,
|
||||
PricingTypes,
|
||||
ProductTypes,
|
||||
} from "@medusajs/framework/types"
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
ProductWorkflowEvents,
|
||||
isPresent,
|
||||
MedusaError,
|
||||
Modules,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
@@ -17,7 +19,11 @@ import {
|
||||
transform,
|
||||
createStep,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { emitEventStep } from "../../common"
|
||||
import {
|
||||
createRemoteLinkStep,
|
||||
emitEventStep,
|
||||
useQueryGraphStep,
|
||||
} from "../../common"
|
||||
import { associateProductsWithSalesChannelsStep } from "../../sales-channel"
|
||||
import { createProductsStep } from "../steps/create-products"
|
||||
import { createProductVariantsWorkflow } from "./create-product-variants"
|
||||
@@ -29,14 +35,19 @@ export interface ValidateProductInputStepInput {
|
||||
/**
|
||||
* The products to validate.
|
||||
*/
|
||||
products: CreateProductWorkflowInputDTO[]
|
||||
products: Omit<CreateProductWorkflowInputDTO, "sales_channels">[]
|
||||
|
||||
/**
|
||||
* The shipping profiles to validate.
|
||||
*/
|
||||
shippingProfiles: { id: string }[]
|
||||
}
|
||||
|
||||
const validateProductInputStepId = "validate-product-input"
|
||||
/**
|
||||
* This step validates that all provided products have options.
|
||||
* If a product is missing options, an error is thrown.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const data = validateProductInputStep({
|
||||
* products: [
|
||||
@@ -71,7 +82,7 @@ const validateProductInputStepId = "validate-product-input"
|
||||
export const validateProductInputStep = createStep(
|
||||
validateProductInputStepId,
|
||||
async (data: ValidateProductInputStepInput) => {
|
||||
const { products } = data
|
||||
const { products, shippingProfiles } = data
|
||||
|
||||
const missingOptionsProductTitles = products
|
||||
.filter((product) => !product.options?.length)
|
||||
@@ -85,6 +96,25 @@ 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(
|
||||
", "
|
||||
)}].`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -102,11 +132,11 @@ export const createProductsWorkflowId = "create-products"
|
||||
/**
|
||||
* This workflow creates one or more products. It's used by the [Create Product Admin API Route](https://docs.medusajs.com/api/admin#products_postproducts).
|
||||
* It can also be useful to you when creating [seed scripts](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/seed-data), for example.
|
||||
*
|
||||
*
|
||||
* This workflow has a hook that allows you to perform custom actions on the created products. You can see an example in [this guide](https://docs.medusajs.com/resources/commerce-modules/product/extend).
|
||||
*
|
||||
*
|
||||
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around product creation.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const { result } = await createProductsWorkflow(container)
|
||||
* .run({
|
||||
@@ -143,26 +173,45 @@ export const createProductsWorkflowId = "create-products"
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
*
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
*
|
||||
* Create one or more products with options and variants.
|
||||
*
|
||||
*
|
||||
* @property hooks.productCreated - This hook is executed after the products are created. You can consume this hook to perform custom actions on the created products.
|
||||
*/
|
||||
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 productWithoutExternalRelations = transform({ input }, (data) =>
|
||||
data.input.products.map((p) => ({
|
||||
...p,
|
||||
sales_channels: undefined,
|
||||
variants: undefined,
|
||||
}))
|
||||
)
|
||||
const { products: productWithoutExternalRelations, shippingPorfileIds } =
|
||||
transform({ input }, (data) => {
|
||||
const shippingPorfileIds: string[] = []
|
||||
const productsData = data.input.products.map((p) => {
|
||||
if (p.shipping_profile_id) {
|
||||
shippingPorfileIds.push(p.shipping_profile_id)
|
||||
}
|
||||
|
||||
validateProductInputStep({ products: productWithoutExternalRelations })
|
||||
return {
|
||||
...p,
|
||||
sales_channels: undefined,
|
||||
shipping_profile_id: undefined,
|
||||
variants: undefined,
|
||||
}
|
||||
})
|
||||
|
||||
return { products: productsData, shippingPorfileIds }
|
||||
})
|
||||
|
||||
const { data: shippingProfiles } = useQueryGraphStep({
|
||||
entity: "shipping_profile",
|
||||
fields: ["id"],
|
||||
filters: {
|
||||
id: shippingPorfileIds,
|
||||
},
|
||||
})
|
||||
|
||||
validateProductInputStep({ products: input.products, shippingProfiles })
|
||||
|
||||
const createdProducts = createProductsStep(productWithoutExternalRelations)
|
||||
|
||||
@@ -182,6 +231,24 @@ export const createProductsWorkflow = createWorkflow(
|
||||
|
||||
associateProductsWithSalesChannelsStep({ links: salesChannelLinks })
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
createRemoteLinkStep(shippingProfileLinks as LinkDefinition[])
|
||||
|
||||
const variantsInput = transform({ input, createdProducts }, (data) => {
|
||||
// TODO: Move this to a unified place for all product workflow types
|
||||
const productVariants: (ProductTypes.CreateProductVariantDTO & {
|
||||
|
||||
@@ -47,6 +47,10 @@ export type UpdateProductsWorkflowInputSelector = {
|
||||
* The variants to update.
|
||||
*/
|
||||
variants?: UpdateProductVariantWorkflowInputDTO[]
|
||||
/**
|
||||
* The shipping profile to set.
|
||||
*/
|
||||
shipping_profile_id?: string
|
||||
}
|
||||
} & AdditionalData
|
||||
|
||||
@@ -66,6 +70,10 @@ export type UpdateProductsWorkflowInputProducts = {
|
||||
* The variants to update.
|
||||
*/
|
||||
variants?: UpdateProductVariantWorkflowInputDTO[]
|
||||
/**
|
||||
* The shipping profile to set.
|
||||
*/
|
||||
shipping_profile_id?: string
|
||||
})[]
|
||||
} & AdditionalData
|
||||
|
||||
@@ -90,6 +98,7 @@ function prepareUpdateProductInput({
|
||||
products: input.products.map((p) => ({
|
||||
...p,
|
||||
sales_channels: undefined,
|
||||
shipping_profile_id: undefined,
|
||||
variants: p.variants?.map((v) => ({
|
||||
...v,
|
||||
prices: undefined,
|
||||
@@ -103,6 +112,7 @@ function prepareUpdateProductInput({
|
||||
update: {
|
||||
...input.update,
|
||||
sales_channels: undefined,
|
||||
shipping_profile_id: undefined,
|
||||
variants: input.update?.variants?.map((v) => ({
|
||||
...v,
|
||||
prices: undefined,
|
||||
@@ -173,6 +183,44 @@ function prepareSalesChannelLinks({
|
||||
return []
|
||||
}
|
||||
|
||||
function prepareShippingProfileLinks({
|
||||
input,
|
||||
updatedProducts,
|
||||
}: {
|
||||
updatedProducts: ProductTypes.ProductDTO[]
|
||||
input: UpdateProductWorkflowInput
|
||||
}): Record<string, Record<string, any>>[] {
|
||||
if ("products" in input) {
|
||||
if (!input.products.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return input.products
|
||||
.filter((p) => p.shipping_profile_id)
|
||||
.map((p) => ({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: p.id,
|
||||
},
|
||||
[Modules.FULFILLMENT]: {
|
||||
shipping_profile_id: p.shipping_profile_id,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
if (input.selector && input.update?.shipping_profile_id) {
|
||||
return updatedProducts.map((p) => ({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: p.id,
|
||||
},
|
||||
[Modules.FULFILLMENT]: {
|
||||
shipping_profile_id: input.update.shipping_profile_id,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function prepareVariantPrices({
|
||||
input,
|
||||
updatedProducts,
|
||||
@@ -243,18 +291,42 @@ function prepareToDeleteSalesChannelLinks({
|
||||
}))
|
||||
}
|
||||
|
||||
function prepareToDeleteShippingProfileLinks({
|
||||
currentShippingProfileLinks,
|
||||
}: {
|
||||
currentShippingProfileLinks: {
|
||||
product_id: string
|
||||
shipping_profile_id: string
|
||||
}[]
|
||||
}) {
|
||||
if (!currentShippingProfileLinks.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return currentShippingProfileLinks.map(
|
||||
({ product_id, shipping_profile_id }) => ({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id,
|
||||
},
|
||||
[Modules.FULFILLMENT]: {
|
||||
shipping_profile_id,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export const updateProductsWorkflowId = "update-products"
|
||||
/**
|
||||
* This workflow updates one or more products. It's used by the [Update Product Admin API Route](https://docs.medusajs.com/api/admin#products_postproductsid).
|
||||
*
|
||||
* This workflow has a hook that allows you to perform custom actions on the updated products. For example, you can pass under `additional_data` custom data that
|
||||
*
|
||||
* This workflow has a hook that allows you to perform custom actions on the updated products. For example, you can pass under `additional_data` custom data that
|
||||
* allows you to update custom data models linked to the products.
|
||||
*
|
||||
*
|
||||
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around product update.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* To update products by their IDs:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* const { result } = await updateProductsWorkflow(container)
|
||||
* .run({
|
||||
@@ -282,9 +354,9 @@ export const updateProductsWorkflowId = "update-products"
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* You can also update products by a selector:
|
||||
*
|
||||
*
|
||||
* ```ts
|
||||
* const { result } = await updateProductsWorkflow(container)
|
||||
* .run({
|
||||
@@ -301,11 +373,11 @@ export const updateProductsWorkflowId = "update-products"
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
*
|
||||
* Update one or more products with options and variants.
|
||||
*
|
||||
*
|
||||
* @property hooks.productsUpdated - This hook is executed after the products are updated. You can consume this hook to perform custom actions on the updated products.
|
||||
*/
|
||||
export const updateProductsWorkflow = createWorkflow(
|
||||
@@ -345,11 +417,20 @@ export const updateProductsWorkflow = createWorkflow(
|
||||
const toUpdateInput = transform({ input }, prepareUpdateProductInput)
|
||||
const updatedProducts = updateProductsStep(toUpdateInput)
|
||||
|
||||
const updatedPorductIds = transform({ updatedProducts }, (data) => {
|
||||
return data.updatedProducts.map((p) => p.id)
|
||||
})
|
||||
|
||||
const salesChannelLinks = transform(
|
||||
{ input, updatedProducts },
|
||||
prepareSalesChannelLinks
|
||||
)
|
||||
|
||||
const shippingProfileLinks = transform(
|
||||
{ input, updatedProducts },
|
||||
prepareShippingProfileLinks
|
||||
)
|
||||
|
||||
const variantPrices = transform(
|
||||
{ input, updatedProducts },
|
||||
prepareVariantPrices
|
||||
@@ -366,16 +447,33 @@ export const updateProductsWorkflow = createWorkflow(
|
||||
variables: { filters: { product_id: productsWithSalesChannels } },
|
||||
}).config({ name: "get-current-sales-channel-links-step" })
|
||||
|
||||
const currentShippingProfileLinks = useRemoteQueryStep({
|
||||
entry_point: "product_shipping_profile",
|
||||
fields: ["product_id", "shipping_profile_id"],
|
||||
variables: { filters: { product_id: updatedPorductIds } },
|
||||
}).config({ name: "get-current-shipping-profile-links-step" })
|
||||
|
||||
const toDeleteSalesChannelLinks = transform(
|
||||
{ currentSalesChannelLinks },
|
||||
prepareToDeleteSalesChannelLinks
|
||||
)
|
||||
|
||||
const toDeleteShippingProfileLinks = transform(
|
||||
{ currentShippingProfileLinks },
|
||||
prepareToDeleteShippingProfileLinks
|
||||
)
|
||||
|
||||
upsertVariantPricesWorkflow.runAsStep({
|
||||
input: { variantPrices, previousVariantIds },
|
||||
})
|
||||
|
||||
dismissRemoteLinkStep(toDeleteSalesChannelLinks)
|
||||
dismissRemoteLinkStep(toDeleteSalesChannelLinks).config({
|
||||
name: "delete-sales-channel-links-step",
|
||||
})
|
||||
|
||||
dismissRemoteLinkStep(toDeleteShippingProfileLinks).config({
|
||||
name: "delete-shipping-profile-links-step",
|
||||
})
|
||||
|
||||
const productIdEvents = transform(
|
||||
{ updatedProducts },
|
||||
@@ -387,7 +485,12 @@ export const updateProductsWorkflow = createWorkflow(
|
||||
)
|
||||
|
||||
parallelize(
|
||||
createRemoteLinkStep(salesChannelLinks),
|
||||
createRemoteLinkStep(salesChannelLinks).config({
|
||||
name: "create-sales-channel-links-step",
|
||||
}),
|
||||
createRemoteLinkStep(shippingProfileLinks).config({
|
||||
name: "create-shipping-profile-links-step",
|
||||
}),
|
||||
emitEventStep({
|
||||
eventName: ProductWorkflowEvents.UPDATED,
|
||||
data: productIdEvents,
|
||||
|
||||
@@ -1,8 +1,31 @@
|
||||
import { createWorkflow, WorkflowData } from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
createStep,
|
||||
createWorkflow,
|
||||
WorkflowData,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { MedusaError, Modules } from "@medusajs/framework/utils"
|
||||
|
||||
import { deleteShippingProfilesStep } from "../steps"
|
||||
import { removeRemoteLinkStep } from "../../common"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { removeRemoteLinkStep, useQueryGraphStep } from "../../common"
|
||||
|
||||
/**
|
||||
* This step validates that the shipping profiles to delete are not linked to any products.
|
||||
*/
|
||||
const validateStepShippingProfileDelete = createStep(
|
||||
"validate-step-shipping-profile-delete",
|
||||
(data: { links: { product_id: string; shipping_profile_id: string }[] }) => {
|
||||
const { links } = data
|
||||
|
||||
if (links.length > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Cannot delete following shipping profiles because they are linked to products: ${links
|
||||
.map((l) => l.product_id)
|
||||
.join(", ")}`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* The data to delete shipping profiles.
|
||||
@@ -19,10 +42,11 @@ export const deleteShippingProfileWorkflowId =
|
||||
/**
|
||||
* This workflow deletes one or more shipping profiles. It's used by the
|
||||
* [Delete Shipping Profile Admin API Route](https://docs.medusajs.com/api/admin#shipping-profiles_deleteshippingprofilesid).
|
||||
*
|
||||
* Shipping profiles that are linked to products cannot be deleted.
|
||||
*
|
||||
* You can use this workflow within your customizations or your own custom workflows, allowing you to
|
||||
* delete shipping profiles within your custom flows.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const { result } = await deleteShippingProfileWorkflow(container)
|
||||
* .run({
|
||||
@@ -30,14 +54,24 @@ export const deleteShippingProfileWorkflowId =
|
||||
* ids: ["sp_123"]
|
||||
* }
|
||||
* })
|
||||
*
|
||||
*
|
||||
* @summary
|
||||
*
|
||||
*
|
||||
* Delete shipping profiles.
|
||||
*/
|
||||
export const deleteShippingProfileWorkflow = createWorkflow(
|
||||
deleteShippingProfileWorkflowId,
|
||||
(input: WorkflowData<DeleteShippingProfilesWorkflowInput>) => {
|
||||
const currentShippingProfileLinks = useQueryGraphStep({
|
||||
entity: "product_shipping_profile",
|
||||
fields: ["product_id", "shipping_profile_id"],
|
||||
filters: { shipping_profile_id: input.ids },
|
||||
})
|
||||
|
||||
validateStepShippingProfileDelete({
|
||||
links: currentShippingProfileLinks.data,
|
||||
})
|
||||
|
||||
deleteShippingProfilesStep(input.ids)
|
||||
|
||||
removeRemoteLinkStep({
|
||||
|
||||
Reference in New Issue
Block a user