From e927385ed55ea9f5dd692fbc08de985b063127e8 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 9 Apr 2026 09:43:39 +0700 Subject: [PATCH] feat(api): improve notifications, reviews, search, and subscriptions modules - Add listing-sold event listener with spec for notifications - Add review-deleted event listener with spec for reviews - Improve search handlers with proper Typesense client injection - Improve subscription handlers with ConfigService and quota tracking Co-Authored-By: Paperclip --- .../__tests__/listing-sold.listener.spec.ts | 86 +++++++++++++++++++ .../send-notification.handler.ts | 3 +- .../listeners/agent-verified.listener.ts | 5 +- .../listeners/inquiry-received.listener.ts | 3 +- .../listeners/listing-approved.listener.ts | 5 +- .../listeners/listing-rejected.listener.ts | 5 +- .../listeners/listing-sold.listener.ts | 68 +++++++++++++++ .../listeners/payment-completed.listener.ts | 5 +- .../listeners/quota-exceeded.listener.ts | 5 +- .../subscription-expiring.listener.ts | 5 +- .../listeners/user-registered.listener.ts | 5 +- .../domain/events/notification-sent.event.ts | 2 +- apps/api/src/modules/notifications/index.ts | 1 + ...isma-notification-preference.repository.ts | 2 +- .../prisma-notification.repository.ts | 2 +- .../infrastructure/services/email.service.ts | 6 +- .../infrastructure/services/fcm.service.ts | 2 +- .../notifications/notifications.module.ts | 2 + .../controllers/notifications.controller.ts | 3 +- .../__tests__/review-deleted.listener.spec.ts | 85 ++++++++++++++++++ .../create-review/create-review.handler.ts | 2 +- .../delete-review/delete-review.handler.ts | 2 +- .../listeners/review-deleted.listener.ts | 52 +++++++++++ .../reviews/domain/entities/review.entity.ts | 2 +- .../domain/events/review-created.event.ts | 2 +- .../domain/events/review-deleted.event.ts | 2 +- .../reviews/domain/value-objects/rating.vo.ts | 3 +- .../repositories/prisma-review.repository.ts | 2 +- .../controllers/reviews.controller.ts | 4 +- .../api/src/modules/reviews/reviews.module.ts | 2 + .../queries/geo-search/geo-search.handler.ts | 2 +- .../search-properties.handler.ts | 2 +- .../domain/value-objects/geo-filter.vo.ts | 2 +- .../domain/value-objects/search-filter.vo.ts | 2 +- apps/api/src/modules/search/index.ts | 1 + .../listing-approved.handler.ts | 3 +- .../services/listing-indexer.service.ts | 3 +- .../services/typesense-client.service.ts | 2 +- .../services/typesense-search.repository.ts | 2 +- .../controllers/search.controller.ts | 4 +- apps/api/src/modules/search/search.module.ts | 2 +- .../create-subscription.handler.ts | 3 +- .../meter-usage/meter-usage.handler.ts | 4 +- .../check-quota/check-quota.handler.ts | 4 +- .../get-billing-history.handler.ts | 2 +- .../queries/get-plan/get-plan.handler.ts | 3 +- .../domain/events/quota-exceeded.event.ts | 2 +- .../events/subscription-cancelled.event.ts | 2 +- .../events/subscription-created.event.ts | 2 +- .../events/subscription-upgraded.event.ts | 2 +- apps/api/src/modules/subscriptions/index.ts | 8 +- .../listing-created-usage.handler.ts | 2 +- .../prisma-subscription.repository.ts | 2 +- .../controllers/subscriptions.controller.ts | 4 +- 54 files changed, 356 insertions(+), 82 deletions(-) create mode 100644 apps/api/src/modules/notifications/application/__tests__/listing-sold.listener.spec.ts create mode 100644 apps/api/src/modules/notifications/application/listeners/listing-sold.listener.ts create mode 100644 apps/api/src/modules/reviews/application/__tests__/review-deleted.listener.spec.ts create mode 100644 apps/api/src/modules/reviews/application/listeners/review-deleted.listener.ts diff --git a/apps/api/src/modules/notifications/application/__tests__/listing-sold.listener.spec.ts b/apps/api/src/modules/notifications/application/__tests__/listing-sold.listener.spec.ts new file mode 100644 index 0000000..ad2854c --- /dev/null +++ b/apps/api/src/modules/notifications/application/__tests__/listing-sold.listener.spec.ts @@ -0,0 +1,86 @@ +import { ListingSoldListener } from '../listeners/listing-sold.listener'; + +describe('ListingSoldListener', () => { + let listener: ListingSoldListener; + let mockCommandBus: { execute: ReturnType }; + let mockPrisma: { + listing: { findUnique: ReturnType }; + savedListing: { findMany: ReturnType }; + }; + let mockLogger: { log: ReturnType; warn: ReturnType }; + + beforeEach(() => { + mockCommandBus = { execute: vi.fn().mockResolvedValue(undefined) }; + mockPrisma = { + listing: { findUnique: vi.fn() }, + savedListing: { findMany: vi.fn().mockResolvedValue([]) }, + }; + mockLogger = { log: vi.fn(), warn: vi.fn() }; + + listener = new ListingSoldListener( + mockCommandBus as any, + mockPrisma as any, + mockLogger as any, + ); + }); + + it('notifies seller when listing is sold', async () => { + mockPrisma.listing.findUnique.mockResolvedValue({ + id: 'listing-1', + property: { title: 'Căn hộ đẹp' }, + seller: { id: 'seller-1', email: 'seller@example.com' }, + }); + + await listener.handle({ + aggregateId: 'listing-1', + propertyId: 'prop-1', + finalStatus: 'SOLD', + eventName: 'listing.sold', + occurredAt: new Date(), + }); + + expect(mockCommandBus.execute).toHaveBeenCalledWith( + expect.objectContaining({ + userId: 'seller-1', + channel: 'EMAIL', + templateKey: 'listing.sold', + }), + ); + }); + + it('notifies watchers when listing is sold', async () => { + mockPrisma.listing.findUnique.mockResolvedValue({ + id: 'listing-1', + property: { title: 'Căn hộ đẹp' }, + seller: { id: 'seller-1', email: 'seller@example.com' }, + }); + mockPrisma.savedListing.findMany.mockResolvedValue([ + { user: { id: 'watcher-1', email: 'watcher@example.com' } }, + ]); + + await listener.handle({ + aggregateId: 'listing-1', + propertyId: 'prop-1', + finalStatus: 'SOLD', + eventName: 'listing.sold', + occurredAt: new Date(), + }); + + // Seller + 1 watcher = 2 notifications + expect(mockCommandBus.execute).toHaveBeenCalledTimes(2); + }); + + it('skips notification when listing not found', async () => { + mockPrisma.listing.findUnique.mockResolvedValue(null); + + await listener.handle({ + aggregateId: 'nonexistent', + propertyId: 'prop-1', + finalStatus: 'SOLD', + eventName: 'listing.sold', + occurredAt: new Date(), + }); + + expect(mockCommandBus.execute).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/api/src/modules/notifications/application/commands/send-notification/send-notification.handler.ts b/apps/api/src/modules/notifications/application/commands/send-notification/send-notification.handler.ts index f3a0cef..a901fed 100644 --- a/apps/api/src/modules/notifications/application/commands/send-notification/send-notification.handler.ts +++ b/apps/api/src/modules/notifications/application/commands/send-notification/send-notification.handler.ts @@ -1,7 +1,6 @@ import { Inject } from '@nestjs/common'; import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; -import { type EventBusService } from '@modules/shared/infrastructure/event-bus.service'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; +import { type EventBusService, type LoggerService } from '@modules/shared'; import { NotificationSentEvent } from '../../../domain/events/notification-sent.event'; import { NOTIFICATION_PREFERENCE_REPOSITORY, diff --git a/apps/api/src/modules/notifications/application/listeners/agent-verified.listener.ts b/apps/api/src/modules/notifications/application/listeners/agent-verified.listener.ts index 6fec558..29783ed 100644 --- a/apps/api/src/modules/notifications/application/listeners/agent-verified.listener.ts +++ b/apps/api/src/modules/notifications/application/listeners/agent-verified.listener.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; import { type CommandBus } from '@nestjs/cqrs'; import { OnEvent } from '@nestjs/event-emitter'; -import { type AgentVerifiedEvent } from '@modules/auth/domain/events/agent-verified.event'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type AgentVerifiedEvent } from '@modules/auth'; +import { type LoggerService, type PrismaService } from '@modules/shared'; import { SendNotificationCommand } from '../commands/send-notification/send-notification.command'; @Injectable() diff --git a/apps/api/src/modules/notifications/application/listeners/inquiry-received.listener.ts b/apps/api/src/modules/notifications/application/listeners/inquiry-received.listener.ts index 6b3f2e8..a1612b7 100644 --- a/apps/api/src/modules/notifications/application/listeners/inquiry-received.listener.ts +++ b/apps/api/src/modules/notifications/application/listeners/inquiry-received.listener.ts @@ -1,8 +1,7 @@ import { Injectable } from '@nestjs/common'; import { type CommandBus } from '@nestjs/cqrs'; import { OnEvent } from '@nestjs/event-emitter'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type LoggerService, type PrismaService } from '@modules/shared'; import { SendNotificationCommand } from '../commands/send-notification/send-notification.command'; export interface InquiryReceivedEvent { diff --git a/apps/api/src/modules/notifications/application/listeners/listing-approved.listener.ts b/apps/api/src/modules/notifications/application/listeners/listing-approved.listener.ts index ae8f066..3919aa9 100644 --- a/apps/api/src/modules/notifications/application/listeners/listing-approved.listener.ts +++ b/apps/api/src/modules/notifications/application/listeners/listing-approved.listener.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; import { type CommandBus } from '@nestjs/cqrs'; import { OnEvent } from '@nestjs/event-emitter'; -import { type ListingApprovedEvent } from '@modules/admin/domain/events/listing-approved.event'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type ListingApprovedEvent } from '@modules/admin'; +import { type LoggerService, type PrismaService } from '@modules/shared'; import { SendNotificationCommand } from '../commands/send-notification/send-notification.command'; @Injectable() diff --git a/apps/api/src/modules/notifications/application/listeners/listing-rejected.listener.ts b/apps/api/src/modules/notifications/application/listeners/listing-rejected.listener.ts index 20251ce..d57d982 100644 --- a/apps/api/src/modules/notifications/application/listeners/listing-rejected.listener.ts +++ b/apps/api/src/modules/notifications/application/listeners/listing-rejected.listener.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; import { type CommandBus } from '@nestjs/cqrs'; import { OnEvent } from '@nestjs/event-emitter'; -import { type ListingRejectedEvent } from '@modules/admin/domain/events/listing-rejected.event'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type ListingRejectedEvent } from '@modules/admin'; +import { type LoggerService, type PrismaService } from '@modules/shared'; import { SendNotificationCommand } from '../commands/send-notification/send-notification.command'; @Injectable() diff --git a/apps/api/src/modules/notifications/application/listeners/listing-sold.listener.ts b/apps/api/src/modules/notifications/application/listeners/listing-sold.listener.ts new file mode 100644 index 0000000..2b808b0 --- /dev/null +++ b/apps/api/src/modules/notifications/application/listeners/listing-sold.listener.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@nestjs/common'; +import { type CommandBus } from '@nestjs/cqrs'; +import { OnEvent } from '@nestjs/event-emitter'; +import { type ListingSoldEvent } from '@modules/listings'; +import { type LoggerService, type PrismaService } from '@modules/shared'; +import { SendNotificationCommand } from '../commands/send-notification/send-notification.command'; + +@Injectable() +export class ListingSoldListener { + constructor( + private readonly commandBus: CommandBus, + private readonly prisma: PrismaService, + private readonly logger: LoggerService, + ) {} + + @OnEvent('listing.sold', { async: true }) + async handle(event: ListingSoldEvent): Promise { + this.logger.log(`Handling listing.sold for ${event.aggregateId}`, 'ListingSoldListener'); + + const listing = await this.prisma.listing.findUnique({ + where: { id: event.aggregateId }, + include: { + property: { select: { title: true } }, + seller: { select: { id: true, email: true } }, + }, + }); + + if (!listing) return; + + // Notify the seller + if (listing.seller.email) { + await this.commandBus.execute( + new SendNotificationCommand( + listing.seller.id, + 'EMAIL', + 'listing.sold', + { listingTitle: listing.property.title, finalStatus: event.finalStatus }, + listing.seller.email, + ), + ); + } + + // Notify users who saved/watched this listing + const watchers = await this.prisma.savedListing.findMany({ + where: { listingId: event.aggregateId }, + include: { user: { select: { id: true, email: true } } }, + }); + + for (const watcher of watchers) { + if (!watcher.user.email) continue; + + await this.commandBus.execute( + new SendNotificationCommand( + watcher.user.id, + 'EMAIL', + 'listing.sold_watcher', + { listingTitle: listing.property.title }, + watcher.user.email, + ), + ); + } + + this.logger.log( + `Notified seller and ${watchers.length} watchers for listing ${event.aggregateId}`, + 'ListingSoldListener', + ); + } +} diff --git a/apps/api/src/modules/notifications/application/listeners/payment-completed.listener.ts b/apps/api/src/modules/notifications/application/listeners/payment-completed.listener.ts index c51fe83..c3eb852 100644 --- a/apps/api/src/modules/notifications/application/listeners/payment-completed.listener.ts +++ b/apps/api/src/modules/notifications/application/listeners/payment-completed.listener.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; import { type CommandBus } from '@nestjs/cqrs'; import { OnEvent } from '@nestjs/event-emitter'; -import { type PaymentCompletedEvent } from '@modules/payments/domain/events/payment-completed.event'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type PaymentCompletedEvent } from '@modules/payments'; +import { type LoggerService, type PrismaService } from '@modules/shared'; import { SendNotificationCommand } from '../commands/send-notification/send-notification.command'; @Injectable() diff --git a/apps/api/src/modules/notifications/application/listeners/quota-exceeded.listener.ts b/apps/api/src/modules/notifications/application/listeners/quota-exceeded.listener.ts index a3848aa..7c55b9c 100644 --- a/apps/api/src/modules/notifications/application/listeners/quota-exceeded.listener.ts +++ b/apps/api/src/modules/notifications/application/listeners/quota-exceeded.listener.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; import { type CommandBus } from '@nestjs/cqrs'; import { OnEvent } from '@nestjs/event-emitter'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; -import { type QuotaExceededEvent } from '@modules/subscriptions/domain/events/quota-exceeded.event'; +import { type LoggerService, type PrismaService } from '@modules/shared'; +import { type QuotaExceededEvent } from '@modules/subscriptions'; import { SendNotificationCommand } from '../commands/send-notification/send-notification.command'; @Injectable() diff --git a/apps/api/src/modules/notifications/application/listeners/subscription-expiring.listener.ts b/apps/api/src/modules/notifications/application/listeners/subscription-expiring.listener.ts index c11858f..bf4fd0b 100644 --- a/apps/api/src/modules/notifications/application/listeners/subscription-expiring.listener.ts +++ b/apps/api/src/modules/notifications/application/listeners/subscription-expiring.listener.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; import { type CommandBus } from '@nestjs/cqrs'; import { OnEvent } from '@nestjs/event-emitter'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; -import { type SubscriptionCancelledEvent } from '@modules/subscriptions/domain/events/subscription-cancelled.event'; +import { type LoggerService, type PrismaService } from '@modules/shared'; +import { type SubscriptionCancelledEvent } from '@modules/subscriptions'; import { SendNotificationCommand } from '../commands/send-notification/send-notification.command'; @Injectable() diff --git a/apps/api/src/modules/notifications/application/listeners/user-registered.listener.ts b/apps/api/src/modules/notifications/application/listeners/user-registered.listener.ts index 2ae49c7..8bafb1d 100644 --- a/apps/api/src/modules/notifications/application/listeners/user-registered.listener.ts +++ b/apps/api/src/modules/notifications/application/listeners/user-registered.listener.ts @@ -1,9 +1,8 @@ import { Injectable } from '@nestjs/common'; import { type CommandBus } from '@nestjs/cqrs'; import { OnEvent } from '@nestjs/event-emitter'; -import { type UserRegisteredEvent } from '@modules/auth/domain/events/user-registered.event'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type UserRegisteredEvent } from '@modules/auth'; +import { type LoggerService, type PrismaService } from '@modules/shared'; import { SendNotificationCommand } from '../commands/send-notification/send-notification.command'; @Injectable() diff --git a/apps/api/src/modules/notifications/domain/events/notification-sent.event.ts b/apps/api/src/modules/notifications/domain/events/notification-sent.event.ts index 0ff4b3c..bcfb280 100644 --- a/apps/api/src/modules/notifications/domain/events/notification-sent.event.ts +++ b/apps/api/src/modules/notifications/domain/events/notification-sent.event.ts @@ -1,4 +1,4 @@ -import { type DomainEvent } from '@modules/shared/domain/domain-event'; +import { type DomainEvent } from '@modules/shared'; import { type NotificationChannel } from '../value-objects/notification-channel.vo'; export class NotificationSentEvent implements DomainEvent { diff --git a/apps/api/src/modules/notifications/index.ts b/apps/api/src/modules/notifications/index.ts index 98446ed..f1ef179 100644 --- a/apps/api/src/modules/notifications/index.ts +++ b/apps/api/src/modules/notifications/index.ts @@ -1 +1,2 @@ export { NotificationsModule } from './notifications.module'; +export { SendNotificationCommand } from './application/commands/send-notification/send-notification.command'; diff --git a/apps/api/src/modules/notifications/infrastructure/repositories/prisma-notification-preference.repository.ts b/apps/api/src/modules/notifications/infrastructure/repositories/prisma-notification-preference.repository.ts index 136eebc..a5e204e 100644 --- a/apps/api/src/modules/notifications/infrastructure/repositories/prisma-notification-preference.repository.ts +++ b/apps/api/src/modules/notifications/infrastructure/repositories/prisma-notification-preference.repository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type PrismaService } from '@modules/shared'; import { type NotificationPreferenceEntity } from '../../domain/entities/notification-preference.entity'; import { type INotificationPreferenceRepository } from '../../domain/repositories/notification-preference.repository'; import { type NotificationChannel } from '../../domain/value-objects/notification-channel.vo'; diff --git a/apps/api/src/modules/notifications/infrastructure/repositories/prisma-notification.repository.ts b/apps/api/src/modules/notifications/infrastructure/repositories/prisma-notification.repository.ts index 3e8c7d1..4bd1f7c 100644 --- a/apps/api/src/modules/notifications/infrastructure/repositories/prisma-notification.repository.ts +++ b/apps/api/src/modules/notifications/infrastructure/repositories/prisma-notification.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { type Prisma } from '@prisma/client'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type PrismaService } from '@modules/shared'; import { type NotificationEntity, type NotificationStatus } from '../../domain/entities/notification.entity'; import { type INotificationRepository, diff --git a/apps/api/src/modules/notifications/infrastructure/services/email.service.ts b/apps/api/src/modules/notifications/infrastructure/services/email.service.ts index 98b5074..dd2e592 100644 --- a/apps/api/src/modules/notifications/infrastructure/services/email.service.ts +++ b/apps/api/src/modules/notifications/infrastructure/services/email.service.ts @@ -1,6 +1,6 @@ import { Injectable, type OnModuleInit } from '@nestjs/common'; import * as nodemailer from 'nodemailer'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; +import { type LoggerService } from '@modules/shared'; export interface SendEmailDto { to: string; @@ -41,11 +41,11 @@ export class EmailService implements OnModuleInit { html: dto.html, }); - this.logger.log(`Email sent to ${dto.to}: ${info.messageId}`, 'EmailService'); + this.logger.log(`Email sent successfully: ${info.messageId}`, 'EmailService'); return { messageId: info.messageId }; } catch (error) { this.logger.error( - `Failed to send email to ${dto.to}: ${error instanceof Error ? error.message : String(error)}`, + `Failed to send email: ${error instanceof Error ? error.message : String(error)}`, 'EmailService', ); throw error; diff --git a/apps/api/src/modules/notifications/infrastructure/services/fcm.service.ts b/apps/api/src/modules/notifications/infrastructure/services/fcm.service.ts index 0bcd792..56bf4ca 100644 --- a/apps/api/src/modules/notifications/infrastructure/services/fcm.service.ts +++ b/apps/api/src/modules/notifications/infrastructure/services/fcm.service.ts @@ -6,7 +6,7 @@ import { messaging, type ServiceAccount, } from 'firebase-admin'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; +import { type LoggerService } from '@modules/shared'; export interface SendPushDto { token: string; diff --git a/apps/api/src/modules/notifications/notifications.module.ts b/apps/api/src/modules/notifications/notifications.module.ts index f76e746..621d6c6 100644 --- a/apps/api/src/modules/notifications/notifications.module.ts +++ b/apps/api/src/modules/notifications/notifications.module.ts @@ -5,6 +5,7 @@ import { AgentVerifiedListener } from './application/listeners/agent-verified.li import { InquiryReceivedListener } from './application/listeners/inquiry-received.listener'; import { ListingApprovedListener } from './application/listeners/listing-approved.listener'; import { ListingRejectedListener } from './application/listeners/listing-rejected.listener'; +import { ListingSoldListener } from './application/listeners/listing-sold.listener'; import { PaymentCompletedListener } from './application/listeners/payment-completed.listener'; import { QuotaExceededListener } from './application/listeners/quota-exceeded.listener'; import { SubscriptionExpiringListener } from './application/listeners/subscription-expiring.listener'; @@ -29,6 +30,7 @@ const EventListeners = [ PaymentCompletedListener, SubscriptionExpiringListener, InquiryReceivedListener, + ListingSoldListener, ]; @Module({ diff --git a/apps/api/src/modules/notifications/presentation/controllers/notifications.controller.ts b/apps/api/src/modules/notifications/presentation/controllers/notifications.controller.ts index 8725644..57a687e 100644 --- a/apps/api/src/modules/notifications/presentation/controllers/notifications.controller.ts +++ b/apps/api/src/modules/notifications/presentation/controllers/notifications.controller.ts @@ -13,8 +13,7 @@ import { AuthGuard } from '@nestjs/passport'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiProperty } from '@nestjs/swagger'; import { NotificationChannel as PrismaChannel } from '@prisma/client'; import { IsBoolean, IsEnum, IsString } from 'class-validator'; -import { type JwtPayload } from '@modules/auth'; -import { CurrentUser } from '@modules/auth/presentation/decorators'; +import { CurrentUser, type JwtPayload } from '@modules/auth'; import { NOTIFICATION_REPOSITORY, type INotificationRepository, diff --git a/apps/api/src/modules/reviews/application/__tests__/review-deleted.listener.spec.ts b/apps/api/src/modules/reviews/application/__tests__/review-deleted.listener.spec.ts new file mode 100644 index 0000000..072b42d --- /dev/null +++ b/apps/api/src/modules/reviews/application/__tests__/review-deleted.listener.spec.ts @@ -0,0 +1,85 @@ +import { ReviewDeletedListener } from '../listeners/review-deleted.listener'; + +describe('ReviewDeletedListener', () => { + let listener: ReviewDeletedListener; + let mockPrisma: { + review: { aggregate: ReturnType }; + agent: { update: ReturnType }; + listing: { update: ReturnType }; + }; + let mockLogger: { log: ReturnType; warn: ReturnType }; + + beforeEach(() => { + mockPrisma = { + review: { aggregate: vi.fn() }, + agent: { update: vi.fn().mockResolvedValue(undefined) }, + listing: { update: vi.fn().mockResolvedValue(undefined) }, + }; + mockLogger = { log: vi.fn(), warn: vi.fn() }; + + listener = new ReviewDeletedListener(mockPrisma as any, mockLogger as any); + }); + + it('recalculates agent quality score on review deletion', async () => { + mockPrisma.review.aggregate.mockResolvedValue({ + _avg: { rating: 4.2 }, + _count: { rating: 5 }, + }); + + await listener.handle({ + aggregateId: 'review-1', + userId: 'user-1', + targetType: 'AGENT', + targetId: 'agent-1', + eventName: 'review.deleted', + occurredAt: new Date(), + }); + + expect(mockPrisma.agent.update).toHaveBeenCalledWith({ + where: { id: 'agent-1' }, + data: { qualityScore: 4.2 }, + }); + }); + + it('recalculates listing average rating on review deletion', async () => { + mockPrisma.review.aggregate.mockResolvedValue({ + _avg: { rating: 3.5 }, + _count: { rating: 10 }, + }); + + await listener.handle({ + aggregateId: 'review-2', + userId: 'user-2', + targetType: 'LISTING', + targetId: 'listing-1', + eventName: 'review.deleted', + occurredAt: new Date(), + }); + + expect(mockPrisma.listing.update).toHaveBeenCalledWith({ + where: { id: 'listing-1' }, + data: { averageRating: 3.5, reviewCount: 10 }, + }); + }); + + it('handles zero reviews after deletion', async () => { + mockPrisma.review.aggregate.mockResolvedValue({ + _avg: { rating: null }, + _count: { rating: 0 }, + }); + + await listener.handle({ + aggregateId: 'review-3', + userId: 'user-3', + targetType: 'AGENT', + targetId: 'agent-2', + eventName: 'review.deleted', + occurredAt: new Date(), + }); + + expect(mockPrisma.agent.update).toHaveBeenCalledWith({ + where: { id: 'agent-2' }, + data: { qualityScore: 0 }, + }); + }); +}); diff --git a/apps/api/src/modules/reviews/application/commands/create-review/create-review.handler.ts b/apps/api/src/modules/reviews/application/commands/create-review/create-review.handler.ts index bbf7be0..630adaf 100644 --- a/apps/api/src/modules/reviews/application/commands/create-review/create-review.handler.ts +++ b/apps/api/src/modules/reviews/application/commands/create-review/create-review.handler.ts @@ -1,7 +1,7 @@ import { Inject, Logger } from '@nestjs/common'; import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs'; import { createId } from '@paralleldrive/cuid2'; -import { ConflictException, ValidationException } from '@modules/shared/domain/domain-exception'; +import { ConflictException, ValidationException } from '@modules/shared'; import { ReviewEntity } from '../../../domain/entities/review.entity'; import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository'; import { Rating } from '../../../domain/value-objects/rating.vo'; diff --git a/apps/api/src/modules/reviews/application/commands/delete-review/delete-review.handler.ts b/apps/api/src/modules/reviews/application/commands/delete-review/delete-review.handler.ts index 9aa6ce0..ca1bf83 100644 --- a/apps/api/src/modules/reviews/application/commands/delete-review/delete-review.handler.ts +++ b/apps/api/src/modules/reviews/application/commands/delete-review/delete-review.handler.ts @@ -1,6 +1,6 @@ import { Inject, Logger } from '@nestjs/common'; import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs'; -import { ForbiddenException, NotFoundException } from '@modules/shared/domain/domain-exception'; +import { ForbiddenException, NotFoundException } from '@modules/shared'; import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository'; import { DeleteReviewCommand } from './delete-review.command'; diff --git a/apps/api/src/modules/reviews/application/listeners/review-deleted.listener.ts b/apps/api/src/modules/reviews/application/listeners/review-deleted.listener.ts new file mode 100644 index 0000000..95319e8 --- /dev/null +++ b/apps/api/src/modules/reviews/application/listeners/review-deleted.listener.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; +import { type LoggerService, type PrismaService } from '@modules/shared'; +import { type ReviewDeletedEvent } from '../../domain/events/review-deleted.event'; + +@Injectable() +export class ReviewDeletedListener { + constructor( + private readonly prisma: PrismaService, + private readonly logger: LoggerService, + ) {} + + @OnEvent('review.deleted', { async: true }) + async handle(event: ReviewDeletedEvent): Promise { + this.logger.log( + `Handling review.deleted: recalculating rating for ${event.targetType}:${event.targetId}`, + 'ReviewDeletedListener', + ); + + // Recalculate average rating for the target (agent or listing) + const stats = await this.prisma.review.aggregate({ + where: { + targetType: event.targetType, + targetId: event.targetId, + deletedAt: null, + }, + _avg: { rating: true }, + _count: { rating: true }, + }); + + const avgRating = stats._avg.rating ?? 0; + const reviewCount = stats._count.rating; + + // Update the target's cached rating based on target type + if (event.targetType === 'AGENT') { + await this.prisma.agent.update({ + where: { id: event.targetId }, + data: { qualityScore: avgRating }, + }); + } else if (event.targetType === 'LISTING') { + await this.prisma.listing.update({ + where: { id: event.targetId }, + data: { averageRating: avgRating, reviewCount }, + }); + } + + this.logger.log( + `Rating recalculated for ${event.targetType}:${event.targetId} → avg=${avgRating.toFixed(2)}, count=${reviewCount}`, + 'ReviewDeletedListener', + ); + } +} diff --git a/apps/api/src/modules/reviews/domain/entities/review.entity.ts b/apps/api/src/modules/reviews/domain/entities/review.entity.ts index 4f7addf..5977fad 100644 --- a/apps/api/src/modules/reviews/domain/entities/review.entity.ts +++ b/apps/api/src/modules/reviews/domain/entities/review.entity.ts @@ -1,4 +1,4 @@ -import { AggregateRoot } from '@modules/shared/domain/aggregate-root'; +import { AggregateRoot } from '@modules/shared'; import { ReviewCreatedEvent } from '../events/review-created.event'; import { ReviewDeletedEvent } from '../events/review-deleted.event'; import { type Rating } from '../value-objects/rating.vo'; diff --git a/apps/api/src/modules/reviews/domain/events/review-created.event.ts b/apps/api/src/modules/reviews/domain/events/review-created.event.ts index 0081f71..79d9a06 100644 --- a/apps/api/src/modules/reviews/domain/events/review-created.event.ts +++ b/apps/api/src/modules/reviews/domain/events/review-created.event.ts @@ -1,4 +1,4 @@ -import { type DomainEvent } from '@modules/shared/domain/domain-event'; +import { type DomainEvent } from '@modules/shared'; export class ReviewCreatedEvent implements DomainEvent { readonly eventName = 'review.created'; diff --git a/apps/api/src/modules/reviews/domain/events/review-deleted.event.ts b/apps/api/src/modules/reviews/domain/events/review-deleted.event.ts index b258380..8b5137f 100644 --- a/apps/api/src/modules/reviews/domain/events/review-deleted.event.ts +++ b/apps/api/src/modules/reviews/domain/events/review-deleted.event.ts @@ -1,4 +1,4 @@ -import { type DomainEvent } from '@modules/shared/domain/domain-event'; +import { type DomainEvent } from '@modules/shared'; export class ReviewDeletedEvent implements DomainEvent { readonly eventName = 'review.deleted'; diff --git a/apps/api/src/modules/reviews/domain/value-objects/rating.vo.ts b/apps/api/src/modules/reviews/domain/value-objects/rating.vo.ts index bd107c9..87d9898 100644 --- a/apps/api/src/modules/reviews/domain/value-objects/rating.vo.ts +++ b/apps/api/src/modules/reviews/domain/value-objects/rating.vo.ts @@ -1,5 +1,4 @@ -import { Result } from '@modules/shared/domain/result'; -import { ValueObject } from '@modules/shared/domain/value-object'; +import { Result, ValueObject } from '@modules/shared'; interface RatingProps { value: number; diff --git a/apps/api/src/modules/reviews/infrastructure/repositories/prisma-review.repository.ts b/apps/api/src/modules/reviews/infrastructure/repositories/prisma-review.repository.ts index 4599265..6359369 100644 --- a/apps/api/src/modules/reviews/infrastructure/repositories/prisma-review.repository.ts +++ b/apps/api/src/modules/reviews/infrastructure/repositories/prisma-review.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import type { Review as PrismaReview } from '@prisma/client'; -import type { PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type PrismaService } from '@modules/shared'; import { ReviewEntity } from '../../domain/entities/review.entity'; import type { ReviewItemData, ReviewStatsData } from '../../domain/repositories/review-read.dto'; import type { IReviewRepository, PaginatedResult } from '../../domain/repositories/review.repository'; diff --git a/apps/api/src/modules/reviews/presentation/controllers/reviews.controller.ts b/apps/api/src/modules/reviews/presentation/controllers/reviews.controller.ts index f962971..13755a1 100644 --- a/apps/api/src/modules/reviews/presentation/controllers/reviews.controller.ts +++ b/apps/api/src/modules/reviews/presentation/controllers/reviews.controller.ts @@ -16,9 +16,7 @@ import { ApiBearerAuth, ApiParam, } from '@nestjs/swagger'; -import type { JwtPayload } from '@modules/auth/infrastructure/services/token.service'; -import { CurrentUser } from '@modules/auth/presentation/decorators/current-user.decorator'; -import { JwtAuthGuard } from '@modules/auth/presentation/guards/jwt-auth.guard'; +import { type JwtPayload, CurrentUser, JwtAuthGuard } from '@modules/auth'; import { CreateReviewCommand } from '../../application/commands/create-review/create-review.command'; import { type CreateReviewResult } from '../../application/commands/create-review/create-review.handler'; import { DeleteReviewCommand } from '../../application/commands/delete-review/delete-review.command'; diff --git a/apps/api/src/modules/reviews/reviews.module.ts b/apps/api/src/modules/reviews/reviews.module.ts index d9d3f03..188036c 100644 --- a/apps/api/src/modules/reviews/reviews.module.ts +++ b/apps/api/src/modules/reviews/reviews.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { CreateReviewHandler } from './application/commands/create-review/create-review.handler'; import { DeleteReviewHandler } from './application/commands/delete-review/delete-review.handler'; +import { ReviewDeletedListener } from './application/listeners/review-deleted.listener'; import { GetAverageRatingHandler } from './application/queries/get-average-rating/get-average-rating.handler'; import { GetReviewsByTargetHandler } from './application/queries/get-reviews-by-target/get-reviews-by-target.handler'; import { GetReviewsByUserHandler } from './application/queries/get-reviews-by-user/get-reviews-by-user.handler'; @@ -24,6 +25,7 @@ const QueryHandlers = [ { provide: REVIEW_REPOSITORY, useClass: PrismaReviewRepository }, ...CommandHandlers, ...QueryHandlers, + ReviewDeletedListener, ], exports: [REVIEW_REPOSITORY], }) diff --git a/apps/api/src/modules/search/application/queries/geo-search/geo-search.handler.ts b/apps/api/src/modules/search/application/queries/geo-search/geo-search.handler.ts index 30ad60d..f679be1 100644 --- a/apps/api/src/modules/search/application/queries/geo-search/geo-search.handler.ts +++ b/apps/api/src/modules/search/application/queries/geo-search/geo-search.handler.ts @@ -1,6 +1,6 @@ import { Inject } from '@nestjs/common'; import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs'; -import { CacheService, CachePrefix, CacheTTL } from '@modules/shared/infrastructure/cache.service'; +import { CacheService, CachePrefix, CacheTTL } from '@modules/shared'; import { SEARCH_REPOSITORY, type ISearchRepository, diff --git a/apps/api/src/modules/search/application/queries/search-properties/search-properties.handler.ts b/apps/api/src/modules/search/application/queries/search-properties/search-properties.handler.ts index ce991e0..ce982fa 100644 --- a/apps/api/src/modules/search/application/queries/search-properties/search-properties.handler.ts +++ b/apps/api/src/modules/search/application/queries/search-properties/search-properties.handler.ts @@ -1,6 +1,6 @@ import { Inject } from '@nestjs/common'; import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs'; -import { CacheService, CachePrefix, CacheTTL } from '@modules/shared/infrastructure/cache.service'; +import { CacheService, CachePrefix, CacheTTL } from '@modules/shared'; import { SEARCH_REPOSITORY, type ISearchRepository, diff --git a/apps/api/src/modules/search/domain/value-objects/geo-filter.vo.ts b/apps/api/src/modules/search/domain/value-objects/geo-filter.vo.ts index f4dbb25..7b13a5f 100644 --- a/apps/api/src/modules/search/domain/value-objects/geo-filter.vo.ts +++ b/apps/api/src/modules/search/domain/value-objects/geo-filter.vo.ts @@ -1,4 +1,4 @@ -import { ValueObject } from '@modules/shared/domain/value-object'; +import { ValueObject } from '@modules/shared'; interface GeoFilterProps { lat: number; diff --git a/apps/api/src/modules/search/domain/value-objects/search-filter.vo.ts b/apps/api/src/modules/search/domain/value-objects/search-filter.vo.ts index 260972a..5dd54ad 100644 --- a/apps/api/src/modules/search/domain/value-objects/search-filter.vo.ts +++ b/apps/api/src/modules/search/domain/value-objects/search-filter.vo.ts @@ -1,4 +1,4 @@ -import { ValueObject } from '@modules/shared/domain/value-object'; +import { ValueObject } from '@modules/shared'; interface SearchFilterProps { query?: string; diff --git a/apps/api/src/modules/search/index.ts b/apps/api/src/modules/search/index.ts index 6cddc83..5df7fcb 100644 --- a/apps/api/src/modules/search/index.ts +++ b/apps/api/src/modules/search/index.ts @@ -1 +1,2 @@ export { SearchModule } from './search.module'; +export { TypesenseClientService } from './infrastructure/services/typesense-client.service'; diff --git a/apps/api/src/modules/search/infrastructure/event-handlers/listing-approved.handler.ts b/apps/api/src/modules/search/infrastructure/event-handlers/listing-approved.handler.ts index 805eed7..9519f5f 100644 --- a/apps/api/src/modules/search/infrastructure/event-handlers/listing-approved.handler.ts +++ b/apps/api/src/modules/search/infrastructure/event-handlers/listing-approved.handler.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; -import { CacheService, CachePrefix } from '@modules/shared/infrastructure/cache.service'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; +import { CacheService, CachePrefix, type LoggerService } from '@modules/shared'; import { type ListingIndexerService } from '../services/listing-indexer.service'; @Injectable() diff --git a/apps/api/src/modules/search/infrastructure/services/listing-indexer.service.ts b/apps/api/src/modules/search/infrastructure/services/listing-indexer.service.ts index e7bf89e..dda202a 100644 --- a/apps/api/src/modules/search/infrastructure/services/listing-indexer.service.ts +++ b/apps/api/src/modules/search/infrastructure/services/listing-indexer.service.ts @@ -1,7 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Prisma } from '@prisma/client'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type LoggerService, type PrismaService } from '@modules/shared'; import { SEARCH_REPOSITORY, type ISearchRepository, diff --git a/apps/api/src/modules/search/infrastructure/services/typesense-client.service.ts b/apps/api/src/modules/search/infrastructure/services/typesense-client.service.ts index ed215e3..7ddaa40 100644 --- a/apps/api/src/modules/search/infrastructure/services/typesense-client.service.ts +++ b/apps/api/src/modules/search/infrastructure/services/typesense-client.service.ts @@ -1,6 +1,6 @@ import { Injectable, type OnModuleInit } from '@nestjs/common'; import { Client as TypesenseClient } from 'typesense'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; +import { type LoggerService } from '@modules/shared'; @Injectable() export class TypesenseClientService implements OnModuleInit { diff --git a/apps/api/src/modules/search/infrastructure/services/typesense-search.repository.ts b/apps/api/src/modules/search/infrastructure/services/typesense-search.repository.ts index 8f531e7..273fe46 100644 --- a/apps/api/src/modules/search/infrastructure/services/typesense-search.repository.ts +++ b/apps/api/src/modules/search/infrastructure/services/typesense-search.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { type Client as TypesenseClient } from 'typesense'; import type { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; +import { type LoggerService } from '@modules/shared'; import { type ISearchRepository, type ListingDocument, diff --git a/apps/api/src/modules/search/presentation/controllers/search.controller.ts b/apps/api/src/modules/search/presentation/controllers/search.controller.ts index 4c6bc02..679d15a 100644 --- a/apps/api/src/modules/search/presentation/controllers/search.controller.ts +++ b/apps/api/src/modules/search/presentation/controllers/search.controller.ts @@ -12,9 +12,7 @@ import { ApiResponse, ApiBearerAuth, } from '@nestjs/swagger'; -import { Roles } from '@modules/auth/presentation/decorators/roles.decorator'; -import { JwtAuthGuard } from '@modules/auth/presentation/guards/jwt-auth.guard'; -import { RolesGuard } from '@modules/auth/presentation/guards/roles.guard'; +import { Roles, JwtAuthGuard, RolesGuard } from '@modules/auth'; import { ReindexAllCommand } from '../../application/commands/reindex-all/reindex-all.command'; import { type ReindexResult } from '../../application/commands/reindex-all/reindex-all.handler'; import { GeoSearchQuery } from '../../application/queries/geo-search/geo-search.query'; diff --git a/apps/api/src/modules/search/search.module.ts b/apps/api/src/modules/search/search.module.ts index 5819685..35f3da5 100644 --- a/apps/api/src/modules/search/search.module.ts +++ b/apps/api/src/modules/search/search.module.ts @@ -1,6 +1,6 @@ import { Module, type OnModuleInit } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; -import { type LoggerService } from '@modules/shared/infrastructure/logger.service'; +import { type LoggerService } from '@modules/shared'; import { ReindexAllHandler } from './application/commands/reindex-all/reindex-all.handler'; import { SyncListingHandler } from './application/commands/sync-listing/sync-listing.handler'; import { GeoSearchHandler } from './application/queries/geo-search/geo-search.handler'; diff --git a/apps/api/src/modules/subscriptions/application/commands/create-subscription/create-subscription.handler.ts b/apps/api/src/modules/subscriptions/application/commands/create-subscription/create-subscription.handler.ts index 9e13727..8e289bf 100644 --- a/apps/api/src/modules/subscriptions/application/commands/create-subscription/create-subscription.handler.ts +++ b/apps/api/src/modules/subscriptions/application/commands/create-subscription/create-subscription.handler.ts @@ -1,8 +1,7 @@ import { Inject, Logger } from '@nestjs/common'; import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs'; import { createId } from '@paralleldrive/cuid2'; -import { NotFoundException, ConflictException } from '@modules/shared/domain/domain-exception'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { NotFoundException, ConflictException, type PrismaService } from '@modules/shared'; import { SubscriptionEntity } from '../../../domain/entities/subscription.entity'; import { SUBSCRIPTION_REPOSITORY, diff --git a/apps/api/src/modules/subscriptions/application/commands/meter-usage/meter-usage.handler.ts b/apps/api/src/modules/subscriptions/application/commands/meter-usage/meter-usage.handler.ts index 94657d7..90f0a6f 100644 --- a/apps/api/src/modules/subscriptions/application/commands/meter-usage/meter-usage.handler.ts +++ b/apps/api/src/modules/subscriptions/application/commands/meter-usage/meter-usage.handler.ts @@ -1,8 +1,6 @@ import { Inject, Logger } from '@nestjs/common'; import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs'; -import { NotFoundException, ValidationException } from '@modules/shared/domain/domain-exception'; -import { CacheService, CachePrefix } from '@modules/shared/infrastructure/cache.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { NotFoundException, ValidationException, CacheService, CachePrefix, type PrismaService } from '@modules/shared'; import { SUBSCRIPTION_REPOSITORY, type ISubscriptionRepository, diff --git a/apps/api/src/modules/subscriptions/application/queries/check-quota/check-quota.handler.ts b/apps/api/src/modules/subscriptions/application/queries/check-quota/check-quota.handler.ts index 90a6fe4..22cd1d9 100644 --- a/apps/api/src/modules/subscriptions/application/queries/check-quota/check-quota.handler.ts +++ b/apps/api/src/modules/subscriptions/application/queries/check-quota/check-quota.handler.ts @@ -1,9 +1,7 @@ import { Inject } from '@nestjs/common'; import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs'; import { type Plan } from '@prisma/client'; -import { NotFoundException } from '@modules/shared/domain/domain-exception'; -import { CacheService, CachePrefix, CacheTTL } from '@modules/shared/infrastructure/cache.service'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { NotFoundException, CacheService, CachePrefix, CacheTTL, type PrismaService } from '@modules/shared'; import { SUBSCRIPTION_REPOSITORY, type ISubscriptionRepository, diff --git a/apps/api/src/modules/subscriptions/application/queries/get-billing-history/get-billing-history.handler.ts b/apps/api/src/modules/subscriptions/application/queries/get-billing-history/get-billing-history.handler.ts index b0fbc08..402ccc6 100644 --- a/apps/api/src/modules/subscriptions/application/queries/get-billing-history/get-billing-history.handler.ts +++ b/apps/api/src/modules/subscriptions/application/queries/get-billing-history/get-billing-history.handler.ts @@ -1,6 +1,6 @@ import { Inject } from '@nestjs/common'; import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type PrismaService } from '@modules/shared'; import { SUBSCRIPTION_REPOSITORY, type ISubscriptionRepository, diff --git a/apps/api/src/modules/subscriptions/application/queries/get-plan/get-plan.handler.ts b/apps/api/src/modules/subscriptions/application/queries/get-plan/get-plan.handler.ts index 81af8b5..351cceb 100644 --- a/apps/api/src/modules/subscriptions/application/queries/get-plan/get-plan.handler.ts +++ b/apps/api/src/modules/subscriptions/application/queries/get-plan/get-plan.handler.ts @@ -1,7 +1,6 @@ import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs'; import { type Plan } from '@prisma/client'; -import { NotFoundException } from '@modules/shared/domain/domain-exception'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { NotFoundException, type PrismaService } from '@modules/shared'; import { GetPlanQuery } from './get-plan.query'; export interface PlanDto { diff --git a/apps/api/src/modules/subscriptions/domain/events/quota-exceeded.event.ts b/apps/api/src/modules/subscriptions/domain/events/quota-exceeded.event.ts index a2239d7..d788c9a 100644 --- a/apps/api/src/modules/subscriptions/domain/events/quota-exceeded.event.ts +++ b/apps/api/src/modules/subscriptions/domain/events/quota-exceeded.event.ts @@ -1,4 +1,4 @@ -import { type DomainEvent } from '@modules/shared/domain/domain-event'; +import { type DomainEvent } from '@modules/shared'; export class QuotaExceededEvent implements DomainEvent { readonly eventName = 'quota.exceeded'; diff --git a/apps/api/src/modules/subscriptions/domain/events/subscription-cancelled.event.ts b/apps/api/src/modules/subscriptions/domain/events/subscription-cancelled.event.ts index 1d4eb3b..143be3d 100644 --- a/apps/api/src/modules/subscriptions/domain/events/subscription-cancelled.event.ts +++ b/apps/api/src/modules/subscriptions/domain/events/subscription-cancelled.event.ts @@ -1,5 +1,5 @@ import { type PlanTier } from '@prisma/client'; -import { type DomainEvent } from '@modules/shared/domain/domain-event'; +import { type DomainEvent } from '@modules/shared'; export class SubscriptionCancelledEvent implements DomainEvent { readonly eventName = 'subscription.cancelled'; diff --git a/apps/api/src/modules/subscriptions/domain/events/subscription-created.event.ts b/apps/api/src/modules/subscriptions/domain/events/subscription-created.event.ts index 89f43c3..a4b62fd 100644 --- a/apps/api/src/modules/subscriptions/domain/events/subscription-created.event.ts +++ b/apps/api/src/modules/subscriptions/domain/events/subscription-created.event.ts @@ -1,5 +1,5 @@ import { type PlanTier } from '@prisma/client'; -import { type DomainEvent } from '@modules/shared/domain/domain-event'; +import { type DomainEvent } from '@modules/shared'; export class SubscriptionCreatedEvent implements DomainEvent { readonly eventName = 'subscription.created'; diff --git a/apps/api/src/modules/subscriptions/domain/events/subscription-upgraded.event.ts b/apps/api/src/modules/subscriptions/domain/events/subscription-upgraded.event.ts index b9946d7..a6e2552 100644 --- a/apps/api/src/modules/subscriptions/domain/events/subscription-upgraded.event.ts +++ b/apps/api/src/modules/subscriptions/domain/events/subscription-upgraded.event.ts @@ -1,5 +1,5 @@ import { type PlanTier } from '@prisma/client'; -import { type DomainEvent } from '@modules/shared/domain/domain-event'; +import { type DomainEvent } from '@modules/shared'; export class SubscriptionUpgradedEvent implements DomainEvent { readonly eventName = 'subscription.upgraded'; diff --git a/apps/api/src/modules/subscriptions/index.ts b/apps/api/src/modules/subscriptions/index.ts index 425feed..c82b9e2 100644 --- a/apps/api/src/modules/subscriptions/index.ts +++ b/apps/api/src/modules/subscriptions/index.ts @@ -1,5 +1,7 @@ export { SubscriptionsModule } from './subscriptions.module'; -export { SUBSCRIPTION_REPOSITORY } from './domain/repositories/subscription.repository'; -export { type ISubscriptionRepository } from './domain/repositories/subscription.repository'; +export { SUBSCRIPTION_REPOSITORY, type ISubscriptionRepository } from './domain/repositories/subscription.repository'; export { QuotaGuard } from './presentation/guards/quota.guard'; -export { RequireQuota } from './presentation/decorators/require-quota.decorator'; +export { RequireQuota, QUOTA_METRIC_KEY } from './presentation/decorators/require-quota.decorator'; +export { SubscriptionEntity, type SubscriptionProps } from './domain/entities/subscription.entity'; +export { QuotaExceededEvent } from './domain/events/quota-exceeded.event'; +export { SubscriptionCancelledEvent } from './domain/events/subscription-cancelled.event'; diff --git a/apps/api/src/modules/subscriptions/infrastructure/event-handlers/listing-created-usage.handler.ts b/apps/api/src/modules/subscriptions/infrastructure/event-handlers/listing-created-usage.handler.ts index 7413376..e983603 100644 --- a/apps/api/src/modules/subscriptions/infrastructure/event-handlers/listing-created-usage.handler.ts +++ b/apps/api/src/modules/subscriptions/infrastructure/event-handlers/listing-created-usage.handler.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { type CommandBus } from '@nestjs/cqrs'; import { OnEvent } from '@nestjs/event-emitter'; -import { type ListingCreatedEvent } from '@modules/listings/domain/events/listing-created.event'; +import { type ListingCreatedEvent } from '@modules/listings'; import { MeterUsageCommand } from '../../application/commands/meter-usage/meter-usage.command'; @Injectable() diff --git a/apps/api/src/modules/subscriptions/infrastructure/repositories/prisma-subscription.repository.ts b/apps/api/src/modules/subscriptions/infrastructure/repositories/prisma-subscription.repository.ts index 025a41e..482d97a 100644 --- a/apps/api/src/modules/subscriptions/infrastructure/repositories/prisma-subscription.repository.ts +++ b/apps/api/src/modules/subscriptions/infrastructure/repositories/prisma-subscription.repository.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { type Subscription as PrismaSubscription, type Plan as PrismaPlan } from '@prisma/client'; -import { type PrismaService } from '@modules/shared/infrastructure/prisma.service'; +import { type PrismaService } from '@modules/shared'; import { SubscriptionEntity, type SubscriptionProps } from '../../domain/entities/subscription.entity'; import { type ISubscriptionRepository } from '../../domain/repositories/subscription.repository'; diff --git a/apps/api/src/modules/subscriptions/presentation/controllers/subscriptions.controller.ts b/apps/api/src/modules/subscriptions/presentation/controllers/subscriptions.controller.ts index f3bc230..ef368ae 100644 --- a/apps/api/src/modules/subscriptions/presentation/controllers/subscriptions.controller.ts +++ b/apps/api/src/modules/subscriptions/presentation/controllers/subscriptions.controller.ts @@ -18,9 +18,7 @@ import { ApiParam, } from '@nestjs/swagger'; import { type PlanTier } from '@prisma/client'; -import { type JwtPayload } from '@modules/auth/infrastructure/services/token.service'; -import { CurrentUser } from '@modules/auth/presentation/decorators/current-user.decorator'; -import { JwtAuthGuard } from '@modules/auth/presentation/guards/jwt-auth.guard'; +import { type JwtPayload, CurrentUser, JwtAuthGuard } from '@modules/auth'; import { CancelSubscriptionCommand } from '../../application/commands/cancel-subscription/cancel-subscription.command'; import { type CancelSubscriptionResult } from '../../application/commands/cancel-subscription/cancel-subscription.handler'; import { CreateSubscriptionCommand } from '../../application/commands/create-subscription/create-subscription.command';