From af7af737455daa0f330840a9678e6339e519dfe6 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 19 Jan 2024 15:54:40 +0100 Subject: [PATCH] feat(medusa,utils): added campaign get endpoints (#6125) what: adds endpoints for the following: - list endpoint (RESOLVES CORE-1682) - retrieve endpoint (RESOLVES CORE-1683) --- .changeset/empty-cherries-battle.md | 6 + .../promotion/admin/list-campaigns.spec.ts | 165 ++++++++++++++++++ .../promotion/admin/retrieve-campaign.spec.ts | 130 ++++++++++++++ .../src/api-v2/admin/campaigns/[id]/route.ts | 19 ++ .../src/api-v2/admin/campaigns/middlewares.ts | 35 ++++ .../api-v2/admin/campaigns/query-config.ts | 28 +++ .../src/api-v2/admin/campaigns/route.ts | 23 +++ .../src/api-v2/admin/campaigns/validators.ts | 17 ++ packages/medusa/src/api-v2/middlewares.ts | 6 +- .../promotion-module/campaign.spec.ts | 65 +++++++ .../src/services/promotion-module.ts | 21 +++ packages/types/src/promotion/service.ts | 6 + 12 files changed, 520 insertions(+), 1 deletion(-) create mode 100644 .changeset/empty-cherries-battle.md create mode 100644 integration-tests/plugins/__tests__/promotion/admin/list-campaigns.spec.ts create mode 100644 integration-tests/plugins/__tests__/promotion/admin/retrieve-campaign.spec.ts create mode 100644 packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts create mode 100644 packages/medusa/src/api-v2/admin/campaigns/middlewares.ts create mode 100644 packages/medusa/src/api-v2/admin/campaigns/query-config.ts create mode 100644 packages/medusa/src/api-v2/admin/campaigns/route.ts create mode 100644 packages/medusa/src/api-v2/admin/campaigns/validators.ts diff --git a/.changeset/empty-cherries-battle.md b/.changeset/empty-cherries-battle.md new file mode 100644 index 0000000000..ea285c5863 --- /dev/null +++ b/.changeset/empty-cherries-battle.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +feat(medusa,utils): added campaign get endpoints diff --git a/integration-tests/plugins/__tests__/promotion/admin/list-campaigns.spec.ts b/integration-tests/plugins/__tests__/promotion/admin/list-campaigns.spec.ts new file mode 100644 index 0000000000..65099ab551 --- /dev/null +++ b/integration-tests/plugins/__tests__/promotion/admin/list-campaigns.spec.ts @@ -0,0 +1,165 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import { CampaignBudgetType } from "@medusajs/utils" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +export const campaignsData = [ + { + id: "campaign-id-1", + name: "campaign 1", + description: "test description", + currency: "USD", + campaign_identifier: "test-1", + starts_at: new Date("01/01/2023"), + ends_at: new Date("01/01/2024"), + budget: { + type: CampaignBudgetType.SPEND, + limit: 1000, + used: 0, + }, + }, + { + id: "campaign-id-2", + name: "campaign 2", + description: "test description", + currency: "USD", + campaign_identifier: "test-2", + starts_at: new Date("01/01/2023"), + ends_at: new Date("01/01/2024"), + budget: { + type: CampaignBudgetType.USAGE, + limit: 1000, + used: 0, + }, + }, +] + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("GET /admin/campaigns", () => { + let dbConnection + let appContainer + let shutdownServer + let promotionModuleService: IPromotionModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + promotionModuleService = appContainer.resolve( + ModuleRegistrationName.PROMOTION + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await promotionModuleService.createCampaigns(campaignsData) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get all campaigns and its count", async () => { + const api = useApi() as any + const response = await api.get(`/admin/campaigns`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(2) + expect(response.data.campaigns).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + name: "campaign 1", + description: "test description", + currency: "USD", + campaign_identifier: "test-1", + starts_at: expect.any(String), + ends_at: expect.any(String), + budget: { + id: expect.any(String), + campaign: expect.any(Object), + type: "spend", + limit: 1000, + used: 0, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }), + expect.objectContaining({ + id: expect.any(String), + name: "campaign 2", + description: "test description", + currency: "USD", + campaign_identifier: "test-2", + starts_at: expect.any(String), + ends_at: expect.any(String), + budget: { + id: expect.any(String), + campaign: expect.any(Object), + type: "usage", + limit: 1000, + used: 0, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }), + ]) + ) + }) + + it("should get all campaigns and its count filtered", async () => { + const api = useApi() as any + const response = await api.get( + `/admin/campaigns?fields=name,created_at,budget.id`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(2) + expect(response.data.campaigns).toEqual([ + { + id: expect.any(String), + name: "campaign 1", + created_at: expect.any(String), + budget: { + id: expect.any(String), + campaign: expect.any(Object), + }, + }, + { + id: expect.any(String), + name: "campaign 2", + created_at: expect.any(String), + budget: { + id: expect.any(String), + campaign: expect.any(Object), + }, + }, + ]) + }) +}) diff --git a/integration-tests/plugins/__tests__/promotion/admin/retrieve-campaign.spec.ts b/integration-tests/plugins/__tests__/promotion/admin/retrieve-campaign.spec.ts new file mode 100644 index 0000000000..6a8236edb6 --- /dev/null +++ b/integration-tests/plugins/__tests__/promotion/admin/retrieve-campaign.spec.ts @@ -0,0 +1,130 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import { CampaignBudgetType } from "@medusajs/utils" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +export const campaignData = { + name: "campaign 1", + description: "test description", + currency: "USD", + campaign_identifier: "test-1", + starts_at: new Date("01/01/2023"), + ends_at: new Date("01/01/2024"), + budget: { + type: CampaignBudgetType.SPEND, + limit: 1000, + used: 0, + }, +} + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("GET /admin/campaigns", () => { + let dbConnection + let appContainer + let shutdownServer + let promotionModuleService: IPromotionModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + promotionModuleService = appContainer.resolve( + ModuleRegistrationName.PROMOTION + ) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + let campaigns + + beforeEach(async () => {}) + + it("should throw an error if id does not exist", async () => { + const api = useApi() as any + const { response } = await api + .get(`/admin/campaigns/does-not-exist`, adminHeaders) + .catch((e) => e) + + expect(response.status).toEqual(404) + expect(response.data.message).toEqual( + "Campaign with id: does-not-exist was not found" + ) + }) + + it("should get the requested campaign", async () => { + const createdCampaign = await promotionModuleService.createCampaigns( + campaignData + ) + + const api = useApi() as any + const response = await api.get( + `/admin/campaigns/${createdCampaign.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.campaign).toEqual({ + id: expect.any(String), + name: "campaign 1", + description: "test description", + currency: "USD", + campaign_identifier: "test-1", + starts_at: expect.any(String), + ends_at: expect.any(String), + budget: { + id: expect.any(String), + campaign: expect.any(Object), + type: "spend", + limit: 1000, + used: 0, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }) + }) + + it("should get the requested campaign with filtered fields and relations", async () => { + const createdCampaign = await promotionModuleService.createCampaigns( + campaignData + ) + + const api = useApi() as any + const response = await api.get( + `/admin/campaigns/${createdCampaign.id}?fields=name&expand=`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.campaign).toEqual({ + id: expect.any(String), + name: "campaign 1", + }) + }) +}) diff --git a/packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts b/packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts new file mode 100644 index 0000000000..8dd8c03983 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/campaigns/[id]/route.ts @@ -0,0 +1,19 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../../types/routing" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const promotionModuleService: IPromotionModuleService = req.scope.resolve( + ModuleRegistrationName.PROMOTION + ) + + const campaign = await promotionModuleService.retrieveCampaign( + req.params.id, + { + select: req.retrieveConfig.select, + relations: req.retrieveConfig.relations, + } + ) + + res.status(200).json({ campaign }) +} diff --git a/packages/medusa/src/api-v2/admin/campaigns/middlewares.ts b/packages/medusa/src/api-v2/admin/campaigns/middlewares.ts new file mode 100644 index 0000000000..dc2d679ec1 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/campaigns/middlewares.ts @@ -0,0 +1,35 @@ +import { MedusaV2Flag } from "@medusajs/utils" +import { isFeatureFlagEnabled, transformQuery } from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import * as QueryConfig from "./query-config" +import { + AdminGetCampaignsCampaignParams, + AdminGetCampaignsParams, +} from "./validators" + +export const adminCampaignRoutesMiddlewares: MiddlewareRoute[] = [ + { + matcher: "/admin/campaigns*", + middlewares: [isFeatureFlagEnabled(MedusaV2Flag.key)], + }, + { + method: ["GET"], + matcher: "/admin/campaigns", + middlewares: [ + transformQuery( + AdminGetCampaignsParams, + QueryConfig.listTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/campaigns/:id", + middlewares: [ + transformQuery( + AdminGetCampaignsCampaignParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api-v2/admin/campaigns/query-config.ts b/packages/medusa/src/api-v2/admin/campaigns/query-config.ts new file mode 100644 index 0000000000..7f94085645 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/campaigns/query-config.ts @@ -0,0 +1,28 @@ +export const defaultAdminCampaignRelations = ["budget"] +export const allowedAdminCampaignRelations = [ + ...defaultAdminCampaignRelations, + "promotions", +] +export const defaultAdminCampaignFields = [ + "name", + "description", + "currency", + "campaign_identifier", + "starts_at", + "ends_at", + "created_at", + "updated_at", + "deleted_at", +] + +export const retrieveTransformQueryConfig = { + defaultFields: defaultAdminCampaignFields, + defaultRelations: defaultAdminCampaignRelations, + allowedRelations: allowedAdminCampaignRelations, + isList: false, +} + +export const listTransformQueryConfig = { + ...retrieveTransformQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/campaigns/route.ts b/packages/medusa/src/api-v2/admin/campaigns/route.ts new file mode 100644 index 0000000000..35fbea1e14 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/campaigns/route.ts @@ -0,0 +1,23 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const promotionModuleService: IPromotionModuleService = req.scope.resolve( + ModuleRegistrationName.PROMOTION + ) + + const [campaigns, count] = await promotionModuleService.listAndCountCampaigns( + req.filterableFields, + req.listConfig + ) + + const { limit, offset } = req.validatedQuery + + res.json({ + count, + campaigns, + offset, + limit, + }) +} diff --git a/packages/medusa/src/api-v2/admin/campaigns/validators.ts b/packages/medusa/src/api-v2/admin/campaigns/validators.ts new file mode 100644 index 0000000000..cdd0676571 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/campaigns/validators.ts @@ -0,0 +1,17 @@ +import { IsOptional, IsString } from "class-validator" +import { FindParams, extendedFindParamsMixin } from "../../../types/common" + +export class AdminGetCampaignsCampaignParams extends FindParams {} + +export class AdminGetCampaignsParams extends extendedFindParamsMixin({ + limit: 100, + offset: 0, +}) { + @IsString() + @IsOptional() + campaign_identifier?: string + + @IsString() + @IsOptional() + currency?: string +} diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 0514e6f74a..9c6ec41da5 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -1,6 +1,10 @@ import { MiddlewaresConfig } from "../loaders/helpers/routing/types" +import { adminCampaignRoutesMiddlewares } from "./admin/campaigns/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" export const config: MiddlewaresConfig = { - routes: [...adminPromotionRoutesMiddlewares], + routes: [ + ...adminPromotionRoutesMiddlewares, + ...adminCampaignRoutesMiddlewares, + ], } diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts index 27ae0f5ab2..b54c7158fd 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts @@ -27,6 +27,71 @@ describe("Promotion Module Service: Campaigns", () => { await MikroOrmWrapper.clearDatabase() }) + describe("listAndCountCampaigns", () => { + beforeEach(async () => { + await createCampaigns(repositoryManager) + }) + + it("should return all campaigns and its count", async () => { + const [campaigns, count] = await service.listAndCountCampaigns() + + expect(count).toEqual(2) + expect(campaigns).toEqual([ + { + id: "campaign-id-1", + name: "campaign 1", + description: "test description", + currency: "USD", + campaign_identifier: "test-1", + starts_at: expect.any(Date), + ends_at: expect.any(Date), + budget: expect.any(String), + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + }, + { + id: "campaign-id-2", + name: "campaign 1", + description: "test description", + currency: "USD", + campaign_identifier: "test-2", + starts_at: expect.any(Date), + ends_at: expect.any(Date), + budget: expect.any(String), + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + }, + ]) + }) + + it("should return all campaigns based on config select and relations param", async () => { + const [campaigns, count] = await service.listAndCountCampaigns( + { + id: ["campaign-id-1"], + }, + { + relations: ["budget"], + select: ["name", "budget.limit"], + } + ) + + expect(count).toEqual(1) + expect(campaigns).toEqual([ + { + id: "campaign-id-1", + name: "campaign 1", + budget: { + id: expect.any(String), + campaign: expect.any(Object), + limit: 1000, + }, + }, + ]) + }) + }) + describe("createCampaigns", () => { it("should throw an error when required params are not passed", async () => { const error = await service diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index 8be752d1e6..56bae63e88 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -938,6 +938,27 @@ export default class PromotionModuleService< ) } + @InjectManager("baseRepository_") + async listAndCountCampaigns( + filters: PromotionTypes.FilterableCampaignProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[PromotionTypes.CampaignDTO[], number]> { + const [campaigns, count] = await this.campaignService_.listAndCount( + filters, + config, + sharedContext + ) + + return [ + await this.baseRepository_.serialize( + campaigns, + { populate: true } + ), + count, + ] + } + async createCampaigns( data: PromotionTypes.CreateCampaignDTO, sharedContext?: Context diff --git a/packages/types/src/promotion/service.ts b/packages/types/src/promotion/service.ts index 8f4d318275..1e2e0e2e77 100644 --- a/packages/types/src/promotion/service.ts +++ b/packages/types/src/promotion/service.ts @@ -116,6 +116,12 @@ export interface IPromotionModuleService extends IModuleService { sharedContext?: Context ): Promise + listAndCountCampaigns( + filters?: FilterableCampaignProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[CampaignDTO[], number]> + retrieveCampaign( id: string, config?: FindConfig,