# Contract API – Chỉ số Thị trường & Ticker (Goodgo Platform AI)

> Liên quan: [TEC-3036](/TEC/issues/TEC-3036), [TEC-3042](/TEC/issues/TEC-3042), [TEC-3043](/TEC/issues/TEC-3043).
> Trạng thái: Draft v1 (chờ CTO + BE TechLead + FE TechLead duyệt).
> Chủ trì: API Architect. Phối hợp: Database Architect, Backend TechLead, Frontend TechLead.

Tài liệu này định nghĩa contract cho các endpoint mới phục vụ UI phong cách "sàn giao dịch" (trading-floor) của Goodgo Platform AI. Frontend **chỉ gọi API thực**, cấm mock. Các endpoint mới được thêm **không breaking** endpoint hiện có (`/analytics/*`, `/listings/*`, `/search/*`).

## 1. Nguyên tắc thiết kế

- **Base path**: `/api/v1/` (prefix chung toàn platform, bearer JWT khi cần).
- **Auth**: Public cho các chỉ số tổng hợp (cache-able). JWT cho các API cá nhân hoá (watchlist ticker).
- **Caching**: `Cache-Control: public, max-age=30, stale-while-revalidate=60` cho chỉ số ticker. `max-age=300` cho heatmap/price-trends.
- **Đơn vị**:
  - Tiền tệ: `VND` (integer, không thập phân). Field nào dùng VND/m² phải ghi rõ `unit: "VND_PER_SQM"`.
  - Thời gian: ISO-8601 UTC (`2026-04-21T03:15:00Z`). Client tự chuyển sang Asia/Ho_Chi_Minh.
  - % change: `number` (phần trăm, ví dụ `2.35` = `+2.35%`). `delta` là giá trị tuyệt đối cùng đơn vị với `value`.
- **Response envelope**: thống nhất với module analytics hiện có:
  ```json
  {
    "data": { ... },
    "meta": {
      "generatedAt": "2026-04-21T03:15:00Z",
      "ttlSeconds": 30,
      "source": "aggregation_v1",
      "baselinePeriod": "PT24H"
    }
  }
  ```
- **Lỗi**: tuân theo `docs/api-error-codes.md` (ErrorResponse `{ code, message, details }`).
- **Pagination** (nếu có): `page`, `pageSize` (max 100), response có `meta.pagination`.
- **i18n**: label/description phục vụ UI trả về tiếng Việt; field system (`code`, `slug`) dùng tiếng Anh.

## 2. Phân loại endpoint

| Nhóm | Mục đích UI | Cache | Auth |
|------|-------------|-------|------|
| Market Index | Header chỉ số GGI/khu vực | 30s | Public |
| Price Trends | Biểu đồ candlestick/line | 300s | Public |
| District Volume | Bar chart volume theo quận | 120s | Public |
| Listing Ticker | Dải chạy real-time tin mới/giá mới | 15s + SSE | Public + (JWT cho watchlist) |
| Top Movers | Bảng top tăng/giảm | 60s | Public |
| Heatmap Summary | Overlay heatmap bản đồ | 300s | Public |

## 3. Đặc tả chi tiết

### 3.1. `GET /api/v1/analytics/market-index`

Trả về chỉ số thị trường tổng hợp (Goodgo Market Index – GGI) theo phân loại.

**Query params**

| Name | Type | Required | Default | Mô tả |
|------|------|----------|---------|-------|
| `scope` | enum(`national`, `city`, `district`) | no | `city` | Phạm vi tính chỉ số |
| `cityCode` | string | when scope≠national | `HCM` | Mã thành phố (HCM, HN, DN...) |
| `districtCode` | string | when scope=district | – | Mã quận/huyện |
| `propertyType` | enum(`all`, `apartment`, `house`, `land`, `commercial`) | no | `all` | Loại hình |
| `baseline` | enum(`PT24H`, `P7D`, `P30D`, `P1Y`) | no | `PT24H` | Khoảng so sánh delta |

**Response 200**

