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