- 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>
12 KiB
12 KiB
Analytics Module — File Paths & Quick Reference
🔗 Core Module Files
/apps/api/src/modules/analytics/
│
├── analytics.module.ts
│ └─ Registers all handlers, repositories, services
│ └─ Module metadata: imports, controllers, providers, exports
│ └─ KEY: CommandHandlers, QueryHandlers, EventHandlers arrays
│
├── index.ts
│ └─ Public exports of analytics module
│
└── README.md
└─ Module documentation
🎯 Controllers (Entry Points)
/apps/api/src/modules/analytics/presentation/controllers/
analytics.controller.ts (19 endpoints)
├─ GET /analytics/market-report
├─ GET /analytics/market-snapshot
├─ GET /analytics/price-trend
├─ GET /analytics/heatmap
├─ GET /analytics/district-stats
├─ GET /analytics/valuation (query param)
├─ POST /analytics/valuation (body)
├─ POST /analytics/valuation/batch
├─ GET /analytics/valuation/history/:propertyId
├─ POST /analytics/valuation/compare
├─ GET /analytics/neighborhoods/:district/score
├─ GET /analytics/pois/nearby
├─ POST /analytics/listings/:id/ai-advice
└─ POST /analytics/projects/:id/ai-advice
avm.controller.ts (5 endpoints)
├─ POST /avm/batch
├─ GET /avm/history/:propertyId
├─ GET /avm/compare?ids=...
├─ GET /avm/explain?valuationId=...
└─ POST /avm/industrial
📋 DTOs (Requests & Responses)
/apps/api/src/modules/analytics/presentation/dto/
REQUEST DTOs (from @Query/@Body):
├─ get-district-stats.dto.ts (city, period)
├─ get-heatmap.dto.ts (city, period)
├─ get-market-report.dto.ts (city, period, propertyType)
├─ get-market-snapshot.dto.ts (city, propertyType)
├─ get-price-trend.dto.ts (district, city, propertyType, periods)
├─ get-valuation.dto.ts (propertyId | lat/lng/areaM2)
├─ get-nearby-pois.dto.ts (lat, lng, radius, limit)
├─ predict-valuation.dto.ts (20+ fields, v1 & v2)
├─ batch-valuation.dto.ts (propertyIds: string[])
├─ valuation-history.dto.ts (limit)
├─ valuation-comparison.dto.ts (propertyIds)
├─ avm-compare-query.dto.ts (ids)
├─ avm-explain-query.dto.ts (valuationId)
├─ industrial-valuation.dto.ts (30+ industrial fields)
└─ get-trending-areas.dto.ts (city, propertyType, limit, period)
RESPONSE DTOs (exported from handlers):
(See handler files below)
🔄 Queries (CQRS Pattern)
/apps/api/src/modules/analytics/application/queries/
STRUCTURE OF EACH QUERY TYPE:
get-market-snapshot/
├─ get-market-snapshot.query.ts
│ └─ export class GetMarketSnapshotQuery { ... }
│
└─ get-market-snapshot.handler.ts
├─ @QueryHandler(GetMarketSnapshotQuery)
├─ execute(query): Promise<MarketSnapshotDto>
└─ export interface MarketSnapshotDto { ... }
ALL QUERY TYPES (15+):
├─ get-market-snapshot/ ..................... Dashboard overview
├─ get-district-stats/ ..................... Stats aggregation
├─ get-price-trend/ ........................ Time-series data
├─ get-heatmap/ ........................... Geographic visualization
├─ get-valuation/ ......................... Single valuation
├─ predict-valuation/ ..................... AI prediction
├─ batch-valuation/ ....................... Multiple valuations
├─ valuation-history/ ..................... Time-series valuations
├─ valuation-comparison/ .................. Side-by-side comparison
├─ valuation-explanation/ ................. Model drivers
├─ get-neighborhood-score/ ................ Neighborhood quality
├─ get-nearby-pois/ ....................... Point of interests
├─ get-listing-ai-advice/ ................. Claude analysis
├─ get-project-ai-advice/ ................. Project analysis
├─ industrial-valuation/ .................. Industrial rent
├─ get-market-report/ ..................... Detailed report
└─ get-trending-areas/ .................... Trending districts
🏛️ Domain Layer
/apps/api/src/modules/analytics/domain/
repositories/ (Interfaces only — NO IMPLEMENTATION)
├─ market-index.repository.ts
│ ├─ export const MARKET_INDEX_REPOSITORY = Symbol(...)
│ ├─ export interface IMarketIndexRepository {
│ │ findById(id)
│ │ findByKey(district, city, propertyType, period)
│ │ save(entity)
│ │ update(entity)
│ │ getMarketReport(city, period, propertyType?)
│ │ getHeatmap(city, period)
│ │ getPriceTrend(district, city, propertyType, periods)
│ │ getDistrictStats(city, period)
│ │ }
│ └─ Result interfaces: MarketReportResult, HeatmapDataPoint, etc.
│
└─ valuation.repository.ts
├─ export const VALUATION_REPOSITORY = Symbol(...)
└─ export interface IValuationRepository { ... }
entities/
├─ market-index.entity.ts
│ └─ Domain logic for market data aggregation
│
└─ valuation.entity.ts
└─ Domain logic for property valuation
services/
├─ avm-service.ts
│ └─ export const AVM_SERVICE = Symbol(...)
│ └─ IAVMService interface: predict(), getComparables(), etc.
│
└─ neighborhood-score.service.ts
└─ INeighborhoodScoreService interface
events/
└─ market-index-updated.event.ts
└─ Domain event when market data updates
🔧 Infrastructure Layer
/apps/api/src/modules/analytics/infrastructure/
repositories/ (IMPLEMENTATIONS)
├─ prisma-market-index.repository.ts
│ ├─ @Injectable()
│ └─ class PrismaMarketIndexRepository implements IMarketIndexRepository {
│ └─ Uses PrismaService for data access
│
└─ prisma-valuation.repository.ts
└─ class PrismaValuationRepository implements IValuationRepository
services/ (IMPLEMENTATIONS)
├─ http-avm.service.ts
│ ├─ @Injectable()
│ ├─ Calls Python AI service (HTTP client)
│ └─ Falls back to PrismaAVMService if Python is down
│
├─ prisma-avm.service.ts
│ ├─ @Injectable()
│ └─ Fallback ML model using Prisma data
│
├─ http-neighborhood-score.service.ts
│ ├─ HTTP proxy to external scoring service
│ └─ Falls back to PrismaNeighborhoodScoreService
│
├─ prisma-neighborhood-score.service.ts
│ └─ In-DB scoring logic
│
├─ ai-service.client.ts
│ ├─ Wrapper around Anthropic SDK
│ └─ Calls Claude API for AI analysis
│
└─ market-index-cron.service.ts
└─ Scheduled job to update MarketIndex table
🎨 Interceptors
/apps/api/src/modules/analytics/presentation/interceptors/
cache-meta.interceptor.ts
├─ @Injectable() CacheMetaInterceptor
├─ Wraps response: T => { data: T; cacheMeta: {...} }
├─ cacheMeta includes: cachedAt, nextRefreshAt, source
└─ Applied via @UseInterceptors(CacheMetaInterceptor)
📦 Shared Module (Reusable Utilities)
/apps/api/src/modules/shared/
infrastructure/
cache.service.ts
├─ @Injectable() CacheService
├─ async getOrSet<T>(key, loader, ttl, resource)
│ └─ Cache-aside pattern
│ └─ Metrics: cache_hit_total, cache_miss_total, cache_degradation_total
├─ async invalidate(key)
├─ async invalidateByPrefix(prefix)
├─ static buildKey(prefix, ...parts)
└─ Graceful degradation when Redis is down
decorators/
├─ cacheable.decorator.ts
│ ├─ @Cacheable(options) method decorator
│ └─ Declarative caching for query handlers
│
└─ other decorators...
cache-meta.store.ts
├─ export const cacheMetaStorage = new AsyncLocalStorage()
└─ Per-request storage of cache metadata
logger.service.ts
├─ @Injectable() LoggerService
├─ log(), warn(), error() with context
└─ Winston integration
prisma.service.ts
├─ @Injectable() PrismaService
├─ Wrapper around Prisma Client
└─ Handles connection lifecycle
redis.service.ts
├─ @Injectable() RedisService
├─ get(), set(), del(), scan()
└─ Health check & graceful degradation
guards/
├─ endpoint-rate-limit.guard.ts
├─ ... other guards
└─ auth module exports JwtAuthGuard
shared.module.ts
└─ Registers all shared services
📊 Database Schema
prisma/schema.prisma
Models relevant to analytics:
├─ Property
│ ├─ id, propertyType, address, district, city
│ ├─ location (PostGIS Point)
│ ├─ areaM2, bedrooms, bathrooms, floors
│ └─ Indexes: [propertyType], [district, city], [location]
│
├─ Listing
│ ├─ id, propertyId (FK), sellerId (FK)
│ ├─ status (ACTIVE, SOLD, EXPIRED, ...)
│ ├─ priceVND (BigInt), pricePerM2, publishedAt
│ ├─ aiPriceEstimate, aiConfidence (for AVM)
│ └─ Indexes: [status], [sellerId, status], [publishedAt]
│
├─ MarketIndex
│ ├─ district, city, propertyType, period
│ ├─ medianPrice (BigInt), avgPriceM2
│ ├─ totalListings, daysOnMarket, inventoryLevel
│ └─ Unique: [district, city, propertyType, period]
│
├─ Valuation
│ ├─ id, propertyId (FK)
│ ├─ estimatedPrice (BigInt), confidence
│ ├─ method (AVM_v1, AVM_v2, MANUAL)
│ ├─ features (Json), comparables (Json), explainers (Json)
│ └─ Index: [propertyId, valuationDate DESC]
│
└─ ProjectDevelopment
├─ id, slug, developer
├─ location (PostGIS Point)
├─ minPrice, maxPrice, pricePerM2Range
└─ Index: [district, city], [location]
🌳 Directory Tree Summary
goodgo-platform-ai/
└─ apps/api/src/modules/
├─ analytics/ (this module) .................. ~2000 LOC
│ ├─ presentation/
│ │ ├─ controllers/
│ │ │ ├─ analytics.controller.ts ......... 331 lines
│ │ │ └─ avm.controller.ts .............. 171 lines
│ │ ├─ dto/ (15+ files)
│ │ └─ interceptors/
│ │ └─ cache-meta.interceptor.ts ....... 61 lines
│ ├─ application/
│ │ ├─ queries/ (15+ handlers)
│ │ ├─ commands/ (3 handlers)
│ │ └─ event-handlers/
│ ├─ domain/
│ │ ├─ repositories/ (2 interfaces)
│ │ ├─ entities/ (2 entities)
│ │ ├─ services/ (2 interfaces)
│ │ └─ events/
│ ├─ infrastructure/
│ │ ├─ repositories/ (2 implementations)
│ │ └─ services/ (6 implementations)
│ └─ analytics.module.ts
│
├─ shared/ ............................. Reusable utilities
│ ├─ infrastructure/
│ │ ├─ cache.service.ts ............... 191 lines (core pattern)
│ │ ├─ decorators/
│ │ │ └─ cacheable.decorator.ts ....... 57 lines
│ │ ├─ cache-meta.store.ts
│ │ ├─ logger.service.ts
│ │ ├─ prisma.service.ts
│ │ ├─ redis.service.ts
│ │ └─ guards/ (rate limit, auth, etc)
│ └─ shared.module.ts
│
├─ auth/ ............................. Authentication
├─ subscriptions/ ..................... Quota & billing
├─ listings/ ......................... Listing management
└─ [other modules]
🔑 Key Imports Pattern
// In analytics.controller.ts
import { QueryBus } from '@nestjs/cqrs';
import { JwtAuthGuard } from '@modules/auth';
import { EndpointRateLimit, EndpointRateLimitGuard } from '@modules/shared';
import { RequireQuota, QuotaGuard } from '@modules/subscriptions';
// In query handlers
import {
DomainException,
CacheService,
CachePrefix,
CacheTTL,
Cacheable,
LoggerService,
PrismaService,
} from '@modules/shared';
import { MARKET_INDEX_REPOSITORY, type IMarketIndexRepository } from '../../domain/...';
// In infrastructure repositories
import { Injectable } from '@nestjs/common';
import { PrismaService } from '@modules/shared';
import { type IMarketIndexRepository } from '../../domain/...';
🚀 Key Numbers
| Metric | Value |
|---|---|
| Controllers | 2 |
| Endpoints | 24 |
| Query Handlers | 15+ |
| DTOs | 15+ |
| Repository Interfaces | 2 |
| Repository Implementations | 2 |
| Services (interfaces) | 2 |
| Services (implementations) | 6+ |
| Cache Prefixes | 10+ |
| Cache TTLs | 20+ |
| Total LOC (analytics module) | ~2000 |
| Total LOC (shared/cache) | ~250 |