From 68b9812aa1fe8a9e368112e721cd868919369980 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:53:09 +0100 Subject: [PATCH] feat(core-flows, medusa): add create stock location endpoint for api-v2 (#6787) --- .changeset/six-flies-dress.md | 6 + .../admin/stock-location/index.spec.ts | 58 ++++++++ integration-tests/api/medusa-config.js | 6 +- packages/core-flows/src/index.ts | 1 + .../core-flows/src/stock-location/index.ts | 2 + .../steps/create-stock-locations.ts | 35 +++++ .../src/stock-location/steps/index.ts | 1 + .../workflows/create-stock-locations.ts | 18 +++ .../src/stock-location/workflows/index.ts | 1 + .../admin/stock-locations/middlewares.ts | 29 ++++ .../admin/stock-locations/query-config.ts | 27 ++++ .../src/api-v2/admin/stock-locations/route.ts | 32 ++++ .../admin/stock-locations/validators.ts | 139 ++++++++++++++++++ packages/medusa/src/api-v2/middlewares.ts | 2 + 14 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 .changeset/six-flies-dress.md create mode 100644 integration-tests/api/__tests__/admin/stock-location/index.spec.ts create mode 100644 packages/core-flows/src/stock-location/index.ts create mode 100644 packages/core-flows/src/stock-location/steps/create-stock-locations.ts create mode 100644 packages/core-flows/src/stock-location/steps/index.ts create mode 100644 packages/core-flows/src/stock-location/workflows/create-stock-locations.ts create mode 100644 packages/core-flows/src/stock-location/workflows/index.ts create mode 100644 packages/medusa/src/api-v2/admin/stock-locations/middlewares.ts create mode 100644 packages/medusa/src/api-v2/admin/stock-locations/query-config.ts create mode 100644 packages/medusa/src/api-v2/admin/stock-locations/route.ts create mode 100644 packages/medusa/src/api-v2/admin/stock-locations/validators.ts diff --git a/.changeset/six-flies-dress.md b/.changeset/six-flies-dress.md new file mode 100644 index 0000000000..191f7a418d --- /dev/null +++ b/.changeset/six-flies-dress.md @@ -0,0 +1,6 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/medusa": patch +--- + +feat(core-flows, medusa): add create stock location endpoint for api-v2 diff --git a/integration-tests/api/__tests__/admin/stock-location/index.spec.ts b/integration-tests/api/__tests__/admin/stock-location/index.spec.ts new file mode 100644 index 0000000000..04de2bda46 --- /dev/null +++ b/integration-tests/api/__tests__/admin/stock-location/index.spec.ts @@ -0,0 +1,58 @@ +import { + adminHeaders, + createAdminUser, +} from "../../../../helpers/create-admin-user" + +import { IStockLocationServiceNext } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +const { medusaIntegrationTestRunner } = require("medusa-test-utils") + +jest.setTimeout(30000) + +medusaIntegrationTestRunner({ + env: { + MEDUSA_FF_MEDUSA_V2: true, + }, + testSuite: ({ dbConnection, getContainer, api }) => { + let appContainer + let service: IStockLocationServiceNext + + beforeEach(async () => { + appContainer = getContainer() + + await createAdminUser(dbConnection, adminHeaders, appContainer) + + service = appContainer.resolve(ModuleRegistrationName.STOCK_LOCATION) + }) + + describe("create stock location", () => { + it("should create a stock location with a name and address", async () => { + const address = { + address_1: "Test Address", + country_code: "US", + } + const location = { + name: "Test Location", + } + + const response = await api.post( + "/admin/stock-locations", + { + ...location, + address, + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.stock_location).toEqual( + expect.objectContaining({ + ...location, + address: expect.objectContaining(address), + }) + ) + }) + }) + }, +}) diff --git a/integration-tests/api/medusa-config.js b/integration-tests/api/medusa-config.js index ab637f6c06..a87c4d7a7f 100644 --- a/integration-tests/api/medusa-config.js +++ b/integration-tests/api/medusa-config.js @@ -67,8 +67,10 @@ module.exports = { resolve: "@medusajs/cache-inmemory", options: { ttl: 0 }, // Cache disabled }, - [Modules.STOCK_LOCATION]: true, - [Modules.INVENTORY]: true, + [Modules.STOCK_LOCATION]: { + resolve: "@medusajs/stock-location-next", + options: {}, + }, [Modules.PRODUCT]: true, [Modules.PRICING]: true, [Modules.PROMOTION]: true, diff --git a/packages/core-flows/src/index.ts b/packages/core-flows/src/index.ts index 00f215b61b..7a46c1866f 100644 --- a/packages/core-flows/src/index.ts +++ b/packages/core-flows/src/index.ts @@ -17,6 +17,7 @@ export * from "./promotion" export * from "./region" export * from "./sales-channel" export * from "./shipping-options" +export * from "./stock-location" export * from "./store" export * from "./tax" export * from "./user" diff --git a/packages/core-flows/src/stock-location/index.ts b/packages/core-flows/src/stock-location/index.ts new file mode 100644 index 0000000000..68de82c9f9 --- /dev/null +++ b/packages/core-flows/src/stock-location/index.ts @@ -0,0 +1,2 @@ +export * from "./steps" +export * from "./workflows" diff --git a/packages/core-flows/src/stock-location/steps/create-stock-locations.ts b/packages/core-flows/src/stock-location/steps/create-stock-locations.ts new file mode 100644 index 0000000000..cd0d2fcbb1 --- /dev/null +++ b/packages/core-flows/src/stock-location/steps/create-stock-locations.ts @@ -0,0 +1,35 @@ +import { + CreateStockLocationInput, + IStockLocationServiceNext, +} from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export const createStockLocationsStepId = "create-stock-locations" +export const createStockLocations = createStep( + createStockLocationsStepId, + async (data: CreateStockLocationInput[], { container }) => { + const stockLocationService = container.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + + const created = await stockLocationService.create(data) + + return new StepResponse( + created, + created.map((i) => i.id) + ) + }, + async (createdStockLocationIds, { container }) => { + if (!createdStockLocationIds?.length) { + return + } + + const stockLocationService = container.resolve( + ModuleRegistrationName.STOCK_LOCATION + ) + + await stockLocationService.delete(createdStockLocationIds) + } +) diff --git a/packages/core-flows/src/stock-location/steps/index.ts b/packages/core-flows/src/stock-location/steps/index.ts new file mode 100644 index 0000000000..6f009f0cf5 --- /dev/null +++ b/packages/core-flows/src/stock-location/steps/index.ts @@ -0,0 +1 @@ +export * from "./create-stock-locations" diff --git a/packages/core-flows/src/stock-location/workflows/create-stock-locations.ts b/packages/core-flows/src/stock-location/workflows/create-stock-locations.ts new file mode 100644 index 0000000000..7bd0206f86 --- /dev/null +++ b/packages/core-flows/src/stock-location/workflows/create-stock-locations.ts @@ -0,0 +1,18 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" + +import { CreateStockLocationInput } from "@medusajs/types" +import { createStockLocations } from "../steps" + +interface WorkflowInput { + locations: CreateStockLocationInput[] +} + +export const createStockLocationsWorkflowId = "create-stock-locations-workflow" +export const createStockLocationsWorkflow = createWorkflow( + createStockLocationsWorkflowId, + (input: WorkflowData) => { + const locations = createStockLocations(input.locations) + + return locations + } +) diff --git a/packages/core-flows/src/stock-location/workflows/index.ts b/packages/core-flows/src/stock-location/workflows/index.ts new file mode 100644 index 0000000000..6f009f0cf5 --- /dev/null +++ b/packages/core-flows/src/stock-location/workflows/index.ts @@ -0,0 +1 @@ +export * from "./create-stock-locations" diff --git a/packages/medusa/src/api-v2/admin/stock-locations/middlewares.ts b/packages/medusa/src/api-v2/admin/stock-locations/middlewares.ts new file mode 100644 index 0000000000..6c03ade172 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/stock-locations/middlewares.ts @@ -0,0 +1,29 @@ +import * as QueryConfig from "./query-config" + +import { + AdminPostStockLocationsParams, + AdminPostStockLocationsReq, +} from "./validators" +import { transformBody, transformQuery } from "../../../api/middlewares" + +import { MiddlewareRoute } from "../../../types/middlewares" +import { authenticate } from "../../../utils/authenticate-middleware" + +export const adminStockLocationRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: "ALL", + matcher: "/admin/stock-locations*", + middlewares: [authenticate("admin", ["session", "bearer", "api-key"])], + }, + { + method: ["POST"], + matcher: "/admin/stock-locations", + middlewares: [ + transformBody(AdminPostStockLocationsReq), + transformQuery( + AdminPostStockLocationsParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api-v2/admin/stock-locations/query-config.ts b/packages/medusa/src/api-v2/admin/stock-locations/query-config.ts new file mode 100644 index 0000000000..227e332fc5 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/stock-locations/query-config.ts @@ -0,0 +1,27 @@ +export const defaultAdminStockLocationFields = [ + "id", + "name", + "metadata", + "created_at", + "updated_at", + "address.id", + "address.address_1", + "address.address_2", + "address.city", + "address.country_code", + "address.phone", + "address.province", + "address.postal_code", + "address.metadata", +] + +export const retrieveTransformQueryConfig = { + defaults: defaultAdminStockLocationFields, + allowed: defaultAdminStockLocationFields, + isList: false, +} + +export const listTransformQueryConfig = { + ...retrieveTransformQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/stock-locations/route.ts b/packages/medusa/src/api-v2/admin/stock-locations/route.ts new file mode 100644 index 0000000000..4ad1ed9d4f --- /dev/null +++ b/packages/medusa/src/api-v2/admin/stock-locations/route.ts @@ -0,0 +1,32 @@ +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" + +import { AdminPostStockLocationsReq } from "./validators" +import { createStockLocationsWorkflow } from "@medusajs/core-flows" + +// Create stock location +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const { result } = await createStockLocationsWorkflow(req.scope).run({ + input: { locations: [req.validatedBody] }, + }) + + const [stock_location] = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "stock_locations", + variables: { + id: result[0].id, + }, + fields: req.remoteQueryConfig.fields, + }) + ) + + res.status(200).json({ stock_location }) +} diff --git a/packages/medusa/src/api-v2/admin/stock-locations/validators.ts b/packages/medusa/src/api-v2/admin/stock-locations/validators.ts new file mode 100644 index 0000000000..f5d757e82f --- /dev/null +++ b/packages/medusa/src/api-v2/admin/stock-locations/validators.ts @@ -0,0 +1,139 @@ +import { + DateComparisonOperator, + FindParams, + NumericalComparisonOperator, + StringComparisonOperator, + extendedFindParamsMixin, +} from "../../../types/common" +import { + IsBoolean, + IsEmail, + IsNotEmpty, + IsNumber, + IsObject, + IsOptional, + IsString, + ValidateNested, +} from "class-validator" +import { Transform, Type } from "class-transformer" + +import { IsType } from "../../../utils" + +/** + * @schema AdminPostStockLocationsReqAddress + * type: object + * required: + * - address_1 + * - country_code + * properties: + * address_1: + * type: string + * description: Stock location address + * example: 35, Jhon Doe Ave + * address_2: + * type: string + * description: Stock location address' complement + * example: apartment 4432 + * company: + * type: string + * description: Stock location address' company + * city: + * type: string + * description: Stock location address' city + * example: Mexico city + * country_code: + * description: "The two character ISO code for the country." + * type: string + * externalDocs: + * url: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements + * description: See a list of codes. + * phone: + * type: string + * description: Stock location address' phone number + * example: +1 555 61646 + * postal_code: + * type: string + * description: Stock location address' postal code + * example: HD3-1G8 + * province: + * type: string + * description: Stock location address' province + * example: Sinaloa + */ +class StockLocationAddress { + @IsString() + address_1: string + + @IsOptional() + @IsString() + address_2?: string + + @IsOptional() + @IsString() + company?: string + + @IsOptional() + @IsString() + city?: string + + @IsString() + country_code: string + + @IsOptional() + @IsString() + phone?: string + + @IsOptional() + @IsString() + postal_code?: string + + @IsOptional() + @IsString() + province?: string +} + +/** + * @schema AdminPostStockLocationsReq + * type: object + * description: "The details of the stock location to create." + * required: + * - name + * properties: + * name: + * description: the name of the stock location + * type: string + * address_id: + * description: the ID of an existing stock location address to associate with the stock location. Only required if `address` is not provided. + * type: string + * metadata: + * type: object + * description: An optional key-value map with additional details + * example: {car: "white"} + * externalDocs: + * description: "Learn about the metadata attribute, and how to delete and update it." + * url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute" + * address: + * description: A new stock location address to create and associate with the stock location. Only required if `address_id` is not provided. + * $ref: "#/components/schemas/StockLocationAddressInput" + */ +export class AdminPostStockLocationsReq { + @IsString() + @IsNotEmpty() + @Transform(({ value }: { value: string }) => value?.trim()) + name: string + + @IsOptional() + @ValidateNested() + @Type(() => StockLocationAddress) + address?: StockLocationAddress + + @IsOptional() + @IsString() + address_id?: string + + @IsObject() + @IsOptional() + metadata?: Record +} + +export class AdminPostStockLocationsParams extends FindParams {} diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 5081211d7e..6776e3b9e2 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -15,6 +15,7 @@ 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 { adminStockLocationRoutesMiddlewares } from "./admin/stock-locations/middlewares" import { adminStoreRoutesMiddlewares } from "./admin/stores/middlewares" import { adminTaxRateRoutesMiddlewares } from "./admin/tax-rates/middlewares" import { adminTaxRegionRoutesMiddlewares } from "./admin/tax-regions/middlewares" @@ -57,5 +58,6 @@ export const config: MiddlewaresConfig = { ...adminPricingRoutesMiddlewares, ...adminFulfillmentRoutesMiddlewares, ...adminSalesChannelRoutesMiddlewares, + ...adminStockLocationRoutesMiddlewares, ], }