```json
{
  "data": {
    "indexCode": "GGI-HCM-ALL",
    "indexLabel": "Chỉ số Goodgo TP.HCM - Tất cả",
    "value": 1284.35,
    "unit": "INDEX_POINT",
    "baseValue": 1000,
    "baselinePeriod": "PT24H",
    "delta": 29.62,
    "changePercent": 2.36,
    "direction": "up",
    "samples": 12845,
    "breakdown": [
      { "code": "apartment", "label": "Chung cư", "value": 1311.2, "changePercent": 3.1 },
      { "code": "house",     "label": "Nhà phố",  "value": 1198.4, "changePercent": 1.0 },
      { "code": "land",      "label": "Đất nền",  "value": 1342.7, "changePercent": 4.2 }
    ],
    "timestamp": "2026-04-21T03:15:00Z"
  },
  "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 30, "source": "aggregation_v1", "baselinePeriod": "PT24H" }
}
```

### 3.2. `GET /api/v1/analytics/price-trends`

Chuỗi giá trung bình theo thời gian (dạng nến OHLC hoặc line tuỳ `granularity`).

**Query params**

| Name | Type | Required | Default | Mô tả |
|------|------|----------|---------|-------|
| `scope` | enum(`city`, `district`, `ward`) | yes | – | |
| `scopeCode` | string | yes | – | Mã tương ứng |
| `propertyType` | enum(...) | no | `all` | |
| `granularity` | enum(`1h`, `1d`, `1w`, `1m`) | no | `1d` | Bucket thời gian |
| `from` | ISO-8601 | no | now-30d | |
| `to` | ISO-8601 | no | now | |
| `series` | enum(`avg`, `ohlc`) | no | `ohlc` | |

**Response 200**

```json
{
  "data": {
    "scope": "district",
    "scopeCode": "HCM-Q1",
    "unit": "VND_PER_SQM",
    "granularity": "1d",
    "points": [
      {
        "timestamp": "2026-04-20T00:00:00Z",
        "open": 95200000,
        "high": 96800000,
        "low": 94900000,
        "close": 96100000,
        "avg": 95600000,
        "volume": 184,
        "changePercent": 0.95
      }
    ]
  },
  "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 300, "source": "listing_snapshot_v1", "baselinePeriod": "P30D" }
}
```

### 3.3. `GET /api/v1/analytics/district-volume`

Khối lượng tin đăng/giao dịch theo quận cho bar chart.

**Query params**

| Name | Type | Required | Default |
|------|------|----------|---------|
| `cityCode` | string | no | `HCM` |
| `metric` | enum(`listings_new`, `listings_active`, `inquiries`, `transactions`) | no | `listings_new` |
| `window` | enum(`PT1H`, `PT24H`, `P7D`, `P30D`) | no | `PT24H` |
| `limit` | int (1..50) | no | 24 |
| `sort` | enum(`value_desc`, `value_asc`, `change_desc`) | no | `value_desc` |

**Response 200**

```json
{
  "data": {
    "cityCode": "HCM",
    "metric": "listings_new",
    "window": "PT24H",
    "items": [
      {
        "districtCode": "HCM-Q7",
        "districtName": "Quận 7",
        "value": 312,
        "previousValue": 284,
        "delta": 28,
        "changePercent": 9.86,
        "rank": 1
      }
    ]
  },
  "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 120, "source": "listing_stream_v1", "baselinePeriod": "PT24H" }
}
```

### 3.4. `GET /api/v1/listings/ticker`

Danh sách tick cho dải chạy real-time. Có cả poll (REST) và stream (SSE/WebSocket) version.

**Query params**

| Name | Type | Required | Default | Mô tả |
|------|------|----------|---------|-------|
| `channel` | enum(`new_listing`, `price_change`, `hot`, `featured`, `watchlist`) | no | `new_listing` | `watchlist` cần JWT |
| `scope` | enum(`national`, `city`, `district`) | no | `city` | |
| `scopeCode` | string | cond | `HCM` | |
| `propertyType` | enum(...) | no | `all` | |
| `limit` | int(1..50) | no | 20 | |
| `sinceTickId` | string | no | – | Lấy tick mới hơn ID này (polling incremental) |

