diff --git a/.changeset/lucky-boats-invent.md b/.changeset/lucky-boats-invent.md new file mode 100644 index 0000000000..6bd75650ba --- /dev/null +++ b/.changeset/lucky-boats-invent.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Add filtering of stock locations based on sales channels diff --git a/integration-tests/plugins/__tests__/stock-location/sales-channels.js b/integration-tests/plugins/__tests__/stock-location/sales-channels.js index 16d13d3962..f85f10f4e9 100644 --- a/integration-tests/plugins/__tests__/stock-location/sales-channels.js +++ b/integration-tests/plugins/__tests__/stock-location/sales-channels.js @@ -8,6 +8,8 @@ const adminSeeder = require("../../helpers/admin-seeder") jest.setTimeout(30000) +const adminHeaders = { headers: { Authorization: "Bearer test_token" } } + describe("Sales channels", () => { let appContainer let dbConnection @@ -38,57 +40,120 @@ describe("Sales channels", () => { }) describe("Stock Locations", () => { - it("When listing a sales channel, it brings all associated locations with it", async () => { - await adminSeeder(dbConnection) + describe("CORE", () => { + it("When listing a sales channel, it brings all associated locations with it", async () => { + await adminSeeder(dbConnection) - const stockLocationService = appContainer.resolve("stockLocationService") - const salesChannelService = appContainer.resolve("salesChannelService") - const salesChannelLocationService = appContainer.resolve( - "salesChannelLocationService" - ) + const stockLocationService = appContainer.resolve( + "stockLocationService" + ) + const salesChannelService = appContainer.resolve("salesChannelService") + const salesChannelLocationService = appContainer.resolve( + "salesChannelLocationService" + ) - const loc = await stockLocationService.create({ - name: "warehouse", - }) - const loc2 = await stockLocationService.create({ - name: "other place", - }) - - const sc = await salesChannelService.create({ name: "channel test" }) - - await salesChannelLocationService.associateLocation(sc.id, loc.id) - await salesChannelLocationService.associateLocation(sc.id, loc2.id) - - expect( - await salesChannelLocationService.listLocationIds(sc.id) - ).toHaveLength(2) - - const [channels] = await salesChannelService.listAndCount( - {}, - { - relations: ["locations"], - } - ) - const createdSC = channels.find((c) => c.id === sc.id) - - expect(channels).toHaveLength(2) - expect(createdSC.locations).toHaveLength(2) - expect(createdSC).toEqual( - expect.objectContaining({ - id: sc.id, - name: "channel test", - locations: expect.arrayContaining([ - expect.objectContaining({ - sales_channel_id: sc.id, - location_id: loc.id, - }), - expect.objectContaining({ - sales_channel_id: sc.id, - location_id: loc2.id, - }), - ]), + const loc = await stockLocationService.create({ + name: "warehouse", }) - ) + const loc2 = await stockLocationService.create({ + name: "other place", + }) + + const sc = await salesChannelService.create({ name: "channel test" }) + + await salesChannelLocationService.associateLocation(sc.id, loc.id) + await salesChannelLocationService.associateLocation(sc.id, loc2.id) + + expect( + await salesChannelLocationService.listLocationIds(sc.id) + ).toHaveLength(2) + + const [channels] = await salesChannelService.listAndCount( + {}, + { + relations: ["locations"], + } + ) + const createdSC = channels.find((c) => c.id === sc.id) + + expect(channels).toHaveLength(2) + expect(createdSC.locations).toHaveLength(2) + expect(createdSC).toEqual( + expect.objectContaining({ + id: sc.id, + name: "channel test", + locations: expect.arrayContaining([ + expect.objectContaining({ + sales_channel_id: sc.id, + location_id: loc.id, + }), + expect.objectContaining({ + sales_channel_id: sc.id, + location_id: loc2.id, + }), + ]), + }) + ) + }) + }) + + describe("API", () => { + it("Filters stock locations based on sales channel ids", async () => { + const api = useApi() + + await adminSeeder(dbConnection) + + const stockLocationService = appContainer.resolve( + "stockLocationService" + ) + const salesChannelService = appContainer.resolve("salesChannelService") + const salesChannelLocationService = appContainer.resolve( + "salesChannelLocationService" + ) + + const loc = await stockLocationService.create({ + name: "warehouse", + }) + const loc2 = await stockLocationService.create({ + name: "other place", + }) + + const sc = await salesChannelService.create({ name: "Default Channel" }) + const sc2 = await salesChannelService.create({ name: "Physical store" }) + + await salesChannelLocationService.associateLocation(sc.id, loc.id) + await salesChannelLocationService.associateLocation(sc.id, loc2.id) + await salesChannelLocationService.associateLocation(sc2.id, loc2.id) + + const defaultSalesChannelFilterRes = await api.get( + `/admin/stock-locations?sales_channel_id=${sc.id}`, + adminHeaders + ) + + expect(defaultSalesChannelFilterRes.data.stock_locations).toHaveLength( + 2 + ) + expect(defaultSalesChannelFilterRes.data.stock_locations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: "warehouse" }), + expect.objectContaining({ name: "other place" }), + ]) + ) + + const physicalStoreSalesChannelFilterRes = await api.get( + `/admin/stock-locations?sales_channel_id=${sc2.id}`, + adminHeaders + ) + expect( + physicalStoreSalesChannelFilterRes.data.stock_locations + ).toHaveLength(1) + + expect(physicalStoreSalesChannelFilterRes.data.stock_locations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: "other place" }), + ]) + ) + }) }) }) }) diff --git a/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts b/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts index df848e3e33..5016dce32e 100644 --- a/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts +++ b/packages/medusa/src/api/routes/admin/stock-locations/list-stock-locations.ts @@ -148,6 +148,8 @@ export default async (req: Request, res: Response) => { const { filterableFields, listConfig } = req const { skip, take } = listConfig + const filterOnSalesChannel = !!filterableFields.sales_channel_id + const includeSalesChannels = !!listConfig.relations?.includes("sales_channels") @@ -157,6 +159,18 @@ export default async (req: Request, res: Response) => { ) } + if (filterOnSalesChannel) { + const ids: string[] = Array.isArray(filterableFields.sales_channel_id) + ? filterableFields.sales_channel_id + : [filterableFields.sales_channel_id] + + delete filterableFields.sales_channel_id + + const locationIds = await channelLocationService.listLocationIds(ids) + + filterableFields.id = [...new Set(locationIds.flat())] + } + let [locations, count] = await stockLocationService.listAndCount( filterableFields, listConfig @@ -193,4 +207,8 @@ export class AdminGetStockLocationsParams extends extendedFindParamsMixin({ @IsOptional() @IsType([String, [String]]) address_id?: string | string[] + + @IsOptional() + @IsType([String, [String]]) + sales_channel_id?: string | string[] } diff --git a/packages/medusa/src/services/sales-channel-location.ts b/packages/medusa/src/services/sales-channel-location.ts index 2c64cb3432..1eb3c54fdf 100644 --- a/packages/medusa/src/services/sales-channel-location.ts +++ b/packages/medusa/src/services/sales-channel-location.ts @@ -1,8 +1,9 @@ -import { EntityManager } from "typeorm" +import { EntityManager, In } from "typeorm" import { IStockLocationService, TransactionBaseService } from "../interfaces" import { EventBusService, SalesChannelService } from "./" import { SalesChannelLocation } from "../models/sales-channel-location" +import { MedusaError } from "medusa-core-utils" type InjectedDependencies = { stockLocationService: IStockLocationService @@ -97,13 +98,24 @@ class SalesChannelLocationService extends TransactionBaseService { * @param salesChannelId - The ID of the sales channel. * @returns A promise that resolves with an array of location IDs. */ - async listLocationIds(salesChannelId: string): Promise { - const salesChannel = await this.salesChannelService_ + async listLocationIds(salesChannelId: string | string[]): Promise { + const ids = Array.isArray(salesChannelId) + ? salesChannelId + : [salesChannelId] + + const [salesChannels, count] = await this.salesChannelService_ .withTransaction(this.activeManager_) - .retrieve(salesChannelId) + .listAndCount({ id: ids }, { select: ["id"], skip: 0 }) + + if (!count) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Sales channel with id: ${ids.join(", ")} was not found` + ) + } const locations = await this.activeManager_.find(SalesChannelLocation, { - where: { sales_channel_id: salesChannel.id }, + where: { sales_channel_id: In(salesChannels.map((sc) => sc.id)) }, select: ["location_id"], })