docs: consolidate exploration & audit reports under docs/ (TEC-3094)

- 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>
This commit is contained in:
Ho Ngoc Hai
2026-04-21 16:29:24 +07:00
parent 912121cf09
commit 08b96f9c2d
39 changed files with 15129 additions and 562 deletions

View File

@@ -0,0 +1,250 @@
# Quick Reference: Analytics Module Architecture
## 🏗️ Layer Stack (DDD + CQRS)
```
┌─────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ @Controller('analytics') / @Controller('avm') ││
│ │ ├─ @Get endpoints (call QueryBus) ││
│ │ ├─ @Post endpoints (call QueryBus or CommandBus) ││
│ │ └─ Guards: JwtAuthGuard, QuotaGuard, EndpointRateLimitGuard ││
│ ├─ DTOs: RequestDto, ResponseDto (validation) ││
│ └─ Interceptors: CacheMetaInterceptor (wraps response) ││
└─────────────────────────────────────────────────────────────────┘
↓ QueryBus.execute()
┌─────────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER (CQRS) │
│ ┌──────────────────────────────────────────────────────────────┐│
│ │ @QueryHandler(SomeQuery) ││
│ │ ├─ Receives Query class instance ││
│ │ ├─ Injects dependencies (Prisma, Cache, Logger) ││
│ │ ├─ Caching: @Cacheable decorator OR cache.getOrSet() ││
│ │ └─ Returns ResponseDto ││
│ └─ Handlers indexed in analytics.module.ts ││
└─────────────────────────────────────────────────────────────────┘
↓ Injected repos/services
┌─────────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER (Business Logic) │
│ ├─ Entities: MarketIndexEntity, ValuationEntity │
│ ├─ Repository Interfaces: IMarketIndexRepository, etc. │
│ ├─ Result DTOs: MarketReportResult, DistrictStatsResult │
│ └─ Services: IAVMService, INeighborhoodScoreService │
└─────────────────────────────────────────────────────────────────┘
↓ Injected implementation
┌─────────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ ├─ Repositories: PrismaMarketIndexRepository (implements iface) │
│ ├─ Services: HttpAVMService, PrismaAVMService │
│ └─ External: Prisma ORM, PostgreSQL, Redis, HTTP clients │
└─────────────────────────────────────────────────────────────────┘
```
## 📂 File Structure Quick Map
```
apps/api/src/modules/analytics/
├── presentation/
│ ├── controllers/
│ │ ├── analytics.controller.ts (14 GET/POST endpoints)
│ │ └── avm.controller.ts (5 GET/POST endpoints)
│ ├── dto/ (15+ DTO files)
│ │ ├── get-market-snapshot.dto.ts
│ │ ├── predict-valuation.dto.ts
│ │ └── ...
│ └── interceptors/
│ └── cache-meta.interceptor.ts (wraps response)
├── application/
│ ├── queries/ (15+ query types)
│ │ ├── get-market-snapshot/
│ │ │ ├── .query.ts (Q: GetMarketSnapshotQuery)
│ │ │ └── .handler.ts (@QueryHandler + cache logic)
│ │ ├── get-district-stats/
│ │ ├── get-price-trend/
│ │ ├── predict-valuation/
│ │ └── ...
│ ├── commands/ (3 command types)
│ ├── event-handlers/ (1 event handler)
│ └── queries/_shared/ (shared utilities)
├── domain/
│ ├── entities/
│ │ ├── market-index.entity.ts
│ │ └── valuation.entity.ts
│ ├── repositories/ (interfaces only)
│ │ ├── market-index.repository.ts
│ │ │ └── IMarketIndexRepository interface
│ │ └── valuation.repository.ts
│ ├── services/ (interfaces only)
│ │ ├── avm-service.ts
│ │ └── neighborhood-score.service.ts
│ └── events/
│ └── market-index-updated.event.ts
├── infrastructure/
│ ├── repositories/
│ │ ├── prisma-market-index.repository.ts (implements)
│ │ └── prisma-valuation.repository.ts (implements)
│ └── services/
│ ├── http-avm.service.ts (calls Python AI)
│ ├── prisma-avm.service.ts (fallback)
│ ├── http-neighborhood-score.service.ts
│ ├── prisma-neighborhood-score.service.ts
│ ├── ai-service.client.ts (Claude API)
│ └── market-index-cron.service.ts
├── analytics.module.ts (NestJS module, registers all)
├── index.ts (exports)
└── README.md
```
## 🔄 Request Flow Example
```
HTTP GET /analytics/market-snapshot?city=Ho Chi Minh
1. AnalyticsController.getMarketSnapshot(@Query dto)
└─ Validates DTO (class-validator)
└─ Calls queryBus.execute(new GetMarketSnapshotQuery(...))
2. QueryBus routes to GetMarketSnapshotHandler
└─ Handler caches key: "cache:analytics:market_snapshot:ho_chi_minh"
└─ Calls cache.getOrSet(key, computeSnapshot, 300s, 'market_snapshot')
3. CacheService.getOrSet():
├─ IF Redis HIT: return cached value, increment cache_hit_total
└─ IF MISS: call computeSnapshot(), store in Redis, increment cache_miss_total
4. GetMarketSnapshotHandler.computeSnapshot():
├─ Parallel queries:
│ ├─ listing.aggregate() → count, avg price
│ ├─ $queryRaw PERCENTILE_CONT → median
│ ├─ $queryRaw AVG(EXTRACT...) → days on market
│ └─ computePriceChangePct (3x for 1d/7d/30d)
└─ Returns MarketSnapshotDto
5. CacheMetaInterceptor wraps response:
└─ Transforms: MarketSnapshotDto
└─ Into: { data: MarketSnapshotDto, cacheMeta: { cachedAt, nextRefreshAt, source } }
6. HTTP 200 with wrapped response
```
## 💾 Caching Strategy
### When to Cache
```
✅ Dashboard tiles → 300s TTL (5 min)
✅ Aggregations (district stats) → 300s TTL
✅ Market reports → 900s TTL (15 min)
✅ Historical trends → 1800s TTL (30 min)
❌ AI predictions → NO CACHE (always fresh)
❌ User-specific data → NO CACHE (personalized)
```
### Cache Prefix Patterns
```
Prefix Example Key
────────────────────────────────────────────────────────
MARKET_SNAPSHOT cache:analytics:market_snapshot:ho_chi_minh
MARKET_DISTRICT cache:market:district:ho_chi_minh:2024_q1
MARKET_TREND cache:market:trend:q1:ho_chi_minh:apartment:...
TRENDING_AREAS cache:analytics:trending_areas:ho_chi_minh:...
VALUATION cache:valuation:prop_123
```
### Cache Invalidation
```
Automatic: Redis TTL expires
Manual: cache.invalidateByPrefix(CachePrefix.MARKET_DISTRICT)
Scans "cache:market:district:*" and deletes all matches
```
## 🛡️ Decorators & Guards
```
┌─ @ApiBearerAuth('JWT') ← Swagger annotation
├─ @UseGuards(JwtAuthGuard) ← Requires JWT token
├─ @UseGuards(QuotaGuard) ← Checks subscription quota
├─ @RequireQuota('analytics_queries') ← Meters usage
├─ @EndpointRateLimit({ limit: 10, windowSeconds: 60 })
├─ @UseGuards(EndpointRateLimitGuard) ← Rate limiter
├─ @UseInterceptors(CacheMetaInterceptor) ← Wraps response
└─ @ApiOperation({ summary: '...' })
@ApiResponse({ status: 200, description: '...' })
@ApiResponse({ status: 403, description: 'Quota exceeded' })
(Swagger documentation)
```
## 📊 Prisma Schema Snapshot
```
Property
├─ id, propertyType (APARTMENT, VILLA, ...)
├─ address, ward, district, city
├─ location (PostGIS geometry Point)
├─ areaM2, bedrooms, bathrooms, floors
├─ yearBuilt, furnishing, condition
├─ createdAt, updatedAt
└─ Indexes: [propertyType], [district, city], [location (Gist)]
Listing
├─ id, propertyId (FK), sellerId (FK)
├─ transactionType (SALE, RENT)
├─ status (ACTIVE, SOLD, EXPIRED, ...)
├─ priceVND (BigInt, CHECK > 0)
├─ pricePerM2, aiPriceEstimate, aiConfidence
├─ publishedAt, expiresAt, createdAt
└─ Indexes: [status], [sellerId, status], [status, publishedAt]
MarketIndex
├─ district, city, propertyType, period (2024-Q1)
├─ medianPrice (BigInt), avgPriceM2
├─ totalListings, daysOnMarket
├─ inventoryLevel, absorptionRate, yoyChange
└─ Unique: [district, city, propertyType, period]
```
## 🎯 Response Structure
```
WITH CacheMetaInterceptor (@UseInterceptors):
{
"data": {
"city": "Hồ Chí Minh",
"activeCount": 2500,
...
},
"cacheMeta": {
"cachedAt": "2024-04-21T10:30:00Z",
"nextRefreshAt": "2024-04-21T10:35:00Z",
"source": "cache"
}
}
WITHOUT interceptor (plain DTO):
{
"city": "Hồ Chí Minh",
"activeCount": 2500,
...
}
```
## 🚀 Adding Endpoint: 7-Step Checklist
- [ ] Create Request DTO: `presentation/dto/get-*.dto.ts`
- [ ] Create Query: `application/queries/get-*/get-*.query.ts`
- [ ] Create Handler: `application/queries/get-*/get-*.handler.ts`
- [ ] @QueryHandler decorator
- [ ] @Cacheable or cache.getOrSet()
- [ ] Try-catch with logger
- [ ] Update module: Add handler to QueryHandlers array
- [ ] Add controller method: `analytics.controller.ts` or `avm.controller.ts`
- [ ] @Get or @Post
- [ ] Guards & decorators (@UseGuards, @RequireQuota, etc.)
- [ ] Swagger annotations (@ApiOperation, @ApiResponse)
- [ ] Export DTOs from `presentation/dto/index.ts`
- [ ] Test: Query handler test, integration test