feat(medusa-js, medusa-react, medusa): Prepare API for admin implementations (#3110)

********What********
Add `joinSalesChannels util to stock locations

Add the following endpoints to medusa-react
- inventory items
    - mutations
        - update
        - delete
        - update location level
        - delete location level
        - create location level
    - queries
        - list inventory items
        - get inventory item
        - list location levels
- Stock locations
    - mutations
        - create stock location
        - update stock location
        - delete stock location
    - queries
        - list stock locations
        - get stock locatoin
- Variants
    - queries
        - get inventory
- Reservations
    - mutations
        - create reservation
        - update reservation
        - delete reservation
    - queries
        - list reservations
        - get reservation
- sales channels 
  - mutations
    - associate location with sc
    - remove location association

**Why**
- Update clients to reflect new api endpoints in the core with inventory modules

Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com>
This commit is contained in:
Rares Stefan
2023-02-16 08:49:48 +00:00
committed by GitHub
parent 121b42acfe
commit 12d304307a
48 changed files with 1756 additions and 57 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/medusa": patch
"@medusajs/medusa-js": patch
"medusa-react": patch
---
feat(medusa,medusa-js,medusa-react): Add inventory module endpoints

View File

@@ -68,7 +68,7 @@ describe("Sales channels", () => {
)
expect(
await salesChannelLocationService.listLocations(sc.id)
await salesChannelLocationService.listLocationIds(sc.id)
).toHaveLength(2)
await api.delete(`/admin/sales-channels/${sc.id}`, {
@@ -78,7 +78,7 @@ describe("Sales channels", () => {
await expect(salesChannelService.retrieve(sc.id)).rejects.toThrowError()
await expect(
salesChannelLocationService.listLocations(sc.id)
salesChannelLocationService.listLocationIds(sc.id)
).rejects.toThrowError()
})
})

View File

@@ -70,11 +70,11 @@ describe("Sales channels", () => {
)
expect(
await salesChannelLocationService.listLocations(saleChannel.id)
await salesChannelLocationService.listLocationIds(saleChannel.id)
).toHaveLength(1)
expect(
await salesChannelLocationService.listLocations(otherChannel.id)
await salesChannelLocationService.listLocationIds(otherChannel.id)
).toHaveLength(1)
await api.delete(`/admin/stock-locations/${loc.id}`, {
@@ -82,11 +82,11 @@ describe("Sales channels", () => {
})
expect(
await salesChannelLocationService.listLocations(saleChannel.id)
await salesChannelLocationService.listLocationIds(saleChannel.id)
).toHaveLength(0)
expect(
await salesChannelLocationService.listLocations(otherChannel.id)
await salesChannelLocationService.listLocationIds(otherChannel.id)
).toHaveLength(0)
})
})

View File

@@ -60,7 +60,7 @@ describe("Sales channels", () => {
await salesChannelLocationService.associateLocation(sc.id, loc2.id)
expect(
await salesChannelLocationService.listLocations(sc.id)
await salesChannelLocationService.listLocationIds(sc.id)
).toHaveLength(2)
const [channels] = await salesChannelService.listAndCount(

View File

@@ -33,6 +33,8 @@ import AdminUsersResource from "./users"
import AdminVariantsResource from "./variants"
import AdminPaymentCollectionsResource from "./payment-collections"
import AdminPaymentsResource from "./payments"
import AdminInventoryItemsResource from "./inventory-item"
import AdminReservationsResource from "./reservations"
import AdminProductCategoriesResource from "./product-categories"
class Admin extends BaseResource {
@@ -46,6 +48,7 @@ class Admin extends BaseResource {
public draftOrders = new AdminDraftOrdersResource(this.client)
public giftCards = new AdminGiftCardsResource(this.client)
public invites = new AdminInvitesResource(this.client)
public inventoryItems = new AdminInventoryItemsResource(this.client)
public notes = new AdminNotesResource(this.client)
public priceLists = new AdminPriceListResource(this.client)
public products = new AdminProductsResource(this.client)
@@ -65,6 +68,7 @@ class Admin extends BaseResource {
public store = new AdminStoresResource(this.client)
public shippingOptions = new AdminShippingOptionsResource(this.client)
public regions = new AdminRegionsResource(this.client)
public reservations = new AdminReservationsResource(this.client)
public notifications = new AdminNotificationsResource(this.client)
public taxRates = new AdminTaxRatesResource(this.client)
public uploads = new AdminUploadsResource(this.client)

View File

@@ -8,6 +8,7 @@ import {
AdminGetInventoryItemsItemParams,
AdminInventoryItemsListWithVariantsAndLocationLevelsRes,
AdminInventoryItemsLocationLevelsRes,
AdminPostInventoryItemsItemLocationLevelsReq,
} from "@medusajs/medusa"
import { ResponsePromise } from "../../typings"
import BaseResource from "../base"
@@ -119,6 +120,29 @@ class AdminInventoryItemsResource extends BaseResource {
return this.client.request("POST", path, payload, {}, customHeaders)
}
/**
* Create stock for an Inventory Item at a Stock Location
* @experimental This feature is under development and may change in the future.
* To use this feature please install @medusajs/inventory
* @description creates stock levle for an Inventory Item
* @returns the Inventory Item
*/
createLocationLevel(
inventoryItemId: string,
payload: AdminPostInventoryItemsItemLocationLevelsReq,
query?: AdminGetInventoryItemsParams,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminInventoryItemsRes> {
let path = `/admin/inventory-items/${inventoryItemId}/location-levels`
if (query) {
const queryString = qs.stringify(query)
path += `?${queryString}`
}
return this.client.request("POST", path, payload, {}, customHeaders)
}
/**
* Removes an Inventory Item from a Stock Location. This erases trace of any quantity currently at the location.
* @experimental This feature is under development and may change in the future.

View File

@@ -0,0 +1,97 @@
import {
AdminPostReservationsReq,
AdminPostReservationsReservationReq,
AdminReservationsDeleteRes,
AdminReservationsRes,
AdminGetReservationsParams,
AdminReservationsListRes,
} from "@medusajs/medusa"
import qs from "qs"
import { ResponsePromise } from "../../typings"
import BaseResource from "../base"
class AdminReservationsResource extends BaseResource {
/**
* Get a reservation
* @experimental This feature is under development and may change in the future.
* To use this feature please install @medusajs/inventory
* @description gets a reservation
* @returns The reservation with the provided id
*/
retrieve(
id: string,
customHeaders: Record<string, unknown> = {}
): ResponsePromise<AdminReservationsRes> {
const path = `/admin/reservations/${id}`
return this.client.request("GET", path, undefined, {}, customHeaders)
}
/**
* List reservations
* @experimental This feature is under development and may change in the future.
* To use this feature please install @medusajs/inventory
* @description Lists reservations
* @returns A list of reservations matching the provided query
*/
list(
query?: AdminGetReservationsParams,
customHeaders: Record<string, unknown> = {}
): ResponsePromise<AdminReservationsListRes> {
let path = `/admin/reservations`
if (query) {
const queryString = qs.stringify(query)
path += `?${queryString}`
}
return this.client.request("GET", path, undefined, {}, customHeaders)
}
/**
* create a reservation
* @description create a reservation
* @experimental This feature is under development and may change in the future.
* To use this feature please install @medusajs/inventory
* @returns the created reservation
*/
create(
payload: AdminPostReservationsReq,
customHeaders: Record<string, unknown> = {}
): ResponsePromise<AdminReservationsRes> {
const path = `/admin/reservations`
return this.client.request("POST", path, payload, {}, customHeaders)
}
/**
* update a reservation
* @description update a reservation
* @experimental This feature is under development and may change in the future.
* To use this feature please install @medusajs/inventory
* @returns The updated reservation
*/
update(
id: string,
payload: AdminPostReservationsReservationReq,
customHeaders: Record<string, unknown> = {}
): ResponsePromise<AdminReservationsRes> {
const path = `/admin/reservations/${id}`
return this.client.request("POST", path, payload, {}, customHeaders)
}
/**
* remove a reservation
* @description remove a reservation
* @experimental This feature is under development and may change in the future.
* To use this feature please install @medusajs/inventory
* @returns reservation removal confirmation
*/
delete(
id: string,
customHeaders: Record<string, unknown> = {}
): ResponsePromise<AdminReservationsDeleteRes> {
const path = `/admin/reservations/${id}`
return this.client.request("DELETE", path, undefined, {}, customHeaders)
}
}
export default AdminReservationsResource

View File

@@ -1140,6 +1140,46 @@
"updated_at": "2021-03-17 12:09:59.156058+01",
"metadata": null
},
"inventory_item": {
"id": "iitem_320ZXDBAB4WJVW6115VEM7",
"sku": "sku123",
"origin_country": "dk",
"hs_code": null,
"mid_code": null,
"material": null,
"weight": 10,
"lenght": 4,
"height": 5,
"width": 2,
"requires_shipping": true,
"created_at": "2021-03-17 11:58:56.975971+01",
"updated_at": "2021-03-17 12:09:59.156058+01",
"metadata": null,
"location_levels": [
{
"id": "ilev_320ZXDBAB4WJVW6115VEM7",
"inventory_item_id": "iitem_320ZXDBAB4WJVW6115VEM7",
"location_id": "loc_01F0ZXDBAB9KVZWJVW6115VEM7",
"stocked_quantity": 1,
"reserved_quantity": 0,
"incoming_quantity": 10
}
]
},
"stock_location": {
"id": "loc_01F0ZXDBAB9KVZWJVW6115VEM7",
"name": "Stock location 1",
"address_id": "addr_01F0ZXDBAB9KVZWJVW6115VEM7",
"created_at": "2021-03-17 11:58:56.975971+01",
"updated_at": "2021-03-17 12:09:59.156058+01"
},
"reservation": {
"id": "res_01F0ZXDBAB9KVZWJVW6115VEM7",
"line_item_id": "li_01F0ZXDBAB9KVZWJVW6115VEM7",
"inventory_item_id": "iitem_320ZXDBAB4WJVW6115VEM7",
"location_id": "loc_01F0ZXDBAB9KVZWJVW6115VEM7",
"quantity": 1
},
"invite": {
"id": "invite_320ZXDBAB4WJVW6115VEM7",
"user_email": "lebron@james.com",

View File

@@ -353,6 +353,61 @@ export const adminHandlers = [
}
),
rest.get("/admin/reservations/", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
reservations: fixtures.list("reservation"),
})
)
}),
rest.post("/admin/reservations/", (req, res, ctx) => {
const body = req.body as Record<string, any>
return res(
ctx.status(200),
ctx.json({
reservation: {
...fixtures.get("reservation"),
...body,
},
})
)
}),
rest.get("/admin/reservations/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
reservation: { ...fixtures.get("reservation"), id: req.params.id },
})
)
}),
rest.post("/admin/reservations/:id", (req, res, ctx) => {
const body = req.body as Record<string, any>
return res(
ctx.status(200),
ctx.json({
reservation: {
...fixtures.get("reservation"),
...body,
id: req.params.id,
},
})
)
}),
rest.delete("/admin/reservations/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
id: req.params.id,
object: "reservation",
deleted: true,
})
)
}),
rest.post("/admin/return-reasons/", (req, res, ctx) => {
const body = req.body as Record<string, any>
return res(
@@ -465,6 +520,61 @@ export const adminHandlers = [
)
}),
rest.get("/admin/stock-locations", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
stock_locations: fixtures.list("stock_location"),
})
)
}),
rest.post("/admin/stock-locations", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
stock_location: {
...fixtures.get("stock_location"),
...(req.body as any),
},
})
)
}),
rest.get("/admin/stock-locations/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
stock_location: {
...fixtures.get("stock_location"),
id: req.params.id,
},
})
)
}),
rest.post("/admin/stock-locations/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
stock_location: {
...fixtures.get("stock_location"),
...(req.body as any),
id: req.params.id,
},
})
)
}),
rest.delete("/admin/stock-locations/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
id: req.params.id,
object: "stock_location",
deleted: true,
})
)
}),
rest.get("/admin/notifications/", (req, res, ctx) => {
return res(
ctx.status(200),
@@ -487,6 +597,112 @@ export const adminHandlers = [
)
}),
// inventory
rest.get("/admin/inventory-items", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
inventory_items: fixtures.list("inventory_item"),
})
)
}),
rest.get("/admin/inventory-items/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
inventory_item: fixtures.get("inventory_item"),
})
)
}),
rest.post("/admin/inventory-items/:id", (req, res, ctx) => {
const body = req.body as Record<string, any>
return res(
ctx.status(200),
ctx.json({
inventory_item: {
...fixtures.get("inventory_item"),
...body,
id: req.params.id,
},
})
)
}),
rest.delete("/admin/inventory-items/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
id: req.params.id,
object: "inventory_item",
deleted: true,
})
)
}),
rest.get("/admin/inventory-items/:id/location-levels", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
inventory_item: {
...fixtures.get("inventory_item"),
id: req.params.id,
},
})
)
}),
rest.post("/admin/inventory-items/:id/location-levels", (req, res, ctx) => {
const body = req.body as Record<string, any>
const { location_levels } = fixtures.get("inventory_item")
return res(
ctx.status(200),
ctx.json({
inventory_item: {
...fixtures.get("inventory_item"),
id: req.params.id,
location_levels: [...location_levels, { ...body, id: "2" }],
},
})
)
}),
rest.post(
"/admin/inventory-items/:id/location-levels/:location_id",
(req, res, ctx) => {
const body = req.body as Record<string, any>
const inventoryItem = fixtures.get("inventory_item")
const locationlevel = { ...inventoryItem.location_levels[0], ...body }
return res(
ctx.status(200),
ctx.json({
inventory_item: {
...fixtures.get("inventory_item"),
id: req.params.id,
location_levels: [locationlevel],
},
})
)
}
),
rest.delete(
"/admin/inventory-items/:id/location-levels/:location_id",
(req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
inventory_item: {
...fixtures.get("inventory_item"),
id: req.params.id,
location_levels: [],
},
})
)
}
),
rest.get("/admin/invites", (req, res, ctx) => {
return res(
ctx.status(200),
@@ -1189,6 +1405,24 @@ export const adminHandlers = [
)
}),
rest.get("/admin/variants/:id/inventory", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
variant: {
...fixtures.get("product_variant"),
sales_channel_availability: [
{
channel_name: "default channel",
channel_id: "1",
available_quantity: 10,
},
],
},
})
)
}),
rest.get("/admin/users/:id", (req, res, ctx) => {
return res(
ctx.status(200),

View File

@@ -1,7 +1,7 @@
import Medusa from "@medusajs/medusa-js"
import {
QueryClientProvider,
QueryClientProviderProps
QueryClientProviderProps,
} from "@tanstack/react-query"
import React from "react"

View File

@@ -8,6 +8,7 @@ export * from "./customers"
export * from "./discounts"
export * from "./draft-orders"
export * from "./gift-cards"
export * from "./inventory-item"
export * from "./invites"
export * from "./notes"
export * from "./notifications"
@@ -21,6 +22,7 @@ export * from "./publishable-api-keys"
export * from "./regions"
export * from "./return-reasons"
export * from "./returns"
export * from "./reservations"
export * from "./sales-channels"
export * from "./shipping-options"
export * from "./shipping-profiles"
@@ -32,3 +34,4 @@ export * from "./users"
export * from "./variants"
export * from "./payment-collections"
export * from "./payments"
export * from "./stock-locations"

View File

@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"

View File

@@ -0,0 +1,151 @@
import {
AdminInventoryItemsDeleteRes,
AdminInventoryItemsRes,
AdminPostInventoryItemsInventoryItemReq,
AdminPostInventoryItemsItemLocationLevelsLevelReq,
AdminPostInventoryItemsItemLocationLevelsReq,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import {
useMutation,
UseMutationOptions,
useQueryClient,
} from "@tanstack/react-query"
import { useMedusa } from "../../../contexts"
import { buildOptions } from "../../utils/buildOptions"
import { adminInventoryItemsKeys } from "./queries"
// inventory item
// update inventory item
export const useAdminUpdateInventoryItem = (
inventoryItemId: string,
options?: UseMutationOptions<
Response<AdminInventoryItemsRes>,
Error,
AdminPostInventoryItemsInventoryItemReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostInventoryItemsInventoryItemReq) =>
client.admin.inventoryItems.update(inventoryItemId, payload),
buildOptions(
queryClient,
[adminInventoryItemsKeys.detail(inventoryItemId)],
options
)
)
}
// delete inventory item
export const useAdminDeleteInventoryItem = (
inventoryItemId: string,
options?: UseMutationOptions<
Response<AdminInventoryItemsDeleteRes>,
Error,
void
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
() => client.admin.inventoryItems.delete(inventoryItemId),
buildOptions(
queryClient,
[adminInventoryItemsKeys.detail(inventoryItemId)],
options
)
)
}
// location level
export const useAdminUpdateLocationLevel = (
inventoryItemId: string,
options?: UseMutationOptions<
Response<AdminInventoryItemsRes>,
Error,
AdminPostInventoryItemsItemLocationLevelsLevelReq & {
stockLocationId: string
}
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(
payload: AdminPostInventoryItemsItemLocationLevelsLevelReq & {
stockLocationId: string
}
) =>
client.admin.inventoryItems.updateLocationLevel(
inventoryItemId,
payload.stockLocationId,
{
incoming_quantity: payload.incoming_quantity,
stocked_quantity: payload.stocked_quantity,
}
),
buildOptions(
queryClient,
[
adminInventoryItemsKeys.detail(inventoryItemId),
adminInventoryItemsKeys.lists(),
],
options
)
)
}
export const useAdminDeleteLocationLevel = (
inventoryItemId: string,
options?: UseMutationOptions<Response<AdminInventoryItemsRes>, Error, string>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(stockLocationId: string) =>
client.admin.inventoryItems.deleteLocationLevel(
inventoryItemId,
stockLocationId
),
buildOptions(
queryClient,
[
adminInventoryItemsKeys.detail(inventoryItemId),
adminInventoryItemsKeys.lists(),
],
options
)
)
}
export const useAdminCreateLocationLevel = (
inventoryItemId: string,
options?: UseMutationOptions<
Response<AdminInventoryItemsRes>,
Error,
AdminPostInventoryItemsItemLocationLevelsReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostInventoryItemsItemLocationLevelsReq) =>
client.admin.inventoryItems.createLocationLevel(inventoryItemId, payload),
buildOptions(
queryClient,
[
adminInventoryItemsKeys.detail(inventoryItemId),
adminInventoryItemsKeys.lists(),
],
options
)
)
}

View File

@@ -0,0 +1,79 @@
import {
AdminGetStockLocationsParams,
AdminInventoryItemsListWithVariantsAndLocationLevelsRes,
AdminInventoryItemsLocationLevelsRes,
AdminInventoryItemsRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "@tanstack/react-query"
import { useMedusa } from "../../../contexts"
import { UseQueryOptionsWrapper } from "../../../types"
import { queryKeysFactory } from "../../utils"
const ADMIN_INVENTORY_ITEMS_QUERY_KEY = `admin_inventory_items` as const
export const adminInventoryItemsKeys = queryKeysFactory(
ADMIN_INVENTORY_ITEMS_QUERY_KEY
)
type InventoryItemsQueryKeys = typeof adminInventoryItemsKeys
export const useAdminInventoryItems = (
query?: AdminGetStockLocationsParams,
options?: UseQueryOptionsWrapper<
Response<AdminInventoryItemsListWithVariantsAndLocationLevelsRes>,
Error,
ReturnType<InventoryItemsQueryKeys["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminInventoryItemsKeys.list(query),
() => client.admin.inventoryItems.list(query),
{ ...options }
)
return { ...data, ...rest } as const
}
export const useAdminInventoryItem = (
inventoryItemId: string,
query?: AdminGetStockLocationsParams,
options?: UseQueryOptionsWrapper<
Response<AdminInventoryItemsRes>,
Error,
ReturnType<InventoryItemsQueryKeys["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminInventoryItemsKeys.detail(inventoryItemId),
() => client.admin.inventoryItems.retrieve(inventoryItemId, query),
{ ...options }
)
return { ...data, ...rest } as const
}
export const useAdminInventoryItemLocationLevels = (
inventoryItemId: string,
query?: AdminGetStockLocationsParams,
options?: UseQueryOptionsWrapper<
Response<AdminInventoryItemsLocationLevelsRes>,
Error,
ReturnType<InventoryItemsQueryKeys["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminInventoryItemsKeys.detail(inventoryItemId),
() =>
client.admin.inventoryItems.listLocationLevels(inventoryItemId, query),
{ ...options }
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1,2 @@
export * from "./mutations"
export * from "./queries"

View File

@@ -0,0 +1,75 @@
import {
AdminPostReservationsReq,
AdminPostReservationsReservationReq,
AdminReservationsDeleteRes,
AdminReservationsRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js/src"
import {
useMutation,
UseMutationOptions,
useQueryClient,
} from "@tanstack/react-query"
import { useMedusa } from "../../../contexts"
import { buildOptions } from "../../utils/buildOptions"
import { adminReservationsKeys } from "./queries"
export const useAdminCreateReservation = (
options?: UseMutationOptions<
Response<AdminReservationsRes>,
Error,
AdminPostReservationsReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostReservationsReq) =>
client.admin.reservations.create(payload),
buildOptions(queryClient, [adminReservationsKeys.list()], options)
)
}
export const useAdminUpdateReservation = (
id: string,
options?: UseMutationOptions<
Response<AdminReservationsRes>,
Error,
AdminPostReservationsReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostReservationsReservationReq) =>
client.admin.reservations.update(id, payload),
buildOptions(
queryClient,
[adminReservationsKeys.lists(), adminReservationsKeys.detail(id)],
options
)
)
}
export const useAdminDeleteReservation = (
id: string,
options?: UseMutationOptions<
Response<AdminReservationsDeleteRes>,
Error,
void
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
() => client.admin.reservations.delete(id),
buildOptions(
queryClient,
[adminReservationsKeys.lists(), adminReservationsKeys.detail(id)],
options
)
)
}

View File

@@ -0,0 +1,56 @@
import {
AdminGetReservationsParams,
AdminReservationsListRes,
AdminReservationsRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "@tanstack/react-query"
import { useMedusa } from "../../../contexts"
import { UseQueryOptionsWrapper } from "../../../types"
import { queryKeysFactory } from "../../utils"
const ADMIN_RESERVATIONS_QUERY_KEY = `admin_stock_locations` as const
export const adminReservationsKeys = queryKeysFactory(
ADMIN_RESERVATIONS_QUERY_KEY
)
type ReservationsQueryKeys = typeof adminReservationsKeys
export const useAdminReservations = (
query?: AdminGetReservationsParams,
options?: UseQueryOptionsWrapper<
Response<AdminReservationsListRes>,
Error,
ReturnType<ReservationsQueryKeys["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminReservationsKeys.list(query),
() => client.admin.reservations.list(query),
{ ...options }
)
return { ...data, ...rest } as const
}
export const useAdminReservation = (
id: string,
options?: UseQueryOptionsWrapper<
Response<AdminReservationsRes>,
Error,
ReturnType<ReservationsQueryKeys["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminReservationsKeys.detail(id),
() => client.admin.reservations.retrieve(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -16,6 +16,7 @@ import {
import { useMedusa } from "../../../contexts"
import { buildOptions } from "../../utils/buildOptions"
import { adminProductKeys } from "../products"
import { adminStockLocationsKeys } from "../stock-locations"
import { adminSalesChannelsKeys } from "./queries"
/**
@@ -162,3 +163,73 @@ export const useAdminAddProductsToSalesChannel = (
)
)
}
/**
* Add a location to a sales channel
* @experimental This feature is under development and may change in the future.
* To use this feature please install the stock location in your medusa backend project.
* @description Add a location to a sales channel
* @param options
*/
export const useAdminAddLocationToSalesChannel = (
options?: UseMutationOptions<
Response<AdminSalesChannelsRes>,
Error,
{
sales_channel_id: string
location_id: string
}
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(({ sales_channel_id, location_id }) => {
return client.admin.salesChannels.addLocation(sales_channel_id, {
location_id,
})
}, buildOptions(
queryClient,
[
adminSalesChannelsKeys.lists(),
adminSalesChannelsKeys.details(),
adminStockLocationsKeys.all
],
options
)
)
}
/**
* Remove a location from a sales channel
* @experimental This feature is under development and may change in the future.
* To use this feature please install the stock location in your medusa backend project.
* @description Remove a location from a sales channel
* @param options
*/
export const useAdminRemoveLocationFromSalesChannel = (
options?: UseMutationOptions<
Response<AdminSalesChannelsRes>,
Error,
{
sales_channel_id: string
location_id: string
}
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(({ sales_channel_id, location_id }) => {
return client.admin.salesChannels.removeLocation(sales_channel_id, {
location_id,
})
}, buildOptions(
queryClient,
[
adminSalesChannelsKeys.lists(),
adminSalesChannelsKeys.details(),
adminStockLocationsKeys.all
],
options
)
)
}

View File

@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"

View File

@@ -0,0 +1,74 @@
import {
AdminPostStockLocationsReq,
AdminStockLocationsDeleteRes,
AdminStockLocationsRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import {
useMutation,
UseMutationOptions,
useQueryClient,
} from "@tanstack/react-query"
import { useMedusa } from "../../../contexts"
import { buildOptions } from "../../utils/buildOptions"
import { adminStockLocationsKeys } from "./queries"
export const useAdminCreateStockLocation = (
options?: UseMutationOptions<
Response<AdminStockLocationsRes>,
Error,
AdminPostStockLocationsReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostStockLocationsReq) =>
client.admin.stockLocations.create(payload),
buildOptions(queryClient, [adminStockLocationsKeys.lists()], options)
)
}
export const useAdminUpdateStockLocation = (
id: string,
options?: UseMutationOptions<
Response<AdminStockLocationsRes>,
Error,
AdminPostStockLocationsReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostStockLocationsReq) =>
client.admin.stockLocations.update(id, payload),
buildOptions(
queryClient,
[adminStockLocationsKeys.lists(), adminStockLocationsKeys.detail(id)],
options
)
)
}
export const useAdminDeleteStockLocation = (
id: string,
options?: UseMutationOptions<
Response<AdminStockLocationsDeleteRes>,
Error,
void
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
() => client.admin.stockLocations.delete(id),
buildOptions(
queryClient,
[adminStockLocationsKeys.lists(), adminStockLocationsKeys.detail(id)],
options
)
)
}

View File

@@ -0,0 +1,56 @@
import {
AdminGetStockLocationsParams,
AdminStockLocationsListRes,
AdminStockLocationsRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "@tanstack/react-query"
import { useMedusa } from "../../../contexts"
import { UseQueryOptionsWrapper } from "../../../types"
import { queryKeysFactory } from "../../utils"
const ADMIN_STOCK_LOCATIONS_QUERY_KEY = `admin_stock_locations` as const
export const adminStockLocationsKeys = queryKeysFactory(
ADMIN_STOCK_LOCATIONS_QUERY_KEY
)
type StockLocationsQueryKeys = typeof adminStockLocationsKeys
export const useAdminStockLocations = (
query?: AdminGetStockLocationsParams,
options?: UseQueryOptionsWrapper<
Response<AdminStockLocationsListRes>,
Error,
ReturnType<StockLocationsQueryKeys["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminStockLocationsKeys.list(query),
() => client.admin.stockLocations.list(query),
options
)
return { ...data, ...rest } as const
}
export const useAdminStockLocation = (
id: string,
options?: UseQueryOptionsWrapper<
Response<AdminStockLocationsRes>,
Error,
ReturnType<StockLocationsQueryKeys["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminStockLocationsKeys.detail(id),
() => client.admin.stockLocations.retrieve(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -1,4 +1,8 @@
import { AdminGetVariantsParams, AdminVariantsListRes } from "@medusajs/medusa"
import {
AdminGetVariantsParams,
AdminGetVariantsVariantInventoryRes,
AdminVariantsListRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "@tanstack/react-query"
import { useMedusa } from "../../../contexts"
@@ -27,3 +31,20 @@ export const useAdminVariants = (
)
return { ...data, ...rest } as const
}
export const useAdminVariantsInventory = (
id: string,
options?: UseQueryOptionsWrapper<
Response<AdminGetVariantsVariantInventoryRes>,
Error,
ReturnType<VariantQueryKeys["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminVariantKeys.detail(id),
() => client.admin.variants.getInventory(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1,144 @@
import {
useAdminUpdateInventoryItem,
useAdminDeleteInventoryItem,
useAdminUpdateLocationLevel,
useAdminDeleteLocationLevel,
useAdminCreateLocationLevel,
} from "../../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { createWrapper } from "../../../utils"
describe("useAdminUpdateInventoryItem hook", () => {
test("updates an inventory item", async () => {
const payload = {
sku: "test-sku",
}
const { result, waitFor } = renderHook(
() => useAdminUpdateInventoryItem("inventory-item-id"),
{
wrapper: createWrapper(),
}
)
result.current.mutate(payload)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.inventory_item).toEqual(
expect.objectContaining({
id: "inventory-item-id",
sku: "test-sku",
})
)
})
})
describe("useAdminDeleteInventoryItem hook", () => {
test("Deletes an inventory item", async () => {
const { result, waitFor } = renderHook(
() => useAdminDeleteInventoryItem("inventory-item-id"),
{
wrapper: createWrapper(),
}
)
result.current.mutate()
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data).toEqual(
expect.objectContaining({
id: "inventory-item-id",
deleted: true,
})
)
})
})
describe("useAdminUpdateLocationLevel hook", () => {
test("Updates a location level", async () => {
const payload = {
incoming_quantity: 10,
}
const { result, waitFor } = renderHook(
() => useAdminUpdateLocationLevel("inventory-item-id"),
{
wrapper: createWrapper(),
}
)
result.current.mutate({ ...payload, stockLocationId: "location_id"})
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.inventory_item).toEqual(
expect.objectContaining({
id: "inventory-item-id",
location_levels: [
expect.objectContaining({
incoming_quantity: 10,
}),
],
})
)
})
})
describe("useAdminDeleteLocationLevel hook", () => {
test("removes a location level", async () => {
const { result, waitFor } = renderHook(
() => useAdminDeleteLocationLevel("inventory-item-id"),
{
wrapper: createWrapper(),
}
)
result.current.mutate("location_id")
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.inventory_item).toEqual(
expect.objectContaining({
id: "inventory-item-id",
location_levels: [],
})
)
})
})
describe("useAdminCreateLocationLevel hook", () => {
test("creates a location level", async () => {
const payload = {
location_id: "loc_1",
incoming_quantity: 10,
stocked_quantity: 10,
}
const { result, waitFor } = renderHook(
() => useAdminCreateLocationLevel("inventory-item-id"),
{
wrapper: createWrapper(),
}
)
result.current.mutate(payload)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.inventory_item).toEqual(
expect.objectContaining({
id: "inventory-item-id",
location_levels: expect.arrayContaining([
expect.objectContaining({ ...payload }),
]),
})
)
})
})

View File

@@ -0,0 +1,75 @@
import {
useAdminInventoryItem,
useAdminInventoryItemLocationLevels,
useAdminInventoryItems,
useAdminPriceList,
useAdminPriceLists,
} from "../../../../src"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils"
describe("useAdminInventoryItems hook", () => {
test("returns a list of inventory items", async () => {
const inventoryItems = fixtures.list("inventory_item")
const { result, waitFor } = renderHook(() => useAdminInventoryItems(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.inventory_items).toEqual(inventoryItems)
})
})
describe("useAdminInventoryItem hook", () => {
test("returns a single inventory item", async () => {
const inventoryItem = fixtures.get("inventory_item")
const { result, waitFor } = renderHook(
() => useAdminInventoryItem(inventoryItem.id),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.inventory_item).toEqual(inventoryItem)
})
})
describe("useAdminInventoryItem hook", () => {
test("returns a location levels for an inventory item", async () => {
const inventoryItem = fixtures.get("inventory_item")
const { result, waitFor } = renderHook(
() => useAdminInventoryItemLocationLevels(inventoryItem.id),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.inventory_item).toEqual(inventoryItem)
})
})
describe("useAdminPriceList hook", () => {
test("returns a price list", async () => {
const priceList = fixtures.get("price_list")
const { result, waitFor } = renderHook(
() => useAdminPriceList(priceList.id),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.price_list).toEqual(priceList)
})
})

View File

@@ -0,0 +1,88 @@
import {
useAdminCreateShippingProfile,
useAdminUpdateShippingProfile,
useAdminDeleteShippingProfile,
useAdminCreateReservation,
useAdminUpdateReservation,
useAdminDeleteReservation,
} from "../../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils"
describe("useAdminCreateShippingProfile hook", () => {
test("creates a shipping profile and returns it", async () => {
const reservationPayload = {
location_id: "loc_1",
inventory_item_id: "inv_1",
quantity: 2,
}
const { result, waitFor } = renderHook(() => useAdminCreateReservation(), {
wrapper: createWrapper(),
})
result.current.mutate(reservationPayload)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.reservation).toEqual(
expect.objectContaining({
...fixtures.get("reservation"),
...reservationPayload,
})
)
})
})
describe("useAdminUpdateShippingProfile hook", () => {
test("updates a shipping profile and returns it", async () => {
const reservationPayload = {
quantity: 3,
}
const { result, waitFor } = renderHook(
() => useAdminUpdateReservation(fixtures.get("reservation").id),
{
wrapper: createWrapper(),
}
)
result.current.mutate(reservationPayload)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.reservation).toEqual(
expect.objectContaining({
...fixtures.get("reservation"),
quantity: 3,
})
)
})
})
describe("useAdminDeleteShippingProfile hook", () => {
test("deletes a shipping profile", async () => {
const { result, waitFor } = renderHook(
() => useAdminDeleteReservation(fixtures.get("reservation").id),
{
wrapper: createWrapper(),
}
)
result.current.mutate()
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data).toEqual(
expect.objectContaining({
id: fixtures.get("reservation").id,
object: "reservation",
deleted: true,
})
)
})
})

View File

@@ -0,0 +1,35 @@
import { useAdminReservation, useAdminReservations } from "../../../../src"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils"
describe("useAdminShippingProfiles hook", () => {
test("returns a list of shipping profiles", async () => {
const reservations = fixtures.list("reservation")
const { result, waitFor } = renderHook(() => useAdminReservations(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.reservations).toEqual(reservations)
})
})
describe("useAdminShippingProfile hook", () => {
test("returns a shipping profile", async () => {
const reservation = fixtures.get("reservation")
const { result, waitFor } = renderHook(
() => useAdminReservation(reservation.id),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.reservation).toEqual(reservation)
})
})

View File

@@ -97,15 +97,15 @@ describe("useAdminDeleteProductsFromSalesChannel hook", () => {
{ wrapper: createWrapper() }
)
result.current.mutate({ product_ids: [
{ id: productId }
]})
result.current.mutate({ product_ids: [{ id: productId }] })
await waitFor(() => result.current.isSuccess)
expect(result.current.data).toEqual(expect.objectContaining({
sales_channel: fixtures.get("sales_channel"),
}))
expect(result.current.data).toEqual(
expect.objectContaining({
sales_channel: fixtures.get("sales_channel"),
})
)
})
})
@@ -119,14 +119,14 @@ describe("useAdminAddProductsToSalesChannel hook", () => {
{ wrapper: createWrapper() }
)
result.current.mutate({ product_ids: [
{ id: productId }
]})
result.current.mutate({ product_ids: [{ id: productId }] })
await waitFor(() => result.current.isSuccess)
expect(result.current.data).toEqual(expect.objectContaining({
sales_channel: fixtures.get("sales_channel"),
}))
expect(result.current.data).toEqual(
expect.objectContaining({
sales_channel: fixtures.get("sales_channel"),
})
)
})
})

View File

@@ -0,0 +1,87 @@
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import {
useAdminCreateStockLocation,
useAdminDeleteStockLocation,
useAdminUpdateStockLocation,
} from "../../../../src"
import { createWrapper } from "../../../utils"
describe("useAdminUpdateStockLocation hook", () => {
test("updates a stock location", async () => {
const payload = {
name: "updated name",
}
const { result, waitFor } = renderHook(
() => useAdminUpdateStockLocation("stock-location-id"),
{
wrapper: createWrapper(),
}
)
result.current.mutate(payload)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.stock_location).toEqual(
expect.objectContaining({
id: "stock-location-id",
name: "updated name",
})
)
})
})
describe("useAdminCreateStockLocation hook", () => {
test("creates a stock location", async () => {
const locationFixture = fixtures.get("stock_location")
const payload = {
name: "updated name",
}
const { result, waitFor } = renderHook(
() => useAdminCreateStockLocation(),
{
wrapper: createWrapper(),
}
)
result.current.mutate(payload)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.stock_location).toEqual(
expect.objectContaining({
...locationFixture,
...payload,
})
)
})
})
describe("useAdminDeleteStockLocation hook", () => {
test("deletes a stock location", async () => {
const { result, waitFor } = renderHook(
() => useAdminDeleteStockLocation("stock-location-id"),
{
wrapper: createWrapper(),
}
)
result.current.mutate()
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data).toEqual(
expect.objectContaining({
id: "stock-location-id",
object: "stock_location",
deleted: true,
})
)
})
})

View File

@@ -0,0 +1,40 @@
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { useAdminStockLocation, useAdminStockLocations } from "../../../../src"
import { createWrapper } from "../../../utils"
describe("useAdminUpdateStockLocation hook", () => {
test("gets a stock location", async () => {
const stockLocation = fixtures.get("stock_location")
const { result, waitFor } = renderHook(
() => useAdminStockLocation("stock-location-id"),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.stock_location).toEqual(
expect.objectContaining({
...stockLocation,
id: "stock-location-id",
})
)
})
})
describe("useAdminUpdateStockLocations hook", () => {
test("lists stock locations", async () => {
const stockLocation = fixtures.list("stock_location")
const { result, waitFor } = renderHook(() => useAdminStockLocations(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.stock_locations).toEqual(stockLocation)
})
})

View File

@@ -1,4 +1,4 @@
import { useAdminVariants } from "../../../../src"
import { useAdminVariants, useAdminVariantsInventory } from "../../../../src"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils"
@@ -16,3 +16,29 @@ describe("useAdminVariants hook", () => {
expect(result.current.variants).toEqual(variants)
})
})
describe("useAdminVariants hook", () => {
test("returns a variant with saleschannel locations", async () => {
const variant = fixtures.get("product_variant")
const { result, waitFor } = renderHook(
() => useAdminVariantsInventory(variant.id),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.variant).toEqual({
...variant,
sales_channel_availability: [
{
channel_name: "default channel",
channel_id: "1",
available_quantity: 10,
},
],
})
})
})

View File

@@ -43,6 +43,7 @@ export * from "./routes/admin/publishable-api-keys"
export * from "./routes/admin/regions"
export * from "./routes/admin/return-reasons"
export * from "./routes/admin/returns"
export * from "./routes/admin/reservations"
export * from "./routes/admin/sales-channels"
export * from "./routes/admin/shipping-options"
export * from "./routes/admin/shipping-profiles"

View File

@@ -82,7 +82,6 @@ export default async (req, res) => {
* @schema AdminPostReservationsReq
* type: object
* required:
* - line_item_id
* - location_id
* - inventory_item_id
* - quantity

View File

@@ -109,3 +109,4 @@ export * from "./create-reservation"
export * from "./delete-reservation"
export * from "./get-reservation"
export * from "./update-reservation"
export * from "./list-reservations"

View File

@@ -15,6 +15,7 @@ import { AdminGetSalesChannelsParams } from "./list-sales-channels"
import { AdminPostSalesChannelsSalesChannelReq } from "./update-sales-channel"
import { AdminPostSalesChannelsChannelStockLocationsReq } from "./associate-stock-location"
import { AdminDeleteSalesChannelsChannelStockLocationsReq } from "./remove-stock-location"
import { checkRegisteredModules } from "../../../middlewares/check-registered-modules"
const route = Router()
@@ -47,11 +48,19 @@ export default (app) => {
)
salesChannelRouter.post(
"/stock-locations",
checkRegisteredModules({
stockLocationService:
"Stock Locations are not enabled. Please add a Stock Location module to enable this functionality.",
}),
transformBody(AdminPostSalesChannelsChannelStockLocationsReq),
middlewares.wrap(require("./associate-stock-location").default)
)
salesChannelRouter.delete(
"/stock-locations",
checkRegisteredModules({
stockLocationService:
"Stock Locations are not enabled. Please add a Stock Location module to enable this functionality.",
}),
transformBody(AdminDeleteSalesChannelsChannelStockLocationsReq),
middlewares.wrap(require("./remove-stock-location").default)
)

View File

@@ -1,6 +1,11 @@
import { IStockLocationService } from "../../../../interfaces"
import { Request, Response } from "express"
import { FindParams } from "../../../../types/common"
import { joinSalesChannels } from "./utils/join-sales-channels"
import {
SalesChannelLocationService,
SalesChannelService,
} from "../../../../services"
/**
* @oas [get] /stock-locations/{id}
@@ -50,7 +55,34 @@ export default async (req: Request, res: Response) => {
const locationService: IStockLocationService = req.scope.resolve(
"stockLocationService"
)
const stockLocation = await locationService.retrieve(id, req.retrieveConfig)
const channelLocationService: SalesChannelLocationService = req.scope.resolve(
"salesChannelLocationService"
)
const salesChannelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
const { retrieveConfig } = req
const includeSalesChannels =
!!retrieveConfig.relations?.includes("sales_channels")
if (includeSalesChannels) {
retrieveConfig.relations = retrieveConfig.relations?.filter(
(r) => r !== "sales_channels"
)
}
let stockLocation = await locationService.retrieve(id, retrieveConfig)
if (includeSalesChannels) {
const [location] = await joinSalesChannels(
[stockLocation],
channelLocationService,
salesChannelService
)
stockLocation = location
}
res.status(200).json({ stock_location: stockLocation })
}

View File

@@ -1,7 +1,10 @@
import { Router } from "express"
import "reflect-metadata"
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
import { StockLocationDTO } from "../../../../types/stock-location"
import {
StockLocationDTO,
StockLocationExpandedDTO,
} from "../../../../types/stock-location"
import middlewares, {
transformBody,
transformQuery,
@@ -113,10 +116,10 @@ export type AdminStockLocationsDeleteRes = DeleteResponse
* type: object
* properties:
* stock_location:
* $ref: "#/components/schemas/StockLocationDTO"
* $ref: "#/components/schemas/StockLocationExpandedDTO"
*/
export type AdminStockLocationsRes = {
stock_location: StockLocationDTO
stock_location: StockLocationExpandedDTO
}
/**
@@ -126,7 +129,7 @@ export type AdminStockLocationsRes = {
* stock_locations:
* type: array
* items:
* $ref: "#/components/schemas/StockLocationDTO"
* $ref: "#/components/schemas/StockLocationExpandedDTO"
* count:
* type: integer
* description: The total number of items available
@@ -138,7 +141,7 @@ export type AdminStockLocationsRes = {
* description: The number of items per page
*/
export type AdminStockLocationsListRes = PaginatedResponse & {
stock_locations: StockLocationDTO[]
stock_locations: StockLocationExpandedDTO[]
}
export * from "./list-stock-locations"

View File

@@ -4,6 +4,11 @@ import { IsType } from "../../../../utils/validators/is-type"
import { IStockLocationService } from "../../../../interfaces"
import { extendedFindParamsMixin } from "../../../../types/common"
import { Request, Response } from "express"
import {
SalesChannelLocationService,
SalesChannelService,
} from "../../../../services"
import { joinSalesChannels } from "./utils/join-sales-channels"
/**
* @oas [get] /stock-locations
@@ -133,15 +138,38 @@ export default async (req: Request, res: Response) => {
const stockLocationService: IStockLocationService = req.scope.resolve(
"stockLocationService"
)
const channelLocationService: SalesChannelLocationService = req.scope.resolve(
"salesChannelLocationService"
)
const salesChannelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
const { filterableFields, listConfig } = req
const { skip, take } = listConfig
const [locations, count] = await stockLocationService.listAndCount(
const includeSalesChannels =
!!listConfig.relations?.includes("sales_channels")
if (includeSalesChannels) {
listConfig.relations = listConfig.relations?.filter(
(r) => r !== "sales_channels"
)
}
let [locations, count] = await stockLocationService.listAndCount(
filterableFields,
listConfig
)
if (includeSalesChannels) {
locations = await joinSalesChannels(
locations,
channelLocationService,
salesChannelService
)
}
res.status(200).json({
stock_locations: locations,
count,

View File

@@ -0,0 +1,31 @@
import {
SalesChannelLocationService,
SalesChannelService,
} from "../../../../../services"
import {
StockLocationDTO,
StockLocationExpandedDTO,
} from "../../../../../types/stock-location"
const joinSalesChannels = async (
locations: StockLocationDTO[],
channelLocationService: SalesChannelLocationService,
salesChannelService: SalesChannelService
): Promise<StockLocationExpandedDTO[]> => {
return await Promise.all(
locations.map(async (location: StockLocationExpandedDTO) => {
const salesChannelIds = await channelLocationService.listSalesChannelIds(
location.id
)
const [salesChannels] = await salesChannelService.listAndCount({
id: salesChannelIds,
})
location.sales_channels = salesChannels
return location
})
)
}
export { joinSalesChannels }

View File

@@ -1,11 +1,9 @@
import { FulfillmentProvider, PaymentProvider, Store } from "../../../../models"
import {
FulfillmentProviderService,
PaymentProviderService,
StoreService,
} from "../../../../services"
import { FeatureFlagsResponse } from "../../../../types/feature-flags"
import { ModulesResponse } from "../../../../types/modules"
import { ExtendedStoreDTO } from "../../../../types/store"
import { FlagRouter } from "../../../../utils/flag-router"
import { ModulesHelper } from "../../../../utils/module-helper"
@@ -77,12 +75,7 @@ export default async (req, res) => {
const data = (await storeService.retrieve({
relations,
})) as Store & {
payment_providers: PaymentProvider[]
fulfillment_providers: FulfillmentProvider[]
feature_flags: FeatureFlagsResponse
modules: ModulesResponse
}
})) as ExtendedStoreDTO
data.feature_flags = featureFlagRouter.listFlags()
data.modules = modulesHelper.modules

View File

@@ -1,6 +1,7 @@
import { Router } from "express"
import { PaymentProvider, Store, TaxProvider } from "./../../../../"
import { PaymentProvider, TaxProvider } from "./../../../../"
import middlewares from "../../../middlewares"
import { ExtendedStoreDTO } from "../../../../types/store"
const route = Router()
@@ -34,10 +35,10 @@ export default (app) => {
* type: object
* properties:
* store:
* $ref: "#/components/schemas/Store"
* $ref: "#/components/schemas/ExtendedStoreDTO"
*/
export type AdminStoresRes = {
store: Store
store: ExtendedStoreDTO
}
/**

View File

@@ -72,7 +72,9 @@ export default async (req, res) => {
const inventoryService: IInventoryService =
req.scope.resolve("inventoryService")
const channelLocationService: SalesChannelLocationService = req.scope.resolve(
"salesChannelLocationService"
)
const channelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
@@ -91,11 +93,17 @@ export default async (req, res) => {
sales_channel_availability: [],
}
const [channels] = await channelService.listAndCount(
{},
{
relations: ["locations"],
}
const [rawChannels] = await channelService.listAndCount({})
const channels: SalesChannelDTO[] = await Promise.all(
rawChannels.map(async (channel) => {
const locationIds = await channelLocationService.listLocationIds(
channel.id
)
return {
...channel,
locations: locationIds,
}
})
)
const inventory =
@@ -116,7 +124,7 @@ export default async (req, res) => {
const quantity = await inventoryService.retrieveAvailableQuantity(
inventory[0].id,
channel.locations.map((loc) => loc.id)
channel.locations
)
return {
@@ -133,6 +141,10 @@ export default async (req, res) => {
})
}
type SalesChannelDTO = Omit<SalesChannel, "beforeInsert" | "locations"> & {
locations: string[]
}
type ResponseInventoryItem = Partial<InventoryItemDTO> & {
location_levels?: InventoryLevelDTO[]
}

View File

@@ -105,9 +105,9 @@ class ProductVariantInventoryService extends TransactionBaseService {
return true
}
let locations: string[] = []
let locationIds: string[] = []
if (context.salesChannelId) {
locations = await this.salesChannelLocationService_.listLocations(
locationIds = await this.salesChannelLocationService_.listLocationIds(
context.salesChannelId
)
} else {
@@ -115,7 +115,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
{},
{ select: ["id"] }
)
locations = stockLocations.map((l) => l.id)
locationIds = stockLocations.map((l) => l.id)
}
const hasInventory = await Promise.all(
@@ -125,7 +125,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
.withTransaction(manager)
.confirmInventory(
inventoryPart.inventory_item_id,
locations,
locationIds,
itemQuantity
)
})
@@ -382,7 +382,7 @@ class ProductVariantInventoryService extends TransactionBaseService {
if (!isDefined(locationId) && context.salesChannelId) {
const locations = await this.salesChannelLocationService_
.withTransaction(manager)
.listLocations(context.salesChannelId)
.listLocationIds(context.salesChannelId)
if (!locations.length) {
throw new MedusaError(

View File

@@ -40,13 +40,13 @@ class SalesChannelInventoryService {
salesChannelId: string,
inventoryItemId: string
): Promise<number> {
const locations = await this.salesChannelLocationService_.listLocations(
const locationIds = await this.salesChannelLocationService_.listLocationIds(
salesChannelId
)
return await this.inventoryService_.retrieveAvailableQuantity(
inventoryItemId,
locations
locationIds
)
}
}

View File

@@ -101,7 +101,7 @@ class SalesChannelLocationService extends TransactionBaseService {
* @param salesChannelId - The ID of the sales channel.
* @returns A promise that resolves with an array of location IDs.
*/
async listLocations(salesChannelId: string): Promise<string[]> {
async listLocationIds(salesChannelId: string): Promise<string[]> {
const manager = this.transactionManager_ || this.manager_
const salesChannel = await this.salesChannelService_
.withTransaction(manager)
@@ -109,10 +109,28 @@ class SalesChannelLocationService extends TransactionBaseService {
const locations = await manager.find(SalesChannelLocation, {
where: { sales_channel_id: salesChannel.id },
select: ["location_id"],
})
return locations.map((l) => l.location_id)
}
/**
* Lists the sales channels associated with a stock location.
* @param {string} salesChannelId - The ID of the stock location.
* @returns {Promise<string[]>} A promise that resolves with an array of sales channel IDs.
*/
async listSalesChannelIds(locationId: string): Promise<string[]> {
const manager = this.transactionManager_ || this.manager_
const location = await this.stockLocationService.retrieve(locationId)
const salesChannelLocations = await manager.find(SalesChannelLocation, {
where: { location_id: location.id },
select: ["sales_channel_id"],
})
return salesChannelLocations.map((l) => l.sales_channel_id)
}
}
export default SalesChannelLocationService

View File

@@ -3,6 +3,22 @@ export interface IFlagRouter {
listFlags: () => FeatureFlagsResponse
}
/**
* @schema FeatureFlagsResponse
* type: array
* items:
* type: object
* required:
* - key
* - value
* properties:
* key:
* description: The key of the feature flag.
* type: string
* value:
* description: The value of the feature flag.
* type: boolean
*/
export type FeatureFlagsResponse = {
key: string
value: boolean

View File

@@ -1,3 +1,19 @@
/**
* @schema ModulesResponse
* type: array
* items:
* type: object
* required:
* - module
* - resolution
* properties:
* module:
* description: The key of the module.
* type: string
* resolution:
* description: The resolution path of the module or false if module is not installed.
* type: string
*/
export type ModulesResponse = {
module: string
resolution: string | false

View File

@@ -1,3 +1,4 @@
import { SalesChannel } from "../models"
import { StringComparisonOperator } from "./common"
/**
@@ -137,6 +138,19 @@ export type StockLocationDTO = {
deleted_at: string | Date | null
}
/**
* @schema StockLocationExpandedDTO
* allOf:
* - $ref: "#/components/schemas/StockLocationDTO"
* - type: object
* properties:
* sales_channels:
* $ref: "#/components/schemas/SalesChannel"
*/
export type StockLocationExpandedDTO = StockLocationDTO & {
sales_channels?: SalesChannel[]
}
export type FilterableStockLocationProps = {
id?: string | string[]
name?: string | string[] | StringComparisonOperator

View File

@@ -1,3 +1,7 @@
import { Store, PaymentProvider, FulfillmentProvider } from "../models"
import { FeatureFlagsResponse } from "./feature-flags"
import { ModulesResponse } from "./modules"
export type UpdateStoreInput = {
name?: string
swap_link_template?: string
@@ -8,3 +12,31 @@ export type UpdateStoreInput = {
metadata?: Record<string, unknown>
default_sales_channel_id?: string
}
/**
* @schema ExtendedStoreDTO
* allOf:
* - $ref: "#/components/schemas/Store"
* - type: object
* required:
* - payment_providers
* - fulfillment_providers
* - feature_flags
* - modules
* properties:
* payment_providers:
* $ref: "#/components/schemas/PaymentProvider"
* fulfillment_providers:
* $ref: "#/components/schemas/FulfillmentProvider"
* feature_flags:
* $ref: "#/components/schemas/FeatureFlagsResponse"
* modules:
* $ref: "#/components/schemas/ModulesResponse"
*
*/
export type ExtendedStoreDTO = Store & {
payment_providers: PaymentProvider[]
fulfillment_providers: FulfillmentProvider[]
feature_flags: FeatureFlagsResponse
modules: ModulesResponse
}