# 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` |

