feat(medusa, core-flows): Create service zones (#6979)

This commit is contained in:
Oli Juhl
2024-04-06 21:31:23 +02:00
committed by GitHub
parent e8587e9f95
commit 81ea044f31
14 changed files with 509 additions and 15 deletions

View 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)}`
)
})
})
},
})

View File

@@ -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)
}
)

View File

@@ -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"

View File

@@ -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
}
)

View File

@@ -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"

View File

@@ -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: {

View File

@@ -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 })
}

View File

@@ -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
),
],
},
]

View File

@@ -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,
}

View File

@@ -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(),
})
)

View File

@@ -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",

View File

@@ -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,
],
}

View File

@@ -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[]
}

View File

@@ -1 +1,2 @@
export * from "./create-service-zones"
export * from "./create-shipping-options"