# GoodGo Platform — API Endpoints & Data Fields Reference
**For UI Component Mapping**

Generated: April 2026

---

## Table of Contents
1. [Analytics Module](#analytics-module)
   - [Market Snapshot](#market-snapshot)
   - [Price Trends](#price-trends)
   - [Heatmap](#heatmap)
   - [District Stats](#district-stats)
   - [Trending Areas](#trending-areas)
   - [Valuations (AVM)](#valuations-avm)
   - [Neighborhood Score](#neighborhood-score)
   - [Nearby POIs](#nearby-pois)
2. [Listings Module](#listings-module)
   - [Listing Entity](#listing-entity)
   - [Property Entity](#property-entity)
3. [Agents Module](#agents-module)
4. [Search Module](#search-module)
5. [Prisma Schema Key Models](#prisma-schema-key-models)

---

## Analytics Module

### Base Info
- **Namespace**: `GET/POST /analytics`
- **Auth**: JWT Bearer Token required (most endpoints)
- **Rate Limiting**: Varies by endpoint
- **Caching**: Yes (30min-1hr TTL)

---

### Market Snapshot
**Endpoint**: `GET /analytics/market-report`

**Query Parameters**:
```typescript
{
  city: string;           // e.g., "HCMC", "Hanoi"
  period?: string;        // time period identifier
  propertyType?: PropertyType;  // APARTMENT | VILLA | TOWNHOUSE | LAND | OFFICE | SHOPHOUSE
}
```

**Response** (`MarketReportDto`):
```typescript
{
  // Array of per-district market data
  [
    {
      district: string;
      city: string;
      propertyType: PropertyType;
      period: string;
      medianPrice: string;        // formatted price (e.g., "2500000000")
      avgPriceM2: number;         // average price per m² (VND)
      totalListings: number;      // active listings count
      daysOnMarket: number;       // average time to sell (days)
      inventoryLevel: number;     // cumulative inventory metric
      absorptionRate: number | null;  // listings sold per month (ratio)
      yoyChange: number | null;   // year-over-year price change (%)
    }
  ]
}
```

---

### Market Snapshot (Quick View)
**Endpoint**: `GET /analytics/market-snapshot`

**Query Parameters**:
```typescript
{
  city: string;                   // e.g., "HCMC"
  propertyType?: PropertyType;    // optional filter
}
```

**Response** (`MarketSnapshotDto`):
```typescript
{
  city: string;
  propertyType?: PropertyType;
  activeCount: number;            // total active listings
  avgPrice: number;               // average listing price (VND)
  medianPrice: number;            // median listing price (VND)
  priceChangePct: {
    d1: number;                   // % change in last 1 day
    d7: number;                   // % change in last 7 days
    d30: number;                  // % change in last 30 days
  };
  avgPricePerM2: number;          // average price per m² (VND)
  daysOnMarket: number;           // average days on market
  newListings24h: number;         // new listings in last 24 hours
  cachedAt: string | null;        // ISO timestamp of cache
  nextRefreshAt: string | null;   // ISO timestamp of next refresh
}
```

**Cache**: 30 minutes

---

### Price Trends
**Endpoint**: `GET /analytics/price-trend`

**Query Parameters**:
```typescript
{
  district: string;               // e.g., "Quận 1"
  city: string;                   // e.g., "Hồ Chí Minh"
  propertyType: PropertyType;     // required
  periods?: string[];             // comma-separated time periods
}
```

**Response** (`PriceTrendDto`):
```typescript
{
  district: string;
  city: string;
  propertyType: string;
  trend: [
    {
      period: string;             // e.g., "2026-Q1"
      medianPrice: string;        // formatted price (VND)
      avgPriceM2: number;
      totalListings: number;
    }
  ]
}
```

**Use Case**: Line chart for price evolution over time  
**Cache**: 1 hour

---

### Heatmap
**Endpoint**: `GET /analytics/heatmap`

**Query Parameters**:
```typescript
{
  city: string;                   // e.g., "HCMC"
  period: string;                 // e.g., "2026-04"
}
```

**Response** (`HeatmapDto`):
```typescript
{
  city: string;
  period: string;
  dataPoints: [
    {
      district: string;
      city: string;
      avgPriceM2: number;         // price intensity for heatmap color
      totalListings: number;
      medianPrice: string;        // formatted price (VND)
    }
  ]
}
```

**UI Mapping**: Use `avgPriceM2` for color intensity, district names for overlay  
**Cache**: 1 hour

---

### District Stats
**Endpoint**: `GET /analytics/district-stats`

**Query Parameters**:
```typescript
{
  city: string;                   // e.g., "HCMC"
  period: string;                 // e.g., "2026-04"
}
```

**Response** (`DistrictStatsDto`):
```typescript
{
  city: string;
  period: string;
  districts: [
    {
      district: string;
      city: string;
      propertyType: PropertyType;
      medianPrice: string;        // formatted price (VND)
      avgPriceM2: number;
      totalListings: number;
      daysOnMarket: number;
      inventoryLevel: number;     // supply metric
      absorptionRate: number | null;  // velocity metric
      yoyChange: number | null;   // year-over-year % change
    }
  ]
}
```

**UI Mapping**: Table or grid showing stats per district  
**Cache**: 6 hours

---

### Trending Areas
**Endpoint**: `GET /analytics/trending-areas`

**Query Parameters**:
```typescript
{
  period?: number;                // days (default: 30)
  limit?: number;                 // max areas to return (default: 10)
  level?: string;                 // "district" | "ward" (default: "district")
}
```

**Response** (`TrendingAreasDto`):
```typescript
{
  period: number;                 // query period in days
  level: string;                  // "district" | "ward"
  limit: number;
  areas: [
    {
      districtId: string;         // district identifier
      name: string;               // display name (e.g., "Quận 1")
      listings: number;           // new listings in period
      inquiries: number;          // buyer inquiries
      views: number;              // total view count
      priceChangePct: number | null;  // YoY price change (%)
      scoreRank: number;          // rank (1 = hottest)
    }
  ]
}
```

**Scoring Formula**: `score = inquiries × 0.6 + views × 0.3 + listings × 0.1`  
**Cache**: 30 minutes

---

### Valuations (AVM)

#### Get Valuation (by Property ID or Coordinates)
**Endpoint**: `GET /analytics/valuation`

**Query Parameters**:
```typescript
{
  propertyId?: string;            // if querying existing property
  latitude?: number;              // if querying coordinates
  longitude?: number;
  areaM2?: number;                // area required if no propertyId
  propertyType?: PropertyType;
}
```

**Response** (`ValuationDto`):
```typescript
{
  estimatedPrice: string;         // formatted price (e.g., "2500000000" VND)
  confidence: number;             // 0.0-1.0 confidence score
  pricePerM2: number;             // estimated price per m²
  comparables: [
    {
      propertyId: string;
      address: string;
      district: string;
      priceVND: string;           // comparable listing price
      pricePerM2: number;
      areaM2: number;
      propertyType: PropertyType;
      distanceMeters: number;     // distance from query point
      soldAt: string;             // ISO timestamp
    }
  ];
  modelVersion: string;           // "v1" | "v2" | "ensemble"
  confidenceExplanation?: string; // explanation of confidence
}
```

**Cache**: 24 hours  
**Rate Limit**: 10 req/min per user (on POST endpoint)

---

#### Predict Valuation (Manual Input)
**Endpoint**: `POST /analytics/valuation`

**Request Body** (`PredictValuationDto`):
```typescript
{
  // ── Core Fields (v1 & v2) ──
  propertyType: PropertyType;     // required
  area: number;                   // m² (required, min: 1)
  district: string;               // required (e.g., "Quận 1")
  city: string;                   // required (e.g., "Hồ Chí Minh")
  
  // ── Optional Details ──
  bedrooms?: number;              // 0-20
  bathrooms?: number;             // 0-20
  floors?: number;                // 0-200
  frontage?: number;              // meters (m)
  roadWidth?: number;             // meters (m)
  yearBuilt?: number;             // 1900-2100
  hasLegalPaper?: boolean;
  latitude?: number;              // if available
  longitude?: number;
  projectId?: string;
  imageUrl?: string;
  description?: string;           // max 2000 chars
  deepAnalysis?: boolean;         // triggers Claude analysis
  
  // ── AVM v2 Features ──
  useV2?: boolean;                // enable enhanced model
  distanceToHospitalKm?: number;
  distanceToParkKm?: number;
  distanceToMallKm?: number;
  floodZoneRisk?: 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH';
  hasElevator?: boolean;
  hasParking?: boolean;
  hasPool?: boolean;
}
```

**Response** (`ValuationDto` - same as above)

**Rate Limit**: 10 req/min per user

---

#### Batch Valuation
**Endpoint**: `POST /analytics/valuation/batch`

**Request Body**:
```typescript
{
  propertyIds: string[];          // 1-50 property IDs
}
```

**Response**:
```typescript
{
  results: [
    {
      propertyId: string;
      valuation: ValuationDto | null;
      error?: string;             // error message if failed
    }
  ]
}
```

**Rate Limit**: 10 req/min per user

---

#### Valuation History (Chart Data)
**Endpoint**: `GET /analytics/valuation/history/:propertyId`

**Query Parameters**:
```typescript
{
  limit?: number;                 // default: 50, max: 365
}
```

**Response**:
```typescript
{
  propertyId: string;
  history: [
    {
      estimatedPrice: string;
      confidence: number;
      pricePerM2: number;
      modelVersion: string;
      valuedAt: string;           // ISO timestamp
    }
  ]
}
```

**UI Mapping**: Time-series chart of valuation estimates  
**Cache**: 24 hours

---

#### Compare Valuations
**Endpoint**: `POST /analytics/valuation/compare`

**Request Body**:
```typescript
{
  propertyIds: string[];          // 2-5 property IDs
}
```

**Response**:
```typescript
{
  comparisons: [
    {
      propertyId: string;
      address: string;
      district: string;
      areaM2: number;
      propertyType: PropertyType;
      valuation: ValuationDto | null;
    }
  ]
}
```

**UI Mapping**: Side-by-side comparison table  
**Rate Limit**: 10 req/min per user

---

### Neighborhood Score
**Endpoint**: `GET /analytics/neighborhoods/:district/score`

**Query Parameters**:
```typescript
{
  city?: string;                  // default: "Hồ Chí Minh"
}
```

**Response** (`NeighborhoodScoreResult`):
```typescript
{
  district: string;
  city: string;
  educationScore: number;         // 0-100
  healthcareScore: number;        // 0-100
  transportScore: number;         // 0-100
  shoppingScore: number;          // 0-100
  greeneryScore: number;          // 0-100
  safetyScore: number;            // 0-100
  totalScore: number;             // weighted average 0-100
  poiCounts: {
    schools: number;
    hospitals: number;
    transit: number;
    shopping: number;
    parks: number;
    restaurants: number;
    [key: string]: number;
  };
  calculatedAt: Date;             // ISO timestamp
}
```

**UI Mapping**: Score gauge + POI count breakdown  
**Cache**: 24 hours  
**Public**: Yes (no auth required)

---

### Nearby POIs
**Endpoint**: `GET /analytics/pois/nearby` (Public)

**Query Parameters**:
```typescript
{
  lat: number;                    // latitude (required)
  lng: number;                    // longitude (required)
  radius?: number;                // meters (default: 2000)
  limit?: number;                 // max results (default: 30)
}
```

**Response** (`NearbyPOIsResultDto`):
```typescript
{
  pois: [
    {
      id: string;
      name: string;
      type: POIType;              // SCHOOL | HOSPITAL | METRO_STATION | MALL | PARK | RESTAURANT | etc.
      category: POICategory;      // 'school' | 'hospital' | 'transit' | 'shopping' | 'restaurant' | 'park'
      lat: number;
      lng: number;
      distance: number;           // meters from query point
      address: string | null;
    }
  ];
  center: {
    lat: number;
    lng: number;
  };
}
```

**UI Mapping**: Map markers grouped by category + list view  
**Cache**: 1 hour  
**Public**: Yes

---

## Listings Module

### Listing Entity Fields

**Entity**: `ListingEntity`

```typescript
{
  id: string;                         // unique listing ID
  propertyId: string;                 // reference to Property
  agentId: string | null;             // assigned agent
  sellerId: string;                   // seller/owner User ID
  transactionType: TransactionType;   // SALE | RENT
  status: ListingStatus;              // DRAFT | PENDING_REVIEW | ACTIVE | RESERVED | SOLD | RENTED | EXPIRED | REJECTED
  
  // Pricing
  price: Price;                       // Price VO (amount in VND)
  pricePerM2: number | null;          // calculated price per m²
  rentPriceMonthly: bigint | null;    // for rental listings
  commissionPct: number | null;       // agent commission (default: 2.0%)
  
  // AI & Moderation
  aiPriceEstimate: bigint | null;     // AVM-generated price estimate (VND)
  aiConfidence: number | null;        // AVM confidence (0.0-1.0)
  moderationScore: number | null;     // quality score (0-100)
  moderationNotes: string | null;     // moderation feedback
  
  // Engagement Metrics
  viewCount: number;                  // total views
  saveCount: number;                  // times saved by users
  inquiryCount: number;               // buyer inquiries
  
  // Dates
  featuredUntil: Date | null;         // promoted listing expiry
  expiresAt: Date | null;             // listing expiration date
  publishedAt: Date | null;           // date activated (ACTIVE status)
  createdAt: Date;
  updatedAt: Date;
}
```

**UI Field Mapping**:
- **Header**: `price` (formatted), `transactionType`, `status`
- **Metadata**: `pricePerM2`, `aiPriceEstimate` (with confidence badge)
- **Engagement**: `viewCount`, `inquiryCount`, `saveCount`
- **Status Badge**: `status`, `moderationScore` if available

---

### Property Entity Fields

**Entity**: `PropertyEntity`

```typescript
{
  id: string;                         // unique property ID
  
  // Core Info
  propertyType: PropertyType;         // APARTMENT | VILLA | TOWNHOUSE | LAND | OFFICE | SHOPHOUSE
  title: string;                      // property title
  description: string;                // detailed description
  
  // Location
  address: Address;                   // VO: { street, ward, district, city }
  location: GeoPoint;                 // VO: { lat, lng } (PostGIS geometry)
  ward: string;                       // administrative ward
  district: string;
  city: string;
  
  // Physical Attributes
  areaM2: number;                     // total area (m²)
  usableAreaM2: number | null;        // usable area (m²)
  bedrooms: number | null;            // -1 means "studio"
  bathrooms: number | null;
  floors: number | null;              // building height in floors
  floor: number | null;               // unit floor level
  totalFloors: number | null;         // total building floors
  direction: Direction | null;        // NORTH | SOUTH | EAST | WEST | NORTHEAST | etc.
  yearBuilt: number | null;
  legalStatus: string | null;         // e.g., "Sở hữu lâu dài"
  
  // Infrastructure & Amenities
  amenities: unknown;                 // JSON array of amenity names
  nearbyPOIs: unknown;                // JSON array of nearby POI references
  metroDistanceM: number | null;      // distance to nearest metro (meters)
  projectName: string | null;         // if part of a development project
  projectDevelopmentId: string | null;
  
  // Enhanced Fields (Phase B)
  furnishing: Furnishing | null;      // FULLY_FURNISHED | BASIC_FURNISHED | UNFURNISHED
  propertyCondition: PropertyCondition | null;  // NEW | LIKE_NEW | RENOVATED | USED
  balconyDirection: Direction | null;
  maintenanceFeeVND: bigint | null;   // monthly/annual fee
  parkingSlots: number | null;
  viewType: string[];                 // e.g., ["street", "park", "water"]
  petFriendly: boolean | null;
  suitableFor: string[];              // e.g., ["young_couples", "families", "investors"]
  whyThisLocation: string | null;     // narrative about location
  
  createdAt: Date;
  updatedAt: Date;
}
```

**UI Field Mapping**:
- **Hero Section**: `propertyType`, `title`, `bedrooms/bathrooms`, `areaM2`
- **Location Badge**: `district`, `ward`, city
- **Details Grid**: `yearBuilt`, `floors`, `direction`, `furnishing`, `propertyCondition`
- **Amenities**: `amenities[]`, `nearbyPOIs[]`
- **Lifestyle**: `suitableFor[]`, `whyThisLocation`, `petFriendly`

---

## Agents Module

### Agent Entity Fields

**Entity**: `AgentEntity`

```typescript
{
  id: string;                         // agent profile ID (different from userId)
  userId: string;                     // reference to User (role=AGENT)
  
  // License & Credentials
  licenseNumber: string | null;
  agency: string | null;              // brokerage/agency name
  isVerified: boolean;                // license verified by admin
  
  // Performance
  qualityScore: QualityScore;         // VO: { value: number, breakdown: {...} }
  totalDeals: number;                 // lifetime transactions
  responseTimeAvg: number | null;     // seconds
  
  // Profile
  bio: string | null;                 // agent biography/pitch
  serviceAreas: string[];             // list of district IDs (e.g., ["quan-1", "quan-7"])
  
  createdAt: Date;
  updatedAt: Date;
}
```

**QualityScore VO**:
```typescript
{
  value: number;                      // 0.0-100.0 composite score
  // Potentially includes breakdown by metric
}
```

**UI Field Mapping**:
- **Agent Card**: `name` (from User), `agency`, `qualityScore.value` (star rating)
- **Badges**: `isVerified`, `licenseNumber` (if available)
- **Stats**: `totalDeals`, `responseTimeAvg` (e.g., "avg 2h response")
- **Service Areas**: `serviceAreas[]` (comma-separated or chip list)
- **Bio**: `bio` (short preview)

---

## Search Module

### Search Result Format

**Endpoint**: Search is handled by Meilisearch (typesense-compatible)

**Search Result Item** (`ListingDocument`):
```typescript
{
  id: string;                         // document ID in search index
  listingId: string;                  // reference to Listing
  propertyId: string;                 // reference to Property
  
  // Display
  title: string;
  description: string;
  
  // Classification
  propertyType: string;               // "APARTMENT", "VILLA", etc.
  transactionType: string;            // "SALE", "RENT"
  
  // Price
  priceVND: number;                   // for sorting/filtering
  pricePerM2: number | null;
  
  // Property Details
  areaM2: number;
  bedrooms: number | null;
  bathrooms: number | null;
  floors: number | null;
  direction: string | null;
  
  // Location
  address: string;                    // full address string
  ward: string;
  district: string;
  city: string;
  location: [number, number];         // [lat, lng]
  
  // Relations
  agentId: string | null;
  sellerId: string;
  
  // Metadata
  status: string;                     // "ACTIVE", etc.
  publishedAt: number;                // unix timestamp
  projectName: string | null;
  
  // Engagement
  viewCount: number;
  saveCount: number;
  
  // Amenities & Features
  amenities: string[];                // e.g., ["elevator", "pool", "gym"]
  isFeatured: number;                 // 1 if featured, 0 otherwise
}
```

**Search Response** (`SearchResult`):
```typescript
{
  hits: ListingDocument[];            // matched listings
  totalFound: number;                 // total matching results
  page: number;                       // current page
  perPage: number;                    // items per page
  totalPages: number;
  searchTimeMs: number;               // query time in milliseconds
}
```

**Supported Filters**:
- `propertyType`: APARTMENT, VILLA, TOWNHOUSE, LAND, OFFICE, SHOPHOUSE
- `transactionType`: SALE, RENT
- `priceVND`: range [min..max] or >= or <=
- `areaM2`: range [min..max] or >= or <=
- `bedrooms`: >= operator
- `district`, `city`: equality
- `status`: fixed to ACTIVE in search
- `isFeatured`: 1 or 0

---

## Prisma Schema Key Models

### Property Model
```prisma
model Property {
  id                   String    @id @default(cuid())
  propertyType         PropertyType
  title                String
  description          String    @db.Text
  address              String
  ward                 String
  district             String
  city                 String
  addressNormalized    String?   // for duplicate detection
  location             geometry(Point, 4326)  // PostGIS
  areaM2               Float
  usableAreaM2         Float?
  bedrooms             Int?
  bathrooms            Int?
  floors               Int?
  floor                Int?
  totalFloors          Int?
  direction            Direction?
  yearBuilt            Int?
  legalStatus          String?
  amenities            Json?
  nearbyPOIs           Json?
  metroDistanceM       Float?
  projectName          String?
  projectDevelopmentId String?
  furnishing           Furnishing?
  propertyCondition    PropertyCondition?
  balconyDirection     Direction?
  maintenanceFeeVND    BigInt?   // CHECK >= 0
  parkingSlots         Int?
  viewType             String[]  @default([])
  petFriendly          Boolean?
  suitableFor          String[]  @default([])
  whyThisLocation      String?   @db.Text
  createdAt            DateTime  @default(now())
  updatedAt            DateTime  @updatedAt
  
  listings   Listing[]
  valuations Valuation[]
  media      PropertyMedia[]
  
  // Indexes for queries
  @@index([propertyType])
  @@index([district, city])
  @@index([location], type: Gist)
  @@index([district, propertyType])
  @@index([district, city, propertyType])
}
```

### Listing Model
```prisma
model Listing {
  id               String    @id @default(cuid())
  propertyId       String
  agentId          String?
  sellerId         String
  transactionType  TransactionType
  status           ListingStatus @default(DRAFT)
  priceVND         BigInt        // CHECK > 0
  pricePerM2       Float?
  rentPriceMonthly BigInt?       // CHECK > 0
  commissionPct    Float?        @default(2.0)
  aiPriceEstimate  BigInt?       // CHECK > 0
  aiConfidence     Float?
  moderationScore  Float?
  moderationNotes  String?
  viewCount        Int           @default(0)
  saveCount        Int           @default(0)
  inquiryCount     Int           @default(0)
  featuredUntil    DateTime?
  expiresAt        DateTime?
  publishedAt      DateTime?
  createdAt        DateTime      @default(now())
  updatedAt        DateTime      @updatedAt
  
  transactions   Transaction[]
  inquiries      Inquiry[]
  orders         Order[]
  priceHistories PriceHistory[]
  savedByUsers   SavedListing[]
  
  @@index([status])
  @@index([transactionType])
  @@index([sellerId, status, publishedAt(sort: Desc)])
  @@index([agentId, status])
  @@index([status, publishedAt(sort: Desc)])
}
```

### Agent Model
```prisma
model Agent {
  id              String    @id @default(cuid())
  userId          String    @unique
  user            User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  licenseNumber   String?
  agency          String?
  qualityScore    Float     @default(0)
  totalDeals      Int       @default(0)
  responseTimeAvg Int?      // seconds
  bio             String?
  serviceAreas    Json      // ["quan-1", "quan-7", "thu-duc"]
  isVerified      Boolean   @default(false)
  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt
  
  listings Listing[]
  leads    Lead[]
  
  @@index([qualityScore])
  @@index([isVerified])
}
```

### Valuation Model (Analytics)
```prisma
model Valuation {
  id             String    @id @default(cuid())
  propertyId     String
  property       Property  @relation(fields: [propertyId], references: [id], onDelete: Cascade)
  estimatedPrice BigInt
  confidence     Float     // 0.0-1.0
  pricePerM2     Float
  comparables    Json      // array of {propertyId, address, priceVND, distance, ...}
  features       Json      // model input features used
  modelVersion   String    // "v1" | "v2" | "ensemble"
  createdAt      DateTime  @default(now())
  
  @@index([propertyId, createdAt(sort: Desc)])
}
```

### MarketIndex Model (Analytics)
```prisma
model MarketIndex {
  id             String       @id @default(cuid())
  district       String
  city           String
  propertyType   PropertyType
  period         String       // e.g., "2026-Q1" or "2026-04"
  medianPrice    BigInt
  avgPriceM2     Float
  totalListings  Int
  daysOnMarket   Float
  inventoryLevel Int
  absorptionRate Float?       // listings sold per month
  yoyChange      Float?       // year-over-year % change
  createdAt      DateTime     @default(now())
  
  @@unique([district, city, propertyType, period])
  @@index([city, period])
}
```

### User Model (Auth)
```prisma
model User {
  id              String    @id @default(cuid())
  email           String?
  phone           String
  passwordHash    String?
  fullName        String
  avatarUrl       String?
  role            UserRole  @default(BUYER)  // BUYER | SELLER | AGENT | DEVELOPER | PARK_OPERATOR | ADMIN
  kycStatus       KYCStatus @default(NONE)  // NONE | PENDING | VERIFIED | REJECTED
  isActive        Boolean   @default(true)
  createdAt       DateTime  @default(now())
  updatedAt       DateTime  @updatedAt
  
  agent           Agent?
  listings        Listing[]
  subscription    Subscription?
  payments        Payment[]
  
  @@index([role])
  @@index([kycStatus])
  @@index([isActive])
}
```

---

## Enums

### PropertyType
```
APARTMENT, VILLA, TOWNHOUSE, LAND, OFFICE, SHOPHOUSE
```

### TransactionType
```
SALE, RENT
```

### ListingStatus
```
DRAFT, PENDING_REVIEW, ACTIVE, RESERVED, SOLD, RENTED, EXPIRED, REJECTED
```

### Direction
```
NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, SOUTHWEST
```

### Furnishing
```
FULLY_FURNISHED, BASIC_FURNISHED, UNFURNISHED
```

### PropertyCondition
```
NEW, LIKE_NEW, RENOVATED, USED
```

---

## Common Patterns

### Price Formatting
All prices in VND are returned as:
- **strings** in DTOs (to preserve precision with large numbers)
- **BigInt** in database and some service responses
- **number** in JavaScript/TS objects (caution: precision loss above 2⁵³)

**Display Pattern**: Format with `Intl.NumberFormat` or custom VND formatter

### Pagination
Search results include:
```typescript
{
  page: number;
  perPage: number;
  totalPages: number;
  totalFound: number;
}
```

### Caching Headers
Responses include cache metadata:
```typescript
{
  cachedAt: string | null;      // ISO timestamp of cache retrieval
  nextRefreshAt: string | null; // ISO timestamp of next refresh
}
```

### Confidence Scores
- **Valuation confidence**: 0.0 - 1.0 (higher = more reliable)
- **Moderation score**: 0-100 (higher = better quality)
- **Quality score**: 0.0-100.0 (agent performance)

### Rate Limits
- **Analytics queries**: 100 req/min per subscription tier
- **Valuation POST**: 10 req/min per user
- **Search**: 200 req/min per user
- **Batch operations**: Limited to 50 items per request

---

## Error Responses

All endpoints return consistent error format:
```typescript
{
  statusCode: number;
  message: string;
  error?: string;  // error type
  details?: Record<string, any>;  // field-level errors
  timestamp: string;  // ISO timestamp
  path: string;  // request path
}
```

**Common Codes**:
- `400`: Invalid parameters (missing required fields, validation errors)
- `401`: Unauthorized (no/invalid JWT token)
- `403`: Forbidden (quota exceeded, permission denied)
- `404`: Not found (property/listing/agent not found)
- `429`: Rate limit exceeded
- `500`: Server error
- `503`: Service unavailable (e.g., AI service down)

---

## References

**Generated from**:
- `apps/api/src/modules/analytics/` — Market data, valuations, neighborhood scores
- `apps/api/src/modules/listings/` — Property and listing entities
- `apps/api/src/modules/agents/` — Agent profiles and quality scores
- `apps/api/src/modules/search/` — Search indexing and retrieval
- `prisma/schema.prisma` — Database models and relationships

**Last Updated**: April 2026

