feat(analytics): AVM v2 batch valuation, comparison, history + frontend upgrade
Add batch valuation (POST /analytics/valuation/batch, max 50 properties), valuation comparison (POST /analytics/valuation/compare, 2-5 properties), and history endpoint (GET /analytics/valuation/history/:propertyId) with confidence explanation helper. Frontend: enhanced valuation form with project autocomplete and deep analysis toggle, results with confidence badges and price range visualization, comparables table, history chart, market context card, and PDF export. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,28 +1,41 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { QueryBus } from '@nestjs/cqrs';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { type QueryBus } from '@nestjs/cqrs';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '@modules/auth';
|
||||
import { EndpointRateLimit, EndpointRateLimitGuard } from '@modules/shared';
|
||||
import { RequireQuota, QuotaGuard } from '@modules/subscriptions';
|
||||
import { DistrictStatsDto } from '../../application/queries/get-district-stats/get-district-stats.handler';
|
||||
import { type BatchValuationDto as BatchValuationQueryDto } from '../../application/queries/batch-valuation/batch-valuation.handler';
|
||||
import { BatchValuationQuery } from '../../application/queries/batch-valuation/batch-valuation.query';
|
||||
import { type 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 { type 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 { type 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 { type 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 { type 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';
|
||||
import { type ValuationComparisonDto as ValuationComparisonResultDto } from '../../application/queries/valuation-comparison/valuation-comparison.handler';
|
||||
import { ValuationComparisonQuery } from '../../application/queries/valuation-comparison/valuation-comparison.query';
|
||||
import { type ValuationHistoryDto as ValuationHistoryResultDto } from '../../application/queries/valuation-history/valuation-history.handler';
|
||||
import { ValuationHistoryQuery } from '../../application/queries/valuation-history/valuation-history.query';
|
||||
import { type BatchValuationDto } from '../dto/batch-valuation.dto';
|
||||
import { type GetDistrictStatsDto } from '../dto/get-district-stats.dto';
|
||||
import { type GetHeatmapDto } from '../dto/get-heatmap.dto';
|
||||
import { type GetMarketReportDto } from '../dto/get-market-report.dto';
|
||||
import { type GetPriceTrendDto } from '../dto/get-price-trend.dto';
|
||||
import { type GetValuationDto } from '../dto/get-valuation.dto';
|
||||
import { type ValuationComparisonDto } from '../dto/valuation-comparison.dto';
|
||||
import { type ValuationHistoryDto } from '../dto/valuation-history.dto';
|
||||
|
||||
@ApiTags('analytics')
|
||||
@Controller('analytics')
|
||||
@@ -96,4 +109,53 @@ export class AnalyticsController {
|
||||
new GetValuationQuery(dto.propertyId, dto.latitude, dto.longitude, dto.areaM2, dto.propertyType),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth('JWT')
|
||||
@EndpointRateLimit({ limit: 10, windowSeconds: 60, keyStrategy: 'user' })
|
||||
@UseGuards(EndpointRateLimitGuard, JwtAuthGuard, QuotaGuard)
|
||||
@RequireQuota('analytics_queries')
|
||||
@Post('valuation/batch')
|
||||
@ApiOperation({ summary: 'Batch valuation for multiple properties (max 50)' })
|
||||
@ApiResponse({ status: 200, description: 'Batch valuation results retrieved' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid parameters' })
|
||||
@ApiResponse({ status: 403, description: 'Quota exceeded' })
|
||||
@ApiResponse({ status: 429, description: 'Rate limit exceeded' })
|
||||
async batchValuation(@Body() dto: BatchValuationDto): Promise<BatchValuationQueryDto> {
|
||||
return this.queryBus.execute(
|
||||
new BatchValuationQuery(dto.propertyIds),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth('JWT')
|
||||
@UseGuards(JwtAuthGuard, QuotaGuard)
|
||||
@RequireQuota('analytics_queries')
|
||||
@Get('valuation/history/:propertyId')
|
||||
@ApiOperation({ summary: 'Get valuation history for a property (chart data)' })
|
||||
@ApiParam({ name: 'propertyId', description: 'Property ID' })
|
||||
@ApiResponse({ status: 200, description: 'Valuation history retrieved' })
|
||||
@ApiResponse({ status: 403, description: 'Quota exceeded' })
|
||||
async getValuationHistory(
|
||||
@Param('propertyId') propertyId: string,
|
||||
@Query() dto: ValuationHistoryDto,
|
||||
): Promise<ValuationHistoryResultDto> {
|
||||
return this.queryBus.execute(
|
||||
new ValuationHistoryQuery(propertyId, dto.limit ?? 50),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiBearerAuth('JWT')
|
||||
@EndpointRateLimit({ limit: 10, windowSeconds: 60, keyStrategy: 'user' })
|
||||
@UseGuards(EndpointRateLimitGuard, JwtAuthGuard, QuotaGuard)
|
||||
@RequireQuota('analytics_queries')
|
||||
@Post('valuation/compare')
|
||||
@ApiOperation({ summary: 'Compare valuations for 2-5 properties side by side' })
|
||||
@ApiResponse({ status: 200, description: 'Valuation comparison retrieved' })
|
||||
@ApiResponse({ status: 400, description: 'Invalid parameters — provide 2-5 property IDs' })
|
||||
@ApiResponse({ status: 403, description: 'Quota exceeded' })
|
||||
@ApiResponse({ status: 429, description: 'Rate limit exceeded' })
|
||||
async compareValuations(@Body() dto: ValuationComparisonDto): Promise<ValuationComparisonResultDto> {
|
||||
return this.queryBus.execute(
|
||||
new ValuationComparisonQuery(dto.propertyIds),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user