**Response 200**

```json
{
  "data": {
    "channel": "new_listing",
    "ticks": [
      {
        "tickId": "tick_01J...",
        "listingId": "lst_01J...",
        "symbol": "HCM-Q7-APT-0934",
        "title": "Căn hộ 2PN view sông Phú Mỹ Hưng",
        "propertyType": "apartment",
        "districtCode": "HCM-Q7",
        "districtName": "Quận 7",
        "priceVnd": 5200000000,
        "pricePerSqmVnd": 68400000,
        "areaSqm": 76,
        "changePercent": -1.45,
        "direction": "down",
        "event": "price_change",
        "timestamp": "2026-04-21T03:14:57Z",
        "url": "/listings/lst_01J..."
      }
    ],
    "nextSinceTickId": "tick_01J...ZZ"
  },
  "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 15, "source": "ticker_stream_v1" }
}
```

#### 3.4.1. `GET /api/v1/listings/ticker/stream` (SSE)

- Content-Type: `text/event-stream`.
- Event types: `tick`, `heartbeat`, `reset`.
- Payload `data:` giống một phần tử `ticks[]` phía trên.
- Query params như REST, thêm `heartbeatSeconds` (default 15).

### 3.5. `GET /api/v1/analytics/top-movers`

Top tăng/giảm theo khu vực/loại hình.

**Query params**

| Name | Type | Required | Default |
|------|------|----------|---------|
| `direction` | enum(`gainers`, `losers`, `both`) | no | `both` |
| `scope` | enum(`city`, `district`, `ward`, `project`) | no | `district` |
| `cityCode` | string | no | `HCM` |
| `metric` | enum(`price_per_sqm`, `index_value`, `volume`) | no | `price_per_sqm` |
| `window` | enum(`PT24H`, `P7D`, `P30D`) | no | `P7D` |
| `limit` | int(1..50) | no | 10 |

**Response 200**

```json
{
  "data": {
    "window": "P7D",
    "metric": "price_per_sqm",
    "gainers": [
      {
        "code": "HCM-Q9",
        "label": "TP Thủ Đức (Q9 cũ)",
        "value": 72400000,
        "previousValue": 68900000,
        "delta": 3500000,
        "changePercent": 5.08,
        "rank": 1,
        "sampleSize": 412
      }
    ],
    "losers": [ /* same schema */ ]
  },
  "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 60, "source": "aggregation_v1", "baselinePeriod": "P7D" }
}
```

### 3.6. `GET /api/v1/analytics/heatmap-summary`

Dữ liệu tóm tắt heatmap cho overlay bản đồ.

**Query params**

| Name | Type | Required | Default | Mô tả |
|------|------|----------|---------|-------|
| `cityCode` | string | no | `HCM` | |
| `metric` | enum(`price_per_sqm`, `volume`, `demand_index`, `change_percent`) | no | `price_per_sqm` | |
| `resolution` | enum(`district`, `ward`, `h3_r7`, `h3_r8`) | no | `district` | H3 cell khi cần granularity cao |
| `window` | enum(`PT24H`, `P7D`, `P30D`) | no | `P30D` | |
| `bbox` | string `minLon,minLat,maxLon,maxLat` | no | – | Bounding box bản đồ |

**Response 200**

```json
{
  "data": {
    "metric": "price_per_sqm",
    "unit": "VND_PER_SQM",
    "resolution": "district",
    "legend": {
      "buckets": [
        { "min": 0,          "max": 40000000,  "color": "#1a9850", "label": "Thấp" },
        { "min": 40000000,   "max": 80000000,  "color": "#fee08b", "label": "Trung bình" },
        { "min": 80000000,   "max": 140000000, "color": "#d73027", "label": "Cao" }
      ]
    },
    "cells": [
      {
        "code": "HCM-Q1",
        "label": "Quận 1",
        "centroid": { "lat": 10.776, "lon": 106.700 },
        "value": 128400000,
        "changePercent": 1.8,
        "sampleSize": 532,
        "polygonRef": "geo/districts/HCM-Q1.geojson"
      }
    ]
  },
  "meta": { "generatedAt": "2026-04-21T03:15:00Z", "ttlSeconds": 300, "source": "aggregation_v1", "baselinePeriod": "P30D" }
}
```

