╔═════════════════════════════════════════════════════════════════════════════════════╗ ║ GOODGO ANALYTICS MODULE - ARCHITECTURE OVERVIEW ║ ╚═════════════════════════════════════════════════════════════════════════════════════╝ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ HTTP CLIENT │ │ ├─ GET /analytics/market-report?city=... │ │ ├─ GET /analytics/price-trend?district=... │ │ ├─ POST /analytics/valuation (form body) │ │ └─ GET /avm/explain?valuationId=... │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ PRESENTATION LAYER │ │ ┌──────────────────────────────────────────────────────────────────────────┐ │ │ │ AnalyticsController AvmController │ │ │ │ ├─ GET /market-report ├─ POST /batch │ │ │ │ ├─ GET /price-trend ├─ GET /history/:id │ │ │ │ ├─ GET /heatmap ├─ GET /compare │ │ │ │ ├─ POST /valuation ├─ GET /explain │ │ │ │ ├─ POST /valuation/batch └─ POST /industrial │ │ │ │ └─ GET /neighborhoods/:d/score │ │ │ └──────────────────────────────────────────────────────────────────────────┘ │ │ (Request/Response DTOs) │ │ GetMarketReportDto PredictValuationDto BatchValuationDto etc. │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ ┌─────────────────────┼─────────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────────────────────────────────────────────────┐ │ SHARED MIDDLEWARE & GUARDS (Global) │ ├───────────────────────────────────────────────────────────┤ │ • EndpointRateLimitGuard (Redis sliding-window) │ │ └─ Rate limit key: "rate:{strategy}:{id}:{path}" │ │ • JwtAuthGuard (verify JWT token) │ │ • QuotaGuard (check subscription quota) │ │ • CorrelationIdMiddleware (trace ID injection) │ │ • RequestLoggingMiddleware (audit logging) │ │ • SanitizeInputMiddleware (input validation) │ └───────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ APPLICATION LAYER (CQRS Pattern) │ │ ┌──────────────────────────────────┐ ┌────────────────────────────────────────┐ │ │ │ QUERIES (Read Operations) │ │ COMMANDS (Write Operations) │ │ │ │ │ │ │ │ │ │ Query Classes & Handlers: │ │ • GenerateReportHandler │ │ │ │ ├─ GetPriceTrendQuery │ │ • TrackEventHandler │ │ │ │ ├─ GetHeatmapQuery │ │ • UpdateMarketIndexHandler │ │ │ │ ├─ GetMarketReportQuery │ │ │ │ │ │ ├─ GetDistrictStatsQuery │ │ EVENT HANDLERS: │ │ │ │ ├─ GetValuationQuery │ │ • ListingCreatedModerationHandler │ │ │ │ ├─ PredictValuationQuery │ │ │ │ │ │ ├─ BatchValuationQuery │ │ QUERYBUS.EXECUTE() │ │ │ │ ├─ IndustrialValuationQuery │ │ COMMANDBUS.EXECUTE() │ │ │ │ ├─ GetNeighborhoodScoreQuery │ │ EVENTBUS.EMIT() │ │ │ │ ├─ GetNearbyPOIsQuery │ │ │ │ │ │ ├─ GetListingAiAdviceQuery │ │ │ │ │ │ ├─ GetProjectAiAdviceQuery │ │ │ │ │ │ ├─ ValuationHistoryQuery │ │ │ │ │ │ ├─ ValuationComparisonQuery │ │ │ │ │ │ └─ ValuationExplanationQuery │ │ │ │ │ └──────────────────────────────────┘ └────────────────────────────────────────┘ │ │ │ │ All handlers follow this pattern: │ │ 1. Build cache key with CacheService.buildKey(CachePrefix.*, ...params) │ │ 2. Call cache.getOrSet(cacheKey, loader, CacheTTL.*, 'metric_label') │ │ 3. Catch DomainException separately; wrap others │ │ 4. Return DTO from handler │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │ DOMAIN LAYER (DDD Pattern) │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │ REPOSITORY INTERFACES (Abstraction) │ │ │ │ • IMarketIndexRepository │ │ │ │ • IValuationRepository │ │ │ │ │ │ │ │ DOMAIN SERVICES (Business Logic) │ │ │ │ • IAVMService (interface) │ │ │ │ • INeighborhoodScoreService (interface) │ │ │ │ │ │ │ │ DOMAIN ENTITIES │ │ │ │ • MarketIndexEntity │ │ │ │ • ValuationEntity │ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ ▼ ▼ ▼ ┌────────────────────┐ ┌────────────────────────┐ ┌─────────────────────────┐ │ INFRASTRUCTURE │ │ REDIS CACHE SERVICE │ │ PRISMA REPOSITORIES │ ├────────────────────┤ ├────────────────────────┤ ├─────────────────────────┤ │ • HttpAVMService │ │ cache.getOrSet() │ │ PrismaMarketIndexRepo │ │ (→ Python AI) │ │ │ │ PrismaValuationRepo │ │ │ │ Metrics: │ │ │ │ • PrismaAVMService │ │ • cache_hit_total │ │ Converts: │ │ (fallback) │ │ • cache_miss_total │ │ Prisma → Domain Entity │ │ │ │ • cache_degradation │ │ │ │ • HttpNbScore │ │ │ │ Query patterns: │ │ Service │ │ Cache Prefixes: │ │ • findById │ │ (→ Python) │ │ • cache:market:trend │ │ • findMany │ │ │ │ • cache:market:report │ │ • getMarketReport │ │ • PrismaNeighbor │ │ • cache:market:heatmap │ │ • getHeatmap │ │ Score Service │ │ • cache:valuation │ │ • getPriceTrend │ │ (fallback) │ │ │ │ • getDistrictStats │ │ │ │ TTLs: │ │ │ │ • AiServiceClient │ │ • MARKET_DATA: 1800s │ │ All use PrismaService │ │ (Claude API) │ │ • MARKET_REPORT: 900s │ │ for database access │ │ │ │ • HEATMAP: 300s │ │ │ │ • MarketIndex │ │ • DISTRICT_STATS: 300s │ │ │ │ CronService │ │ • REFERENCE_DATA: 86400s │ │ │ └────────────────────┘ └────────────────────────┘ └─────────────────────────┘ │ │ │ │ │ │ │ ┌────────────┴───────────┐ │ │ ▼ ▼ │ │ ┌──────────────────┐ ┌───────────────────┐ │ │ │ REDIS │ │ POSTGRESQL 16 │ │ │ ├──────────────────┤ ├───────────────────┤ │ │ │ Sliding Window │ │ Tables: │ │ │ │ Rate Limiter │ │ • Property │ │ │ │ │ │ • Listing │ │ │ │ Cache Storage │ │ • PriceHistory │ │ │ │ (cache-aside) │ │ • MarketIndex │ │ │ │ │ │ • Valuation │ │ │ │ Metrics: │ │ • User │ │ │ │ rate:*:*:* │ │ • Subscription │ │ │ └──────────────────┘ │ + PostGIS │ │ │ │ (geometry) │ │ │ └───────────────────┘ │ │ │ └────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────┐ │ EXTERNAL SERVICES │ ├──────────────────────────────────────┤ │ • Python AI Service │ │ (AVM, Neighborhood Score) │ │ • Anthropic Claude API │ │ (Listing/Project AI Advice) │ │ • Google Maps/OSM API │ │ (Nearby POIs) │ └──────────────────────────────────────┘ ╔═════════════════════════════════════════════════════════════════════════════════════╗ ║ DATA FLOW EXAMPLE ║ ║ GET /analytics/price-trend?district=... ║ ╚═════════════════════════════════════════════════════════════════════════════════════╝ 1. Controller receives query DTO └─ @Query() dto: GetPriceTrendDto 2. Controller validates & creates Query object └─ new GetPriceTrendQuery(dto.district, dto.city, dto.propertyType, dto.periods) 3. Controller sends to QueryBus └─ queryBus.execute(query) 4. QueryBus routes to GetPriceTrendHandler 5. Handler builds cache key └─ CacheService.buildKey(CachePrefix.MARKET_TREND, district, city, propertyType, periods) └─ Result: "cache:market:trend:Quận 1:Hồ Chí Minh:APARTMENT:2024-Q1,2024-Q2" 6. Handler calls cache.getOrSet() a) Redis lookup → FOUND? Return cached data b) MISS? Call loader function: • Call marketIndexRepo.getPriceTrend() • PrismaMarketIndexRepository queries PostgreSQL • Fetch: SELECT * FROM "MarketIndex" WHERE district=? AND city=? AND ... • Convert Prisma model → DomainEntity • Return trend data c) Store in Redis with TTL (1800s = 30 min) d) Return data to caller 7. Handler returns PriceTrendDto to controller 8. Controller returns JSON to client 9. Response includes: • Cached status (if applicable) • Rate-limit headers (X-RateLimit-*) • CorrelationId (for tracing) • Standard error format (if error) ╔═════════════════════════════════════════════════════════════════════════════════════╗ ║ SHARED UTILITIES & EXPORTS ║ ╚═════════════════════════════════════════════════════════════════════════════════════╝ From @modules/shared: • CacheService • CachePrefix (enum) • CacheTTL (const) • RedisService • LoggerService • PrismaService • DomainException & subclasses • EndpointRateLimit (decorator) • EndpointRateLimitGuard • ErrorResponseBody interface • JwtAuthGuard • QuotaGuard • @RequireQuota decorator Exports from analytics.module.ts: • MARKET_INDEX_REPOSITORY • VALUATION_REPOSITORY • AVM_SERVICE • AI_SERVICE_CLIENT