Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core-flows, medusa): add create stock location endpoint for api-v2 #6787

Merged
merged 10 commits into from
Mar 25, 2024
6 changes: 6 additions & 0 deletions .changeset/six-flies-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@medusajs/core-flows": patch
"@medusajs/medusa": patch
---

feat(core-flows, medusa): add create stock location endpoint for api-v2
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

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),
})
)
})
})
},
})
6 changes: 4 additions & 2 deletions integration-tests/api/medusa-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/core-flows/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions packages/core-flows/src/stock-location/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./steps"
export * from "./workflows"
Original file line number Diff line number Diff line change
@@ -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<IStockLocationServiceNext>(
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)
}
)
1 change: 1 addition & 0 deletions packages/core-flows/src/stock-location/steps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./create-stock-locations"
Original file line number Diff line number Diff line change
@@ -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<WorkflowInput>) => {
const locations = createStockLocations(input.locations)

return locations
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./create-stock-locations"
29 changes: 29 additions & 0 deletions packages/medusa/src/api-v2/admin/stock-locations/middlewares.ts
Original file line number Diff line number Diff line change
@@ -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
),
],
},
]
Original file line number Diff line number Diff line change
@@ -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,
}
32 changes: 32 additions & 0 deletions packages/medusa/src/api-v2/admin/stock-locations/route.ts
Original file line number Diff line number Diff line change
@@ -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<AdminPostStockLocationsReq>,
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 })
}
139 changes: 139 additions & 0 deletions packages/medusa/src/api-v2/admin/stock-locations/validators.ts
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to just pass the address_id here? what will be the behavior?


@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}

export class AdminPostStockLocationsParams extends FindParams {}
2 changes: 2 additions & 0 deletions packages/medusa/src/api-v2/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -57,5 +58,6 @@ export const config: MiddlewaresConfig = {
...adminPricingRoutesMiddlewares,
...adminFulfillmentRoutesMiddlewares,
...adminSalesChannelRoutesMiddlewares,
...adminStockLocationRoutesMiddlewares,
],
}
Loading