## 4. Nguồn dữ liệu & aggregation (phối hợp Database Architect)

| Endpoint | Nguồn | Aggregation chính | Refresh |
|----------|-------|-------------------|---------|
| `market-index` | `Listing`, `Transaction`, `PriceSnapshot` | Weighted avg price/m² chuẩn hoá về base 2025-01-01 = 1000 | 1 phút (materialized view) |
| `price-trends` | `PriceSnapshot` (daily), `Listing.price_history` | OHLC theo bucket | 5 phút |
| `district-volume` | `Listing.createdAt`, `Inquiry`, `Transaction` | COUNT + window function | 1 phút |
| `listings/ticker` | Redis stream `listings:events` + DB fallback | Event bus (create/price_change/feature) | real-time |
| `top-movers` | `PriceSnapshot` | % change theo bucket + ranking | 5 phút |
| `heatmap-summary` | `Listing` + `District`/`H3Cell` precomputed | PostGIS aggregate theo polygon/H3 | 15 phút |

Database Architect xác nhận: (i) thêm materialized views `mv_market_index_*`, `mv_price_snapshot_*`; (ii) Redis stream `listings:events` dùng cho ticker/SSE; (iii) bảng `H3Cell` cho heatmap r7/r8.

## 5. Phân việc cho Backend TechLead ([TEC-3042](/TEC/issues/TEC-3042))

| Endpoint | Đề xuất cấp độ BE | Ghi chú |
|----------|-------------------|--------|
| `market-index` | Senior | Cần design materialized view + cache invalidation |
| `price-trends` | Middle | Query trên view sẵn có |
| `district-volume` | Middle | Window function + Redis cache |
| `listings/ticker` (REST+SSE) | Senior | Pub/sub, backpressure, auth watchlist |
| `top-movers` | Middle | Dựa trên view price-trends |
| `heatmap-summary` | Senior | PostGIS + H3, geojson ref |
| DTO/OpenAPI sync | Junior | Viết DTO, Swagger decorators, update `docs/api-endpoints.md` |

## 6. OpenAPI & Swagger

- DTO mới ở `apps/api/src/modules/analytics/presentation/dto/` và `apps/api/src/modules/listings/presentation/dto/` (ticker).
- Controller:
  - `analytics.controller.ts`: thêm `@Get('market-index')`, `@Get('price-trends')`, `@Get('district-volume')`, `@Get('top-movers')`, `@Get('heatmap-summary')`.
  - `listings.controller.ts`: thêm `@Get('ticker')`, `@Get('ticker/stream')`.
- Tag Swagger: `Analytics - Market`, `Listings - Ticker`.
- Cập nhật `docs/api-endpoints.md` (phần Analytics & Listings) sau khi merge.

## 7. Versioning & rollout

- Tất cả endpoint mới nằm dưới `/api/v1/` hiện tại — **không bump major**.
- Feature flag (nếu cần) dùng `FEATURE_MARKET_TICKER` cho ticker SSE.
- SLO: p95 < 250ms cho REST; SSE duy trì kết nối ≥ 5 phút, heartbeat 15s.

## 8. Checklist duyệt

- [ ] CTO phê duyệt phạm vi & SLO.
- [ ] BE TechLead xác nhận phân rã task & timeline.
- [ ] FE TechLead xác nhận response schema đủ cho UI sàn giao dịch.
- [ ] Database Architect ký nguồn dữ liệu & materialized views.
- [ ] DTO/OpenAPI/Swagger & `docs/api-endpoints.md` cập nhật.

---

*Revision 1 — 2026-04-21 — API Architect (TEC-3043).* 
