- Remove `type` keyword from NestJS injectable class imports across all modules to fix runtime DI resolution (330+ handler/listener files) - Offset CI docker-compose ports (5433/6380/8109/9002) to avoid conflicts with running dev containers - Update .env.test, playwright.config.ts, and e2e workflow to use isolated CI ports with configurable overrides - Fix prisma/seed.ts to use deterministic IDs for Prisma 7 upsert compatibility (phoneHash replaced phone as unique index) - Add dedicated Docker bridge network for CI service containers Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
100 lines
4.4 KiB
TypeScript
100 lines
4.4 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Query,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import { QueryBus } from '@nestjs/cqrs';
|
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
|
import { JwtAuthGuard } from '@modules/auth';
|
|
import { RequireQuota, QuotaGuard } from '@modules/subscriptions';
|
|
import { DistrictStatsDto } from '../../application/queries/get-district-stats/get-district-stats.handler';
|
|
import { GetDistrictStatsQuery } from '../../application/queries/get-district-stats/get-district-stats.query';
|
|
import { HeatmapDto } from '../../application/queries/get-heatmap/get-heatmap.handler';
|
|
import { GetHeatmapQuery } from '../../application/queries/get-heatmap/get-heatmap.query';
|
|
import { MarketReportDto } from '../../application/queries/get-market-report/get-market-report.handler';
|
|
import { GetMarketReportQuery } from '../../application/queries/get-market-report/get-market-report.query';
|
|
import { PriceTrendDto } from '../../application/queries/get-price-trend/get-price-trend.handler';
|
|
import { GetPriceTrendQuery } from '../../application/queries/get-price-trend/get-price-trend.query';
|
|
import { ValuationDto } from '../../application/queries/get-valuation/get-valuation.handler';
|
|
import { GetValuationQuery } from '../../application/queries/get-valuation/get-valuation.query';
|
|
import { GetDistrictStatsDto } from '../dto/get-district-stats.dto';
|
|
import { GetHeatmapDto } from '../dto/get-heatmap.dto';
|
|
import { GetMarketReportDto } from '../dto/get-market-report.dto';
|
|
import { GetPriceTrendDto } from '../dto/get-price-trend.dto';
|
|
import { GetValuationDto } from '../dto/get-valuation.dto';
|
|
|
|
@ApiTags('analytics')
|
|
@Controller('analytics')
|
|
export class AnalyticsController {
|
|
constructor(
|
|
private readonly queryBus: QueryBus,
|
|
) {}
|
|
|
|
@ApiBearerAuth('JWT')
|
|
@UseGuards(JwtAuthGuard, QuotaGuard)
|
|
@RequireQuota('analytics_queries')
|
|
@Get('market-report')
|
|
@ApiOperation({ summary: 'Get market report for a city' })
|
|
@ApiResponse({ status: 200, description: 'Market report retrieved' })
|
|
@ApiResponse({ status: 403, description: 'Quota exceeded' })
|
|
async getMarketReport(@Query() dto: GetMarketReportDto): Promise<MarketReportDto> {
|
|
return this.queryBus.execute(
|
|
new GetMarketReportQuery(dto.city, dto.period, dto.propertyType),
|
|
);
|
|
}
|
|
|
|
@ApiBearerAuth('JWT')
|
|
@UseGuards(JwtAuthGuard, QuotaGuard)
|
|
@RequireQuota('analytics_queries')
|
|
@Get('price-trend')
|
|
@ApiOperation({ summary: 'Get price trend for a district' })
|
|
@ApiResponse({ status: 200, description: 'Price trend data retrieved' })
|
|
@ApiResponse({ status: 403, description: 'Quota exceeded' })
|
|
async getPriceTrend(@Query() dto: GetPriceTrendDto): Promise<PriceTrendDto> {
|
|
return this.queryBus.execute(
|
|
new GetPriceTrendQuery(dto.district, dto.city, dto.propertyType, dto.periods),
|
|
);
|
|
}
|
|
|
|
@ApiBearerAuth('JWT')
|
|
@UseGuards(JwtAuthGuard, QuotaGuard)
|
|
@RequireQuota('analytics_queries')
|
|
@Get('heatmap')
|
|
@ApiOperation({ summary: 'Get price heatmap for a city' })
|
|
@ApiResponse({ status: 200, description: 'Heatmap data retrieved' })
|
|
@ApiResponse({ status: 403, description: 'Quota exceeded' })
|
|
async getHeatmap(@Query() dto: GetHeatmapDto): Promise<HeatmapDto> {
|
|
return this.queryBus.execute(
|
|
new GetHeatmapQuery(dto.city, dto.period),
|
|
);
|
|
}
|
|
|
|
@ApiBearerAuth('JWT')
|
|
@UseGuards(JwtAuthGuard, QuotaGuard)
|
|
@RequireQuota('analytics_queries')
|
|
@Get('district-stats')
|
|
@ApiOperation({ summary: 'Get statistics by district' })
|
|
@ApiResponse({ status: 200, description: 'District statistics retrieved' })
|
|
@ApiResponse({ status: 403, description: 'Quota exceeded' })
|
|
async getDistrictStats(@Query() dto: GetDistrictStatsDto): Promise<DistrictStatsDto> {
|
|
return this.queryBus.execute(
|
|
new GetDistrictStatsQuery(dto.city, dto.period),
|
|
);
|
|
}
|
|
|
|
@ApiBearerAuth('JWT')
|
|
@UseGuards(JwtAuthGuard, QuotaGuard)
|
|
@RequireQuota('analytics_queries')
|
|
@Get('valuation')
|
|
@ApiOperation({ summary: 'Get automated property valuation (AVM)' })
|
|
@ApiResponse({ status: 200, description: 'Valuation estimate retrieved' })
|
|
@ApiResponse({ status: 400, description: 'Invalid parameters — provide propertyId or (lat, lng, areaM2)' })
|
|
@ApiResponse({ status: 403, description: 'Quota exceeded' })
|
|
async getValuation(@Query() dto: GetValuationDto): Promise<ValuationDto> {
|
|
return this.queryBus.execute(
|
|
new GetValuationQuery(dto.propertyId, dto.latitude, dto.longitude, dto.areaM2, dto.propertyType),
|
|
);
|
|
}
|
|
}
|