Files
goodgo-platform/apps/api/src/modules/analytics

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

{
  "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:

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

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