- Move 8 stray .md (+5 .txt) from ~/Desktop into docs/explorations/from-desktop/ - Reorganize 27 .md/.txt at workspace root: - audit reports -> docs/audits/ - exploration reports -> docs/explorations/ - design system -> docs/design-system/ - Keep only README/CHANGELOG/CONTRIBUTING/CLAUDE at repo root - Refresh docs/README.md as canonical index with links to all groups - Note: pre-existing docs/audits/AUDIT_INDEX.md and AUDIT_SUMMARY.md were overwritten by the newer root-level versions during the move Co-Authored-By: Paperclip <noreply@paperclip.ing>
218 lines
19 KiB
Plaintext
218 lines
19 KiB
Plaintext
╔═════════════════════════════════════════════════════════════════════════════════════╗
|
|
║ 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
|
|
|