diff --git a/.changeset/little-cobras-doubt.md b/.changeset/little-cobras-doubt.md new file mode 100644 index 0000000000000..8b9ebca5d4e1c --- /dev/null +++ b/.changeset/little-cobras-doubt.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/types": patch +--- + +feat(medusa,types): add promotion list and get endpoint diff --git a/integration-tests/plugins/__tests__/promotion/admin/list-promotions.spec.ts b/integration-tests/plugins/__tests__/promotion/admin/list-promotions.spec.ts new file mode 100644 index 0000000000000..129d19c83d573 --- /dev/null +++ b/integration-tests/plugins/__tests__/promotion/admin/list-promotions.spec.ts @@ -0,0 +1,122 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import { PromotionType } 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" + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("GET /admin/promotions", () => { + 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() + }) + + it("should get all promotions and its count", async () => { + await promotionModuleService.create([ + { + code: "TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "order", + value: "100", + }, + }, + ]) + + const api = useApi() as any + const response = await api.get(`/admin/promotions`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.promotions).toEqual([ + expect.objectContaining({ + id: expect.any(String), + code: "TEST", + campaign: null, + is_automatic: false, + type: "standard", + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + application_method: expect.objectContaining({ + id: expect.any(String), + value: 100, + type: "fixed", + target_type: "order", + allocation: null, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }), + }), + ]) + }) + + it("should get all promotions and its count filtered", async () => { + const [createdPromotion] = await promotionModuleService.create([ + { + code: "TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "order", + value: "100", + }, + }, + ]) + + const api = useApi() as any + const response = await api.get( + `/admin/promotions?fields=code,created_at,application_method.id`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.promotions).toEqual([ + { + id: expect.any(String), + code: "TEST", + created_at: expect.any(String), + application_method: { + id: expect.any(String), + promotion: expect.any(Object), + }, + }, + ]) + }) +}) diff --git a/integration-tests/plugins/__tests__/promotion/admin/retrieve-promotion.spec.ts b/integration-tests/plugins/__tests__/promotion/admin/retrieve-promotion.spec.ts new file mode 100644 index 0000000000000..f0cc7301f0e76 --- /dev/null +++ b/integration-tests/plugins/__tests__/promotion/admin/retrieve-promotion.spec.ts @@ -0,0 +1,124 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPromotionModuleService } from "@medusajs/types" +import { PromotionType } 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" + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("GET /admin/promotions", () => { + 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() + }) + + it("should throw an error if id does not exist", async () => { + const api = useApi() as any + const { response } = await api + .get(`/admin/promotions/does-not-exist`, adminHeaders) + .catch((e) => e) + + expect(response.status).toEqual(404) + expect(response.data.message).toEqual( + "Promotion with id: does-not-exist was not found" + ) + }) + + it("should get the requested promotion", async () => { + const createdPromotion = await promotionModuleService.create({ + code: "TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "order", + value: "100", + }, + }) + + const api = useApi() as any + const response = await api.get( + `/admin/promotions/${createdPromotion.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.promotion).toEqual({ + id: expect.any(String), + code: "TEST", + campaign: null, + is_automatic: false, + type: "standard", + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + application_method: { + id: expect.any(String), + promotion: expect.any(Object), + value: 100, + type: "fixed", + target_type: "order", + max_quantity: 0, + allocation: null, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }, + }) + }) + + it("should get the requested promotion with filtered fields and relations", async () => { + const createdPromotion = await promotionModuleService.create({ + code: "TEST", + type: PromotionType.STANDARD, + application_method: { + type: "fixed", + target_type: "order", + value: "100", + }, + }) + + const api = useApi() as any + const response = await api.get( + `/admin/promotions/${createdPromotion.id}?fields=id,code&expand=`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.promotion).toEqual({ + id: expect.any(String), + code: "TEST", + }) + }) +}) diff --git a/integration-tests/plugins/medusa-config.js b/integration-tests/plugins/medusa-config.js index ebdd77b7b22dc..6927791ed71dc 100644 --- a/integration-tests/plugins/medusa-config.js +++ b/integration-tests/plugins/medusa-config.js @@ -66,5 +66,10 @@ module.exports = { resources: "shared", resolve: "@medusajs/pricing", }, + [Modules.PROMOTION]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/promotion", + }, }, } diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index 35a2ab67ecce5..b2b3a47a4b93e 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -16,6 +16,8 @@ "@medusajs/modules-sdk": "workspace:^", "@medusajs/pricing": "workspace:^", "@medusajs/product": "workspace:^", + "@medusajs/promotion": "workspace:^", + "@medusajs/utils": "workspace:^", "faker": "^5.5.3", "medusa-fulfillment-webshipper": "workspace:*", "medusa-interfaces": "workspace:*", @@ -27,6 +29,7 @@ "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/node": "^7.12.10", + "@medusajs/types": "workspace:^", "babel-preset-medusa-package": "*", "jest": "^26.6.3", "jest-environment-node": "26.6.2" diff --git a/packages/medusa/src/api-v2/admin/promotions/[id]/route.ts b/packages/medusa/src/api-v2/admin/promotions/[id]/route.ts new file mode 100644 index 0000000000000..8cdc90e91f9b3 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/promotions/[id]/route.ts @@ -0,0 +1,16 @@ +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 promotion = await promotionModuleService.retrieve(req.params.id, { + select: req.retrieveConfig.select, + relations: req.retrieveConfig.relations, + }) + + res.status(200).json({ promotion }) +} diff --git a/packages/medusa/src/api-v2/admin/promotions/middlewares.ts b/packages/medusa/src/api-v2/admin/promotions/middlewares.ts new file mode 100644 index 0000000000000..b69c7705fbf58 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/promotions/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 { + AdminGetPromotionsParams, + AdminGetPromotionsPromotionParams, +} from "./validators" + +export const adminPromotionRoutesMiddlewares: MiddlewareRoute[] = [ + { + matcher: "/admin/promotions*", + middlewares: [isFeatureFlagEnabled(MedusaV2Flag.key)], + }, + { + method: ["GET"], + matcher: "/admin/promotions", + middlewares: [ + transformQuery( + AdminGetPromotionsParams, + QueryConfig.listTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/promotions/:id", + middlewares: [ + transformQuery( + AdminGetPromotionsPromotionParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api-v2/admin/promotions/query-config.ts b/packages/medusa/src/api-v2/admin/promotions/query-config.ts new file mode 100644 index 0000000000000..06633e78f2fb4 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/promotions/query-config.ts @@ -0,0 +1,26 @@ +export const defaultAdminPromotionRelations = ["campaign", "application_method"] +export const allowedAdminPromotionRelations = [ + ...defaultAdminPromotionRelations, +] +export const defaultAdminPromotionFields = [ + "id", + "code", + "campaign", + "is_automatic", + "type", + "created_at", + "updated_at", + "deleted_at", +] + +export const retrieveTransformQueryConfig = { + defaultFields: defaultAdminPromotionFields, + defaultRelations: defaultAdminPromotionRelations, + allowedRelations: allowedAdminPromotionRelations, + isList: false, +} + +export const listTransformQueryConfig = { + ...retrieveTransformQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/promotions/route.ts b/packages/medusa/src/api-v2/admin/promotions/route.ts new file mode 100644 index 0000000000000..ee642be0dff5b --- /dev/null +++ b/packages/medusa/src/api-v2/admin/promotions/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 [promotions, count] = await promotionModuleService.listAndCount( + req.filterableFields, + req.listConfig + ) + + const { limit, offset } = req.validatedQuery + + res.json({ + count, + promotions, + offset, + limit, + }) +} diff --git a/packages/medusa/src/api-v2/admin/promotions/validators.ts b/packages/medusa/src/api-v2/admin/promotions/validators.ts new file mode 100644 index 0000000000000..20068f61c820f --- /dev/null +++ b/packages/medusa/src/api-v2/admin/promotions/validators.ts @@ -0,0 +1,13 @@ +import { IsOptional, IsString } from "class-validator" +import { FindParams, extendedFindParamsMixin } from "../../../types/common" + +export class AdminGetPromotionsPromotionParams extends FindParams {} + +export class AdminGetPromotionsParams extends extendedFindParamsMixin({ + limit: 100, + offset: 0, +}) { + @IsString() + @IsOptional() + code?: string +} diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts new file mode 100644 index 0000000000000..0514e6f74a48d --- /dev/null +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -0,0 +1,6 @@ +import { MiddlewaresConfig } from "../loaders/helpers/routing/types" +import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" + +export const config: MiddlewaresConfig = { + routes: [...adminPromotionRoutesMiddlewares], +} diff --git a/packages/medusa/src/api/middlewares/index.ts b/packages/medusa/src/api/middlewares/index.ts index 83e83ad8d8669..f34c92648e600 100644 --- a/packages/medusa/src/api/middlewares/index.ts +++ b/packages/medusa/src/api/middlewares/index.ts @@ -6,11 +6,12 @@ import { default as requireCustomerAuthentication } from "./require-customer-aut export { default as authenticate } from "./authenticate" export { default as authenticateCustomer } from "./authenticate-customer" -export { default as errorHandler } from "./error-handler" export { default as wrapHandler } from "./await-middleware" export { canAccessBatchJob } from "./batch-job/can-access-batch-job" export { getRequestedBatchJob } from "./batch-job/get-requested-batch-job" export { doesConditionBelongToDiscount } from "./discount/does-condition-belong-to-discount" +export { default as errorHandler } from "./error-handler" +export { isFeatureFlagEnabled } from "./feature-flag-enabled" export { default as normalizeQuery } from "./normalized-query" export { default as requireCustomerAuthentication } from "./require-customer-authentication" export { transformBody } from "./transform-body" diff --git a/packages/medusa/src/loaders/api.ts b/packages/medusa/src/loaders/api.ts index 5e36090898b0d..60b91c585129b 100644 --- a/packages/medusa/src/loaders/api.ts +++ b/packages/medusa/src/loaders/api.ts @@ -1,8 +1,8 @@ +import { AwilixContainer } from "awilix" +import bodyParser from "body-parser" import { Express } from "express" import qs from "qs" -import bodyParser from "body-parser" import routes from "../api" -import { AwilixContainer } from "awilix" import { ConfigModule } from "../types/global" type Options = { diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 4fcaa8931598b..308879a10a061 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -13,6 +13,7 @@ import { asValue } from "awilix" import { createMedusaContainer } from "medusa-core-utils" import { track } from "medusa-telemetry" import { EOL } from "os" +import path from "path" import requestIp from "request-ip" import { Connection } from "typeorm" import { MedusaContainer } from "../types/global" @@ -21,6 +22,7 @@ import loadConfig from "./config" import defaultsLoader from "./defaults" import expressLoader from "./express" import featureFlagsLoader from "./feature-flags" +import { RoutesLoader } from "./helpers/routing" import Logger from "./logger" import loadMedusaApp, { mergeDefaultModules } from "./medusa-app" import modelsLoader from "./models" @@ -195,6 +197,22 @@ export default async ({ next() }) + // TODO: Figure out why this is causing issues with test when placed inside ./api.ts + // Adding this here temporarily + // Test: (packages/medusa/src/api/routes/admin/currencies/update-currency.ts) + try { + /** + * Register the Medusa CORE API routes using the file based routing. + */ + await new RoutesLoader({ + app: expressApp, + rootDir: path.join(__dirname, "../api-v2"), + configModule, + }).load() + } catch (err) { + throw Error("An error occurred while registering Medusa Core API Routes") + } + const pluginsActivity = Logger.activity(`Initializing plugins${EOL}`) track("PLUGINS_INIT_STARTED") await pluginsLoader({ diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts index 72edf031e9a0b..1a07483b8b436 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts @@ -1,5 +1,9 @@ import { IPromotionModuleService } from "@medusajs/types" -import { CampaignBudgetType, PromotionType } from "@medusajs/utils" +import { + ApplicationMethodType, + CampaignBudgetType, + PromotionType, +} from "@medusajs/utils" import { SqlEntityManager } from "@mikro-orm/postgresql" import { initialize } from "../../../../src" import { createCampaigns } from "../../../__fixtures__/campaigns" @@ -680,6 +684,83 @@ describe("Promotion Service", () => { }) }) + describe("listAndCount", () => { + beforeEach(async () => { + await createPromotions(repositoryManager, [ + { + id: "promotion-id-1", + code: "PROMOTION_1", + type: PromotionType.STANDARD, + application_method: { + type: ApplicationMethodType.FIXED, + value: "200", + target_type: "items", + }, + }, + { + id: "promotion-id-2", + code: "PROMOTION_2", + type: PromotionType.STANDARD, + }, + ]) + }) + + it("should return all promotions and count", async () => { + const [promotions, count] = await service.listAndCount() + + expect(count).toEqual(2) + expect(promotions).toEqual([ + { + id: "promotion-id-1", + code: "PROMOTION_1", + campaign: null, + is_automatic: false, + type: "standard", + application_method: expect.any(String), + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + }, + { + id: "promotion-id-2", + code: "PROMOTION_2", + campaign: null, + is_automatic: false, + type: "standard", + application_method: null, + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + }, + ]) + }) + + it("should return all promotions based on config select and relations param", async () => { + const [promotions, count] = await service.listAndCount( + { + id: ["promotion-id-1"], + }, + { + relations: ["application_method"], + select: ["code", "application_method.type"], + } + ) + + expect(count).toEqual(1) + expect(promotions).toEqual([ + { + id: "promotion-id-1", + code: "PROMOTION_1", + application_method: { + id: expect.any(String), + promotion: expect.any(Object), + type: "fixed", + }, + }, + ]) + }) + }) + describe("delete", () => { beforeEach(async () => { await createPromotions(repositoryManager) diff --git a/packages/promotion/src/migrations/.snapshot-medusa-promotion.json b/packages/promotion/src/migrations/.snapshot-medusa-promotion.json index 70f2559a81453..5fb5f4530512b 100644 --- a/packages/promotion/src/migrations/.snapshot-medusa-promotion.json +++ b/packages/promotion/src/migrations/.snapshot-medusa-promotion.json @@ -2,6 +2,247 @@ "namespaces": ["public"], "name": "public", "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "name": { + "name": "name", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "description": { + "name": "description", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "currency": { + "name": "currency", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "campaign_identifier": { + "name": "campaign_identifier", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "starts_at": { + "name": "starts_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "ends_at": { + "name": "ends_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "campaign", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_campaign_identifier_unique", + "columnNames": ["campaign_identifier"], + "composite": false, + "primary": false, + "unique": true + }, + { + "keyName": "campaign_pkey", + "columnNames": ["id"], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "type": { + "name": "type", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "enumItems": ["spend", "usage"], + "mappedType": "enum" + }, + "campaign_id": { + "name": "campaign_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "limit": { + "name": "limit", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "default": "null", + "mappedType": "decimal" + }, + "used": { + "name": "used", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "decimal" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "campaign_budget", + "schema": "public", + "indexes": [ + { + "columnNames": ["type"], + "composite": false, + "keyName": "IDX_campaign_budget_type", + "primary": false, + "unique": false + }, + { + "columnNames": ["campaign_id"], + "composite": false, + "keyName": "campaign_budget_campaign_id_unique", + "primary": false, + "unique": true + }, + { + "keyName": "campaign_budget_pkey", + "columnNames": ["id"], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "campaign_budget_campaign_id_foreign": { + "constraintName": "campaign_budget_campaign_id_foreign", + "columnNames": ["campaign_id"], + "localTableName": "public.campaign_budget", + "referencedColumnNames": ["id"], + "referencedTableName": "public.campaign", + "updateRule": "cascade" + } + } + }, { "columns": { "id": { @@ -22,6 +263,15 @@ "nullable": false, "mappedType": "text" }, + "campaign_id": { + "name": "campaign_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, "is_automatic": { "name": "is_automatic", "type": "boolean", @@ -108,7 +358,17 @@ } ], "checks": [], - "foreignKeys": {} + "foreignKeys": { + "promotion_campaign_id_foreign": { + "constraintName": "promotion_campaign_id_foreign", + "columnNames": ["campaign_id"], + "localTableName": "public.promotion", + "referencedColumnNames": ["id"], + "referencedTableName": "public.campaign", + "deleteRule": "set null", + "updateRule": "cascade" + } + } }, { "columns": { @@ -156,7 +416,7 @@ "autoincrement": false, "primary": false, "nullable": false, - "enumItems": ["order", "shipping", "item"], + "enumItems": ["order", "shipping_methods", "items"], "mappedType": "enum" }, "allocation": { diff --git a/packages/promotion/src/migrations/Migration20240102130345.ts b/packages/promotion/src/migrations/Migration20240117090706.ts similarity index 65% rename from packages/promotion/src/migrations/Migration20240102130345.ts rename to packages/promotion/src/migrations/Migration20240117090706.ts index 878399a637c5e..09b3edd6a68a4 100644 --- a/packages/promotion/src/migrations/Migration20240102130345.ts +++ b/packages/promotion/src/migrations/Migration20240117090706.ts @@ -1,9 +1,26 @@ import { Migration } from "@mikro-orm/migrations" -export class Migration20240102130345 extends Migration { +export class Migration20240117090706 extends Migration { async up(): Promise { this.addSql( - 'create table "promotion" ("id" text not null, "code" text not null, "is_automatic" boolean not null default false, "type" text check ("type" in (\'standard\', \'buyget\')) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_pkey" primary key ("id"));' + 'create table "campaign" ("id" text not null, "name" text not null, "description" text null, "currency" text null, "campaign_identifier" text not null, "starts_at" timestamptz null, "ends_at" timestamptz null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "campaign_pkey" primary key ("id"));' + ) + this.addSql( + 'alter table "campaign" add constraint "IDX_campaign_identifier_unique" unique ("campaign_identifier");' + ) + + this.addSql( + 'create table "campaign_budget" ("id" text not null, "type" text check ("type" in (\'spend\', \'usage\')) not null, "campaign_id" text not null, "limit" numeric null default null, "used" numeric not null default 0, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "campaign_budget_pkey" primary key ("id"));' + ) + this.addSql( + 'create index "IDX_campaign_budget_type" on "campaign_budget" ("type");' + ) + this.addSql( + 'alter table "campaign_budget" add constraint "campaign_budget_campaign_id_unique" unique ("campaign_id");' + ) + + this.addSql( + 'create table "promotion" ("id" text not null, "code" text not null, "campaign_id" text null, "is_automatic" boolean not null default false, "type" text check ("type" in (\'standard\', \'buyget\')) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "promotion_pkey" primary key ("id"));' ) this.addSql('create index "IDX_promotion_code" on "promotion" ("code");') this.addSql('create index "IDX_promotion_type" on "promotion" ("type");') @@ -12,7 +29,7 @@ export class Migration20240102130345 extends Migration { ) this.addSql( - 'create table "application_method" ("id" text not null, "value" numeric null, "max_quantity" numeric null, "type" text check ("type" in (\'fixed\', \'percentage\')) not null, "target_type" text check ("target_type" in (\'order\', \'shipping\', \'item\')) not null, "allocation" text check ("allocation" in (\'each\', \'across\')) null, "promotion_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "application_method_pkey" primary key ("id"));' + 'create table "application_method" ("id" text not null, "value" numeric null, "max_quantity" numeric null, "type" text check ("type" in (\'fixed\', \'percentage\')) not null, "target_type" text check ("target_type" in (\'order\', \'shipping_methods\', \'items\')) not null, "allocation" text check ("allocation" in (\'each\', \'across\')) null, "promotion_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "application_method_pkey" primary key ("id"));' ) this.addSql( 'create index "IDX_application_method_type" on "application_method" ("type");' @@ -52,6 +69,14 @@ export class Migration20240102130345 extends Migration { 'create index "IDX_promotion_rule_promotion_rule_value_id" on "promotion_rule_value" ("promotion_rule_id");' ) + this.addSql( + 'alter table "campaign_budget" add constraint "campaign_budget_campaign_id_foreign" foreign key ("campaign_id") references "campaign" ("id") on update cascade;' + ) + + this.addSql( + 'alter table "promotion" add constraint "promotion_campaign_id_foreign" foreign key ("campaign_id") references "campaign" ("id") on update cascade on delete set null;' + ) + this.addSql( 'alter table "application_method" add constraint "application_method_promotion_id_foreign" foreign key ("promotion_id") references "promotion" ("id") on update cascade;' ) diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index 90d61307e9f1a..8be752d1e6509 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -368,9 +368,7 @@ export default class PromotionModuleService< return await this.baseRepository_.serialize( promotion, - { - populate: true, - } + { populate: true } ) } @@ -388,10 +386,29 @@ export default class PromotionModuleService< return await this.baseRepository_.serialize( promotions, - { - populate: true, - } + { populate: true } + ) + } + + @InjectManager("baseRepository_") + async listAndCount( + filters: PromotionTypes.FilterablePromotionProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[PromotionTypes.PromotionDTO[], number]> { + const [promotions, count] = await this.promotionService_.listAndCount( + filters, + config, + sharedContext ) + + return [ + await this.baseRepository_.serialize( + promotions, + { populate: true } + ), + count, + ] } async create( @@ -899,9 +916,7 @@ export default class PromotionModuleService< return await this.baseRepository_.serialize( campaign, - { - populate: true, - } + { populate: true } ) } @@ -919,9 +934,7 @@ export default class PromotionModuleService< return await this.baseRepository_.serialize( campaigns, - { - populate: true, - } + { populate: true } ) } diff --git a/packages/types/src/promotion/service.ts b/packages/types/src/promotion/service.ts index 0644ae71e5f87..8f4d318275f69 100644 --- a/packages/types/src/promotion/service.ts +++ b/packages/types/src/promotion/service.ts @@ -51,6 +51,12 @@ export interface IPromotionModuleService extends IModuleService { sharedContext?: Context ): Promise + listAndCount( + filters?: FilterablePromotionProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[PromotionDTO[], number]> + retrieve( id: string, config?: FindConfig, diff --git a/yarn.lock b/yarn.lock index 678bd7b10901a..69fb14775c682 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8462,7 +8462,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/promotion@workspace:packages/promotion": +"@medusajs/promotion@workspace:^, @medusajs/promotion@workspace:packages/promotion": version: 0.0.0-use.local resolution: "@medusajs/promotion@workspace:packages/promotion" dependencies: @@ -8637,7 +8637,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/utils@^1.10.5, @medusajs/utils@^1.11.1, @medusajs/utils@^1.11.2, @medusajs/utils@^1.11.3, @medusajs/utils@^1.9.2, @medusajs/utils@^1.9.4, @medusajs/utils@workspace:packages/utils": +"@medusajs/utils@^1.10.5, @medusajs/utils@^1.11.1, @medusajs/utils@^1.11.2, @medusajs/utils@^1.11.3, @medusajs/utils@^1.9.2, @medusajs/utils@^1.9.4, @medusajs/utils@workspace:^, @medusajs/utils@workspace:packages/utils": version: 0.0.0-use.local resolution: "@medusajs/utils@workspace:packages/utils" dependencies: @@ -31241,6 +31241,9 @@ __metadata: "@medusajs/modules-sdk": "workspace:^" "@medusajs/pricing": "workspace:^" "@medusajs/product": "workspace:^" + "@medusajs/promotion": "workspace:^" + "@medusajs/types": "workspace:^" + "@medusajs/utils": "workspace:^" babel-preset-medusa-package: "*" faker: ^5.5.3 jest: ^26.6.3