From 9c4647383ebf0a183ccc566636bcf7af06409060 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Wed, 18 Jan 2023 17:39:33 +0100 Subject: [PATCH] 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 --- .changeset/shy-boxes-clean.md | 5 + .../create-reservation-for-line-item.ts | 128 +++++++++++++++++ .../routes/admin/orders/get-reservations.ts | 84 +++++++++++ .../src/api/routes/admin/orders/index.ts | 36 ++++- .../admin/reservations/create-reservation.ts | 122 ++++++++++++++++ .../admin/reservations/delete-reservation.ts | 79 +++++++++++ .../admin/reservations/get-reservation.ts | 70 +++++++++ .../api/routes/admin/reservations/index.ts | 111 +++++++++++++++ .../admin/reservations/list-reservations.ts | 134 ++++++++++++++++++ .../admin/reservations/update-reservation.ts | 111 +++++++++++++++ .../src/services/product-variant-inventory.ts | 15 +- packages/medusa/src/types/inventory.ts | 41 ++++++ 12 files changed, 930 insertions(+), 6 deletions(-) create mode 100644 .changeset/shy-boxes-clean.md create mode 100644 packages/medusa/src/api/routes/admin/orders/create-reservation-for-line-item.ts create mode 100644 packages/medusa/src/api/routes/admin/orders/get-reservations.ts create mode 100644 packages/medusa/src/api/routes/admin/reservations/create-reservation.ts create mode 100644 packages/medusa/src/api/routes/admin/reservations/delete-reservation.ts create mode 100644 packages/medusa/src/api/routes/admin/reservations/get-reservation.ts create mode 100644 packages/medusa/src/api/routes/admin/reservations/index.ts create mode 100644 packages/medusa/src/api/routes/admin/reservations/list-reservations.ts create mode 100644 packages/medusa/src/api/routes/admin/reservations/update-reservation.ts diff --git a/.changeset/shy-boxes-clean.md b/.changeset/shy-boxes-clean.md new file mode 100644 index 0000000000..cedf211491 --- /dev/null +++ b/.changeset/shy-boxes-clean.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +add reservation endpoints diff --git a/packages/medusa/src/api/routes/admin/orders/create-reservation-for-line-item.ts b/packages/medusa/src/api/routes/admin/orders/create-reservation-for-line-item.ts new file mode 100644 index 0000000000..12460ea0dc --- /dev/null +++ b/packages/medusa/src/api/routes/admin/orders/create-reservation-for-line-item.ts @@ -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 +} diff --git a/packages/medusa/src/api/routes/admin/orders/get-reservations.ts b/packages/medusa/src/api/routes/admin/orders/get-reservations.ts new file mode 100644 index 0000000000..352cee1724 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/orders/get-reservations.ts @@ -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, + } +) {} diff --git a/packages/medusa/src/api/routes/admin/orders/index.ts b/packages/medusa/src/api/routes/admin/orders/index.ts index 6f2202ac77..bd85cb273a 100644 --- a/packages/medusa/src/api/routes/admin/orders/index.ts +++ b/packages/medusa/src/api/routes/admin/orders/index.ts @@ -1,9 +1,19 @@ import { Router } from "express" import "reflect-metadata" import { Order } from "../../../.." -import { FindParams, PaginatedResponse } from "../../../../types/common" +import { + DeleteResponse, + FindParams, + PaginatedResponse, +} from "../../../../types/common" 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" const route = Router() @@ -223,6 +233,28 @@ export default (app, featureFlagRouter: FlagRouter) => { 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 } diff --git a/packages/medusa/src/api/routes/admin/reservations/create-reservation.ts b/packages/medusa/src/api/routes/admin/reservations/create-reservation.ts new file mode 100644 index 0000000000..eace5a6339 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/reservations/create-reservation.ts @@ -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 +} diff --git a/packages/medusa/src/api/routes/admin/reservations/delete-reservation.ts b/packages/medusa/src/api/routes/admin/reservations/delete-reservation.ts new file mode 100644 index 0000000000..353ab99177 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/reservations/delete-reservation.ts @@ -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, + }) +} diff --git a/packages/medusa/src/api/routes/admin/reservations/get-reservation.ts b/packages/medusa/src/api/routes/admin/reservations/get-reservation.ts new file mode 100644 index 0000000000..3376a65c54 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/reservations/get-reservation.ts @@ -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] }) +} diff --git a/packages/medusa/src/api/routes/admin/reservations/index.ts b/packages/medusa/src/api/routes/admin/reservations/index.ts new file mode 100644 index 0000000000..e79e95148a --- /dev/null +++ b/packages/medusa/src/api/routes/admin/reservations/index.ts @@ -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" diff --git a/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts b/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts new file mode 100644 index 0000000000..48e69043a0 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/reservations/list-reservations.ts @@ -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 +} diff --git a/packages/medusa/src/api/routes/admin/reservations/update-reservation.ts b/packages/medusa/src/api/routes/admin/reservations/update-reservation.ts new file mode 100644 index 0000000000..b845ff450d --- /dev/null +++ b/packages/medusa/src/api/routes/admin/reservations/update-reservation.ts @@ -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 +} diff --git a/packages/medusa/src/services/product-variant-inventory.ts b/packages/medusa/src/services/product-variant-inventory.ts index 8e5da9d4c3..33c22088e0 100644 --- a/packages/medusa/src/services/product-variant-inventory.ts +++ b/packages/medusa/src/services/product-variant-inventory.ts @@ -7,7 +7,11 @@ import { } from "../interfaces" import { ProductVariantInventoryItem } from "../models/product-variant-inventory-item" import { ProductVariantService, SalesChannelLocationService } from "./" -import { InventoryItemDTO, ReserveQuantityContext } from "../types/inventory" +import { + InventoryItemDTO, + ReservationItemDTO, + ReserveQuantityContext, +} from "../types/inventory" import { LineItem, ProductVariant } from "../models" type InjectedDependencies = { @@ -331,7 +335,7 @@ class ProductVariantInventoryService extends TransactionBaseService { variantId: string, quantity: number, context: ReserveQuantityContext = {} - ): Promise { + ): Promise { const manager = this.transactionManager_ || this.manager_ if (!this.inventoryService_) { @@ -375,7 +379,7 @@ class ProductVariantInventoryService extends TransactionBaseService { locationId = locations[0] } - await Promise.all( + return await Promise.all( variantInventory.map(async (inventoryPart) => { const itemQuantity = inventoryPart.required_quantity * quantity return await this.inventoryService_ @@ -470,7 +474,10 @@ class ProductVariantInventoryService extends TransactionBaseService { * @param locationId Location to validate stock at * @returns nothing if successful, throws error if not */ - async validateInventoryAtLocation(items: LineItem[], locationId: string) { + async validateInventoryAtLocation( + items: Omit[], + locationId: string + ) { if (!this.inventoryService_) { return } diff --git a/packages/medusa/src/types/inventory.ts b/packages/medusa/src/types/inventory.ts index 80950ce636..78c97938c2 100644 --- a/packages/medusa/src/types/inventory.ts +++ b/packages/medusa/src/types/inventory.ts @@ -18,11 +18,52 @@ export type InventoryItemDTO = { 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 = { id: string location_id: string inventory_item_id: string quantity: number + line_item_id?: string | null metadata: Record | null created_at: string | Date updated_at: string | Date