Feat/reservations endpoints (#2995)
**What** - add reservation endpoints: - `create-reservation` - `update-reservation` - `delete-reservation` - `get-reservation` - `list-reservations` - `orders/create-reservation-for-line-item` - `orders/get-reservations` Fixes CORE-979
This commit is contained in:
1 parent
838948c4e8
commit
9c4647383e
12 files changed
+930
-6
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
add reservation endpoints
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
import {
|
||||||
|
LineItemService,
|
||||||
|
ProductVariantInventoryService,
|
||||||
|
} from "../../../../services"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [post] /orders/{id}/line-items/{line_item_id}/reserve
|
||||||
|
* operationId: "PostOrdersOrderLineItemReservations"
|
||||||
|
* summary: "Create a Reservation for a line item"
|
||||||
|
* description: "Creates a Reservation for a line item at a specified location, optionally for a partial quantity."
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (path) id=* {string} The ID of the Order.
|
||||||
|
* - (path) line_item_id=* {string} The ID of the Line item.
|
||||||
|
* requestBody:
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: "#/components/schemas/AdminOrdersOrderLineItemReservationReq"
|
||||||
|
* x-codeSamples:
|
||||||
|
* - lang: JavaScript
|
||||||
|
* label: JS Client
|
||||||
|
* source: |
|
||||||
|
* import Medusa from "@medusajs/medusa-js"
|
||||||
|
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||||
|
* // must be previously logged in or use api token
|
||||||
|
* medusa.admin.orders.createReservation(order_id, line_item_id, {
|
||||||
|
* location_id
|
||||||
|
* })
|
||||||
|
* .then(({ reservation }) => {
|
||||||
|
* console.log(reservation.id);
|
||||||
|
* });
|
||||||
|
* - lang: Shell
|
||||||
|
* label: cURL
|
||||||
|
* source: |
|
||||||
|
* curl --location --request POST 'https://medusa-url.com/admin/orders/{id}/line-items/{line_item_id}/reservations' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}' \
|
||||||
|
* --header 'Content-Type: application/json' \
|
||||||
|
* --data-raw '{
|
||||||
|
* "location_id": "loc_1"
|
||||||
|
* }'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - Order
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: "#/components/schemas/AdminPostReservationsReq"
|
||||||
|
* "400":
|
||||||
|
* $ref: "#/components/responses/400_error"
|
||||||
|
* "401":
|
||||||
|
* $ref: "#/components/responses/unauthorized"
|
||||||
|
* "404":
|
||||||
|
* $ref: "#/components/responses/not_found_error"
|
||||||
|
* "409":
|
||||||
|
* $ref: "#/components/responses/invalid_state_error"
|
||||||
|
* "422":
|
||||||
|
* $ref: "#/components/responses/invalid_request_error"
|
||||||
|
* "500":
|
||||||
|
* $ref: "#/components/responses/500_error"
|
||||||
|
*/
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id, line_item_id } = req.params
|
||||||
|
|
||||||
|
const { validatedBody } = req as {
|
||||||
|
validatedBody: AdminOrdersOrderLineItemReservationReq
|
||||||
|
}
|
||||||
|
const productVariantInventoryService: ProductVariantInventoryService =
|
||||||
|
req.scope.resolve("productVariantInventoryService")
|
||||||
|
|
||||||
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
|
|
||||||
|
const lineItemService: LineItemService = req.scope.resolve("lineItemService")
|
||||||
|
|
||||||
|
const reservations = await manager.transaction(async (manager) => {
|
||||||
|
const lineItem = await lineItemService
|
||||||
|
.withTransaction(manager)
|
||||||
|
.retrieve(line_item_id)
|
||||||
|
|
||||||
|
if (!lineItem.variant_id) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Can't create a reservation for a Line Item wihtout a variant`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantity = validatedBody.quantity || lineItem.quantity
|
||||||
|
|
||||||
|
const productVariantInventoryServiceTx =
|
||||||
|
productVariantInventoryService.withTransaction(manager)
|
||||||
|
|
||||||
|
return await productVariantInventoryServiceTx.reserveQuantity(
|
||||||
|
lineItem.variant_id,
|
||||||
|
quantity,
|
||||||
|
{
|
||||||
|
locationId: validatedBody.location_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({ reservation: reservations[0] })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @schema AdminOrdersOrderLineItemReservationReq
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - location_id
|
||||||
|
* properties:
|
||||||
|
* location_id:
|
||||||
|
* description: "The id of the location of the reservation"
|
||||||
|
* type: string
|
||||||
|
* quantity:
|
||||||
|
* description: "The quantity to reserve"
|
||||||
|
* type: number
|
||||||
|
*/
|
||||||
|
export class AdminOrdersOrderLineItemReservationReq {
|
||||||
|
location_id: string
|
||||||
|
|
||||||
|
quantity?: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Request, Response } from "express"
|
||||||
|
import { IInventoryService } from "../../../../interfaces"
|
||||||
|
import { OrderService } from "../../../../services"
|
||||||
|
import { extendedFindParamsMixin } from "../../../../types/common"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [get] /orders/{id}/reservations
|
||||||
|
* operationId: "GetOrdersOrderReservations"
|
||||||
|
* summary: "Get reservations for an Order"
|
||||||
|
* description: "Retrieves reservations for an Order"
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (path) id=* {string} The ID of the Order.
|
||||||
|
* - (query) offset=0 {integer} How many reservations to skip before the results.
|
||||||
|
* - (query) limit=20 {integer} Limit the number of reservations returned.
|
||||||
|
* x-codeSamples:
|
||||||
|
* - lang: JavaScript
|
||||||
|
* label: JS Client
|
||||||
|
* source: |
|
||||||
|
* import Medusa from "@medusajs/medusa-js"
|
||||||
|
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||||
|
* // must be previously logged in or use api token
|
||||||
|
* medusa.admin.orders.retrieveReservations(order_id)
|
||||||
|
* .then(({ reservations }) => {
|
||||||
|
* console.log(reservations[0].id);
|
||||||
|
* });
|
||||||
|
* - lang: Shell
|
||||||
|
* label: cURL
|
||||||
|
* source: |
|
||||||
|
* curl --location --request GET 'https://medusa-url.com/admin/orders/{id}/reservations' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - Order
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: "#/components/schemas/AdminGetReservationReservationsReq"
|
||||||
|
* "400":
|
||||||
|
* $ref: "#/components/responses/400_error"
|
||||||
|
* "401":
|
||||||
|
* $ref: "#/components/responses/unauthorized"
|
||||||
|
* "404":
|
||||||
|
* $ref: "#/components/responses/not_found_error"
|
||||||
|
* "409":
|
||||||
|
* $ref: "#/components/responses/invalid_state_error"
|
||||||
|
* "422":
|
||||||
|
* $ref: "#/components/responses/invalid_request_error"
|
||||||
|
* "500":
|
||||||
|
* $ref: "#/components/responses/500_error"
|
||||||
|
*/
|
||||||
|
export default async (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
const inventoryService: IInventoryService =
|
||||||
|
req.scope.resolve("inventoryService")
|
||||||
|
const orderService: OrderService = req.scope.resolve("orderService")
|
||||||
|
|
||||||
|
const order = await orderService.retrieve(id, { relations: ["items"] })
|
||||||
|
|
||||||
|
const [reservations, count] = await inventoryService.listReservationItems(
|
||||||
|
{
|
||||||
|
line_item_id: order.items.map((i) => i.id),
|
||||||
|
},
|
||||||
|
req.listConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
const { limit, offset } = req.validatedQuery
|
||||||
|
|
||||||
|
res.json({ reservations, count, limit, offset })
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
export class AdminGetOrdersOrderReservationsParams extends extendedFindParamsMixin(
|
||||||
|
{
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
}
|
||||||
|
) {}
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
import { Order } from "../../../.."
|
import { Order } from "../../../.."
|
||||||
import { FindParams, PaginatedResponse } from "../../../../types/common"
|
import {
|
||||||
|
DeleteResponse,
|
||||||
|
FindParams,
|
||||||
|
PaginatedResponse,
|
||||||
|
} from "../../../../types/common"
|
||||||
import { FlagRouter } from "../../../../utils/flag-router"
|
import { FlagRouter } from "../../../../utils/flag-router"
|
||||||
import middlewares, { transformQuery } from "../../../middlewares"
|
import middlewares, {
|
||||||
|
transformBody,
|
||||||
|
transformQuery,
|
||||||
|
} from "../../../middlewares"
|
||||||
|
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
|
||||||
|
import { AdminOrdersOrderLineItemReservationReq } from "./create-reservation-for-line-item"
|
||||||
|
import { AdminGetOrdersOrderReservationsParams } from "./get-reservations"
|
||||||
import { AdminGetOrdersParams } from "./list-orders"
|
import { AdminGetOrdersParams } from "./list-orders"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
@@ -223,6 +233,28 @@ export default (app, featureFlagRouter: FlagRouter) => {
|
|||||||
middlewares.wrap(require("./create-claim-shipment").default)
|
middlewares.wrap(require("./create-claim-shipment").default)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
"/:id/reservations",
|
||||||
|
checkRegisteredModules({
|
||||||
|
inventoryService:
|
||||||
|
"Inventory is not enabled. Please add an Inventory module to enable this functionality.",
|
||||||
|
}),
|
||||||
|
transformQuery(AdminGetOrdersOrderReservationsParams, {
|
||||||
|
isList: true,
|
||||||
|
}),
|
||||||
|
middlewares.wrap(require("./get-reservations").default)
|
||||||
|
)
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
"/:id/line-items/:line_item_id/reserve",
|
||||||
|
checkRegisteredModules({
|
||||||
|
inventoryService:
|
||||||
|
"Inventory is not enabled. Please add an Inventory module to enable this functionality.",
|
||||||
|
}),
|
||||||
|
transformBody(AdminOrdersOrderLineItemReservationReq),
|
||||||
|
middlewares.wrap(require("./create-reservation-for-line-item").default)
|
||||||
|
)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { IsNumber, IsObject, IsOptional, IsString } from "class-validator"
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
import { IInventoryService } from "../../../../interfaces"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [post] /reservations
|
||||||
|
* operationId: "PostReservations"
|
||||||
|
* summary: "Creates a Reservation"
|
||||||
|
* description: "Creates a Reservation which can be associated with any resource as required."
|
||||||
|
* x-authenticated: true
|
||||||
|
* requestBody:
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: "#/components/schemas/AdminPostReservationsReq"
|
||||||
|
* x-codeSamples:
|
||||||
|
* - lang: JavaScript
|
||||||
|
* label: JS Client
|
||||||
|
* source: |
|
||||||
|
* import Medusa from "@medusajs/medusa-js"
|
||||||
|
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||||
|
* // must be previously logged in or use api token
|
||||||
|
* medusa.admin.reservations.create({
|
||||||
|
* })
|
||||||
|
* .then(({ reservations }) => {
|
||||||
|
* console.log(reservations.id);
|
||||||
|
* });
|
||||||
|
* - lang: Shell
|
||||||
|
* label: cURL
|
||||||
|
* source: |
|
||||||
|
* curl --location --request POST 'https://medusa-url.com/admin/reservations' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}' \
|
||||||
|
* --header 'Content-Type: application/json' \
|
||||||
|
* --data-raw '{
|
||||||
|
* "resource_id": "{resource_id}",
|
||||||
|
* "resource_type": "order",
|
||||||
|
* "value": "We delivered this order"
|
||||||
|
* }'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - Reservation
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: "#/components/schemas/AdminPostReservationsReq"
|
||||||
|
* "400":
|
||||||
|
* $ref: "#/components/responses/400_error"
|
||||||
|
* "401":
|
||||||
|
* $ref: "#/components/responses/unauthorized"
|
||||||
|
* "404":
|
||||||
|
* $ref: "#/components/responses/not_found_error"
|
||||||
|
* "409":
|
||||||
|
* $ref: "#/components/responses/invalid_state_error"
|
||||||
|
* "422":
|
||||||
|
* $ref: "#/components/responses/invalid_request_error"
|
||||||
|
* "500":
|
||||||
|
* $ref: "#/components/responses/500_error"
|
||||||
|
*/
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { validatedBody } = req as { validatedBody: AdminPostReservationsReq }
|
||||||
|
|
||||||
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
|
|
||||||
|
const inventoryService: IInventoryService =
|
||||||
|
req.scope.resolve("inventoryService")
|
||||||
|
|
||||||
|
const reservation = await manager.transaction(async (manager) => {
|
||||||
|
return await inventoryService
|
||||||
|
.withTransaction(manager)
|
||||||
|
.createReservationItem(validatedBody)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({ reservation })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @schema AdminPostReservationsReq
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - line_item_id
|
||||||
|
* - location_id
|
||||||
|
* - inventory_item_id
|
||||||
|
* - quantity
|
||||||
|
* properties:
|
||||||
|
* line_item_id:
|
||||||
|
* description: "The id of the location of the reservation"
|
||||||
|
* type: string
|
||||||
|
* location_id:
|
||||||
|
* description: "The id of the location of the reservation"
|
||||||
|
* type: string
|
||||||
|
* inventory_item_id:
|
||||||
|
* description: "The id of the inventory item the reservation relates to"
|
||||||
|
* type: string
|
||||||
|
* quantity:
|
||||||
|
* description: "The id of the reservation item"
|
||||||
|
* type: number
|
||||||
|
* metadata:
|
||||||
|
* description: An optional set of key-value pairs with additional information.
|
||||||
|
* type: object
|
||||||
|
*/
|
||||||
|
export class AdminPostReservationsReq {
|
||||||
|
@IsString()
|
||||||
|
line_item_id?: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
location_id: string
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
inventory_item_id: string
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
quantity: number
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
import { IInventoryService } from "../../../../interfaces"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [delete] /reservations/{id}
|
||||||
|
* operationId: "DeleteReservationsReservation"
|
||||||
|
* summary: "Delete a Reservation"
|
||||||
|
* description: "Deletes a Reservation."
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (path) id=* {string} The ID of the Reservation to delete.
|
||||||
|
* x-codeSamples:
|
||||||
|
* - lang: JavaScript
|
||||||
|
* label: JS Client
|
||||||
|
* source: |
|
||||||
|
* import Medusa from "@medusajs/medusa-js"
|
||||||
|
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||||
|
* // must be previously logged in or use api token
|
||||||
|
* medusa.admin.reservations.delete(reservation.id)
|
||||||
|
* .then(({ id, object, deleted }) => {
|
||||||
|
* console.log(id);
|
||||||
|
* });
|
||||||
|
* - lang: Shell
|
||||||
|
* label: cURL
|
||||||
|
* source: |
|
||||||
|
* curl --location --request DELETE 'https://medusa-url.com/admin/reservations/{id}' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - Reservation
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: string
|
||||||
|
* description: The ID of the deleted Reservation.
|
||||||
|
* object:
|
||||||
|
* type: string
|
||||||
|
* description: The type of the object that was deleted.
|
||||||
|
* default: reservation
|
||||||
|
* deleted:
|
||||||
|
* type: boolean
|
||||||
|
* description: Whether or not the Reservation was deleted.
|
||||||
|
* default: true
|
||||||
|
* "400":
|
||||||
|
* $ref: "#/components/responses/400_error"
|
||||||
|
* "401":
|
||||||
|
* $ref: "#/components/responses/unauthorized"
|
||||||
|
* "404":
|
||||||
|
* $ref: "#/components/responses/not_found_error"
|
||||||
|
* "409":
|
||||||
|
* $ref: "#/components/responses/invalid_state_error"
|
||||||
|
* "422":
|
||||||
|
* $ref: "#/components/responses/invalid_request_error"
|
||||||
|
* "500":
|
||||||
|
* $ref: "#/components/responses/500_error"
|
||||||
|
*/
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const inventoryService: IInventoryService = req.resolve("inventoryService")
|
||||||
|
const manager: EntityManager = req.resolve("manager")
|
||||||
|
|
||||||
|
await manager.transaction(async (manager) => {
|
||||||
|
await inventoryService.withTransaction(manager).deleteReservationItem(id)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id,
|
||||||
|
object: "reservation",
|
||||||
|
deleted: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
import { IInventoryService } from "../../../../interfaces"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [get] /reservations/{id}
|
||||||
|
* operationId: "GetReservationsReservation"
|
||||||
|
* summary: "Get a Reservation"
|
||||||
|
* description: "Retrieves a single reservation using its id"
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (path) id=* {string} The ID of the reservation to retrieve.
|
||||||
|
* x-codeSamples:
|
||||||
|
* - lang: JavaScript
|
||||||
|
* label: JS Client
|
||||||
|
* source: |
|
||||||
|
* import Medusa from "@medusajs/medusa-js"
|
||||||
|
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||||
|
* // must be previously logged in or use api token
|
||||||
|
* medusa.admin.reservations.retrieve(reservation_id)
|
||||||
|
* .then(({ reservation }) => {
|
||||||
|
* console.log(reservation.id);
|
||||||
|
* });
|
||||||
|
* - lang: Shell
|
||||||
|
* label: cURL
|
||||||
|
* source: |
|
||||||
|
* curl --location --request GET 'https://medusa-url.com/admin/reservations/{id}' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - Reservation
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: "#/components/schemas/AdminPostReservationsReq"
|
||||||
|
* "400":
|
||||||
|
* $ref: "#/components/responses/400_error"
|
||||||
|
* "401":
|
||||||
|
* $ref: "#/components/responses/unauthorized"
|
||||||
|
* "404":
|
||||||
|
* $ref: "#/components/responses/not_found_error"
|
||||||
|
* "409":
|
||||||
|
* $ref: "#/components/responses/invalid_state_error"
|
||||||
|
* "422":
|
||||||
|
* $ref: "#/components/responses/invalid_request_error"
|
||||||
|
* "500":
|
||||||
|
* $ref: "#/components/responses/500_error"
|
||||||
|
*/
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const inventoryService: IInventoryService =
|
||||||
|
req.scope.resolve("inventoryService")
|
||||||
|
|
||||||
|
const [reservations, count] = await inventoryService.listReservationItems({
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!count) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Reservation with id ${id} not found`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({ reservation: reservations[0] })
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Router } from "express"
|
||||||
|
import { Note, ReservationItemDTO } from "../../../.."
|
||||||
|
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||||
|
import middlewares, {
|
||||||
|
transformBody,
|
||||||
|
transformQuery,
|
||||||
|
} from "../../../middlewares"
|
||||||
|
import "reflect-metadata"
|
||||||
|
import { AdminPostReservationsReq } from "./create-reservation"
|
||||||
|
import { AdminPostReservationsReservationReq } from "./update-reservation"
|
||||||
|
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
|
||||||
|
import { AdminGetReservationsParams } from "./list-reservations"
|
||||||
|
|
||||||
|
const route = Router()
|
||||||
|
|
||||||
|
export default (app) => {
|
||||||
|
app.use(
|
||||||
|
"/reservations",
|
||||||
|
checkRegisteredModules({
|
||||||
|
inventoryService:
|
||||||
|
"Inventory is not enabled. Please add an Inventory module to enable this functionality.",
|
||||||
|
}),
|
||||||
|
route
|
||||||
|
)
|
||||||
|
|
||||||
|
route.get("/:id", middlewares.wrap(require("./get-reservation").default))
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
"/",
|
||||||
|
transformBody(AdminPostReservationsReq),
|
||||||
|
middlewares.wrap(require("./create-reservation").default)
|
||||||
|
)
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
"/",
|
||||||
|
transformQuery(AdminGetReservationsParams, {
|
||||||
|
defaultFields: defaultReservationFields,
|
||||||
|
defaultRelations: defaultAdminReservationRelations,
|
||||||
|
isList: true,
|
||||||
|
}),
|
||||||
|
middlewares.wrap(require("./list-reservations").default)
|
||||||
|
)
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
"/:id",
|
||||||
|
transformBody(AdminPostReservationsReservationReq),
|
||||||
|
middlewares.wrap(require("./update-reservation").default)
|
||||||
|
)
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
"/:id",
|
||||||
|
middlewares.wrap(require("./delete-reservation").default)
|
||||||
|
)
|
||||||
|
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @schema AdminPostReservationsReq
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - reservation
|
||||||
|
* properties:
|
||||||
|
* reservation:
|
||||||
|
* $ref: "#/components/schemas/ReservationItemDTO"
|
||||||
|
*/
|
||||||
|
export type AdminReservationsRes = {
|
||||||
|
reservation: ReservationItemDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @schema AdminGetReservationReservationsReq
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* reservations:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: "#/components/schemas/ReservationItemDTO"
|
||||||
|
* count:
|
||||||
|
* type: integer
|
||||||
|
* description: The total number of items available
|
||||||
|
* offset:
|
||||||
|
* type: integer
|
||||||
|
* description: The number of items skipped before these items
|
||||||
|
* limit:
|
||||||
|
* type: integer
|
||||||
|
* description: The number of items per page
|
||||||
|
*/
|
||||||
|
export type AdminReservationsListRes = PaginatedResponse & {
|
||||||
|
reservations: ReservationItemDTO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultAdminReservationRelations = []
|
||||||
|
|
||||||
|
export const defaultReservationFields = [
|
||||||
|
"id",
|
||||||
|
"location_id",
|
||||||
|
"inventory_item_id",
|
||||||
|
"quantity",
|
||||||
|
"line_item_id",
|
||||||
|
"metadata",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
export type AdminReservationsDeleteRes = DeleteResponse
|
||||||
|
|
||||||
|
export * from "./create-reservation"
|
||||||
|
export * from "./delete-reservation"
|
||||||
|
export * from "./get-reservation"
|
||||||
|
export * from "./update-reservation"
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { Type } from "class-transformer"
|
||||||
|
import { IsArray, IsOptional, IsString, ValidateNested } from "class-validator"
|
||||||
|
import { Request, Response } from "express"
|
||||||
|
import { IInventoryService } from "../../../../interfaces"
|
||||||
|
import {
|
||||||
|
extendedFindParamsMixin,
|
||||||
|
NumericalComparisonOperator,
|
||||||
|
} from "../../../../types/common"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [get] /reservations
|
||||||
|
* operationId: "GetReservations"
|
||||||
|
* summary: "List Reservations"
|
||||||
|
* description: "Retrieve a list of Reservations."
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: location_id
|
||||||
|
* style: form
|
||||||
|
* explode: false
|
||||||
|
* description: Location ids to search for.
|
||||||
|
* schema:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* - in: query
|
||||||
|
* name: inventory_item_id
|
||||||
|
* style: form
|
||||||
|
* explode: false
|
||||||
|
* description: Inventory Item ids to search for.
|
||||||
|
* schema:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* - in: query
|
||||||
|
* name: line_item_id
|
||||||
|
* style: form
|
||||||
|
* explode: false
|
||||||
|
* description: Line Item ids to search for.
|
||||||
|
* schema:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* - in: query
|
||||||
|
* name: quantity
|
||||||
|
* description: Filter by reservation quantity
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* lt:
|
||||||
|
* type: number
|
||||||
|
* description: filter by reservation quantity less than this number
|
||||||
|
* gt:
|
||||||
|
* type: number
|
||||||
|
* description: filter by reservation quantity greater than this number
|
||||||
|
* lte:
|
||||||
|
* type: number
|
||||||
|
* description: filter by reservation quantity less than or equal to this number
|
||||||
|
* gte:
|
||||||
|
* type: number
|
||||||
|
* description: filter by reservation quantity greater than or equal to this number
|
||||||
|
* - (query) offset=0 {integer} How many Reservations to skip in the result.
|
||||||
|
* - (query) limit=20 {integer} Limit the number of Reservations returned.
|
||||||
|
* - (query) expand {string} (Comma separated) Which fields should be expanded in the product category.
|
||||||
|
* - (query) fields {string} (Comma separated) Which fields should be included in the product category.
|
||||||
|
* x-codeSamples:
|
||||||
|
* - lang: Shell
|
||||||
|
* label: cURL
|
||||||
|
* source: |
|
||||||
|
* curl --location --request GET 'https://medusa-url.com/admin/product-categories' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - Product Category
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: "#/components/schemas/AdminGetReservationReservationsReq"
|
||||||
|
* "400":
|
||||||
|
* $ref: "#/components/responses/400_error"
|
||||||
|
* "401":
|
||||||
|
* $ref: "#/components/responses/unauthorized"
|
||||||
|
* "404":
|
||||||
|
* $ref: "#/components/responses/not_found_error"
|
||||||
|
* "409":
|
||||||
|
* $ref: "#/components/responses/invalid_state_error"
|
||||||
|
* "422":
|
||||||
|
* $ref: "#/components/responses/invalid_request_error"
|
||||||
|
* "500":
|
||||||
|
* $ref: "#/components/responses/500_error"
|
||||||
|
*/
|
||||||
|
export default async (req: Request, res: Response) => {
|
||||||
|
const inventoryService: IInventoryService =
|
||||||
|
req.scope.resolve("inventoryService")
|
||||||
|
|
||||||
|
const [reservations, count] = await inventoryService.listReservationItems(
|
||||||
|
req.filterableFields,
|
||||||
|
req.listConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
const { limit, offset } = req.validatedQuery
|
||||||
|
|
||||||
|
res.json({ reservations, count, limit, offset })
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdminGetReservationsParams extends extendedFindParamsMixin({
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
}) {
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@IsOptional()
|
||||||
|
location_id?: string[]
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@IsOptional()
|
||||||
|
inventory_item_id?: string[]
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
@IsOptional()
|
||||||
|
line_item_id?: string[]
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => NumericalComparisonOperator)
|
||||||
|
quantity?: NumericalComparisonOperator
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { IsNumber, IsObject, IsOptional, IsString } from "class-validator"
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
import { IInventoryService } from "../../../../interfaces"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @oas [post] /reservations/{id}
|
||||||
|
* operationId: "PostReservationsReservation"
|
||||||
|
* summary: "Updates a Reservation"
|
||||||
|
* description: "Updates a Reservation which can be associated with any resource as required."
|
||||||
|
* x-authenticated: true
|
||||||
|
* parameters:
|
||||||
|
* - (path) id=* {string} The ID of the Reservation to update.
|
||||||
|
* requestBody:
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: "#/components/schemas/AdminPostReservationsReservationReq"
|
||||||
|
* x-codeSamples:
|
||||||
|
* - lang: JavaScript
|
||||||
|
* label: JS Client
|
||||||
|
* source: |
|
||||||
|
* import Medusa from "@medusajs/medusa-js"
|
||||||
|
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
|
||||||
|
* // must be previously logged in or use api token
|
||||||
|
* medusa.admin.reservations.update(reservation.id, {
|
||||||
|
* quantity: 3
|
||||||
|
* })
|
||||||
|
* .then(({ reservations }) => {
|
||||||
|
* console.log(reservations.id);
|
||||||
|
* });
|
||||||
|
* - lang: Shell
|
||||||
|
* label: cURL
|
||||||
|
* source: |
|
||||||
|
* curl --location --request POST 'https://medusa-url.com/admin/reservations/{id}' \
|
||||||
|
* --header 'Authorization: Bearer {api_token}' \
|
||||||
|
* --header 'Content-Type: application/json' \
|
||||||
|
* --data-raw '{
|
||||||
|
* "quantity": 3,
|
||||||
|
* }'
|
||||||
|
* security:
|
||||||
|
* - api_token: []
|
||||||
|
* - cookie_auth: []
|
||||||
|
* tags:
|
||||||
|
* - Reservation
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: OK
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: "#/components/schemas/AdminPostReservationsReq"
|
||||||
|
* "400":
|
||||||
|
* $ref: "#/components/responses/400_error"
|
||||||
|
* "401":
|
||||||
|
* $ref: "#/components/responses/unauthorized"
|
||||||
|
* "404":
|
||||||
|
* $ref: "#/components/responses/not_found_error"
|
||||||
|
* "409":
|
||||||
|
* $ref: "#/components/responses/invalid_state_error"
|
||||||
|
* "422":
|
||||||
|
* $ref: "#/components/responses/invalid_request_error"
|
||||||
|
* "500":
|
||||||
|
* $ref: "#/components/responses/500_error"
|
||||||
|
*/
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const { validatedBody } = req as {
|
||||||
|
validatedBody: AdminPostReservationsReservationReq
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
|
|
||||||
|
const inventoryService: IInventoryService =
|
||||||
|
req.scope.resolve("inventoryService")
|
||||||
|
|
||||||
|
const result = await manager.transaction(async (manager) => {
|
||||||
|
await inventoryService
|
||||||
|
.withTransaction(manager)
|
||||||
|
.updateReservationItem(id, validatedBody)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({ reservation: result })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @schema AdminPostReservationsReservationReq
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* location_id:
|
||||||
|
* description: "The id of the location of the reservation"
|
||||||
|
* type: string
|
||||||
|
* quantity:
|
||||||
|
* description: "The id of the reservation item"
|
||||||
|
* type: number
|
||||||
|
* metadata:
|
||||||
|
* description: An optional set of key-value pairs with additional information.
|
||||||
|
* type: object
|
||||||
|
*/
|
||||||
|
export class AdminPostReservationsReservationReq {
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
quantity?: number
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
location_id?: string
|
||||||
|
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
}
|
||||||
@@ -7,7 +7,11 @@ import {
|
|||||||
} from "../interfaces"
|
} from "../interfaces"
|
||||||
import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item"
|
import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item"
|
||||||
import { ProductVariantService, SalesChannelLocationService } from "./"
|
import { ProductVariantService, SalesChannelLocationService } from "./"
|
||||||
import { InventoryItemDTO, ReserveQuantityContext } from "../types/inventory"
|
import {
|
||||||
|
InventoryItemDTO,
|
||||||
|
ReservationItemDTO,
|
||||||
|
ReserveQuantityContext,
|
||||||
|
} from "../types/inventory"
|
||||||
import { LineItem, ProductVariant } from "../models"
|
import { LineItem, ProductVariant } from "../models"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
@@ -331,7 +335,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
variantId: string,
|
variantId: string,
|
||||||
quantity: number,
|
quantity: number,
|
||||||
context: ReserveQuantityContext = {}
|
context: ReserveQuantityContext = {}
|
||||||
): Promise<void> {
|
): Promise<void | ReservationItemDTO[]> {
|
||||||
const manager = this.transactionManager_ || this.manager_
|
const manager = this.transactionManager_ || this.manager_
|
||||||
|
|
||||||
if (!this.inventoryService_) {
|
if (!this.inventoryService_) {
|
||||||
@@ -375,7 +379,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
locationId = locations[0]
|
locationId = locations[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
return await Promise.all(
|
||||||
variantInventory.map(async (inventoryPart) => {
|
variantInventory.map(async (inventoryPart) => {
|
||||||
const itemQuantity = inventoryPart.required_quantity * quantity
|
const itemQuantity = inventoryPart.required_quantity * quantity
|
||||||
return await this.inventoryService_
|
return await this.inventoryService_
|
||||||
@@ -470,7 +474,10 @@ class ProductVariantInventoryService extends TransactionBaseService {
|
|||||||
* @param locationId Location to validate stock at
|
* @param locationId Location to validate stock at
|
||||||
* @returns nothing if successful, throws error if not
|
* @returns nothing if successful, throws error if not
|
||||||
*/
|
*/
|
||||||
async validateInventoryAtLocation(items: LineItem[], locationId: string) {
|
async validateInventoryAtLocation(
|
||||||
|
items: Omit<LineItem, "beforeInsert">[],
|
||||||
|
locationId: string
|
||||||
|
) {
|
||||||
if (!this.inventoryService_) {
|
if (!this.inventoryService_) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,52 @@ export type InventoryItemDTO = {
|
|||||||
deleted_at: string | Date | null
|
deleted_at: string | Date | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @schema ReservationItemDTO
|
||||||
|
* title: "Reservation item"
|
||||||
|
* description: "Represents a reservation of an inventory item at a stock location"
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - id
|
||||||
|
* - location_id
|
||||||
|
* - inventory_item_id
|
||||||
|
* - quantity
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* description: "The id of the reservation item"
|
||||||
|
* type: string
|
||||||
|
* location_id:
|
||||||
|
* description: "The id of the location of the reservation"
|
||||||
|
* type: string
|
||||||
|
* inventory_item_id:
|
||||||
|
* description: "The id of the inventory item the reservation relates to"
|
||||||
|
* type: string
|
||||||
|
* quantity:
|
||||||
|
* description: "The id of the reservation item"
|
||||||
|
* type: number
|
||||||
|
* metadata:
|
||||||
|
* type: object
|
||||||
|
* description: An optional key-value map with additional details
|
||||||
|
* example: {car: "white"}
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* description: "The date with timezone at which the resource was created."
|
||||||
|
* format: date-time
|
||||||
|
* updated_at:
|
||||||
|
* type: string
|
||||||
|
* description: "The date with timezone at which the resource was updated."
|
||||||
|
* format: date-time
|
||||||
|
* deleted_at:
|
||||||
|
* type: string
|
||||||
|
* description: "The date with timezone at which the resource was deleted."
|
||||||
|
* format: date-time
|
||||||
|
*/
|
||||||
export type ReservationItemDTO = {
|
export type ReservationItemDTO = {
|
||||||
id: string
|
id: string
|
||||||
location_id: string
|
location_id: string
|
||||||
inventory_item_id: string
|
inventory_item_id: string
|
||||||
quantity: number
|
quantity: number
|
||||||
|
line_item_id?: string | null
|
||||||
metadata: Record<string, unknown> | null
|
metadata: Record<string, unknown> | null
|
||||||
created_at: string | Date
|
created_at: string | Date
|
||||||
updated_at: string | Date
|
updated_at: string | Date
|
||||||
|
|||||||
Reference in new issue
Block a user