feat(medusa, medusa-js, medusa-react): Bulk add Products to a SalesChannel (#1833)
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel"
|
||||
|
||||
describe("POST /admin/sales-channels/:id/products/batch", () => {
|
||||
describe("add product to a sales channel", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/sales-channels/${IdMap.getId("sales_channel_1")}/products/batch`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
product_ids: [{ id: IdMap.getId("sales_channel_1_product_1") }]
|
||||
},
|
||||
flags: ["sales_channels"],
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls the retrieve method from the sales channel service", () => {
|
||||
expect(SalesChannelServiceMock.addProducts).toHaveBeenCalledTimes(1)
|
||||
expect(SalesChannelServiceMock.addProducts).toHaveBeenCalledWith(
|
||||
IdMap.getId("sales_channel_1"),
|
||||
[IdMap.getId("sales_channel_1_product_1")]
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Request, Response } from "express"
|
||||
import { SalesChannelService } from "../../../../services"
|
||||
import { IsArray, ValidateNested } from "class-validator"
|
||||
import { Type } from "class-transformer"
|
||||
import { ProductBatchSalesChannel } from "../../../../types/sales-channels"
|
||||
|
||||
/**
|
||||
* @oas [post] /sales-channels/{id}/products/batch
|
||||
* operationId: "PostSalesChannelsChannelProductsBatch"
|
||||
* summary: "Assign a batch of product to a sales channel"
|
||||
* description: "Assign a batch of product to a sales channel."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id=* {string} The id of the Sales channel.
|
||||
* - (body) product_ids=* {ProductBatchSalesChannel} The product ids that must be assigned to the sales channel.
|
||||
* tags:
|
||||
* - Sales Channel
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* sales_channel:
|
||||
* $ref: "#/components/schemas/sales_channel"
|
||||
*/
|
||||
export default async (req: Request, res: Response): Promise<void> => {
|
||||
const { id } = req.params
|
||||
|
||||
const salesChannelService: SalesChannelService = req.scope.resolve(
|
||||
"salesChannelService"
|
||||
)
|
||||
|
||||
const validatedBody =
|
||||
req.validatedBody as AdminPostSalesChannelsChannelProductsBatchReq
|
||||
const salesChannel = await salesChannelService.addProducts(
|
||||
id,
|
||||
validatedBody.product_ids.map((p) => p.id)
|
||||
)
|
||||
res.status(200).json({ sales_channel: salesChannel })
|
||||
}
|
||||
|
||||
export class AdminPostSalesChannelsChannelProductsBatchReq {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ProductBatchSalesChannel)
|
||||
product_ids: ProductBatchSalesChannel[]
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { AdminPostSalesChannelsSalesChannelReq } from "./update-sales-channel"
|
||||
import { AdminPostSalesChannelsReq } from "./create-sales-channel"
|
||||
import { AdminGetSalesChannelsParams } from "./list-sales-channels"
|
||||
import { AdminDeleteSalesChannelsChannelProductsBatchReq } from "./delete-products-batch"
|
||||
import { AdminPostSalesChannelsChannelProductsBatchReq } from "./add-product-batch"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -32,6 +33,11 @@ export default (app) => {
|
||||
"/",
|
||||
middlewares.wrap(require("./get-sales-channel").default)
|
||||
)
|
||||
salesChannelRouter.post(
|
||||
"/",
|
||||
transformBody(AdminPostSalesChannelsSalesChannelReq),
|
||||
middlewares.wrap(require("./update-sales-channel").default)
|
||||
)
|
||||
salesChannelRouter.delete(
|
||||
"/",
|
||||
middlewares.wrap(require("./delete-sales-channel").default)
|
||||
@@ -46,6 +52,11 @@ export default (app) => {
|
||||
transformBody(AdminDeleteSalesChannelsChannelProductsBatchReq),
|
||||
middlewares.wrap(require("./delete-products-batch").default)
|
||||
)
|
||||
salesChannelRouter.post(
|
||||
"/products/batch",
|
||||
transformBody(AdminPostSalesChannelsChannelProductsBatchReq),
|
||||
middlewares.wrap(require("./add-product-batch").default)
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/",
|
||||
@@ -53,12 +64,6 @@ export default (app) => {
|
||||
middlewares.wrap(require("./create-sales-channel").default)
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/:id",
|
||||
transformBody(AdminPostSalesChannelsSalesChannelReq),
|
||||
middlewares.wrap(require("./update-sales-channel").default)
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -68,10 +73,6 @@ export type AdminSalesChannelsRes = {
|
||||
|
||||
export type AdminSalesChannelsDeleteRes = DeleteResponse
|
||||
|
||||
export type AdminSalesChannelListRes = PaginatedResponse & {
|
||||
sales_channels: SalesChannel[]
|
||||
}
|
||||
|
||||
export type AdminSalesChannelsListRes = PaginatedResponse & {
|
||||
sales_channels: SalesChannel[]
|
||||
}
|
||||
@@ -82,3 +83,4 @@ export * from "./list-sales-channels"
|
||||
export * from "./update-sales-channel"
|
||||
export * from "./delete-sales-channel"
|
||||
export * from "./delete-products-batch"
|
||||
export * from "./add-product-batch"
|
||||
|
||||
@@ -44,4 +44,21 @@ export class SalesChannelRepository extends Repository<SalesChannel> {
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
|
||||
async addProducts(
|
||||
salesChannelId: string,
|
||||
productIds: string[]
|
||||
): Promise<void> {
|
||||
await this.createQueryBuilder()
|
||||
.insert()
|
||||
.into("product_sales_channel")
|
||||
.values(
|
||||
productIds.map((id) => ({
|
||||
sales_channel_id: salesChannelId,
|
||||
product_id: id,
|
||||
}))
|
||||
)
|
||||
.orIgnore()
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,10 @@ export const SalesChannelServiceMock = {
|
||||
removeProducts: jest.fn().mockImplementation((id, productIds) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
|
||||
addProducts: jest.fn().mockImplementation((id, productIds) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import { FindConditions, FindOneOptions } from "typeorm"
|
||||
import { SalesChannel } from "../../models"
|
||||
import { EventBusService, StoreService } from "../index"
|
||||
import SalesChannelService from "../sales-channel"
|
||||
import { EventBusServiceMock } from "../__mocks__/event-bus"
|
||||
import { store, StoreServiceMock } from "../__mocks__/store"
|
||||
import { EventBusService, ProductService, StoreService } from "../index"
|
||||
import { FindConditions, FindOneOptions } from "typeorm"
|
||||
import { SalesChannel } from "../../models"
|
||||
import { ProductServiceMock } from "../__mocks__/product";
|
||||
import { store, StoreServiceMock } from "../__mocks__/store";
|
||||
|
||||
describe("SalesChannelService", () => {
|
||||
const salesChannelData = {
|
||||
@@ -15,7 +16,10 @@ describe("SalesChannelService", () => {
|
||||
|
||||
const salesChannelRepositoryMock = {
|
||||
...MockRepository({
|
||||
findOne: jest.fn().mockImplementation((queryOrId: string | FindOneOptions<SalesChannel>): any => {
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(queryOrId: string | FindOneOptions<SalesChannel>): any => {
|
||||
return Promise.resolve({
|
||||
id:
|
||||
typeof queryOrId === "string"
|
||||
@@ -40,7 +44,7 @@ describe("SalesChannelService", () => {
|
||||
...salesChannel
|
||||
}),
|
||||
softRemove: jest.fn().mockImplementation((id: string): any => {
|
||||
return Promise.resolve()
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}),
|
||||
getFreeTextSearchResultsAndCount: jest.fn().mockImplementation(() =>
|
||||
@@ -49,9 +53,17 @@ describe("SalesChannelService", () => {
|
||||
id: IdMap.getId("sales_channel_1"),
|
||||
...salesChannelData
|
||||
},
|
||||
]),
|
||||
])
|
||||
),
|
||||
removeProducts: jest.fn().mockImplementation((id: string, productIds: string[]): any => {
|
||||
Promise.resolve([
|
||||
{
|
||||
id: IdMap.getId("sales_channel_1"),
|
||||
...salesChannelData
|
||||
},
|
||||
])
|
||||
}),
|
||||
addProducts: jest.fn().mockImplementation((id: string, productIds: string[]): any => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
@@ -62,6 +74,7 @@ describe("SalesChannelService", () => {
|
||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||
salesChannelRepository: salesChannelRepositoryMock,
|
||||
storeService: StoreServiceMock as unknown as StoreService,
|
||||
productService: ProductServiceMock as unknown as ProductService,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -84,6 +97,7 @@ describe("SalesChannelService", () => {
|
||||
manager: MockManager,
|
||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||
salesChannelRepository: salesChannelRepositoryMock,
|
||||
productService: ProductServiceMock as unknown as ProductService,
|
||||
storeService: {
|
||||
...StoreServiceMock,
|
||||
retrieve: jest.fn().mockImplementation(() => {
|
||||
@@ -116,6 +130,7 @@ describe("SalesChannelService", () => {
|
||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||
salesChannelRepository: salesChannelRepositoryMock,
|
||||
storeService: StoreServiceMock as unknown as StoreService,
|
||||
productService: ProductServiceMock as unknown as ProductService,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -146,6 +161,7 @@ describe("SalesChannelService", () => {
|
||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||
salesChannelRepository: salesChannelRepositoryMock,
|
||||
storeService: StoreServiceMock as unknown as StoreService,
|
||||
productService: ProductServiceMock as unknown as ProductService,
|
||||
})
|
||||
|
||||
const update = {
|
||||
@@ -181,6 +197,7 @@ describe("SalesChannelService", () => {
|
||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||
salesChannelRepository: salesChannelRepositoryMock,
|
||||
storeService: StoreServiceMock as unknown as StoreService,
|
||||
productService: ProductServiceMock as unknown as ProductService,
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -244,6 +261,7 @@ describe("SalesChannelService", () => {
|
||||
manager: MockManager,
|
||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||
salesChannelRepository: salesChannelRepositoryMock,
|
||||
productService: ProductServiceMock as unknown as ProductService,
|
||||
storeService: {
|
||||
...StoreServiceMock,
|
||||
retrieve: jest.fn().mockImplementation(() => {
|
||||
@@ -299,6 +317,7 @@ describe("SalesChannelService", () => {
|
||||
manager: MockManager,
|
||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||
salesChannelRepository: salesChannelRepositoryMock,
|
||||
productService: ProductServiceMock as unknown as ProductService,
|
||||
storeService: StoreServiceMock as unknown as StoreService,
|
||||
})
|
||||
|
||||
@@ -324,4 +343,36 @@ describe("SalesChannelService", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Add products", () => {
|
||||
const salesChannelService = new SalesChannelService({
|
||||
manager: MockManager,
|
||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||
salesChannelRepository: salesChannelRepositoryMock,
|
||||
storeService: StoreServiceMock as unknown as StoreService,
|
||||
productService: ProductServiceMock as unknown as ProductService,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should add a list of product to a sales channel', async () => {
|
||||
const salesChannel = await salesChannelService.addProducts(
|
||||
IdMap.getId("sales_channel_1"),
|
||||
[IdMap.getId("sales_channel_1_product_1")]
|
||||
)
|
||||
|
||||
expect(salesChannelRepositoryMock.addProducts).toHaveBeenCalledTimes(1)
|
||||
expect(salesChannelRepositoryMock.addProducts).toHaveBeenCalledWith(
|
||||
IdMap.getId("sales_channel_1"),
|
||||
[IdMap.getId("sales_channel_1_product_1")]
|
||||
)
|
||||
expect(salesChannel).toBeTruthy()
|
||||
expect(salesChannel).toEqual({
|
||||
id: IdMap.getId("sales_channel_1"),
|
||||
...salesChannelData,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,12 +12,15 @@ import {
|
||||
import { buildQuery } from "../utils"
|
||||
import EventBusService from "./event-bus"
|
||||
import StoreService from "./store"
|
||||
import { formatException, PostgresError } from "../utils/exception-formatter"
|
||||
import ProductService from "./product"
|
||||
|
||||
type InjectedDependencies = {
|
||||
salesChannelRepository: typeof SalesChannelRepository
|
||||
eventBusService: EventBusService
|
||||
manager: EntityManager
|
||||
storeService: StoreService
|
||||
productService: ProductService
|
||||
}
|
||||
|
||||
class SalesChannelService extends TransactionBaseService<SalesChannelService> {
|
||||
@@ -33,12 +36,14 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
|
||||
protected readonly salesChannelRepository_: typeof SalesChannelRepository
|
||||
protected readonly eventBusService_: EventBusService
|
||||
protected readonly storeService_: StoreService
|
||||
protected readonly productService_: ProductService
|
||||
|
||||
constructor({
|
||||
salesChannelRepository,
|
||||
eventBusService,
|
||||
manager,
|
||||
storeService,
|
||||
productService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -47,6 +52,7 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
|
||||
this.salesChannelRepository_ = salesChannelRepository
|
||||
this.eventBusService_ = eventBusService
|
||||
this.storeService_ = storeService
|
||||
this.productService_ = productService
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,6 +271,48 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
|
||||
return await this.retrieve(salesChannelId)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a batch of product to a sales channel
|
||||
* @param salesChannelId - The id of the sales channel on which to add the products
|
||||
* @param productIds - The products ids to attach to the sales channel
|
||||
* @return the sales channel on which the products have been added
|
||||
*/
|
||||
async addProducts(
|
||||
salesChannelId: string,
|
||||
productIds: string[]
|
||||
): Promise<SalesChannel | never> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager) => {
|
||||
const salesChannelRepo = transactionManager.getCustomRepository(
|
||||
this.salesChannelRepository_
|
||||
)
|
||||
|
||||
await salesChannelRepo.addProducts(salesChannelId, productIds)
|
||||
|
||||
return await this.retrieve(salesChannelId)
|
||||
},
|
||||
async (error: { code: string }) => {
|
||||
if (error.code === PostgresError.FOREIGN_KEY_ERROR) {
|
||||
const existingProducts = await this.productService_.list({
|
||||
id: productIds,
|
||||
})
|
||||
|
||||
const nonExistingProducts = productIds.filter(
|
||||
(cId) => existingProducts.findIndex((el) => el.id === cId) === -1
|
||||
)
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`The following product ids do not exist: ${JSON.stringify(
|
||||
nonExistingProducts.join(", ")
|
||||
)}`
|
||||
)
|
||||
}
|
||||
throw formatException(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SalesChannelService
|
||||
|
||||
Reference in New Issue
Block a user