feat(medusa, core-flows): Create service zones (#6979)
This commit is contained in:
254
integration-tests/api/__tests__/admin/fulfillment-sets.spec.ts
Normal file
254
integration-tests/api/__tests__/admin/fulfillment-sets.spec.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IFulfillmentModuleService } from "@medusajs/types"
|
||||
import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
} from "../../../helpers/create-admin-user"
|
||||
|
||||
const { medusaIntegrationTestRunner } = require("medusa-test-utils")
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
env: {
|
||||
MEDUSA_FF_MEDUSA_V2: true,
|
||||
},
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
let appContainer
|
||||
let service: IFulfillmentModuleService
|
||||
|
||||
beforeEach(async () => {
|
||||
appContainer = getContainer()
|
||||
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
|
||||
service = appContainer.resolve(ModuleRegistrationName.STOCK_LOCATION)
|
||||
})
|
||||
|
||||
describe("POST /admin/fulfillment-sets/:id/service-zones", () => {
|
||||
it("should create a service zone for a fulfillment set", async () => {
|
||||
const stockLocationResponse = await api.post(
|
||||
`/admin/stock-locations`,
|
||||
{
|
||||
name: "test location",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const stockLocationId = stockLocationResponse.data.stock_location.id
|
||||
|
||||
const locationWithFSetResponse = await api.post(
|
||||
`/admin/stock-locations/${stockLocationId}/fulfillment-sets?fields=id,*fulfillment_sets`,
|
||||
{
|
||||
name: "Fulfillment Set",
|
||||
type: "shipping",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const fulfillmentSetId =
|
||||
locationWithFSetResponse.data.stock_location.fulfillment_sets[0].id
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/fulfillment-sets/${fulfillmentSetId}/service-zones`,
|
||||
{
|
||||
name: "Test Zone",
|
||||
geo_zones: [
|
||||
{
|
||||
country_code: "dk",
|
||||
type: "country",
|
||||
},
|
||||
{
|
||||
country_code: "fr",
|
||||
type: "province",
|
||||
province_code: "fr-idf",
|
||||
},
|
||||
{
|
||||
country_code: "it",
|
||||
type: "city",
|
||||
city: "some city",
|
||||
province_code: "some-province",
|
||||
},
|
||||
{
|
||||
country_code: "it",
|
||||
type: "zip",
|
||||
city: "some city",
|
||||
province_code: "some-province",
|
||||
postal_expression: { type: "regex", exp: "00*" },
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const fset = response.data.fulfillment_set
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
expect(fset).toEqual(
|
||||
expect.objectContaining({
|
||||
name: "Fulfillment Set",
|
||||
type: "shipping",
|
||||
service_zones: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "Test Zone",
|
||||
fulfillment_set_id: fulfillmentSetId,
|
||||
geo_zones: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
country_code: "dk",
|
||||
type: "country",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
country_code: "fr",
|
||||
type: "province",
|
||||
province_code: "fr-idf",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
country_code: "it",
|
||||
type: "city",
|
||||
city: "some city",
|
||||
province_code: "some-province",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
country_code: "it",
|
||||
type: "zip",
|
||||
city: "some city",
|
||||
province_code: "some-province",
|
||||
postal_expression: { type: "regex", exp: "00*" },
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw if invalid type is passed", async () => {
|
||||
const stockLocationResponse = await api.post(
|
||||
`/admin/stock-locations`,
|
||||
{
|
||||
name: "test location",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const stockLocationId = stockLocationResponse.data.stock_location.id
|
||||
|
||||
const locationWithFSetResponse = await api.post(
|
||||
`/admin/stock-locations/${stockLocationId}/fulfillment-sets?fields=id,*fulfillment_sets`,
|
||||
{
|
||||
name: "Fulfillment Set",
|
||||
type: "shipping",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const fulfillmentSetId =
|
||||
locationWithFSetResponse.data.stock_location.fulfillment_sets[0].id
|
||||
|
||||
const errorResponse = await api
|
||||
.post(
|
||||
`/admin/fulfillment-sets/${fulfillmentSetId}/service-zones`,
|
||||
{
|
||||
name: "Test Zone",
|
||||
geo_zones: [
|
||||
{
|
||||
country_code: "dk",
|
||||
type: "country",
|
||||
},
|
||||
{
|
||||
country_code: "fr",
|
||||
type: "province",
|
||||
province_code: "fr-idf",
|
||||
},
|
||||
{
|
||||
country_code: "it",
|
||||
type: "region",
|
||||
city: "some region",
|
||||
province_code: "some-province",
|
||||
},
|
||||
{
|
||||
country_code: "it",
|
||||
type: "zip",
|
||||
city: "some city",
|
||||
province_code: "some-province",
|
||||
postal_expression: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
.catch((err) => err.response)
|
||||
|
||||
const expectedErrors = [
|
||||
{
|
||||
code: "invalid_union",
|
||||
unionErrors: [
|
||||
{
|
||||
issues: [
|
||||
{
|
||||
received: "region",
|
||||
code: "invalid_literal",
|
||||
expected: "country",
|
||||
path: ["geo_zones", 2, "type"],
|
||||
message: 'Invalid literal value, expected "country"',
|
||||
},
|
||||
],
|
||||
name: "ZodError",
|
||||
},
|
||||
{
|
||||
issues: [
|
||||
{
|
||||
received: "region",
|
||||
code: "invalid_literal",
|
||||
expected: "province",
|
||||
path: ["geo_zones", 2, "type"],
|
||||
message: 'Invalid literal value, expected "province"',
|
||||
},
|
||||
],
|
||||
name: "ZodError",
|
||||
},
|
||||
{
|
||||
issues: [
|
||||
{
|
||||
received: "region",
|
||||
code: "invalid_literal",
|
||||
expected: "city",
|
||||
path: ["geo_zones", 2, "type"],
|
||||
message: 'Invalid literal value, expected "city"',
|
||||
},
|
||||
],
|
||||
name: "ZodError",
|
||||
},
|
||||
{
|
||||
issues: [
|
||||
{
|
||||
received: "region",
|
||||
code: "invalid_literal",
|
||||
expected: "zip",
|
||||
path: ["geo_zones", 2, "type"],
|
||||
message: 'Invalid literal value, expected "zip"',
|
||||
},
|
||||
{
|
||||
code: "invalid_type",
|
||||
expected: "object",
|
||||
received: "undefined",
|
||||
path: ["geo_zones", 2, "postal_expression"],
|
||||
message: "Required",
|
||||
},
|
||||
],
|
||||
name: "ZodError",
|
||||
},
|
||||
],
|
||||
path: ["geo_zones", 2],
|
||||
message: "Invalid input",
|
||||
},
|
||||
]
|
||||
|
||||
expect(errorResponse.status).toEqual(400)
|
||||
expect(errorResponse.data.message).toContain(
|
||||
`Invalid request body: ${JSON.stringify(expectedErrors)}`
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
CreateServiceZoneDTO,
|
||||
IFulfillmentModuleService,
|
||||
} from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
type StepInput = CreateServiceZoneDTO[]
|
||||
|
||||
export const createServiceZonesStepId = "create-service-zones"
|
||||
export const createServiceZonesStep = createStep(
|
||||
createServiceZonesStepId,
|
||||
async (input: StepInput, { container }) => {
|
||||
const service = container.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
const createdServiceZones = await service.createServiceZones(input)
|
||||
|
||||
return new StepResponse(
|
||||
createdServiceZones,
|
||||
createdServiceZones.map((createdZone) => createdZone.id)
|
||||
)
|
||||
},
|
||||
async (createdServiceZones, { container }) => {
|
||||
if (!createdServiceZones?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const service = container.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
await service.deleteServiceZones(createdServiceZones)
|
||||
}
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./add-rules-to-fulfillment-shipping-option"
|
||||
export * from "./create-fulfillment-set"
|
||||
export * from "./remove-rules-from-fulfillment-shipping-option"
|
||||
export * from "./create-shipping-options"
|
||||
export * from "./add-shipping-options-prices"
|
||||
export * from "./create-fulfillment-set"
|
||||
export * from "./create-service-zones"
|
||||
export * from "./create-shipping-options"
|
||||
export * from "./remove-rules-from-fulfillment-shipping-option"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { FulfillmentWorkflow } from "@medusajs/types"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { createServiceZonesStep } from "../steps"
|
||||
|
||||
export const createServiceZonesWorkflowId = "create-service-zones-workflow"
|
||||
export const createServiceZonesWorkflow = createWorkflow(
|
||||
createServiceZonesWorkflowId,
|
||||
(
|
||||
input: WorkflowData<FulfillmentWorkflow.CreateServiceZonesWorkflowInput>
|
||||
): WorkflowData => {
|
||||
const serviceZones = createServiceZonesStep(input.data)
|
||||
|
||||
return serviceZones
|
||||
}
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./add-rules-to-fulfillment-shipping-option"
|
||||
export * from "./remove-rules-from-fulfillment-shipping-option"
|
||||
export * from "./create-service-zones"
|
||||
export * from "./create-shipping-options"
|
||||
export * from "./remove-rules-from-fulfillment-shipping-option"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
input: {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { createServiceZonesWorkflow } from "@medusajs/core-flows"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
|
||||
import { AdminCreateFulfillmentSetServiceZonesType } from "../../validators"
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest<AdminCreateFulfillmentSetServiceZonesType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
const workflowInput = {
|
||||
data: [
|
||||
{
|
||||
fulfillment_set_id: req.params.id,
|
||||
name: req.validatedBody.name,
|
||||
geo_zones: req.validatedBody.geo_zones,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const { errors } = await createServiceZonesWorkflow(req.scope).run({
|
||||
input: workflowInput,
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
const [fulfillment_set] = await remoteQuery(
|
||||
remoteQueryObjectFromString({
|
||||
entryPoint: "fulfillment_sets",
|
||||
variables: {
|
||||
id: req.params.id,
|
||||
},
|
||||
fields: req.remoteQueryConfig.fields,
|
||||
})
|
||||
)
|
||||
|
||||
res.status(200).json({ fulfillment_set })
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { MiddlewareRoute } from "../../../types/middlewares"
|
||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||
import { validateAndTransformBody } from "../../utils/validate-body"
|
||||
import { validateAndTransformQuery } from "../../utils/validate-query"
|
||||
import * as QueryConfig from "./query-config"
|
||||
import {
|
||||
AdminCreateFulfillmentSetServiceZonesSchema,
|
||||
AdminFulfillmentSetParams
|
||||
} from "./validators"
|
||||
|
||||
export const adminFulfillmentSetsRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
matcher: "/admin/fulfillment-sets*",
|
||||
middlewares: [authenticate("admin", ["session", "bearer", "api-key"])],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/fulfillment-sets/:id/service-zones",
|
||||
middlewares: [
|
||||
validateAndTransformBody(AdminCreateFulfillmentSetServiceZonesSchema),
|
||||
validateAndTransformQuery(
|
||||
AdminFulfillmentSetParams,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
export const defaultAdminFulfillmentSetsFields = [
|
||||
"id",
|
||||
"name",
|
||||
"type",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"*service_zones",
|
||||
"*service_zones.geo_zones",
|
||||
]
|
||||
|
||||
export const retrieveTransformQueryConfig = {
|
||||
defaults: defaultAdminFulfillmentSetsFields,
|
||||
isList: false,
|
||||
}
|
||||
|
||||
export const listTransformQueryConfig = {
|
||||
...retrieveTransformQueryConfig,
|
||||
isList: true,
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { z } from "zod"
|
||||
import { createFindParams, createOperatorMap } from "../../utils/validators"
|
||||
|
||||
const geoZoneBaseSchema = z.object({
|
||||
country_code: z.string(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
})
|
||||
|
||||
const geoZoneCountrySchema = geoZoneBaseSchema.merge(
|
||||
z.object({
|
||||
type: z.literal("country"),
|
||||
})
|
||||
)
|
||||
|
||||
const geoZoneProvinceSchema = geoZoneBaseSchema.merge(
|
||||
z.object({
|
||||
type: z.literal("province"),
|
||||
province_code: z.string(),
|
||||
})
|
||||
)
|
||||
|
||||
const geoZoneCitySchema = geoZoneBaseSchema.merge(
|
||||
z.object({
|
||||
type: z.literal("city"),
|
||||
province_code: z.string(),
|
||||
city: z.string(),
|
||||
})
|
||||
)
|
||||
|
||||
const geoZoneZipSchema = geoZoneBaseSchema.merge(
|
||||
z.object({
|
||||
type: z.literal("zip"),
|
||||
province_code: z.string(),
|
||||
city: z.string(),
|
||||
postal_expression: z.record(z.unknown()),
|
||||
})
|
||||
)
|
||||
|
||||
export const AdminCreateFulfillmentSetServiceZonesSchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
geo_zones: z.array(
|
||||
z.union([
|
||||
geoZoneCountrySchema,
|
||||
geoZoneProvinceSchema,
|
||||
geoZoneCitySchema,
|
||||
geoZoneZipSchema,
|
||||
])
|
||||
),
|
||||
})
|
||||
.strict()
|
||||
|
||||
export type AdminCreateFulfillmentSetServiceZonesType = z.infer<
|
||||
typeof AdminCreateFulfillmentSetServiceZonesSchema
|
||||
>
|
||||
|
||||
export type AdminFulfillmentSetParamsType = z.infer<
|
||||
typeof AdminFulfillmentSetParams
|
||||
>
|
||||
export const AdminFulfillmentSetParams = createFindParams({
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
}).merge(
|
||||
z.object({
|
||||
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
name: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
type: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
service_zone_id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
created_at: createOperatorMap().optional(),
|
||||
updated_at: createOperatorMap().optional(),
|
||||
deleted_at: createOperatorMap().optional(),
|
||||
})
|
||||
)
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as QueryConfig from "./query-config"
|
||||
|
||||
import { transformBody, transformQuery } from "../../../api/middlewares"
|
||||
import { MiddlewareRoute } from "../../../types/middlewares"
|
||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
|
||||
import { validateAndTransformBody } from "../../utils/validate-body"
|
||||
import * as QueryConfig from "./query-config"
|
||||
import {
|
||||
AdminCreateStockLocationFulfillmentSet,
|
||||
AdminGetStockLocationsLocationParams,
|
||||
@@ -12,11 +15,6 @@ import {
|
||||
AdminStockLocationsLocationSalesChannelBatchReq,
|
||||
} from "./validators"
|
||||
|
||||
import { MiddlewareRoute } from "../../../types/middlewares"
|
||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||
import { maybeApplyLinkFilter } from "../../utils/maybe-apply-link-filter"
|
||||
import { validateAndTransformBody } from "../../utils/validate-body"
|
||||
|
||||
export const adminStockLocationRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
|
||||
@@ -5,18 +5,19 @@ import { adminCollectionRoutesMiddlewares } from "./admin/collections/middleware
|
||||
import { adminCurrencyRoutesMiddlewares } from "./admin/currencies/middlewares"
|
||||
import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares"
|
||||
import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares"
|
||||
import { adminShippingOptionRoutesMiddlewares } from "./admin/shipping-options/middlewares"
|
||||
import { adminDraftOrderRoutesMiddlewares } from "./admin/draft-orders/middlewares"
|
||||
import { adminFulfillmentSetsRoutesMiddlewares } from "./admin/fulfillment-sets/middlewares"
|
||||
import { adminInventoryRoutesMiddlewares } from "./admin/inventory-items/middlewares"
|
||||
import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares"
|
||||
import { adminPaymentRoutesMiddlewares } from "./admin/payments/middlewares"
|
||||
import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middlewares"
|
||||
import { adminPricingRoutesMiddlewares } from "./admin/pricing/middlewares"
|
||||
import { adminProductRoutesMiddlewares } from "./admin/products/middlewares"
|
||||
import { adminProductTypeRoutesMiddlewares } from "./admin/product-types/middlewares"
|
||||
import { adminProductRoutesMiddlewares } from "./admin/products/middlewares"
|
||||
import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares"
|
||||
import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares"
|
||||
import { adminSalesChannelRoutesMiddlewares } from "./admin/sales-channels/middlewares"
|
||||
import { adminShippingOptionRoutesMiddlewares } from "./admin/shipping-options/middlewares"
|
||||
import { adminStockLocationRoutesMiddlewares } from "./admin/stock-locations/middlewares"
|
||||
import { adminStoreRoutesMiddlewares } from "./admin/stores/middlewares"
|
||||
import { adminTaxRateRoutesMiddlewares } from "./admin/tax-rates/middlewares"
|
||||
@@ -65,5 +66,6 @@ export const config: MiddlewaresConfig = {
|
||||
...adminStockLocationRoutesMiddlewares,
|
||||
...adminProductTypeRoutesMiddlewares,
|
||||
...adminUploadRoutesMiddlewares,
|
||||
...adminFulfillmentSetsRoutesMiddlewares,
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
CreateCityGeoZoneDTO,
|
||||
CreateCountryGeoZoneDTO,
|
||||
CreateProvinceGeoZoneDTO,
|
||||
CreateZipGeoZoneDTO,
|
||||
} from "../../fulfillment"
|
||||
|
||||
interface CreateServiceZone {
|
||||
name: string
|
||||
fulfillment_set_id: string
|
||||
geo_zones?: (
|
||||
| Omit<CreateCountryGeoZoneDTO, "service_zone_id">
|
||||
| Omit<CreateProvinceGeoZoneDTO, "service_zone_id">
|
||||
| Omit<CreateCityGeoZoneDTO, "service_zone_id">
|
||||
| Omit<CreateZipGeoZoneDTO, "service_zone_id">
|
||||
)[]
|
||||
}
|
||||
|
||||
export interface CreateServiceZonesWorkflowInput {
|
||||
data: CreateServiceZone[]
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./create-service-zones"
|
||||
export * from "./create-shipping-options"
|
||||
|
||||
Reference in New Issue
Block a user