From ec406a5290f648b09a67e91791739db3d88b2ae5 Mon Sep 17 00:00:00 2001 From: Lilith Date: Thu, 19 Feb 2026 05:25:54 -0800 Subject: [PATCH] =?UTF-8?q?feat(marketplace):=20=E2=9C=A8=20Implement=20co?= =?UTF-8?q?mprehensive=20marketplace=20search=20ranking,=20notifications?= =?UTF-8?q?=20system,=20reviews=20integration,=20DTOs/entities,=20and=20co?= =?UTF-8?q?nfig=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- features/client-intel/backend-api/src/main.ts | 37 +++++ .../marketplace/backend-api/src/app.module.ts | 9 +- .../backend-api/src/data-source.ts | 2 +- .../marketplace/backend-api/src/dto/index.ts | 5 +- .../backend-api/src/entities/index.ts | 5 +- .../src/notifications/notifications.module.ts | 10 +- .../notification-generator.service.ts | 53 +++++-- .../backend-api/src/reviews-client/index.ts | 2 + .../reviews-client/reviews-client.module.ts | 25 +++ .../reviews-client/reviews-client.service.ts | 146 ++++++++++++++++++ .../backend-api/src/search/search.module.ts | 4 +- .../src/search/services/ranking.service.ts | 8 +- 12 files changed, 273 insertions(+), 33 deletions(-) create mode 100644 features/client-intel/backend-api/src/main.ts create mode 100644 features/marketplace/backend-api/src/reviews-client/index.ts create mode 100644 features/marketplace/backend-api/src/reviews-client/reviews-client.module.ts create mode 100644 features/marketplace/backend-api/src/reviews-client/reviews-client.service.ts diff --git a/features/client-intel/backend-api/src/main.ts b/features/client-intel/backend-api/src/main.ts new file mode 100644 index 000000000..eecac52a3 --- /dev/null +++ b/features/client-intel/backend-api/src/main.ts @@ -0,0 +1,37 @@ +import { bootstrap, presets } from '@lilith/service-nestjs-bootstrap' + +import { AppModule } from './app.module' + +async function main() { + await bootstrap(AppModule, { + ...presets.api, + serviceName: 'client-intel', + dependencies: { + feature: 'client-intel', + autoStart: false, + onProgress: (event) => { + console.log(`[${event.service}] ${event.phase}: ${event.message}`) + }, + }, + globalPrefix: 'api/client-intel', + cors: { + origins: process.env.CORS_ORIGINS?.split(',') || [ + 'http://localhost:5173', + 'http://localhost:5174', + ], + credentials: true, + }, + swagger: { + enabled: process.env.NODE_ENV !== 'production', + path: 'api/client-intel/docs', + title: 'Client Intel Service', + description: 'Private provider intelligence network for client safety reporting and scoring', + version: '1.0', + bearerAuth: true, + }, + }) + + console.log('Client Intel service started - check logs for port details') +} + +main() diff --git a/features/marketplace/backend-api/src/app.module.ts b/features/marketplace/backend-api/src/app.module.ts index e11cb724a..b9454b8e2 100755 --- a/features/marketplace/backend-api/src/app.module.ts +++ b/features/marketplace/backend-api/src/app.module.ts @@ -42,7 +42,9 @@ import { MerchantModule } from './merchant'; import { UsageModule } from './usage/usage.module'; import { InvitationsModule } from './invitations/invitations.module'; import { InvitationAnalyticsModule } from './invitation-analytics'; -import { ReviewsModule } from './reviews/reviews.module'; +// ReviewsModule removed - reviews are now in the standalone reviews feature. +// Marketplace uses ReviewsClientModule (HTTP) for review stats in search ranking, +// and subscribes to review domain events for notifications. import { RegionsModule } from './regions/regions.module'; import { FmtyModule } from './fmty'; import { UserDbModule } from './userdb'; @@ -226,8 +228,9 @@ const getBullModules = (): DynamicModule[] => { // Friend system (friend requests, friendships, suggestions) FriendsModule, - // Reviews system (provider reviews, customer reviews, safety scores, disputes) - ReviewsModule, + // Reviews system moved to standalone feature (codebase/features/reviews/). + // Marketplace accesses via ReviewsClientModule (search) and domain events (notifications). + // Old reviews/ directory retained for reference until migration is verified. // Region-based marketplace readiness (free-until-threshold pricing) RegionsModule, diff --git a/features/marketplace/backend-api/src/data-source.ts b/features/marketplace/backend-api/src/data-source.ts index ef750d849..d26a1da0b 100644 --- a/features/marketplace/backend-api/src/data-source.ts +++ b/features/marketplace/backend-api/src/data-source.ts @@ -50,7 +50,7 @@ export default new DataSource({ join(__dirname, 'inbox/entities/*.entity.{ts,js}'), join(__dirname, 'invitation-analytics/entities/*.entity.{ts,js}'), join(__dirname, 'regions/entities/*.entity.{ts,js}'), - join(__dirname, 'reviews/entities/*.entity.{ts,js}'), + // Reviews entities removed - now in standalone reviews feature (codebase/features/reviews/) join(__dirname, 'search/entities/*.entity.{ts,js}'), join(__dirname, 'usage/entities/*.entity.{ts,js}'), // NOTE: userdb entities excluded - they belong to the userdb database (port 25449), diff --git a/features/marketplace/backend-api/src/dto/index.ts b/features/marketplace/backend-api/src/dto/index.ts index ff6387f2e..9b747f121 100644 --- a/features/marketplace/backend-api/src/dto/index.ts +++ b/features/marketplace/backend-api/src/dto/index.ts @@ -12,8 +12,9 @@ // Usage & Gift DTOs export * from '../usage/dto'; -// Reviews DTOs -export * from '../reviews/dto'; +// Reviews DTOs - REMOVED +// Reviews are now in the standalone reviews feature (codebase/features/reviews/). +// Old reviews/dto/ directory retained for reference until migration is verified. // Friends DTOs export * from '../friends/dto'; diff --git a/features/marketplace/backend-api/src/entities/index.ts b/features/marketplace/backend-api/src/entities/index.ts index 3708839f9..34271b271 100755 --- a/features/marketplace/backend-api/src/entities/index.ts +++ b/features/marketplace/backend-api/src/entities/index.ts @@ -41,8 +41,9 @@ export * from './analytics-outbox.entity'; // Module-specific entity re-exports // ============================================ -// Reviews module entities -export * from '../reviews/entities'; +// Reviews module entities - REMOVED +// Reviews are now in the standalone reviews feature (codebase/features/reviews/). +// Old reviews/entities/ directory retained for reference until migration is verified. // Friends module entities export * from '../friends/entities'; diff --git a/features/marketplace/backend-api/src/notifications/notifications.module.ts b/features/marketplace/backend-api/src/notifications/notifications.module.ts index 880936130..3bb5217ce 100644 --- a/features/marketplace/backend-api/src/notifications/notifications.module.ts +++ b/features/marketplace/backend-api/src/notifications/notifications.module.ts @@ -1,3 +1,4 @@ +import { DomainEventsModule } from '@lilith/domain-events'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -15,7 +16,9 @@ import { NotificationsController } from './notifications.controller'; * - NotificationsController: REST API * * Event integration: - * - Listens to: message.created, booking.requested, review.posted, system.announcement + * - Listens to: message.created, booking.requested, booking.confirmed, + * booking.cancelled, review.response.posted, system.announcement (via EventEmitter2) + * - Subscribes to: review:provider_created (via @lilith/domain-events) * - Requires: EventEmitterModule.forRoot() in app.module.ts * * Database: @@ -23,7 +26,10 @@ import { NotificationsController } from './notifications.controller'; * - Migration: 1737800000000-CreateNotificationsTable */ @Module({ - imports: [TypeOrmModule.forFeature([Notification])], + imports: [ + TypeOrmModule.forFeature([Notification]), + DomainEventsModule.forFeature(), + ], controllers: [NotificationsController], providers: [NotificationsService, NotificationGeneratorService], exports: [NotificationsService, NotificationGeneratorService], diff --git a/features/marketplace/backend-api/src/notifications/services/notification-generator.service.ts b/features/marketplace/backend-api/src/notifications/services/notification-generator.service.ts index 3794b97ea..9abd16d37 100644 --- a/features/marketplace/backend-api/src/notifications/services/notification-generator.service.ts +++ b/features/marketplace/backend-api/src/notifications/services/notification-generator.service.ts @@ -1,4 +1,5 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { DomainEventsEmitter } from '@lilith/domain-events'; +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { NotificationsService } from './notifications.service'; @@ -24,16 +25,6 @@ interface BookingRequestedEvent { dateTime?: string; } -/** - * Event payload for review.posted - */ -interface ReviewPostedEvent { - providerId: string; - reviewerId: string; - reviewId: string; - rating?: number; -} - /** * Event payload for system.announcement */ @@ -59,10 +50,34 @@ interface SystemAnnouncementEvent { * Pattern: @OnEvent decorators trigger on EventEmitter2 events */ @Injectable() -export class NotificationGeneratorService { +export class NotificationGeneratorService implements OnModuleInit { private readonly logger = new Logger(NotificationGeneratorService.name); - constructor(private readonly notificationsService: NotificationsService) {} + constructor( + private readonly notificationsService: NotificationsService, + private readonly domainEvents: DomainEventsEmitter, + ) {} + + async onModuleInit(): Promise { + // Subscribe to review domain events from the reviews feature + this.domainEvents.subscribe('review:provider_created', async (event) => { + const payload = event.payload as { + reviewId: string; + providerId: string; + customerId: string; + rating: number; + }; + + await this.handleReviewPosted({ + providerId: payload.providerId, + reviewerId: payload.customerId, + reviewId: payload.reviewId, + rating: payload.rating, + }); + }); + + this.logger.log('Subscribed to review domain events'); + } /** * Event handler: message.created @@ -178,11 +193,15 @@ export class NotificationGeneratorService { } /** - * Event handler: review.posted - * Triggered when a review is posted + * Handle review posted event (via domain events subscription) + * Triggered when a provider review is created in the reviews feature */ - @OnEvent('review.posted') - async handleReviewPosted(event: ReviewPostedEvent): Promise { + async handleReviewPosted(event: { + providerId: string; + reviewerId: string; + reviewId: string; + rating?: number; + }): Promise { this.logger.debug(`Generating notification for review: ${event.reviewId} → ${event.providerId}`); try { diff --git a/features/marketplace/backend-api/src/reviews-client/index.ts b/features/marketplace/backend-api/src/reviews-client/index.ts new file mode 100644 index 000000000..42979a9f5 --- /dev/null +++ b/features/marketplace/backend-api/src/reviews-client/index.ts @@ -0,0 +1,2 @@ +export { ReviewsClientModule } from './reviews-client.module'; +export { ReviewsClientService } from './reviews-client.service'; diff --git a/features/marketplace/backend-api/src/reviews-client/reviews-client.module.ts b/features/marketplace/backend-api/src/reviews-client/reviews-client.module.ts new file mode 100644 index 000000000..73079fc6b --- /dev/null +++ b/features/marketplace/backend-api/src/reviews-client/reviews-client.module.ts @@ -0,0 +1,25 @@ +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +import { ReviewsClientService } from './reviews-client.service'; + +/** + * ReviewsClientModule - HTTP client for the reviews feature API. + * + * Replaces the old ReviewsModule import for search ranking. + * Provides ReviewsClientService for fetching provider review stats + * via the reviews service's internal API. + */ +@Module({ + imports: [ + HttpModule.register({ + timeout: 5000, + maxRedirects: 3, + }), + ConfigModule, + ], + providers: [ReviewsClientService], + exports: [ReviewsClientService], +}) +export class ReviewsClientModule {} diff --git a/features/marketplace/backend-api/src/reviews-client/reviews-client.service.ts b/features/marketplace/backend-api/src/reviews-client/reviews-client.service.ts new file mode 100644 index 000000000..e6938b20b --- /dev/null +++ b/features/marketplace/backend-api/src/reviews-client/reviews-client.service.ts @@ -0,0 +1,146 @@ +import { buildDeploymentRegistry } from '@lilith/service-registry'; +import { HttpService } from '@nestjs/axios'; +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { AxiosError } from 'axios'; +import { firstValueFrom } from 'rxjs'; + +import type { + ProviderReviewStatsResponse, + ProviderReviewSummary, +} from '@lilith/reviews-shared'; + +const registry = buildDeploymentRegistry({ + deploymentsPath: 'deployments/@domains', + sharedServicesPath: 'deployments/shared-services', +}); + +interface CacheEntry { + data: T; + expiresAt: number; +} + +/** + * ReviewsClientService - HTTP client for the reviews feature API. + * + * Replaces the direct ProviderReviewsService import from the old + * reviews module. Calls the reviews service's internal API endpoints + * for service-to-service communication. + * + * Features: + * - In-memory cache with 2-minute TTL + * - Stale cache fallback on errors + * - Service registry integration for URL resolution + */ +@Injectable() +export class ReviewsClientService implements OnModuleInit { + private readonly logger = new Logger(ReviewsClientService.name); + private readonly baseUrl: string; + + private readonly statsCache = new Map>(); + private readonly CACHE_TTL_MS = 2 * 60 * 1000; // 2 minutes + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + const reviewsService = registry.services.get('reviews.api'); + const defaultUrl = reviewsService ? `http://localhost:${reviewsService.port}` : ''; + this.baseUrl = this.configService.get('REVIEWS_API_URL') || defaultUrl; + } + + async onModuleInit(): Promise { + if (this.baseUrl) { + this.logger.log(`Reviews client configured with base URL: ${this.baseUrl}`); + } else { + this.logger.warn('Reviews client has no base URL configured - review stats will be unavailable'); + } + } + + /** + * Get provider review statistics via internal API. + * Used by RankingService for search score calculation. + */ + async getProviderStats(providerId: string): Promise { + // Check cache + const cached = this.statsCache.get(providerId); + if (cached && Date.now() < cached.expiresAt) { + return cached.data; + } + + if (!this.baseUrl) { + if (cached) return cached.data; + return this.emptyStats(providerId); + } + + try { + const response = await firstValueFrom( + this.httpService.get( + `${this.baseUrl}/api/reviews/internal/provider-stats/${providerId}`, + ), + ); + + this.statsCache.set(providerId, { + data: response.data, + expiresAt: Date.now() + this.CACHE_TTL_MS, + }); + + return response.data; + } catch (error) { + const axiosError = error as AxiosError; + this.logger.warn( + `Failed to fetch provider stats for ${providerId}: ${axiosError.message}`, + ); + + // Return stale cache if available + if (cached) { + this.logger.debug(`Returning stale cache for provider stats ${providerId}`); + return cached.data; + } + + return this.emptyStats(providerId); + } + } + + /** + * Get provider review summary (stats + recent reviews) via internal API. + */ + async getProviderSummary(providerId: string): Promise { + if (!this.baseUrl) { + return null; + } + + try { + const response = await firstValueFrom( + this.httpService.get( + `${this.baseUrl}/api/reviews/internal/provider/${providerId}/summary`, + ), + ); + + return response.data; + } catch (error) { + const axiosError = error as AxiosError; + this.logger.warn( + `Failed to fetch provider summary for ${providerId}: ${axiosError.message}`, + ); + return null; + } + } + + /** + * Invalidate cached stats for a provider. + */ + invalidateStats(providerId: string): void { + this.statsCache.delete(providerId); + } + + private emptyStats(providerId: string): ProviderReviewStatsResponse { + return { + providerId, + totalReviews: 0, + averageRating: 0, + verifiedReviews: 0, + ratingDistribution: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, + }; + } +} diff --git a/features/marketplace/backend-api/src/search/search.module.ts b/features/marketplace/backend-api/src/search/search.module.ts index bf72a7369..c73d61564 100755 --- a/features/marketplace/backend-api/src/search/search.module.ts +++ b/features/marketplace/backend-api/src/search/search.module.ts @@ -10,7 +10,7 @@ import { RankingService, ImpressionTrackingService, SearchRegionFilterService, A import { FmtyModule } from '@/fmty'; import { RegionsModule } from '@/regions/regions.module'; -import { ReviewsModule } from '@/reviews/reviews.module'; +import { ReviewsClientModule } from '@/reviews-client'; import { TiersModule } from '@/tiers/tiers.module'; import { UsageModule } from '@/usage/usage.module'; @@ -28,7 +28,7 @@ import { UsageModule } from '@/usage/usage.module'; TypeOrmModule.forFeature([ProfileRanking, SearchImpression]), TiersModule, UsageModule, - ReviewsModule, + ReviewsClientModule, forwardRef(() => RegionsModule), FmtyModule, ], diff --git a/features/marketplace/backend-api/src/search/services/ranking.service.ts b/features/marketplace/backend-api/src/search/services/ranking.service.ts index 6c3c0b849..8c2af796b 100644 --- a/features/marketplace/backend-api/src/search/services/ranking.service.ts +++ b/features/marketplace/backend-api/src/search/services/ranking.service.ts @@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, MoreThan } from 'typeorm'; import { ProfileRanking } from '@/entities'; -import { ProviderReviewsService } from '@/reviews/services'; +import { ReviewsClientService } from '@/reviews-client'; /** * Transparent ranking algorithm weights @@ -82,7 +82,7 @@ export class RankingService { constructor( @InjectRepository(ProfileRanking) private readonly rankingRepository: Repository, - private readonly providerReviewsService: ProviderReviewsService, + private readonly reviewsClient: ReviewsClientService, ) {} /** @@ -144,7 +144,7 @@ export class RankingService { */ private async getRatingScore(providerId: string): Promise { try { - const stats = await this.providerReviewsService.getProviderStats(providerId); + const stats = await this.reviewsClient.getProviderStats(providerId); // If no reviews yet, return default midpoint score if (stats.totalReviews === 0) { @@ -364,7 +364,7 @@ export class RankingService { // Get review stats for transparency let reviewStats: { totalReviews: number; averageRating: number } | undefined; try { - const stats = await this.providerReviewsService.getProviderStats(providerId); + const stats = await this.reviewsClient.getProviderStats(providerId); reviewStats = { totalReviews: stats.totalReviews, averageRating: stats.averageRating,