Files
goodgo-platform/docs/explorations/ANALYTICS_QUICK_REFERENCE.md
Ho Ngoc Hai 08b96f9c2d 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>
2026-04-21 16:29:24 +07:00

410 lines
11 KiB
Markdown

# Analytics Module - Quick Reference Card
## 🏗️ Architecture Stack
```
PRESENTATION (controllers) → APPLICATION (CQRS) → DOMAIN (entities/services) → INFRASTRUCTURE (data access)
```
**Files:**
- Controllers: `presentation/controllers/*.controller.ts`
- DTOs: `presentation/dto/*.dto.ts`
- Queries/Commands: `application/queries/*.query.ts` + `*.handler.ts`
- Repositories (interface): `domain/repositories/*.ts`
- Repositories (impl): `infrastructure/repositories/prisma-*.repository.ts`
---
## 📍 Key File Paths
```
ANALYTICS MODULE ROOT
└── apps/api/src/modules/analytics/
CONTROLLERS
├── analytics.controller.ts (GET/POST /analytics/*)
└── avm.controller.ts (GET/POST /avm/*)
QUERY HANDLERS (14+ queries)
├── get-price-trend/
├── get-heatmap/
├── get-market-report/
├── get-district-stats/
├── get-valuation/
├── predict-valuation/
├── batch-valuation/
├── industrial-valuation/
├── get-neighborhood-score/
├── get-nearby-pois/
├── get-listing-ai-advice/ (Claude)
├── get-project-ai-advice/ (Claude)
├── valuation-history/
├── valuation-comparison/
└── valuation-explanation/
REPOSITORIES (abstraction)
├── domain/repositories/market-index.repository.ts
└── domain/repositories/valuation.repository.ts
REPOSITORIES (Prisma impl)
├── infrastructure/repositories/prisma-market-index.repository.ts
└── infrastructure/repositories/prisma-valuation.repository.ts
SERVICES
├── infrastructure/services/http-avm.service.ts (→ Python AI)
├── infrastructure/services/prisma-avm.service.ts (fallback)
├── infrastructure/services/neighborhood-score.service.ts
└── infrastructure/services/ai-service.client.ts (Claude)
SHARED GUARDS & DECORATORS
├── @EndpointRateLimit({limit, windowSeconds, keyStrategy})
├── @RequireQuota('analytics_queries')
├── @UseGuards(EndpointRateLimitGuard, JwtAuthGuard, QuotaGuard)
└── /modules/shared/infrastructure/guards/
```
---
## 🔐 Guards & Decorators Stack
```typescript
@ApiBearerAuth('JWT')
@EndpointRateLimit({ limit: 10, windowSeconds: 60, keyStrategy: 'user' })
@UseGuards(EndpointRateLimitGuard, JwtAuthGuard, QuotaGuard)
@RequireQuota('analytics_queries')
async handler() { }
```
**Order matters:**
1. `EndpointRateLimitGuard` — Redis sliding-window rate limit
2. `JwtAuthGuard` — Verify JWT token
3. `QuotaGuard` — Check subscription quota
---
## 💾 Cache Patterns
### Cache-Aside Pattern
```typescript
return this.cache.getOrSet(
cacheKey, // Unique cache key
async () => { /* loader */ }, // Function to load data if miss
CacheTTL.MARKET_DATA, // TTL in seconds (1800 = 30 min)
'price_trend' // Metric label for Prometheus
);
```
### Cache Key Building
```typescript
CacheService.buildKey(
CachePrefix.MARKET_TREND, // Prefix enum
query.district, // Key component 1
query.city, // Key component 2
query.propertyType, // Key component 3
query.periods?.join(',') // Key component N
)
// Result: "cache:market:trend:Quận 1:Hồ Chí Minh:APARTMENT:2024-Q1,2024-Q2"
```
### Useful TTLs
```
CacheTTL.MARKET_DATA = 1800 (30 min, price trends)
CacheTTL.MARKET_REPORT = 900 (15 min, summaries)
CacheTTL.HEATMAP = 300 (5 min, heatmaps)
CacheTTL.DISTRICT_STATS = 300 (5 min, stats)
CacheTTL.LISTING_DETAIL = 300 (5 min, detail pages)
CacheTTL.SEARCH_RESULTS = 120 (2 min, search results)
CacheTTL.REFERENCE_DATA = 86400 (24 hours, static data)
```
### Cache Prefixes for Analytics
```
CachePrefix.MARKET_REPORT "cache:market:report"
CachePrefix.MARKET_TREND "cache:market:trend"
CachePrefix.MARKET_HEATMAP "cache:market:heatmap"
CachePrefix.MARKET_DISTRICT "cache:market:district"
CachePrefix.VALUATION "cache:valuation"
```
---
## 🗃️ Prisma Models (Analytics-Related)
### Property
```prisma
model Property {
propertyType PropertyType // APARTMENT, HOUSE, LAND, COMMERCIAL
status PropertyStatus // ACTIVE, SOLD, RENTED
areaM2 Float?
bedrooms Int?
bathrooms Int?
district String
city String
location geometry(Point) // PostGIS
createdAt DateTime
updatedAt DateTime
}
```
### Listing (with Analytics Fields)
```prisma
model Listing {
priceVND BigInt // Main price
pricePerM2 Float? // Derived for analytics
transactionType TransactionType // BUY_SELL, RENT
status ListingStatus // DRAFT, ACTIVE, SOLD, etc.
// AI Valuation
aiPriceEstimate BigInt?
aiConfidence Float?
// Tracking
viewCount Int
saveCount Int
inquiryCount Int
publishedAt DateTime?
createdAt DateTime
updatedAt DateTime
}
```
### MarketIndex (Pre-calculated)
```prisma
model MarketIndex {
district String
city String
propertyType PropertyType
period String // "2024-Q1" or "2024-04"
medianPrice BigInt
avgPriceM2 Float
totalListings Int
daysOnMarket Int
inventoryLevel Int
absorptionRate Float?
yoyChange Float?
@@unique([district, city, propertyType, period])
}
```
### Valuation
```prisma
model Valuation {
propertyId String
estimatedPrice BigInt
confidence Float // 0-1
drivers Json? // Key price drivers
comparables Json? // Similar properties
explanation String?
model String // "v1" or "v2"
}
```
---
## 📊 CQRS Handler Pattern
### Query Class
```typescript
// application/queries/get-price-trend/get-price-trend.query.ts
export class GetPriceTrendQuery {
constructor(
public readonly district: string,
public readonly city: string,
public readonly propertyType: PropertyType,
public readonly periods: string[],
) {}
}
```
### Handler
```typescript
// application/queries/get-price-trend/get-price-trend.handler.ts
@QueryHandler(GetPriceTrendQuery)
export class GetPriceTrendHandler implements IQueryHandler<GetPriceTrendQuery> {
constructor(
@Inject(MARKET_INDEX_REPOSITORY)
private readonly repo: IMarketIndexRepository,
private readonly cache: CacheService,
private readonly logger: LoggerService,
) {}
async execute(query: GetPriceTrendQuery): Promise<PriceTrendDto> {
try {
const cacheKey = CacheService.buildKey(
CachePrefix.MARKET_TREND,
query.district,
query.city,
query.propertyType,
query.periods?.join(','),
);
return this.cache.getOrSet(
cacheKey,
async () => {
const trend = await this.repo.getPriceTrend(
query.district,
query.city,
query.propertyType,
query.periods,
);
return {
district: query.district,
city: query.city,
propertyType: query.propertyType,
trend
};
},
CacheTTL.MARKET_DATA,
'price_trend',
);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(`Failed...`, error?.stack, this.constructor.name);
throw new InternalServerErrorException('...');
}
}
}
```
### Controller Integration
```typescript
@ApiBearerAuth('JWT')
@UseGuards(JwtAuthGuard, QuotaGuard)
@RequireQuota('analytics_queries')
@Get('price-trend')
async getPriceTrend(@Query() dto: GetPriceTrendDto): Promise<PriceTrendDto> {
return this.queryBus.execute(
new GetPriceTrendQuery(dto.district, dto.city, dto.propertyType, dto.periods)
);
}
```
---
## 🛡️ Error Handling Pattern
```typescript
async execute(query: Query): Promise<Result> {
try {
// Business logic
return this.cache.getOrSet(cacheKey, loader, ttl, 'metric');
} catch (error) {
// Re-throw domain errors as-is
if (error instanceof DomainException) throw error;
// Log and wrap unexpected errors
this.logger.error(
`Failed to process: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
this.constructor.name,
);
// Return user-friendly message
throw new InternalServerErrorException('Không thể xử lý yêu cầu.');
}
}
```
### Exception Hierarchy
```
DomainException (base)
├── NotFoundException
├── ValidationException
├── ConflictException
├── UnauthorizedException
└── ForbiddenException
```
---
## 🎯 Common Endpoints
```bash
# Market Analytics
GET /analytics/market-report?city=...&period=...&propertyType=...
GET /analytics/price-trend?district=...&city=...&propertyType=...&periods=...
GET /analytics/heatmap?city=...&period=...
GET /analytics/district-stats?city=...&period=...
# Property Valuation (AVM)
GET /analytics/valuation?propertyId=... OR ?latitude=...&longitude=...&areaM2=...
POST /analytics/valuation (form input)
POST /analytics/valuation/batch (1-50 properties)
GET /analytics/valuation/history/:id
POST /analytics/valuation/compare (2-5 properties)
# AVM Endpoints (alias routes)
POST /avm/batch
GET /avm/history/:propertyId
GET /avm/compare?ids=...
GET /avm/explain?valuationId=...
POST /avm/industrial
# Neighborhood & Location
GET /analytics/neighborhoods/:district/score
GET /analytics/pois/nearby?lat=...&lng=...&radius=...&limit=...
# AI Advice (Claude)
POST /analytics/listings/:id/ai-advice
POST /analytics/projects/:id/ai-advice
```
---
## 📋 Dependency Injection
### Module Providers
```typescript
@Module({
providers: [
// Repositories (abstraction → implementation)
{ provide: MARKET_INDEX_REPOSITORY, useClass: PrismaMarketIndexRepository },
{ provide: VALUATION_REPOSITORY, useClass: PrismaValuationRepository },
// Services (fallback pattern)
PrismaAVMService,
{ provide: AVM_SERVICE, useClass: HttpAVMService }, // Tries HTTP first
// All handlers
...CommandHandlers,
...QueryHandlers,
...EventHandlers,
],
exports: [
MARKET_INDEX_REPOSITORY,
VALUATION_REPOSITORY,
AVM_SERVICE,
],
})
```
### Injection Pattern
```typescript
constructor(
@Inject(MARKET_INDEX_REPOSITORY)
private readonly repo: IMarketIndexRepository,
private readonly cache: CacheService,
private readonly logger: LoggerService,
) {}
```
---
## ✅ Key Conventions
| Aspect | Convention |
|--------|-----------|
| **Query File** | `get-price-trend.query.ts` |
| **Handler File** | `get-price-trend.handler.ts` |
| **Class Names** | `GetPriceTrendQuery`, `GetPriceTrendHandler`, `PriceTrendDto` |
| **Cache Key** | `CacheService.buildKey(CachePrefix.*, ...params)` |
| **Cache TTL** | Use `CacheTTL.*` constants |
| **Metric Label** | Lowercase, underscore-separated: `'price_trend'` |
| **Rate Limit** | `{ limit: N, windowSeconds: 60, keyStrategy: 'user' \| 'ip' }` |
| **Exception** | Catch `DomainException` separately; wrap others |
| **BigInt in JSON** | Always stringify: `.toString()` |
| **Shared Services** | Import from `@modules/shared` |