From 2f594291ad8d227b499b80a5bfe66f5963d42d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Tue, 19 Aug 2025 20:48:24 +0200 Subject: [PATCH] fix(core-flows, dashboard, types): improve allocation flows on Admin (#12572) **What** - handle single inventory items with `required_quantity > 1` correctly in the allocate UI flow - display correct availability amount in the create fulullment form for inventory kit items - fix check in the create fulfillment flow that would allow negative reservation because `required_quantity` wasn't included in the check - show the most recent reservations first in the reservations table - display an error in the allocation form if a reservation is not created for some inventory items - display inventory kit in the order summary if the product has multiple same inventory items --- CLOSES SUP-1655 --- .changeset/cool-kiwis-hear.md | 7 ++ .../http/__tests__/order/admin/order.spec.ts | 102 +++++++++++++++++- .../src/i18n/translations/$schema.json | 48 +++++++-- .../dashboard/src/i18n/translations/en.json | 3 +- .../edit-sales-channels-form.tsx | 2 +- .../order-allocate-items-form.tsx | 30 ++++-- .../order-allocate-items-item.tsx | 4 +- .../order-create-fulfillment-form/utils.ts | 28 +++++ .../order-create-fulfillment-form.tsx | 10 +- .../order-create-fulfillment-item.tsx | 87 ++++++++++++--- .../order-summary-section.tsx | 4 +- .../use-reservation-table-query.tsx | 3 +- .../src/order/workflows/create-fulfillment.ts | 14 +-- .../types/src/http/product/admin/entitites.ts | 4 + .../src/http/reservation/admin/queries.ts | 4 + 15 files changed, 295 insertions(+), 55 deletions(-) create mode 100644 .changeset/cool-kiwis-hear.md create mode 100644 packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/utils.ts diff --git a/.changeset/cool-kiwis-hear.md b/.changeset/cool-kiwis-hear.md new file mode 100644 index 0000000000..868be5f141 --- /dev/null +++ b/.changeset/cool-kiwis-hear.md @@ -0,0 +1,7 @@ +--- +"@medusajs/dashboard": patch +"@medusajs/core-flows": patch +"@medusajs/types": patch +--- + +fix(core-flows, dashboard, types): improve allocation flows in Admin diff --git a/integration-tests/http/__tests__/order/admin/order.spec.ts b/integration-tests/http/__tests__/order/admin/order.spec.ts index 62bca9e6a2..e06efb432b 100644 --- a/integration-tests/http/__tests__/order/admin/order.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order.spec.ts @@ -13,7 +13,12 @@ jest.setTimeout(300000) medusaIntegrationTestRunner({ testSuite: ({ dbConnection, getContainer, api }) => { - let order, seeder, inventoryItemOverride3, productOverride3, shippingProfile + let order, + seeder, + inventoryItemOverride3, + productOverride3, + shippingProfile, + productOverride4 beforeEach(async () => { const container = getContainer() @@ -836,6 +841,56 @@ medusaIntegrationTestRunner({ ) ).data.product + const inventoryItemOverride4 = ( + await api.post( + `/admin/inventory-items`, + { sku: "test-variant-4-no-shipping", requires_shipping: false }, + adminHeaders + ) + ).data.inventory_item + + await api.post( + `/admin/inventory-items/${inventoryItemOverride4.id}/location-levels`, + { + location_id: stockChannelOverride.id, + stocked_quantity: 10, + }, + adminHeaders + ) + + productOverride4 = ( + await api.post( + "/admin/products", + { + title: `Test override 4`, + shipping_profile_id: shippingProfile.id, + options: [{ title: "size", values: ["large"] }], + variants: [ + { + title: "Test variant 4", + sku: "test-variant-4-override", + inventory_items: [ + { + inventory_item_id: inventoryItemOverride4.id, + required_quantity: 3, + }, + ], + prices: [ + { + currency_code: "usd", + amount: 100, + }, + ], + options: { + size: "large", + }, + }, + ], + }, + adminHeaders + ) + ).data.product + shippingProfileOverride = ( await api.post( `/admin/shipping-profiles`, @@ -889,6 +944,7 @@ medusaIntegrationTestRunner({ additionalProducts: [ { variant_id: productOverride2.variants[0].id, quantity: 1 }, { variant_id: productOverride3.variants[0].id, quantity: 3 }, + { variant_id: productOverride4.variants[0].id, quantity: 1 }, { variant_id: productOverride4WithOverrideShippingProfile.variants[0].id, @@ -1024,6 +1080,50 @@ medusaIntegrationTestRunner({ ) }) + it("should throw if trying to fulfillment more items than it is reserved when item has required quantity", async () => { + const orderItemId = order.items.find( + (i) => i.variant_id === productOverride4.variants[0].id + ).id + + let reservation = ( + await api.get( + `/admin/reservations?line_item_id=${orderItemId}`, + adminHeaders + ) + ).data.reservations[0] + + expect(reservation.quantity).toBe(3) // one item with required quantity 3 + + reservation = ( + await api.post( + `/admin/reservations/${reservation.id}`, + { + quantity: 2, + }, + adminHeaders + ) + ).data.reservation + + expect(reservation.quantity).toBe(2) + + const res = await api + .post( + `/admin/orders/${order.id}/fulfillments`, + { + shipping_option_id: seeder.shippingOption.id, + location_id: seeder.stockLocation.id, + items: [{ id: orderItemId, quantity: 1 }], // fulfill 1 orer item which requires 3 inventor items + }, + adminHeaders + ) + .catch((e) => e) + + expect(res.response.status).toBe(400) + expect(res.response.data.message).toBe( + `Quantity to fulfill exceeds the reserved quantity for the item: ${orderItemId}` + ) + }) + it("should throw if shipping profile of the product doesn't match the shipping profile of the shipping option", async () => { const orderItemId = order.items.find( (i) => diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index 4884a90e85..3a1258bc7e 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -3816,16 +3816,31 @@ "credit": { "type": "string" }, + "creditDescription": { + "type": "string" + }, "debit": { "type": "string" }, "debitDescription": { "type": "string" - }, - "creditDescription": { - "type": "string" } - } + }, + "required": [ + "title", + "total", + "creditOrDebit", + "createCreditLine", + "createCreditLineSuccess", + "createCreditLineError", + "createCreditLineDescription", + "operation", + "credit", + "creditDescription", + "debit", + "debitDescription" + ], + "additionalProperties": false }, "balanceSettlement": { "type": "object", @@ -3851,9 +3866,18 @@ "creditLineDescription": { "type": "string" } - } + }, + "required": [ + "paymentMethod", + "paymentMethodDescription", + "creditLine", + "creditLineDescription" + ], + "additionalProperties": false } - } + }, + "required": ["title", "settlementType", "settlementTypes"], + "additionalProperties": false }, "domain": { "type": "string" @@ -4876,9 +4900,12 @@ "properties": { "created": { "type": "string" + }, + "error": { + "type": "string" } }, - "required": ["created"], + "required": ["created", "error"], "additionalProperties": false }, "error": { @@ -5539,6 +5566,9 @@ } }, "required": [ + "giftCardsStoreCreditLines", + "creditLines", + "balanceSettlement", "domain", "claim", "exchange", @@ -11229,6 +11259,8 @@ }, "required": [ "amount", + "reference", + "reference_id", "refundAmount", "name", "default", @@ -11315,6 +11347,7 @@ "account", "total", "paidTotal", + "creditTotal", "totalExclTax", "subtotal", "shipping", @@ -11497,6 +11530,7 @@ "invite", "resetPassword", "workflowExecutions", + "shippingOptionTypes", "productTypes", "productTags", "notifications", diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 605f78194e..50a449b433 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -1305,7 +1305,8 @@ "consistsOf": "Consists of {{num}}x inventory items", "requires": "Requires {{num}} per variant", "toast": { - "created": "Items successfully allocated" + "created": "Items successfully allocated", + "error": "Failed to allocate following items: {{items}}" }, "error": { "quantityNotAllocated": "There are unallocated items." diff --git a/packages/admin/dashboard/src/routes/locations/location-sales-channels/components/edit-sales-channels-form/edit-sales-channels-form.tsx b/packages/admin/dashboard/src/routes/locations/location-sales-channels/components/edit-sales-channels-form/edit-sales-channels-form.tsx index 6dfad6d1ce..5e998ee13c 100644 --- a/packages/admin/dashboard/src/routes/locations/location-sales-channels/components/edit-sales-channels-form/edit-sales-channels-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-sales-channels/components/edit-sales-channels-form/edit-sales-channels-form.tsx @@ -31,7 +31,7 @@ const EditSalesChannelsSchema = zod.object({ sales_channels: zod.array(zod.string()).optional(), }) -const PAGE_SIZE = 50 +const PAGE_SIZE = 20 const PREFIX = "sc" export const LocationEditSalesChannelsForm = ({ diff --git a/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/order-allocate-items-form.tsx b/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/order-allocate-items-form.tsx index b07023a711..9e1b3e6276 100644 --- a/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/order-allocate-items-form.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/order-allocate-items-form.tsx @@ -19,6 +19,7 @@ import { useStockLocations } from "../../../../../hooks/api/stock-locations" import { queryClient } from "../../../../../lib/query-client" import { AllocateItemsSchema } from "./constants" import { OrderAllocateItemsItem } from "./order-allocate-items-item" +import { checkInventoryKit } from "./utils" type OrderAllocateItemsFormProps = { order: AdminOrder @@ -84,16 +85,18 @@ export function OrderAllocateItemsForm({ order }: OrderAllocateItemsFormProps) { const promises = payload.map(([itemId, inventoryId, quantity]) => allocateItems({ location_id: data.location_id, - inventory_item_id: inventoryId, - line_item_id: itemId, - quantity, + inventory_item_id: inventoryId as string, + line_item_id: itemId as string, + quantity: Number(quantity), }) + .then(() => ({ success: true, inventory_item_id: inventoryId })) + .catch(() => ({ success: false, inventory_item_id: inventoryId })) ) /** * TODO: we should have bulk endpoint for this so this is executed in a workflow and can be reverted */ - await Promise.all(promises) + const results = await Promise.all(promises) // invalidate order details so we get new item.variant.inventory items await queryClient.invalidateQueries({ @@ -102,10 +105,19 @@ export function OrderAllocateItemsForm({ order }: OrderAllocateItemsFormProps) { handleSuccess(`/orders/${order.id}`) - toast.success(t("general.success"), { - description: t("orders.allocateItems.toast.created"), - dismissLabel: t("actions.close"), - }) + if (results.some((r) => !r.success)) { + const failedItems = results + .filter((r) => !r.success) + .map((r) => r.inventory_item_id) + .join(", ") + + toast.error(t("general.error"), { + description: t("orders.allocateItems.toast.error", { + items: failedItems, + }), + dismissLabel: t("actions.close"), + }) + } } catch (e) { toast.error(t("general.error"), { description: e.message, @@ -313,7 +325,7 @@ function defaultAllocations(items: OrderLineItemDTO) { const ret = {} items.forEach((item) => { - const hasInventoryKit = item.variant?.inventory_items.length > 1 + const hasInventoryKit = checkInventoryKit(item) ret[ hasInventoryKit diff --git a/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/order-allocate-items-item.tsx b/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/order-allocate-items-item.tsx index 187f5655f9..d4d4791d3e 100644 --- a/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/order-allocate-items-item.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/order-allocate-items-item.tsx @@ -14,6 +14,7 @@ import { Thumbnail } from "../../../../../components/common/thumbnail" import { getFulfillableQuantity } from "../../../../../lib/order-item" import { Form } from "../../../../../components/common/form" import { AllocateItemsSchema } from "./constants" +import { checkInventoryKit } from "./utils" type OrderEditItemProps = { item: OrderLineItemDTO @@ -46,8 +47,7 @@ export function OrderAllocateItemsItem({ name: "quantity", }) - const hasInventoryKit = - !!variant?.inventory_items.length && variant?.inventory_items.length > 1 + const hasInventoryKit = checkInventoryKit(item) const { availableQuantity, inStockQuantity } = useMemo(() => { if (!variant || !locationId) { diff --git a/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/utils.ts b/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/utils.ts new file mode 100644 index 0000000000..8423e1518b --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-allocate-items/components/order-create-fulfillment-form/utils.ts @@ -0,0 +1,28 @@ +import { + AdminProductVariant, + AdminProductVariantInventoryItemLink, + OrderLineItemDTO, +} from "@medusajs/types" + +/** + * Check if the line item has inventory kit. + */ +export function checkInventoryKit( + item: OrderLineItemDTO & { + variant?: AdminProductVariant & { + inventory_items: AdminProductVariantInventoryItemLink[] + } + } +) { + const variant = item.variant + + if (!variant) { + return false + } + + return ( + (!!variant.inventory_items.length && variant.inventory_items.length > 1) || + (variant.inventory_items.length === 1 && + variant.inventory_items[0].required_quantity! > 1) + ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx b/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx index 609c4ae546..0999864ab3 100644 --- a/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-form.tsx @@ -57,12 +57,6 @@ export function OrderCreateFulfillmentForm({ })), }) - const itemReservedQuantitiesMap = useMemo( - () => - new Map((reservations || []).map((r) => [r.line_item_id, r.quantity])), - [reservations] - ) - const [fulfillableItems, setFulfillableItems] = useState(() => (order.items || []).filter( (item) => @@ -364,9 +358,7 @@ export function OrderCreateFulfillmentForm({ disabled={ requiresShipping && !isShippingProfileMatching } - itemReservedQuantitiesMap={ - itemReservedQuantitiesMap - } + reservations={reservations} /> ) })} diff --git a/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-item.tsx b/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-item.tsx index a3d335519f..f7b0ebadba 100644 --- a/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-item.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-create-fulfillment/components/order-create-fulfillment-form/order-create-fulfillment-item.tsx @@ -17,7 +17,7 @@ type OrderEditItemProps = { currencyCode: string locationId?: string onItemRemove: (itemId: string) => void - itemReservedQuantitiesMap: Map + reservations: HttpTypes.AdminReservation[] form: UseFormReturn> disabled: boolean } @@ -26,7 +26,7 @@ export function OrderCreateFulfillmentItem({ item, form, locationId, - itemReservedQuantitiesMap, + reservations, disabled, }: OrderEditItemProps) { const { t } = useTranslation() @@ -35,7 +35,7 @@ export function OrderCreateFulfillmentItem({ item.product_id, item.variant_id, { - fields: "*inventory,*inventory.location_levels", + fields: "*inventory,*inventory.location_levels,*inventory_items", }, { enabled: !!item.variant, @@ -43,28 +43,83 @@ export function OrderCreateFulfillmentItem({ ) const { availableQuantity, inStockQuantity } = useMemo(() => { - if (!variant || !locationId) { + if ( + !variant?.inventory_items?.length || + !variant?.inventory?.length || + !locationId + ) { return {} } - const { inventory } = variant + const { inventory, inventory_items } = variant - const locationInventory = inventory[0]?.location_levels?.find( - (inv) => inv.location_id === locationId + const locationHasEveryInventoryItem = inventory.every((i) => + i.location_levels?.find((inv) => inv.location_id === locationId) ) - if (!locationInventory) { + if (!locationHasEveryInventoryItem) { return {} } - const reservedQuantityForItem = itemReservedQuantitiesMap.get(item.id) ?? 0 + const inventoryItemRequiredQuantityMap = new Map( + inventory_items.map((i) => [i.inventory_item_id, i.required_quantity]) + ) + + // since we don't allow split fulifllments only one reservation from inventory kit is enough to calculate avalabel product quantity + const reservation = reservations?.find((r) => r.line_item_id === item.id) + const iitemRequiredQuantity = inventory_items.find( + (i) => i.inventory_item_id === reservation?.inventory_item_id + )?.required_quantity + + const reservedQuantityForItem = !reservation + ? 0 + : reservation?.quantity / (iitemRequiredQuantity || 1) + + const locationInventoryLevels = inventory.map((i) => { + const level = i.location_levels?.find( + (inv) => inv.location_id === locationId + ) + + const requiredQuantity = inventoryItemRequiredQuantityMap.get(i.id) + + if (!level || !requiredQuantity) { + return { + availableQuantity: Number.MAX_SAFE_INTEGER, + stockedQuantity: Number.MAX_SAFE_INTEGER, + } + } + + const availableQuantity = level.available_quantity / requiredQuantity + const stockedQuantity = level.stocked_quantity / requiredQuantity + + return { + availableQuantity, + stockedQuantity, + } + }) + + const maxAvailableQuantity = Math.min( + ...locationInventoryLevels.map((i) => i.availableQuantity) + ) + + const maxStockedQuantity = Math.min( + ...locationInventoryLevels.map((i) => i.stockedQuantity) + ) + + if ( + maxAvailableQuantity === Number.MAX_SAFE_INTEGER || + maxStockedQuantity === Number.MAX_SAFE_INTEGER + ) { + return {} + } return { - availableQuantity: - locationInventory.available_quantity + reservedQuantityForItem, - inStockQuantity: locationInventory.stocked_quantity, + availableQuantity: Math.floor( + maxAvailableQuantity + reservedQuantityForItem + ), + inStockQuantity: Math.floor(maxStockedQuantity), } - }, [variant, locationId, itemReservedQuantitiesMap]) + }, [variant, locationId, reservations]) const minValue = 0 const maxValue = Math.min( @@ -76,7 +131,7 @@ export function OrderCreateFulfillmentItem({
{disabled && ( -
+
diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx index 90d0c7d01f..e77ffdfc69 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx @@ -397,7 +397,9 @@ const Item = ({ const isInventoryManaged = item.variant?.manage_inventory const hasInventoryKit = - isInventoryManaged && (item.variant?.inventory_items?.length || 0) > 1 + isInventoryManaged && + ((item.variant?.inventory_items?.length || 0) > 1 || + item.variant?.inventory_items?.some((i) => i.required_quantity > 1)) const hasUnfulfilledItems = item.quantity - item.detail.fulfilled_quantity > 0 return ( diff --git a/packages/admin/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-query.tsx b/packages/admin/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-query.tsx index 5fc93f485e..b184dbfecc 100644 --- a/packages/admin/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-query.tsx +++ b/packages/admin/dashboard/src/routes/reservations/reservation-list/components/reservation-list-table/use-reservation-table-query.tsx @@ -13,7 +13,7 @@ export const useReservationTableQuery = ({ prefix ) - const { location_id, created_at, updated_at, quantity, offset, ...rest } = raw + const { location_id, created_at, updated_at, order, offset, ...rest } = raw const searchParams: HttpTypes.AdminGetReservationsParams = { limit: pageSize, @@ -21,6 +21,7 @@ export const useReservationTableQuery = ({ location_id: location_id, created_at: created_at ? JSON.parse(created_at) : undefined, updated_at: updated_at ? JSON.parse(updated_at) : undefined, + order: order ?? "-created_at", ...rest, } diff --git a/packages/core/core-flows/src/order/workflows/create-fulfillment.ts b/packages/core/core-flows/src/order/workflows/create-fulfillment.ts index 8e958886ca..13c571cb42 100644 --- a/packages/core/core-flows/src/order/workflows/create-fulfillment.ts +++ b/packages/core/core-flows/src/order/workflows/create-fulfillment.ts @@ -306,13 +306,6 @@ function prepareInventoryUpdate({ const inputQuantity = inputItemsMap[item.id]?.quantity ?? item.quantity reservations.forEach((reservation) => { - if (MathBN.gt(inputQuantity, reservation.quantity)) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Quantity to fulfill exceeds the reserved quantity for the item: ${item.id}` - ) - } - const iItem = orderItem?.variant?.inventory_items.find( (ii) => ii.inventory.id === reservation.inventory_item_id ) @@ -327,6 +320,13 @@ function prepareInventoryUpdate({ adjustemntQuantity ) + if (MathBN.lt(remainingReservationQuantity, 0)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Quantity to fulfill exceeds the reserved quantity for the item: ${item.id}` + ) + } + inventoryAdjustment.push({ inventory_item_id: reservation.inventory_item_id, location_id: input.location_id ?? reservation.location_id, diff --git a/packages/core/types/src/http/product/admin/entitites.ts b/packages/core/types/src/http/product/admin/entitites.ts index e578a23346..a7c4c5e8a3 100644 --- a/packages/core/types/src/http/product/admin/entitites.ts +++ b/packages/core/types/src/http/product/admin/entitites.ts @@ -36,6 +36,10 @@ export interface AdminProductVariantInventoryItemLink { * The inventory item that is linked to the variant. */ inventory?: AdminInventoryItem + /** + * The quantity of the inventory item that is required to fulfill the variant. + */ + required_quantity?: number } export interface AdminProductVariant extends BaseProductVariant { diff --git a/packages/core/types/src/http/reservation/admin/queries.ts b/packages/core/types/src/http/reservation/admin/queries.ts index f4bf1748ea..0fc5621247 100644 --- a/packages/core/types/src/http/reservation/admin/queries.ts +++ b/packages/core/types/src/http/reservation/admin/queries.ts @@ -25,6 +25,10 @@ export interface AdminGetReservationsParams { * reservations for. */ line_item_id?: string | string[] + /** + * Sort the reservations by the given field. + */ + order_id?: string | string[] /** * Filter by the ID(s) of the user(s) to retrieve the * reservations they created.