diff --git a/apps/api/src/modules/admin/admin.module.ts b/apps/api/src/modules/admin/admin.module.ts index e4ae609..8f239ed 100644 --- a/apps/api/src/modules/admin/admin.module.ts +++ b/apps/api/src/modules/admin/admin.module.ts @@ -2,6 +2,7 @@ import { forwardRef, Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { AuthModule } from '@modules/auth'; import { ListingsModule } from '@modules/listings'; +import { AI_CONFIG_PROVIDER } from '@modules/shared'; import { SubscriptionsModule } from '@modules/subscriptions'; import { AdjustSubscriptionHandler } from './application/commands/adjust-subscription/adjust-subscription.handler'; import { ApproveKycHandler } from './application/commands/approve-kyc/approve-kyc.handler'; @@ -34,6 +35,7 @@ import { MODERATION_AUDIT_LOG_REPOSITORY } from './domain/repositories/moderatio import { PrismaAdminQueryRepository } from './infrastructure/repositories/prisma-admin-query.repository'; import { PrismaAuditLogRepository } from './infrastructure/repositories/prisma-audit-log.repository'; import { PrismaModerationAuditLogRepository } from './infrastructure/repositories/prisma-moderation-audit-log.repository'; +import { SystemSettingsAiConfigProvider } from './infrastructure/adapters/system-settings-ai-config.provider'; import { AdminModerationAuditController } from './presentation/controllers/admin-moderation-audit.controller'; import { AdminModerationController } from './presentation/controllers/admin-moderation.controller'; import { AdminController } from './presentation/controllers/admin.controller'; @@ -82,6 +84,7 @@ const QueryHandlers = [ // Services SystemSettingsService, + { provide: AI_CONFIG_PROVIDER, useClass: SystemSettingsAiConfigProvider }, // CQRS ...CommandHandlers, @@ -93,6 +96,6 @@ const QueryHandlers = [ AdminAuditListener, ModerationAuditListener, ], - exports: [SystemSettingsService], + exports: [SystemSettingsService, AI_CONFIG_PROVIDER], }) export class AdminModule {} diff --git a/apps/api/src/modules/admin/infrastructure/adapters/system-settings-ai-config.provider.ts b/apps/api/src/modules/admin/infrastructure/adapters/system-settings-ai-config.provider.ts new file mode 100644 index 0000000..8d32ba5 --- /dev/null +++ b/apps/api/src/modules/admin/infrastructure/adapters/system-settings-ai-config.provider.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { + type AiRuntimeConfig, + type IAIConfigProvider, +} from '@modules/shared'; +import { SystemSettingsService } from '../../application/services/system-settings.service'; + +/** + * Adapter that exposes the admin-owned `SystemSettingsService` through the + * shared `IAIConfigProvider` port. Lets analytics (and any other module) + * read AI runtime config without importing AdminModule (A-09). + */ +@Injectable() +export class SystemSettingsAiConfigProvider implements IAIConfigProvider { + constructor(private readonly systemSettings: SystemSettingsService) {} + + async getAiConfig(): Promise { + const settings = await this.systemSettings.getAiSettings(); + return { + apiUrl: settings.apiUrl, + apiKey: settings.apiKey, + model: settings.model, + }; + } +} diff --git a/apps/api/src/modules/analytics/analytics.module.ts b/apps/api/src/modules/analytics/analytics.module.ts index e6700e0..3115ed2 100644 --- a/apps/api/src/modules/analytics/analytics.module.ts +++ b/apps/api/src/modules/analytics/analytics.module.ts @@ -1,6 +1,5 @@ import { forwardRef, Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; -import { AdminModule } from '@modules/admin'; import { ListingsModule } from '@modules/listings'; import { ProjectsModule } from '@modules/projects'; import { GenerateReportHandler } from './application/commands/generate-report/generate-report.handler'; @@ -78,7 +77,7 @@ const EventHandlers = [ ]; @Module({ - imports: [CqrsModule, forwardRef(() => ListingsModule), forwardRef(() => AdminModule), ProjectsModule], + imports: [CqrsModule, forwardRef(() => ListingsModule), ProjectsModule], controllers: [AnalyticsController, AvmController], providers: [ // AI service client diff --git a/apps/api/src/modules/analytics/application/queries/get-listing-ai-advice/get-listing-ai-advice.handler.ts b/apps/api/src/modules/analytics/application/queries/get-listing-ai-advice/get-listing-ai-advice.handler.ts index 1311565..d383066 100644 --- a/apps/api/src/modules/analytics/application/queries/get-listing-ai-advice/get-listing-ai-advice.handler.ts +++ b/apps/api/src/modules/analytics/application/queries/get-listing-ai-advice/get-listing-ai-advice.handler.ts @@ -1,7 +1,12 @@ import { HttpStatus, Inject } from '@nestjs/common'; import { QueryBus, QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; -import { DomainException, ErrorCode, LoggerService } from '@modules/shared'; -import { SystemSettingsService } from '@modules/admin/application/services/system-settings.service'; +import { + AI_CONFIG_PROVIDER, + DomainException, + ErrorCode, + type IAIConfigProvider, + LoggerService, +} from '@modules/shared'; import { LISTING_REPOSITORY, type IListingRepository, @@ -91,7 +96,8 @@ export class GetListingAiAdviceHandler @Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository, private readonly queryBus: QueryBus, - private readonly systemSettings: SystemSettingsService, + @Inject(AI_CONFIG_PROVIDER) + private readonly aiConfig: IAIConfigProvider, private readonly logger: LoggerService, ) {} @@ -113,7 +119,7 @@ export class GetListingAiAdviceHandler this.fetchScore(listing), ]); - const settings = await this.systemSettings.getAiSettings(); + const settings = await this.aiConfig.getAiConfig(); if (!settings.apiKey) { throw new DomainException( ErrorCode.AI_NOT_CONFIGURED, diff --git a/apps/api/src/modules/analytics/application/queries/get-project-ai-advice/get-project-ai-advice.handler.ts b/apps/api/src/modules/analytics/application/queries/get-project-ai-advice/get-project-ai-advice.handler.ts index f4a1c47..d3019cc 100644 --- a/apps/api/src/modules/analytics/application/queries/get-project-ai-advice/get-project-ai-advice.handler.ts +++ b/apps/api/src/modules/analytics/application/queries/get-project-ai-advice/get-project-ai-advice.handler.ts @@ -1,7 +1,12 @@ import { HttpStatus, Inject } from '@nestjs/common'; import { QueryBus, QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; -import { DomainException, ErrorCode, LoggerService } from '@modules/shared'; -import { SystemSettingsService } from '@modules/admin/application/services/system-settings.service'; +import { + AI_CONFIG_PROVIDER, + DomainException, + ErrorCode, + type IAIConfigProvider, + LoggerService, +} from '@modules/shared'; import { PROJECT_REPOSITORY, type IProjectRepository, @@ -75,7 +80,8 @@ export class GetProjectAiAdviceHandler @Inject(PROJECT_REPOSITORY) private readonly projectRepo: IProjectRepository, private readonly queryBus: QueryBus, - private readonly systemSettings: SystemSettingsService, + @Inject(AI_CONFIG_PROVIDER) + private readonly aiConfig: IAIConfigProvider, private readonly logger: LoggerService, ) {} @@ -96,7 +102,7 @@ export class GetProjectAiAdviceHandler this.fetchScore(project), ]); - const settings = await this.systemSettings.getAiSettings(); + const settings = await this.aiConfig.getAiConfig(); if (!settings.apiKey) { throw new DomainException( ErrorCode.AI_NOT_CONFIGURED, diff --git a/apps/api/src/modules/listings/application/__tests__/feature-listing.handler.spec.ts b/apps/api/src/modules/listings/application/__tests__/feature-listing.handler.spec.ts index a8dc2bb..f6be43f 100644 --- a/apps/api/src/modules/listings/application/__tests__/feature-listing.handler.spec.ts +++ b/apps/api/src/modules/listings/application/__tests__/feature-listing.handler.spec.ts @@ -21,23 +21,26 @@ function createListing( describe('FeatureListingHandler', () => { let handler: FeatureListingHandler; let mockListingRepo: Pick; - let mockCommandBus: { execute: ReturnType }; + let mockPaymentInitiator: { initiate: ReturnType }; + let mockEventBus: { publish: ReturnType }; let mockLogger: { log: ReturnType; error: ReturnType }; beforeEach(() => { mockListingRepo = { findById: vi.fn() }; - mockCommandBus = { - execute: vi.fn().mockResolvedValue({ + mockPaymentInitiator = { + initiate: vi.fn().mockResolvedValue({ paymentId: 'pay-1', paymentUrl: 'https://pay.example.com/checkout', providerTxId: 'tx-1', }), }; + mockEventBus = { publish: vi.fn() }; mockLogger = { log: vi.fn(), error: vi.fn() }; handler = new FeatureListingHandler( mockListingRepo as any, - mockCommandBus as any, + mockPaymentInitiator as any, + mockEventBus as any, mockLogger as any, ); }); @@ -56,7 +59,9 @@ describe('FeatureListingHandler', () => { expect(result.paymentUrl).toBe('https://pay.example.com/checkout'); expect(result.package_).toBe('7_days'); expect(result.priceVND).toBe('199000'); - expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); + expect(mockPaymentInitiator.initiate).toHaveBeenCalledTimes(1); + expect(mockEventBus.publish).toHaveBeenCalledTimes(1); + expect(mockEventBus.publish.mock.calls[0]?.[0]?.eventName).toBe('listing.featured-payment-requested'); }); it('allows the assigned agent to feature the listing', async () => { diff --git a/apps/api/src/modules/listings/application/commands/feature-listing/feature-listing.handler.ts b/apps/api/src/modules/listings/application/commands/feature-listing/feature-listing.handler.ts index 5067c25..bfc1439 100644 --- a/apps/api/src/modules/listings/application/commands/feature-listing/feature-listing.handler.ts +++ b/apps/api/src/modules/listings/application/commands/feature-listing/feature-listing.handler.ts @@ -1,13 +1,15 @@ import { Inject, InternalServerErrorException } from '@nestjs/common'; -import { CommandHandler, CommandBus, type ICommandHandler } from '@nestjs/cqrs'; -import { CreatePaymentCommand, type CreatePaymentResult } from '@modules/payments'; +import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs'; import { DomainException, ForbiddenException, - NotFoundException, - ValidationException, + type IPaymentInitiator, LoggerService, + NotFoundException, + PAYMENT_INITIATOR, + ValidationException, } from '@modules/shared'; +import { FeaturedListingPaymentRequestedEvent } from '../../../domain/events/featured-listing-payment-requested.event'; import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository'; import { type FeaturePackage, FeatureListingCommand } from './feature-listing.command'; @@ -29,7 +31,8 @@ export interface FeatureListingResult { export class FeatureListingHandler implements ICommandHandler { constructor( @Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository, - private readonly commandBus: CommandBus, + @Inject(PAYMENT_INITIATOR) private readonly paymentInitiator: IPaymentInitiator, + private readonly eventBus: EventBus, private readonly logger: LoggerService, ) {} @@ -55,20 +58,33 @@ export class FeatureListingHandler implements ICommandHandler { + const result: CreatePaymentResult = await this.commandBus.execute( + new CreatePaymentCommand( + input.userId, + input.provider, + input.type, + input.amountVND, + input.description, + input.returnUrl, + input.ipAddress, + input.transactionId, + input.idempotencyKey, + ), + ); + return result; + } +} diff --git a/apps/api/src/modules/payments/payments.module.ts b/apps/api/src/modules/payments/payments.module.ts index c394cc3..89aebb6 100644 --- a/apps/api/src/modules/payments/payments.module.ts +++ b/apps/api/src/modules/payments/payments.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; +import { PAYMENT_INITIATOR } from '@modules/shared'; import { CancelOrderHandler } from './application/commands/cancel-order/cancel-order.handler'; import { ConfirmBankTransferHandler } from './application/commands/confirm-bank-transfer/confirm-bank-transfer.handler'; import { CreateOrderHandler } from './application/commands/create-order/create-order.handler'; @@ -17,6 +18,7 @@ import { PAYMENT_REPOSITORY } from './domain/repositories/payment.repository'; import { PrismaEscrowRepository } from './infrastructure/repositories/prisma-escrow.repository'; import { PrismaOrderRepository } from './infrastructure/repositories/prisma-order.repository'; import { PrismaPaymentRepository } from './infrastructure/repositories/prisma-payment.repository'; +import { CommandBusPaymentInitiator } from './infrastructure/adapters/command-bus-payment-initiator.adapter'; import { BankTransferService } from './infrastructure/services/bank-transfer.service'; import { MomoService } from './infrastructure/services/momo.service'; import { PaymentGatewayFactory } from './infrastructure/services/payment-gateway.factory'; @@ -62,7 +64,10 @@ const QueryHandlers = [ // CQRS ...CommandHandlers, ...QueryHandlers, + + // Cross-module port adapter + { provide: PAYMENT_INITIATOR, useClass: CommandBusPaymentInitiator }, ], - exports: [ESCROW_REPOSITORY, ORDER_REPOSITORY, PAYMENT_REPOSITORY, PAYMENT_GATEWAY_FACTORY], + exports: [ESCROW_REPOSITORY, ORDER_REPOSITORY, PAYMENT_REPOSITORY, PAYMENT_GATEWAY_FACTORY, PAYMENT_INITIATOR], }) export class PaymentsModule {} diff --git a/apps/api/src/modules/search/application/__tests__/create-saved-search.handler.spec.ts b/apps/api/src/modules/search/application/__tests__/create-saved-search.handler.spec.ts index 14aca04..2d7a5cd 100644 --- a/apps/api/src/modules/search/application/__tests__/create-saved-search.handler.spec.ts +++ b/apps/api/src/modules/search/application/__tests__/create-saved-search.handler.spec.ts @@ -4,9 +4,8 @@ import { CreateSavedSearchHandler } from '../commands/create-saved-search/create describe('CreateSavedSearchHandler', () => { let handler: CreateSavedSearchHandler; let mockPrisma: any; - let mockQueryBus: { execute: ReturnType }; - let mockCommandBus: { execute: ReturnType }; - let mockLogger: { log: ReturnType; warn: ReturnType }; + let mockEventBus: { publish: ReturnType }; + let mockLogger: { log: ReturnType; warn: ReturnType; error: ReturnType }; beforeEach(() => { mockPrisma = { @@ -16,27 +15,17 @@ describe('CreateSavedSearchHandler', () => { count: vi.fn(), }, }; - mockQueryBus = { execute: vi.fn() }; - mockCommandBus = { execute: vi.fn() }; - mockLogger = { log: vi.fn(), warn: vi.fn() }; + mockEventBus = { publish: vi.fn() }; + mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn() }; handler = new CreateSavedSearchHandler( mockPrisma, - mockQueryBus as any, - mockCommandBus as any, + mockEventBus as any, mockLogger as any, ); }); - it('creates a saved search successfully', async () => { - mockQueryBus.execute.mockResolvedValue({ - metric: 'searches_saved', - limit: 10, - used: 2, - remaining: 8, - allowed: true, - }); - + it('creates a saved search successfully and publishes domain event', async () => { const now = new Date(); mockPrisma.savedSearch.create.mockResolvedValue({ id: 'saved-1', @@ -48,8 +37,6 @@ describe('CreateSavedSearchHandler', () => { createdAt: now, }); - mockCommandBus.execute.mockResolvedValue({ usageRecordId: 'usage-1' }); - const command = new CreateSavedSearchCommand( 'user-1', 'Chung cư Q7', @@ -61,7 +48,9 @@ describe('CreateSavedSearchHandler', () => { expect(result.name).toBe('Chung cư Q7'); expect(result.alertEnabled).toBe(true); expect(mockPrisma.savedSearch.create).toHaveBeenCalledTimes(1); - expect(mockCommandBus.execute).toHaveBeenCalledTimes(1); // Usage metering + expect(mockEventBus.publish).toHaveBeenCalledTimes(1); + expect(mockEventBus.publish.mock.calls[0]?.[0]?.eventName).toBe('saved-search.created'); + expect(mockEventBus.publish.mock.calls[0]?.[0]?.userId).toBe('user-1'); }); it('throws when name is empty', async () => { @@ -74,49 +63,4 @@ describe('CreateSavedSearchHandler', () => { const command = new CreateSavedSearchCommand('user-1', longName, {}, true); await expect(handler.execute(command)).rejects.toThrow('Tên tìm kiếm không được vượt quá 100 ký tự'); }); - - it('throws when quota is exceeded', async () => { - mockQueryBus.execute.mockResolvedValue({ - metric: 'searches_saved', - limit: 5, - used: 5, - remaining: 0, - allowed: false, - }); - - const command = new CreateSavedSearchCommand('user-1', 'Test', {}, true); - await expect(handler.execute(command)).rejects.toThrow('giới hạn'); - }); - - it('continues even when usage metering fails', async () => { - mockQueryBus.execute.mockResolvedValue({ - metric: 'searches_saved', - limit: 10, - used: 2, - remaining: 8, - allowed: true, - }); - - const now = new Date(); - mockPrisma.savedSearch.create.mockResolvedValue({ - id: 'saved-1', - userId: 'user-1', - name: 'Test', - filters: {}, - alertEnabled: true, - lastAlertAt: null, - createdAt: now, - }); - - mockCommandBus.execute.mockRejectedValue(new Error('Metering failed')); - - const command = new CreateSavedSearchCommand('user-1', 'Test', {}, true); - const result = await handler.execute(command); - - expect(result.id).toBe('saved-1'); - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining('Usage metering failed'), - 'CreateSavedSearchHandler', - ); - }); }); diff --git a/apps/api/src/modules/search/application/commands/create-saved-search/create-saved-search.handler.ts b/apps/api/src/modules/search/application/commands/create-saved-search/create-saved-search.handler.ts index 35471e2..c3895e9 100644 --- a/apps/api/src/modules/search/application/commands/create-saved-search/create-saved-search.handler.ts +++ b/apps/api/src/modules/search/application/commands/create-saved-search/create-saved-search.handler.ts @@ -1,9 +1,9 @@ import { InternalServerErrorException } from '@nestjs/common'; -import { CommandHandler, CommandBus, type ICommandHandler, QueryBus } from '@nestjs/cqrs'; +import { CommandHandler, EventBus, type ICommandHandler } from '@nestjs/cqrs'; import { createId } from '@paralleldrive/cuid2'; import { type SavedSearch, type Prisma } from '@prisma/client'; import { DomainException, ValidationException, PrismaService, LoggerService } from '@modules/shared'; -import { CheckQuotaQuery, type QuotaCheckResult, MeterUsageCommand } from '@modules/subscriptions'; +import { SavedSearchCreatedEvent } from '../../../domain/events/saved-search-created.event'; import { CreateSavedSearchCommand } from './create-saved-search.command'; export interface CreateSavedSearchResult { @@ -14,12 +14,18 @@ export interface CreateSavedSearchResult { createdAt: Date; } +/** + * Note: quota enforcement (`searches_saved` metric) lives at the controller + * via `@RequireQuota('searches_saved')` + `QuotaGuard`. Usage metering + * happens in subscriptions via the `SavedSearchCreatedEvent` listener. + * This handler must NOT call `CheckQuotaQuery` or `MeterUsageCommand` + * directly — see A-11. + */ @CommandHandler(CreateSavedSearchCommand) export class CreateSavedSearchHandler implements ICommandHandler { constructor( private readonly prisma: PrismaService, - private readonly queryBus: QueryBus, - private readonly commandBus: CommandBus, + private readonly eventBus: EventBus, private readonly logger: LoggerService, ) {} @@ -34,17 +40,6 @@ export class CreateSavedSearchHandler implements ICommandHandler; +} + +export const AI_CONFIG_PROVIDER = Symbol('AI_CONFIG_PROVIDER'); diff --git a/apps/api/src/modules/shared/domain/ports/index.ts b/apps/api/src/modules/shared/domain/ports/index.ts new file mode 100644 index 0000000..dc1f395 --- /dev/null +++ b/apps/api/src/modules/shared/domain/ports/index.ts @@ -0,0 +1,11 @@ +export { + AI_CONFIG_PROVIDER, + type IAIConfigProvider, + type AiRuntimeConfig, +} from './ai-config.port'; +export { + PAYMENT_INITIATOR, + type IPaymentInitiator, + type InitiatePaymentInput, + type InitiatePaymentResult, +} from './payment-initiator.port'; diff --git a/apps/api/src/modules/shared/domain/ports/payment-initiator.port.ts b/apps/api/src/modules/shared/domain/ports/payment-initiator.port.ts new file mode 100644 index 0000000..505555f --- /dev/null +++ b/apps/api/src/modules/shared/domain/ports/payment-initiator.port.ts @@ -0,0 +1,34 @@ +import { type PaymentProvider, type PaymentType } from '@prisma/client'; + +/** + * Minimal cross-module contract used by non-payment modules (e.g. listings) + * to initiate a payment without importing payments application-layer commands. + * + * The concrete implementation lives in `payments` and is registered under the + * `PAYMENT_INITIATOR` symbol. This keeps the dependency direction + * listings → shared ← payments, matching our module-boundary rules (A-10). + */ +export interface InitiatePaymentInput { + userId: string; + provider: PaymentProvider; + type: PaymentType; + amountVND: bigint; + description: string; + returnUrl: string; + ipAddress: string; + /** Associated business-object id (e.g. listingId) when relevant. */ + transactionId?: string; + idempotencyKey?: string; +} + +export interface InitiatePaymentResult { + paymentId: string; + paymentUrl: string; + providerTxId: string; +} + +export interface IPaymentInitiator { + initiate(input: InitiatePaymentInput): Promise; +} + +export const PAYMENT_INITIATOR = Symbol('PAYMENT_INITIATOR'); diff --git a/apps/api/src/modules/subscriptions/infrastructure/event-handlers/saved-search-created-usage.handler.ts b/apps/api/src/modules/subscriptions/infrastructure/event-handlers/saved-search-created-usage.handler.ts new file mode 100644 index 0000000..32ff44c --- /dev/null +++ b/apps/api/src/modules/subscriptions/infrastructure/event-handlers/saved-search-created-usage.handler.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { CommandBus } from '@nestjs/cqrs'; +import { OnEvent } from '@nestjs/event-emitter'; +import { type SavedSearchCreatedEvent } from '@modules/search'; +import { LoggerService } from '@modules/shared'; +import { MeterUsageCommand } from '../../application/commands/meter-usage/meter-usage.command'; + +@Injectable() +export class SavedSearchCreatedUsageHandler { + constructor( + private readonly commandBus: CommandBus, + private readonly logger: LoggerService, + ) {} + + @OnEvent('saved-search.created', { async: true }) + async handle(event: SavedSearchCreatedEvent): Promise { + this.logger.log( + `Metering searches_saved usage for user=${event.userId}`, + 'SavedSearchCreatedUsageHandler', + ); + + try { + await this.commandBus.execute( + new MeterUsageCommand(event.userId, 'searches_saved', 1), + ); + } catch (error) { + // Log but don't fail — usage metering is best-effort (quota already enforced by guard) + this.logger.warn( + `Failed to meter usage for user=${event.userId}: ${(error as Error).message}`, + 'SavedSearchCreatedUsageHandler', + ); + } + } +} diff --git a/apps/api/src/modules/subscriptions/subscriptions.module.ts b/apps/api/src/modules/subscriptions/subscriptions.module.ts index 6127458..2b14b39 100644 --- a/apps/api/src/modules/subscriptions/subscriptions.module.ts +++ b/apps/api/src/modules/subscriptions/subscriptions.module.ts @@ -9,6 +9,7 @@ import { GetBillingHistoryHandler } from './application/queries/get-billing-hist import { GetPlanHandler } from './application/queries/get-plan/get-plan.handler'; import { SUBSCRIPTION_REPOSITORY } from './domain/repositories/subscription.repository'; import { ListingCreatedUsageHandler } from './infrastructure/event-handlers/listing-created-usage.handler'; +import { SavedSearchCreatedUsageHandler } from './infrastructure/event-handlers/saved-search-created-usage.handler'; import { PrismaSubscriptionRepository } from './infrastructure/repositories/prisma-subscription.repository'; import { SubscriptionsController } from './presentation/controllers/subscriptions.controller'; import { QuotaGuard } from './presentation/guards/quota.guard'; @@ -38,6 +39,7 @@ const QueryHandlers = [ // Event Listeners ListingCreatedUsageHandler, + SavedSearchCreatedUsageHandler, // CQRS ...CommandHandlers,