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,312 @@
# 🎯 Analytics Module Architecture Exploration — Complete
## Documents Created
I've prepared **3 comprehensive guides** for you:
### 1⃣ **analytics_architecture_guide.md** (36 KB)
**The Comprehensive Reference** — Read this first for deep understanding
- ✅ DDD layer breakdown with code examples
- ✅ All 24 endpoints documented
- ✅ Query handler patterns (2 styles: @Cacheable vs manual)
- ✅ Complete caching system (Redis patterns, TTLs, invalidation)
- ✅ Real code examples from `GetMarketSnapshotHandler`, `GetDistrictStatsHandler`
- ✅ Full Prisma schema for Property, Listing, MarketIndex, Valuation
- ✅ Shared module utilities (CacheService, Cacheable decorator, interceptor)
-**7-step guide: How to add GET /analytics/trending-areas endpoint**
- ✅ Testing patterns
- ✅ Error handling conventions
### 2⃣ **quick_reference.md** (8 KB)
**The Visual Quick Start** — Use this to navigate fast
- ✅ Layer stack diagram (Presentation → Application → Domain → Infrastructure)
- ✅ Request flow example (HTTP → Controller → QueryHandler → Cache → DB)
- ✅ Caching strategy matrix (when to cache, TTLs, prefixes)
- ✅ Decorators & guards cheat sheet
- ✅ Prisma schema snapshot
- ✅ Response structure with/without cache metadata
- ✅ 7-step endpoint addition checklist
### 3⃣ **file_paths_reference.md** (8 KB)
**The Navigation Map** — Find files & understand structure
- ✅ Core module files (analytics.module.ts, index.ts)
- ✅ All 24 endpoints mapped to file paths
- ✅ DTO files organized by type (request vs response)
- ✅ All 15+ query types with descriptions
- ✅ Domain, Infrastructure, and Shared layer breakdowns
- ✅ Database schema models with fields & indexes
- ✅ Directory tree with line counts
- ✅ Import patterns reference
- ✅ Key metrics & numbers
---
## ✨ Key Findings
### Architecture Pattern
```
Domain-Driven Design (DDD) + CQRS (Command Query Responsibility Segregation)
4-layer structure: Presentation → Application → Domain → Infrastructure
```
### Controllers (Entry Points)
- **AnalyticsController**: 19 endpoints (`/analytics/...`)
- **AvmController**: 5 endpoints (`/avm/...`)
- **Total**: 24 endpoints, all with guards (JWT, Quota, Rate Limit)
### Query Handlers: 2 Caching Patterns
**Pattern 1: @Cacheable Decorator** (Simpler)
```ts
@QueryHandler(GetDistrictStatsQuery)
export class GetDistrictStatsHandler {
@Cacheable({
prefix: CachePrefix.MARKET_DISTRICT,
ttl: CacheTTL.DISTRICT_STATS,
resource: 'district_stats',
keyFrom: (query) => [query.city, query.period],
})
async execute(query): Promise<DistrictStatsDto> {
return this.marketIndexRepo.getDistrictStats(query.city, query.period);
}
}
```
**Pattern 2: cache.getOrSet()** (For complex computation)
```ts
async execute(query): Promise<MarketSnapshotDto> {
const cacheKey = CacheService.buildKey(CachePrefix.MARKET_SNAPSHOT, query.city);
return await this.cache.getOrSet(
cacheKey,
() => this.computeSnapshot(query.city), // Heavy computation
CacheTTL.MARKET_SNAPSHOT, // TTL in seconds
'market_snapshot', // Prometheus label
);
}
```
### Redis Caching Strategy
- **Cache-aside pattern**: Try Redis → if miss, call loader, store result
- **Envelope format**: `{ __v: data, cachedAt: ISO, ttlSeconds: 300 }`
- **Graceful degradation**: If Redis down, calls loader directly (no error)
- **Metrics**: `cache_hit_total`, `cache_miss_total`, `cache_degradation_total`
- **TTLs**: Dashboard=300s, Reports=900s, Trends=1800s, Predictions=NO_CACHE
### Prisma Schema
- **Property**: id, type, address, district, city, location (PostGIS Point), area, rooms, etc.
- **Listing**: id, propertyId, sellerId, status, priceVND (BigInt), aiPriceEstimate, publishedAt
- **MarketIndex**: district, city, propertyType, period; medianPrice (BigInt), avgPriceM2, stats
- **Valuation**: id, propertyId, estimatedPrice, confidence, method, features (Json)
### DDD Layers
1. **Presentation**: Controllers, DTOs, Interceptors
2. **Application**: Query/Command Handlers (@QueryHandler, @CommandHandler)
3. **Domain**: Entities, Repository Interfaces, Service Interfaces
4. **Infrastructure**: Prisma Repositories, HTTP Services, External clients
### Shared Module Utilities
- **CacheService**: Core cache-aside with Redis
- **@Cacheable**: Method decorator for handlers
- **CacheMetaInterceptor**: Wraps responses with `{ data, cacheMeta }`
- **LoggerService**: Winston-based logging
- **PrismaService**: ORM wrapper
- **RedisService**: Redis client wrapper
- **Guards**: JWT, Quota, Rate Limit
---
## 🚀 Quick Start: Adding New Endpoint
Example: **GET /analytics/trending-areas**
### 7-Step Process
1. **Request DTO** (`presentation/dto/get-trending-areas.dto.ts`)
```ts
export class GetTrendingAreasDto {
@IsOptional() city?: string = 'Hồ Chí Minh';
@IsOptional() propertyType?: PropertyType;
@IsOptional() @Min(1) limit?: number = 10;
}
```
2. **Query Class** (`application/queries/get-trending-areas/get-trending-areas.query.ts`)
```ts
export class GetTrendingAreasQuery {
constructor(
public readonly city: string,
public readonly propertyType: PropertyType | undefined,
public readonly limit: number,
) {}
}
```
3. **Handler** (`application/queries/get-trending-areas/get-trending-areas.handler.ts`)
```ts
@QueryHandler(GetTrendingAreasQuery)
export class GetTrendingAreasHandler implements IQueryHandler {
@Cacheable({
prefix: CachePrefix.TRENDING_AREAS,
ttl: CacheTTL.TRENDING_AREAS,
resource: 'trending_areas',
keyFrom: (query) => [query.city, query.propertyType, query.limit],
})
async execute(query: GetTrendingAreasQuery): Promise<GetTrendingAreasDto> {
// Your logic here
return { city: query.city, areas: [...], cachedAt: null, nextRefreshAt: null };
}
}
export interface GetTrendingAreasDto {
city: string;
areas: TrendingAreaDto[];
cachedAt: string | null;
nextRefreshAt: string | null;
}
```
4. **Register Handler** (in `analytics.module.ts`)
```ts
const QueryHandlers = [
// ... existing
GetTrendingAreasHandler, // Add here
];
```
5. **Controller Method** (in `analytics.controller.ts`)
```ts
@ApiBearerAuth('JWT')
@UseGuards(JwtAuthGuard, QuotaGuard)
@RequireQuota('analytics_queries')
@Get('trending-areas')
@ApiOperation({ summary: 'Get trending districts' })
@ApiResponse({ status: 200, description: 'Trending areas retrieved' })
async getTrendingAreas(@Query() dto: GetTrendingAreasDto): Promise<GetTrendingAreasDto> {
return this.queryBus.execute(
new GetTrendingAreasQuery(dto.city || 'Hồ Chí Minh', dto.propertyType, dto.limit || 10),
);
}
```
6. **Export DTOs** (in `presentation/dto/index.ts`)
```ts
export * from './get-trending-areas.dto';
```
7. **Test** (in `__tests__/get-trending-areas.handler.spec.ts`)
```ts
it('should return trending areas', async () => {
const query = new GetTrendingAreasQuery('Hồ Chí Minh', undefined, 10);
const result = await handler.execute(query);
expect(result.areas).toBeDefined();
});
```
---
## 📊 Architecture Decision Points
| Decision | Current Approach | Why |
|----------|------------------|-----|
| **Caching** | Redis + cache-aside | TTL-based expiry is simple & performant |
| **Cache Invalidation** | Prefix-based SCAN | Non-blocking, doesn't require key enumeration |
| **Cache Metadata** | AsyncLocalStorage + Interceptor | Per-request context without global state |
| **Query Patterns** | CQRS with QueryBus | Separates reads from writes, enables caching layer |
| **Rate Limiting** | EndpointRateLimitGuard | Per-endpoint control, different rates for different ops |
| **Quota Metering** | @RequireQuota decorator | Subscription-aware, tracks usage |
| **Response Format** | DTO with cache metadata | Frontend knows freshness of data |
| **Error Handling** | DomainException + InternalServerError | Differentiates logic errors from system errors |
| **Graceful Degradation** | Cache bypass if Redis down | Service stays up during Redis maintenance |
---
## 🎯 Core Conventions to Remember
✅ **Always cache with TTL**
- Dashboard tiles: 300s
- Aggregations: 300s
- Reports: 900s
- Trends: 1800s
- Predictions: NO CACHE (always fresh)
✅ **Always use CacheService.buildKey()**
- Ensures deterministic, lowercase keys
- Replaces spaces with underscores
- Format: `prefix:param1:param2:param3`
✅ **Always wrap handlers in try-catch**
```ts
try {
// logic
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(...);
throw new InternalServerErrorException('...');
}
```
✅ **Always return DTO with null metadata**
```ts
return {
// ...data fields
cachedAt: null, // Filled by CacheMetaInterceptor
nextRefreshAt: null, // Filled by CacheMetaInterceptor
};
```
✅ **Always use @UseInterceptors(CacheMetaInterceptor)**
- On controllers to wrap response
- Adds: `{ data: T, cacheMeta: { cachedAt, nextRefreshAt, source } }`
✅ **Always add guards to endpoints**
```ts
@UseGuards(JwtAuthGuard, QuotaGuard) // Auth + quota check
@RequireQuota('analytics_queries') // Meter usage
@EndpointRateLimit({ limit: 10, windowSeconds: 60 }) // Rate limit if needed
```
---
## 📁 File Summary
| File Path | Purpose | Lines |
|-----------|---------|-------|
| `presentation/controllers/analytics.controller.ts` | Main endpoints | 331 |
| `presentation/controllers/avm.controller.ts` | Valuation endpoints | 171 |
| `application/queries/*/get-*.handler.ts` | Query execution + caching | 50-100 ea |
| `domain/repositories/market-index.repository.ts` | Repository interface | 58 |
| `infrastructure/repositories/prisma-market-index.repository.ts` | Prisma implementation | 150+ |
| `presentation/interceptors/cache-meta.interceptor.ts` | Response wrapper | 61 |
| `../shared/infrastructure/cache.service.ts` | Redis cache layer | 191 |
| `../shared/infrastructure/decorators/cacheable.decorator.ts` | @Cacheable | 57 |
| `analytics.module.ts` | NestJS module definition | 102 |
---
## 🎓 Next Steps
1. **Read** → Start with `quick_reference.md` for visual understanding
2. **Reference** → Use `file_paths_reference.md` to find specific files
3. **Deep Dive** → Study `analytics_architecture_guide.md` for patterns & code
4. **Build** → Follow the 7-step checklist to add your first endpoint
5. **Test** → Create query handler spec following existing patterns
---
## Questions to Validate Understanding
After reading the guides, you should be able to answer:
1. What are the 4 DDD layers and what goes in each?
2. How does the cache-aside pattern work when Redis is down?
3. What's the difference between @Cacheable and cache.getOrSet()?
4. Why do response DTOs have `cachedAt: null` and `nextRefreshAt: null`?
5. How is the cache key built deterministically?
6. What TTLs are used for different endpoint types?
7. What guards are required on all analytics endpoints?
8. How do you add a new GET endpoint in 7 steps?
9. What's the purpose of CacheMetaInterceptor?
10. How does graceful degradation work if Redis is unavailable?
---