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