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:
250
docs/explorations/from-desktop/02_quick_reference.md
Normal file
250
docs/explorations/from-desktop/02_quick_reference.md
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user