# Analytics Module

Vietnamese real estate analytics endpoints: market reports, price trends, heatmaps, district stats, AVM (property valuation), neighborhood scores, POIs, AI-powered listing/project advice.

---

## Cache Metadata Pattern

All `/analytics/*` and `/avm/*` responses are **automatically wrapped** by `CacheMetaInterceptor` with a `cacheMeta` field that tells the frontend how fresh the data is.

### Response shape

```json
{
  "data": { /* original payload */ },
  "cacheMeta": {
    "cachedAt": "2026-04-21T10:00:00.000Z",
    "nextRefreshAt": "2026-04-21T10:15:00.000Z",
    "source": "cache"
  }
}
```

| Field | Type | Description |
|---|---|---|
| `cachedAt` | `string \| null` | ISO-8601 timestamp when the cache entry was written. `null` for legacy entries or when Redis is unavailable. |
| `nextRefreshAt` | `string \| null` | ISO-8601 timestamp when the entry will expire. Computed as `cachedAt + ttlSeconds`. `null` when `cachedAt` is null. |
| `source` | `"cache" \| "fresh"` | `"cache"` = data served from Redis; `"fresh"` = freshly fetched from DB/AI. |

### Frontend usage

Use `cacheMeta` to show a "Cập nhật lúc..." badge or tooltip:

```tsx
const label = cacheMeta.cachedAt
  ? `Cập nhật lúc ${new Date(cacheMeta.cachedAt).toLocaleTimeString('vi-VN')}`
  : 'Dữ liệu mới nhất';
```

### How it works (for backend devs)

Three components cooperate:

1. **`CacheMetaStore`** (`shared/infrastructure/cache-meta.store.ts`)  
   An `AsyncLocalStorage<{ meta: CacheMeta | null }>` that lives for the duration of a single HTTP request. Provides request isolation so concurrent requests never share metadata.

2. **`CacheService.getOrSet`** (`shared/infrastructure/cache.service.ts`)  
   Cache entries are now stored as JSON envelopes `{ __v: data, cachedAt, ttlSeconds }`.  
   On each call, `getOrSet` writes the resolved metadata into the ALS store:
   - **Cache hit** → reads `cachedAt`/`ttlSeconds` from the stored envelope, computes `nextRefreshAt`, writes `source: "cache"`.
   - **Cache miss / fresh** → writes `cachedAt = now`, computes `nextRefreshAt`, writes `source: "fresh"`.
   - **Redis unavailable** → writes `{ cachedAt: null, nextRefreshAt: null, source: "fresh" }`.

3. **`CacheMetaInterceptor`** (`analytics/presentation/interceptors/cache-meta.interceptor.ts`)  
   Applied at controller class level via `@UseInterceptors(CacheMetaInterceptor)`.  
   Wraps each response with the ALS-sourced `cacheMeta` after the handler resolves.

### Adding the pattern to a new controller

```ts
import { UseInterceptors } from '@nestjs/common';
import { CacheMetaInterceptor } from '../interceptors/cache-meta.interceptor';

@UseInterceptors(CacheMetaInterceptor)
@Controller('my-endpoint')
export class MyController { ... }
```

No other changes needed — `CacheService.getOrSet` handles metadata population automatically.

### Legacy cache entries

Entries written by previous versions of `CacheService` (plain JSON, no `__v` envelope) are still served correctly. `cacheMeta` will have `cachedAt: null` and `nextRefreshAt: null` for these entries.

---

## Endpoints

| Method | Path | Auth | Description |
|---|---|---|---|
| GET | `/analytics/market-report` | JWT + Quota | Market report per city/period |
| GET | `/analytics/price-trend` | JWT + Quota | Price trend per district |
| GET | `/analytics/heatmap` | JWT + Quota | Price heatmap |
| GET | `/analytics/district-stats` | JWT + Quota | District statistics |
| GET | `/analytics/valuation` | JWT + Quota | AVM property valuation |
| POST | `/analytics/valuation` | JWT + Quota + Rate limit | AVM from manual input |
| POST | `/analytics/valuation/batch` | JWT + Quota + Rate limit | Batch AVM (up to 50) |
| GET | `/analytics/valuation/history/:propertyId` | JWT + Quota | Valuation history |
| POST | `/analytics/valuation/compare` | JWT + Quota + Rate limit | Side-by-side comparison |
| GET | `/analytics/neighborhoods/:district/score` | Public | Neighborhood score |
| GET | `/analytics/pois/nearby` | Public | Nearby POIs |
| POST | `/analytics/listings/:id/ai-advice` | JWT | Claude AI advice for listing |
| POST | `/analytics/projects/:id/ai-advice` | JWT | Claude AI advice for project |
| POST | `/avm/batch` | JWT + Quota + Rate limit | AVM controller batch |
| GET | `/avm/history/:propertyId` | JWT + Quota | AVM controller history |
| GET | `/avm/compare` | JWT + Quota + Rate limit | AVM controller compare |
| GET | `/avm/explain` | JWT + Quota | Valuation explanation |
| POST | `/avm/industrial` | JWT + Quota + Rate limit | Industrial rent estimate |
