diff --git a/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap index d0b7eeee11..580b079ce4 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap @@ -12,6 +12,16 @@ Object { } `; +exports[`sales channels POST /admin/sales-channels successfully creates a sales channel 1`] = ` +Object { + "sales_channel": ObjectContaining { + "description": "sales channel description", + "is_disabled": false, + "name": "sales channel name", + }, +} +`; + exports[`sales channels POST /admin/sales-channels/:id updates sales channel properties 1`] = ` Object { "created_at": Any, diff --git a/integration-tests/api/__tests__/admin/sales-channels.js b/integration-tests/api/__tests__/admin/sales-channels.js index a6d1cac161..0ee2c0c5a0 100644 --- a/integration-tests/api/__tests__/admin/sales-channels.js +++ b/integration-tests/api/__tests__/admin/sales-channels.js @@ -26,6 +26,7 @@ describe("sales channels", () => { const [process, connection] = await startServerWithEnvironment({ cwd, env: { MEDUSA_FF_SALES_CHANNELS: true }, + verbose: false, }) dbConnection = connection medusaProcess = process @@ -38,14 +39,6 @@ describe("sales channels", () => { medusaProcess.kill() }) - describe("GET /admin/sales-channels", () => { - it("is true", () => { - // dummy test to ensure test suite passes - expect(true).toBeTruthy() - }) - }) - describe("POST /admin/sales-channels", () => {}) - describe("GET /admin/sales-channels/:id", () => { let salesChannel @@ -136,5 +129,49 @@ describe("sales channels", () => { }) }) + + describe("POST /admin/sales-channels", () => { + beforeEach(async () => { + try { + await adminSeeder(dbConnection) + } catch (e) { + console.error(e) + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("successfully creates a sales channel", async () => { + const api = useApi() + + const newSalesChannel = { + name: "sales channel name", + description: "sales channel description", + } + + const response = await api + .post("/admin/sales-channels", newSalesChannel, adminReqConfig) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.sales_channel).toBeTruthy() + + expect(response.data).toMatchSnapshot({ + sales_channel: expect.objectContaining({ + name: newSalesChannel.name, + description: newSalesChannel.description, + is_disabled: false, + }), + }) + }) + }) + + describe("GET /admin/sales-channels/:id", () => {}) + describe("POST /admin/sales-channels/:id", () => {}) describe("DELETE /admin/sales-channels/:id", () => {}) }) diff --git a/packages/medusa-js/src/resources/admin/sales-channels.ts b/packages/medusa-js/src/resources/admin/sales-channels.ts index 0318812db9..46a78087bc 100644 --- a/packages/medusa-js/src/resources/admin/sales-channels.ts +++ b/packages/medusa-js/src/resources/admin/sales-channels.ts @@ -1,4 +1,5 @@ import { + AdminPostSalesChannelsReq, AdminSalesChannelsRes, AdminPostSalesChannelsSalesChannelReq, } from "@medusajs/medusa" @@ -20,10 +21,17 @@ class AdminSalesChannelsResource extends BaseResource { return this.client.request("GET", path, {}, {}, customHeaders) } - /* create( - payload: any, + /* * + * Create a medusa sales channel + * @returns the created channel + */ + create( + payload: AdminPostSalesChannelsReq, customHeaders: Record = {} - ): ResponsePromise {}*/ + ): ResponsePromise { + const path = `/admin/sales-channels` + return this.client.request("POST", path, payload, {}, customHeaders) + } /** update a sales channel * @experimental This feature is under development and may change in the future. diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index 4ae9140ff3..d8492a2cee 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -869,7 +869,7 @@ export const adminHandlers = [ discount_condition: { ...fixtures .get("discount") - .rule.conditions.find(c => c.id === req.params.conditionId), + .rule.conditions.find((c) => c.id === req.params.conditionId), }, }) ) @@ -1692,5 +1692,15 @@ export const adminHandlers = [ }, }) ) - }) + }), + + rest.post("/admin/sales-channels", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + sales_channel: fixtures.get("sales_channel"), + ...(req.body as Record), + }) + ) + }), ] diff --git a/packages/medusa-react/src/hooks/admin/sales-channels/mutations.ts b/packages/medusa-react/src/hooks/admin/sales-channels/mutations.ts index 7649001df4..3a4c3e093f 100644 --- a/packages/medusa-react/src/hooks/admin/sales-channels/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/sales-channels/mutations.ts @@ -1,16 +1,41 @@ -import { useMutation, UseMutationOptions, useQueryClient } from "react-query" import { + AdminPostSalesChannelsReq, AdminSalesChannelsRes, AdminPostSalesChannelsSalesChannelReq, } from "@medusajs/medusa" import { Response } from "@medusajs/medusa-js" +import { useMutation, UseMutationOptions, useQueryClient } from "react-query" + import { useMedusa } from "../../../contexts" import { buildOptions } from "../../utils/buildOptions" import { adminSalesChannelsKeys } from "./queries" +/** + * Hook provides a mutation function for creating sales channel. + * + * @experimental This feature is under development and may change in the future. + * To use this feature please enable the corresponding feature flag in your medusa backend project. + */ +export const useAdminCreateSalesChannel = ( + options?: UseMutationOptions< + Response, + Error, + AdminPostSalesChannelsReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: AdminPostSalesChannelsReq) => + client.admin.salesChannels.create(payload), + buildOptions(queryClient, [adminSalesChannelsKeys.list()], options) + ) +} + /** update a sales channel * @experimental This feature is under development and may change in the future. - * To use this feature please enable featureflag `sales_channels` in your medusa backend project. + * To use this feature please enable feature flag `sales_channels` in your medusa backend project. * @description updates a sales channel * @returns the updated medusa sales channel */ @@ -34,35 +59,3 @@ export const useAdminUpdateSalesChannel = ( ) ) } - -/*export const useAdminCreateSalesChannel = ( - options?: UseMutationOptions< - Response, - Error, - AdminPostSalesChannelsReq - > -) => { - const { client } = useMedusa() - const queryClient = useQueryClient() - return useMutation( - (payload: AdminPostSalesChannelsReq) => client.admin.salesChannels.create(payload), - buildOptions(queryClient, adminSalesChannelsKeys.lists(), options) - ) -}*/ - -/*export const useAdminDeleteSalesChannel = ( - id: string, - options?: UseMutationOptions, Error, void> -) => { - const { client } = useMedusa() - const queryClient = useQueryClient() - - return useMutation( - () => client.admin.salesChannels.delete(id), - buildOptions( - queryClient, - [adminSalesChannelsKeys.lists(), adminSalesChannelsKeys.detail(id)], - options - ) - ) -}*/ diff --git a/packages/medusa-react/src/hooks/admin/sales-channels/queries.ts b/packages/medusa-react/src/hooks/admin/sales-channels/queries.ts index 0384fd76d8..c6a1f7e28c 100644 --- a/packages/medusa-react/src/hooks/admin/sales-channels/queries.ts +++ b/packages/medusa-react/src/hooks/admin/sales-channels/queries.ts @@ -15,7 +15,7 @@ type SalesChannelsQueryKeys = typeof adminSalesChannelsKeys /** retrieve a sales channel * @experimental This feature is under development and may change in the future. - * To use this feature please enable featureflag `sales_channels` in your medusa backend project. + * To use this feature please enable feature flag `sales_channels` in your medusa backend project. * @description gets a sales channel * @returns a medusa sales channel */ diff --git a/packages/medusa-react/test/hooks/admin/sales-channels/mutations.test.ts b/packages/medusa-react/test/hooks/admin/sales-channels/mutations.test.ts index b5f5a13393..5b7c4f1a52 100644 --- a/packages/medusa-react/test/hooks/admin/sales-channels/mutations.test.ts +++ b/packages/medusa-react/test/hooks/admin/sales-channels/mutations.test.ts @@ -1,9 +1,35 @@ -import { useAdminUpdateSalesChannel } from "../../../../src" import { renderHook } from "@testing-library/react-hooks" + +import { useAdminCreateSalesChannel, useAdminUpdateSalesChannel } from "../../../../src" import { fixtures } from "../../../../mocks/data" import { createWrapper } from "../../../utils" -describe("useAdminUpdateStore hook", () => { +describe("useAdminCreateSalesChannel hook", () => { + test("returns a sales channel", async () => { + const salesChannel = { + name: "sales channel 1 name", + description: "sales channel 1 description", + } + + const { result, waitFor } = renderHook(() => useAdminCreateSalesChannel(), { + wrapper: createWrapper(), + }) + + result.current.mutate(salesChannel) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.sales_channel).toEqual( + expect.objectContaining({ + ...fixtures.get("sales_channel"), + ...salesChannel, + }) + ) + }) +}) + +describe("useAdminUpdateSalesChannel hook", () => { test("updates a store", async () => { const salesChannel = { name: "medusa sales channel", diff --git a/packages/medusa-react/test/hooks/admin/sales-channels/queries.test.ts b/packages/medusa-react/test/hooks/admin/sales-channels/queries.test.ts index c4b9bbf038..35bc5b29f8 100644 --- a/packages/medusa-react/test/hooks/admin/sales-channels/queries.test.ts +++ b/packages/medusa-react/test/hooks/admin/sales-channels/queries.test.ts @@ -6,9 +6,12 @@ import { createWrapper } from "../../../utils" describe("useAdminSalesChannel hook", () => { test("returns a product", async () => { const salesChannel = fixtures.get("sales_channel") - const { result, waitFor } = renderHook(() => useAdminSalesChannel(salesChannel.id), { - wrapper: createWrapper(), - }) + const { result, waitFor } = renderHook( + () => useAdminSalesChannel(salesChannel.id), + { + wrapper: createWrapper(), + } + ) await waitFor(() => result.current.isSuccess) diff --git a/packages/medusa/src/api/routes/admin/sales-channels/__tests__/create-sales-channel.js b/packages/medusa/src/api/routes/admin/sales-channels/__tests__/create-sales-channel.js new file mode 100644 index 0000000000..12c033d814 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/sales-channels/__tests__/create-sales-channel.js @@ -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", () => { + describe("successfully get a sales channel", () => { + let subject + + beforeAll(async () => { + subject = await request("POST", `/admin/sales-channels`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + payload: { + name: "sales channel 1 name", + description: "sales channel 1 description", + }, + flags: ["sales_channels"], + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls the create method from the sales channel service", () => { + expect(SalesChannelServiceMock.create).toHaveBeenCalledTimes(1) + expect(SalesChannelServiceMock.create).toHaveBeenCalledWith( + expect.objectContaining({ + name: "sales channel 1 name", + description: "sales channel 1 description", + }) + ) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/sales-channels/create-sales-channel.ts b/packages/medusa/src/api/routes/admin/sales-channels/create-sales-channel.ts new file mode 100644 index 0000000000..dd18fb9c9f --- /dev/null +++ b/packages/medusa/src/api/routes/admin/sales-channels/create-sales-channel.ts @@ -0,0 +1,47 @@ +import { Request, Response } from "express" +import { IsObject, IsOptional, IsString } from "class-validator" + +import SalesChannelService from "../../../../services/sales-channel" +import { CreateSalesChannelInput } from "../../../../types/sales-channels" + +/** + * @oas [post] /sales-channels + * operationId: "PostSalesChannels" + * summary: "Create a sales channel" + * description: "Creates a sales channel." + * x-authenticated: true + * parameters: + * - (body) name=* {string} Name of the sales channel + * - (body) description=* {string} Description of the sales channel + * tags: + * - Sales Channels + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * sales_channel: + * $ref: "#/components/schemas/sales_channel" + */ + +export default async (req: Request, res: Response) => { + const salesChannelService: SalesChannelService = req.scope.resolve( + "salesChannelService" + ) + + const salesChannel = await salesChannelService.create( + req.validatedBody as CreateSalesChannelInput + ) + res.status(200).json({ sales_channel: salesChannel }) +} + +export class AdminPostSalesChannelsReq { + @IsString() + name: string + + @IsString() + @IsOptional() + description: string +} diff --git a/packages/medusa/src/api/routes/admin/sales-channels/get-sales-channel.ts b/packages/medusa/src/api/routes/admin/sales-channels/get-sales-channel.ts index 3484e5b8f1..b16d505f38 100644 --- a/packages/medusa/src/api/routes/admin/sales-channels/get-sales-channel.ts +++ b/packages/medusa/src/api/routes/admin/sales-channels/get-sales-channel.ts @@ -19,7 +19,7 @@ import SalesChannelService from "../../../../services/sales-channel" * schema: * properties: * sales_channel: - * $ref: "#/components/schemas/sales-channel" + * $ref: "#/components/schemas/sales_channel" */ export default async (req: Request, res: Response): Promise => { const { id } = req.params diff --git a/packages/medusa/src/api/routes/admin/sales-channels/index.ts b/packages/medusa/src/api/routes/admin/sales-channels/index.ts index 67a1831a53..b72f0b69b5 100644 --- a/packages/medusa/src/api/routes/admin/sales-channels/index.ts +++ b/packages/medusa/src/api/routes/admin/sales-channels/index.ts @@ -5,6 +5,7 @@ import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import { SalesChannel } from "../../../../models" import middlewares, { transformBody } from "../../../middlewares" import { AdminPostSalesChannelsSalesChannelReq } from "./update-sales-channel" +import { AdminPostSalesChannelsReq } from "./create-sales-channel" const route = Router() @@ -21,7 +22,11 @@ export default (app) => { route.get("/", (req, res) => {}) - route.post("/", (req, res) => {}) + route.post( + "/", + transformBody(AdminPostSalesChannelsReq), + middlewares.wrap(require("./create-sales-channel").default) + ) route.post( "/:id", @@ -45,6 +50,7 @@ export type AdminSalesChannelListRes = PaginatedResponse & { } export * from "./get-sales-channel" +export * from "./create-sales-channel" // export * from './' // export * from './' export * from "./update-sales-channel" diff --git a/packages/medusa/src/services/__mocks__/sales-channel.js b/packages/medusa/src/services/__mocks__/sales-channel.js index f62d45d5a7..62fec39465 100644 --- a/packages/medusa/src/services/__mocks__/sales-channel.js +++ b/packages/medusa/src/services/__mocks__/sales-channel.js @@ -17,9 +17,14 @@ export const SalesChannelServiceMock = { listAndCount: jest.fn().mockImplementation(() => {}), - create: jest.fn().mockImplementation(() => {}), - delete: jest.fn().mockImplementation(() => {}), + + create: jest.fn().mockImplementation((data) => { + return Promise.resolve({ + id: id, + ...data, + }) + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/sales-channel.ts b/packages/medusa/src/services/sales-channel.ts index 2e1a22923f..69d9ea463b 100644 --- a/packages/medusa/src/services/sales-channel.ts +++ b/packages/medusa/src/services/sales-channel.ts @@ -1,5 +1,6 @@ -import { EntityManager } from "typeorm" import { MedusaError } from "medusa-core-utils" +import { EntityManager } from "typeorm" + import { TransactionBaseService } from "../interfaces" import { SalesChannel } from "../models" import { SalesChannelRepository } from "../repositories/sales-channel" @@ -10,6 +11,7 @@ import { } from "../types/sales-channels" import EventBusService from "./event-bus" import { buildQuery } from "../utils" +import { PostgresError } from "../utils/exception-formatter" type InjectedDependencies = { salesChannelRepository: typeof SalesChannelRepository @@ -20,6 +22,7 @@ type InjectedDependencies = { class SalesChannelService extends TransactionBaseService { static Events = { UPDATED: "sales_channel.updated", + CREATED: "sales_channel.created", } protected manager_: EntityManager @@ -41,6 +44,13 @@ class SalesChannelService extends TransactionBaseService { this.eventBusService_ = eventBusService } + /** + * Retrieve a SalesChannel by id + * + * @experimental This feature is under development and may change in the future. + * To use this feature please enable the corresponding feature flag in your medusa backend project. + * @returns a sales channel + */ async retrieve( salesChannelId: string, config: FindConfig = {} @@ -77,8 +87,28 @@ class SalesChannelService extends TransactionBaseService { throw new Error("Method not implemented.") } - async create(data: CreateSalesChannelInput): Promise { - throw new Error("Method not implemented.") + /** + * Creates a SalesChannel + * + * @experimental This feature is under development and may change in the future. + * To use this feature please enable the corresponding feature flag in your medusa backend project. + * @returns the created channel + */ + async create(data: CreateSalesChannelInput): Promise { + return await this.atomicPhase_(async (manager) => { + const salesChannelRepo: SalesChannelRepository = + manager.getCustomRepository(this.salesChannelRepository_) + + const salesChannel = salesChannelRepo.create(data) + + await this.eventBusService_ + .withTransaction(manager) + .emit(SalesChannelService.Events.CREATED, { + id: salesChannel.id, + }) + + return await salesChannelRepo.save(salesChannel) + }) } async update(