feat: Add create location fulfillment set flow + API (#6945)
This commit is contained in:
@@ -4,8 +4,8 @@ import {
|
|||||||
createAdminUser,
|
createAdminUser,
|
||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
|
||||||
import { IStockLocationServiceNext } from "@medusajs/types"
|
import { IStockLocationServiceNext } from "@medusajs/types"
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
|
||||||
const { medusaIntegrationTestRunner } = require("medusa-test-utils")
|
const { medusaIntegrationTestRunner } = require("medusa-test-utils")
|
||||||
|
|
||||||
@@ -374,5 +374,59 @@ medusaIntegrationTestRunner({
|
|||||||
).toHaveLength(1)
|
).toHaveLength(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Location fulfillment sets", () => {
|
||||||
|
let stockLocationId
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const createResponse = await api.post(
|
||||||
|
`/admin/stock-locations`,
|
||||||
|
{
|
||||||
|
name: "test location",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
stockLocationId = createResponse.data.stock_location.id
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create a fulfillment set for the location", async () => {
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/stock-locations/${stockLocationId}/fulfillment-sets?fields=id,*fulfillment_sets`,
|
||||||
|
{
|
||||||
|
name: "Fulfillment Set",
|
||||||
|
type: "shipping",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
|
||||||
|
expect(response.data.stock_location.fulfillment_sets).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
// This is really just to test the new Zod middleware. We don't need more of these.
|
||||||
|
it("should throw a validation error on wrong input", async () => {
|
||||||
|
const errorResponse = await api
|
||||||
|
.post(
|
||||||
|
`/admin/stock-locations/${stockLocationId}/fulfillment-sets?fields=id,*fulfillment_sets`,
|
||||||
|
{
|
||||||
|
name: "Fulfillment Set",
|
||||||
|
type: "shipping",
|
||||||
|
foo: "bar",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e.response)
|
||||||
|
|
||||||
|
expect(errorResponse.status).toEqual(400)
|
||||||
|
|
||||||
|
expect(errorResponse.data.message).toContain("Invalid request body: ")
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ module.exports = {
|
|||||||
[Modules.TAX]: true,
|
[Modules.TAX]: true,
|
||||||
[Modules.CURRENCY]: true,
|
[Modules.CURRENCY]: true,
|
||||||
[Modules.PAYMENT]: true,
|
[Modules.PAYMENT]: true,
|
||||||
|
[Modules.FULFILLMENT]: true,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
import {
|
||||||
|
CreateFulfillmentSetDTO,
|
||||||
|
IFulfillmentModuleService,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
export const createFulfillmentSetsId = "create-fulfillment-sets"
|
||||||
|
export const createFulfillmentSets = createStep(
|
||||||
|
createFulfillmentSetsId,
|
||||||
|
async (data: CreateFulfillmentSetDTO[], { container }) => {
|
||||||
|
const service = container.resolve<IFulfillmentModuleService>(
|
||||||
|
ModuleRegistrationName.FULFILLMENT
|
||||||
|
)
|
||||||
|
|
||||||
|
const createSets = await service.create(data)
|
||||||
|
|
||||||
|
return new StepResponse(
|
||||||
|
createSets,
|
||||||
|
createSets.map((createdSet) => createdSet.id)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
async (createSetIds, { container }) => {
|
||||||
|
if (!createSetIds?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = container.resolve<IFulfillmentModuleService>(
|
||||||
|
ModuleRegistrationName.FULFILLMENT
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.delete(createSetIds)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./add-rules-to-fulfillment-shipping-option"
|
export * from "./add-rules-to-fulfillment-shipping-option"
|
||||||
|
export * from "./create-fulfillment-set"
|
||||||
export * from "./remove-rules-from-fulfillment-shipping-option"
|
export * from "./remove-rules-from-fulfillment-shipping-option"
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
|
||||||
|
interface StepInput {
|
||||||
|
input: {
|
||||||
|
location_id: string
|
||||||
|
fulfillment_set_ids: string[]
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const associateFulfillmentSetsWithLocationStepId =
|
||||||
|
"associate-fulfillment-sets-with-location-step"
|
||||||
|
export const associateFulfillmentSetsWithLocationStep = createStep(
|
||||||
|
associateFulfillmentSetsWithLocationStepId,
|
||||||
|
async (data: StepInput, { container }) => {
|
||||||
|
if (!data.input.length) {
|
||||||
|
return new StepResponse([], [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||||
|
|
||||||
|
const links = data.input
|
||||||
|
.map((link) => {
|
||||||
|
return link.fulfillment_set_ids.map((id) => {
|
||||||
|
return {
|
||||||
|
[Modules.FULFILLMENT]: {
|
||||||
|
fulfillment_set_id: id,
|
||||||
|
},
|
||||||
|
[Modules.STOCK_LOCATION]: {
|
||||||
|
stock_location_id: link.location_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
|
||||||
|
const createdLinks = await remoteLink.create(links)
|
||||||
|
|
||||||
|
return new StepResponse(createdLinks, links)
|
||||||
|
},
|
||||||
|
async (links, { container }) => {
|
||||||
|
if (!links?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||||
|
|
||||||
|
await remoteLink.dismiss(links)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { CreateLocationFulfillmentSetWorkflowInputDTO } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import { createFulfillmentSets } from "../../fulfillment"
|
||||||
|
import { associateFulfillmentSetsWithLocationStep } from "../steps/associate-locations-with-fulfillment-sets"
|
||||||
|
|
||||||
|
export const createLocationFulfillmentSetWorkflowId =
|
||||||
|
"create-location-fulfillment-set"
|
||||||
|
export const createLocationFulfillmentSetWorkflow = createWorkflow(
|
||||||
|
createLocationFulfillmentSetWorkflowId,
|
||||||
|
(input: WorkflowData<CreateLocationFulfillmentSetWorkflowInputDTO>) => {
|
||||||
|
const fulfillmentSet = createFulfillmentSets([
|
||||||
|
{
|
||||||
|
name: input.fulfillment_set_data.name,
|
||||||
|
type: input.fulfillment_set_data.type,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const data = transform({ input, fulfillmentSet }, (data) => [
|
||||||
|
{
|
||||||
|
location_id: data.input.location_id,
|
||||||
|
fulfillment_set_ids: [data.fulfillmentSet[0].id],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
associateFulfillmentSetsWithLocationStep({
|
||||||
|
input: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from "./create-location-fulfillment-set"
|
||||||
export * from "./create-stock-locations"
|
export * from "./create-stock-locations"
|
||||||
export * from "./update-stock-locations"
|
|
||||||
export * from "./delete-stock-locations"
|
export * from "./delete-stock-locations"
|
||||||
|
export * from "./update-stock-locations"
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { createLocationFulfillmentSetWorkflow } from "@medusajs/core-flows"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
remoteQueryObjectFromString,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
|
||||||
|
import { AdminCreateStockLocationFulfillmentSet } from "../../validators"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: MedusaRequest<z.infer<typeof AdminCreateStockLocationFulfillmentSet>>,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||||
|
|
||||||
|
const { errors } = await createLocationFulfillmentSetWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
|
location_id: req.params.id,
|
||||||
|
fulfillment_set_data: {
|
||||||
|
name: req.validatedBody.name,
|
||||||
|
type: req.validatedBody.type,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(errors) && errors[0]) {
|
||||||
|
throw errors[0].error
|
||||||
|
}
|
||||||
|
|
||||||
|
const [stock_location] = await remoteQuery(
|
||||||
|
remoteQueryObjectFromString({
|
||||||
|
entryPoint: "stock_locations",
|
||||||
|
variables: {
|
||||||
|
id: req.params.id,
|
||||||
|
},
|
||||||
|
fields: req.remoteQueryConfig.fields,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
res.status(200).json({ stock_location })
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as QueryConfig from "./query-config"
|
import * as QueryConfig from "./query-config"
|
||||||
|
|
||||||
|
import { transformBody, transformQuery } from "../../../api/middlewares"
|
||||||
import {
|
import {
|
||||||
|
AdminCreateStockLocationFulfillmentSet,
|
||||||
AdminGetStockLocationsLocationParams,
|
AdminGetStockLocationsLocationParams,
|
||||||
AdminGetStockLocationsParams,
|
AdminGetStockLocationsParams,
|
||||||
AdminPostStockLocationsLocationParams,
|
AdminPostStockLocationsLocationParams,
|
||||||
@@ -9,11 +11,11 @@ import {
|
|||||||
AdminPostStockLocationsReq,
|
AdminPostStockLocationsReq,
|
||||||
AdminStockLocationsLocationSalesChannelBatchReq,
|
AdminStockLocationsLocationSalesChannelBatchReq,
|
||||||
} from "./validators"
|
} from "./validators"
|
||||||
import { transformBody, transformQuery } from "../../../api/middlewares"
|
|
||||||
|
|
||||||
import { MiddlewareRoute } from "../../../types/middlewares"
|
import { MiddlewareRoute } from "../../../types/middlewares"
|
||||||
import { applySalesChannelsFilter } from "./utils/apply-sales-channel-filter"
|
|
||||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||||
|
import { validateAndTransformBody } from "../../utils/validate-body"
|
||||||
|
import { applySalesChannelsFilter } from "./utils/apply-sales-channel-filter"
|
||||||
|
|
||||||
export const adminStockLocationRoutesMiddlewares: MiddlewareRoute[] = [
|
export const adminStockLocationRoutesMiddlewares: MiddlewareRoute[] = [
|
||||||
{
|
{
|
||||||
@@ -75,4 +77,15 @@ export const adminStockLocationRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/stock-locations/:id/fulfillment-sets",
|
||||||
|
middlewares: [
|
||||||
|
validateAndTransformBody(AdminCreateStockLocationFulfillmentSet),
|
||||||
|
transformQuery(
|
||||||
|
AdminPostStockLocationsParams,
|
||||||
|
QueryConfig.retrieveTransformQueryConfig
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
|
import { Transform, Type } from "class-transformer"
|
||||||
import {
|
import {
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsObject,
|
IsObject,
|
||||||
@@ -6,8 +6,9 @@ import {
|
|||||||
IsString,
|
IsString,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { Transform, Type } from "class-transformer"
|
import { FindParams, extendedFindParamsMixin } from "../../../types/common"
|
||||||
|
|
||||||
|
import { z } from "zod"
|
||||||
import { IsType } from "../../../utils"
|
import { IsType } from "../../../utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -286,3 +287,10 @@ export class AdminStockLocationsLocationSalesChannelBatchReq {
|
|||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
sales_channel_ids: string[]
|
sales_channel_ids: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AdminCreateStockLocationFulfillmentSet = z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Customer, User } from "../models"
|
|
||||||
import type { NextFunction, Request, Response } from "express"
|
import type { NextFunction, Request, Response } from "express"
|
||||||
|
import type { Customer, User } from "../models"
|
||||||
|
|
||||||
import { MedusaContainer, RequestQueryFields } from "@medusajs/types"
|
import { MedusaContainer, RequestQueryFields } from "@medusajs/types"
|
||||||
import { FindConfig } from "./common"
|
import { FindConfig } from "./common"
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from "./stock-locations"
|
||||||
export * from "./fulfillment"
|
export * from "./fulfillment"
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { AdminFulfillmentSetResponse } from "../fulfillment"
|
||||||
|
|
||||||
|
export type AdminStockLocationAddressResponse = {
|
||||||
|
id?: string
|
||||||
|
address_1: string
|
||||||
|
address_2?: string | null
|
||||||
|
company?: string | null
|
||||||
|
country_code: string
|
||||||
|
city?: string | null
|
||||||
|
phone?: string | null
|
||||||
|
postal_code?: string | null
|
||||||
|
province?: string | null
|
||||||
|
metadata?: Record<string, unknown> | null
|
||||||
|
created_at: string | Date
|
||||||
|
updated_at: string | Date
|
||||||
|
deleted_at: string | Date | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminStockLocationResponse {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
metadata: Record<string, unknown> | null
|
||||||
|
address_id: string
|
||||||
|
address?: AdminStockLocationAddressResponse
|
||||||
|
created_at: string | Date
|
||||||
|
updated_at: string | Date
|
||||||
|
deleted_at: string | Date | null
|
||||||
|
|
||||||
|
fulfillment_sets?: AdminFulfillmentSetResponse[]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./common"
|
||||||
@@ -5,12 +5,15 @@ export * from "./bundles"
|
|||||||
export * from "./cache"
|
export * from "./cache"
|
||||||
export * from "./cart"
|
export * from "./cart"
|
||||||
export * from "./common"
|
export * from "./common"
|
||||||
|
export * from "./currency"
|
||||||
export * from "./customer"
|
export * from "./customer"
|
||||||
export * from "./dal"
|
export * from "./dal"
|
||||||
export * from "./event-bus"
|
export * from "./event-bus"
|
||||||
export * from "./feature-flag"
|
export * from "./feature-flag"
|
||||||
|
export * from "./file"
|
||||||
export * from "./file-service"
|
export * from "./file-service"
|
||||||
export * from "./fulfillment"
|
export * from "./fulfillment"
|
||||||
|
export * from "./http"
|
||||||
export * from "./inventory"
|
export * from "./inventory"
|
||||||
export * from "./joiner"
|
export * from "./joiner"
|
||||||
export * from "./link-modules"
|
export * from "./link-modules"
|
||||||
@@ -34,6 +37,4 @@ export * from "./totals"
|
|||||||
export * from "./transaction-base"
|
export * from "./transaction-base"
|
||||||
export * from "./user"
|
export * from "./user"
|
||||||
export * from "./workflow"
|
export * from "./workflow"
|
||||||
export * from "./currency"
|
export * from "./workflows"
|
||||||
export * from "./http"
|
|
||||||
export * from "./file"
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./stock-locations"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./mutations"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export interface CreateLocationFulfillmentSetWorkflowInputDTO {
|
||||||
|
location_id: string
|
||||||
|
fulfillment_set_data: {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user