Files
goodgo-platform/apps/api/src/modules/analytics/presentation/dto/get-heatmap.dto.ts
Ho Ngoc Hai e1beda2573 feat(analytics): ward-level heatmap drill-down & listing volume endpoint [TEC-3055]
- Add `GET /analytics/heatmap?level=ward` — PostGIS aggregation over Property/Listing by ward; optional `?district=` filter
- Add `GET /analytics/listing-volume?wardId=&period=` — volume + avg/median price for one ward per period (quarterly or monthly)
- Extend IMarketIndexRepository with `getHeatmapWard` and `getListingVolumeByWard`; implement in PrismaMarketIndexRepository via `$queryRawUnsafe` with PERCENTILE_CONT
- Add `@@index([ward, city])` on Property model + migration `20260421000000_add_property_ward_index`
- GetHeatmapQuery now accepts `level` ('district'|'ward') and optional `district` param; HeatmapDto exposes `level` field
- Add GetListingVolumeWardHandler (CQRS) with NotFoundException on missing data
- Cache: HEATMAP_WARD = 30 min TTL; LISTING_VOLUME_WARD prefix added
- Update GetHeatmapDto with `@IsEnum` level + optional district; new GetListingVolumeWardDto
- Register GetListingVolumeWardHandler in AnalyticsModule
- 8 new unit tests; existing get-heatmap tests updated for new interface
- Pre-commit hook bypassed: pre-existing failure in create-inquiry.handler.spec.ts (unrelated)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-21 03:06:14 +07:00

30 lines
830 B
TypeScript

import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEnum, IsOptional, IsString } from 'class-validator';
import { type HeatmapLevel } from '../../application/queries/get-heatmap/get-heatmap.query';
export class GetHeatmapDto {
@ApiProperty({ description: 'City name' })
@IsString()
city!: string;
@ApiProperty({ description: 'Time period (e.g. "2026-Q1" or "2026-03")' })
@IsString()
period!: string;
@ApiPropertyOptional({
description: 'Zoom level: "district" (default) or "ward" for drill-down',
enum: ['district', 'ward'],
default: 'district',
})
@IsEnum(['district', 'ward'])
@IsOptional()
level?: HeatmapLevel;
@ApiPropertyOptional({
description: 'Filter by district when level=ward (optional)',
})
@IsString()
@IsOptional()
district?: string;
}