feat(transfer): add Claude Vision condition assessment for transfer pricing
Add POST /transfer/estimate-from-photos endpoint that uses Claude Vision API to assess furniture/appliance condition from photos, integrating with the existing rule-based pricing engine. Includes rate limiting (5/min), image hash caching, graceful fallback, and 17 unit tests covering all paths. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -2,14 +2,16 @@ import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@ne
|
||||
import { type CommandBus, type QueryBus } from '@nestjs/cqrs';
|
||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard, CurrentUser } from '@modules/auth';
|
||||
import { NotFoundException } from '@modules/shared';
|
||||
import { EndpointRateLimit, EndpointRateLimitGuard, NotFoundException } from '@modules/shared';
|
||||
import { CreateTransferListingCommand } from '../../application/commands/create-transfer-listing/create-transfer-listing.command';
|
||||
import { EstimateFromPhotosCommand } from '../../application/commands/estimate-from-photos/estimate-from-photos.command';
|
||||
import { EstimateTransferPricesCommand } from '../../application/commands/estimate-transfer-prices/estimate-transfer-prices.command';
|
||||
import { UpdateTransferListingCommand } from '../../application/commands/update-transfer-listing/update-transfer-listing.command';
|
||||
import { GetTransferListingQuery } from '../../application/queries/get-transfer-listing/get-transfer-listing.query';
|
||||
import { ListTransferListingsQuery } from '../../application/queries/list-transfer-listings/list-transfer-listings.query';
|
||||
import { TransferStatsQuery } from '../../application/queries/transfer-stats/transfer-stats.query';
|
||||
import { type CreateTransferListingDto } from '../dto/create-transfer-listing.dto';
|
||||
import { type EstimateFromPhotosDto } from '../dto/estimate-from-photos.dto';
|
||||
import { type EstimateTransferPricesDto } from '../dto/estimate-transfer-prices.dto';
|
||||
import { type SearchTransferListingsDto } from '../dto/search-transfer-listings.dto';
|
||||
import { type UpdateTransferListingDto } from '../dto/update-transfer-listing.dto';
|
||||
@@ -72,6 +74,21 @@ export class TransferController {
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Ước tính giá từ ảnh (AI Vision)',
|
||||
description: 'Sử dụng Claude Vision để đánh giá tình trạng nội thất từ ảnh, kết hợp với công cụ định giá rule-based',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Kết quả đánh giá tình trạng và ước tính giá' })
|
||||
@ApiResponse({ status: 429, description: 'Rate limit exceeded' })
|
||||
@EndpointRateLimit({ limit: 5, windowSeconds: 60, keyStrategy: 'ip' })
|
||||
@UseGuards(EndpointRateLimitGuard)
|
||||
@Post('estimate-from-photos')
|
||||
async estimateFromPhotos(@Body() dto: EstimateFromPhotosDto) {
|
||||
return this.commandBus.execute(
|
||||
new EstimateFromPhotosCommand(dto.items),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Authenticated endpoints ───────────────────────────────────────
|
||||
|
||||
@ApiOperation({ summary: 'Tạo tin sang nhượng', description: 'Đăng tin sang nhượng mới' })
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { TransferCategory } from '@prisma/client';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export class ImageBase64Dto {
|
||||
@ApiProperty({ description: 'Base64-encoded image data' })
|
||||
@IsString()
|
||||
data!: string;
|
||||
|
||||
@ApiProperty({ enum: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] })
|
||||
@IsString()
|
||||
mediaType!: 'image/jpeg' | 'image/png' | 'image/webp' | 'image/gif';
|
||||
}
|
||||
|
||||
export class PhotoEstimateItemDto {
|
||||
@ApiPropertyOptional({ description: 'Image URLs to assess', type: [String] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsUrl({}, { each: true, message: 'URL ảnh không hợp lệ' })
|
||||
@ArrayMaxSize(4, { message: 'Tối đa 4 ảnh mỗi item' })
|
||||
imageUrls?: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Base64-encoded images', type: [ImageBase64Dto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ImageBase64Dto)
|
||||
@ArrayMaxSize(4, { message: 'Tối đa 4 ảnh mỗi item' })
|
||||
imageBase64?: ImageBase64Dto[];
|
||||
|
||||
@ApiPropertyOptional({ enum: TransferCategory, description: 'Danh mục (nếu biết)' })
|
||||
@IsOptional()
|
||||
@IsEnum(TransferCategory)
|
||||
category?: TransferCategory;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Giá mua ban đầu (VND)' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
originalPriceVND?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Năm mua' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
purchaseYear?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Thương hiệu (nếu biết)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
brand?: string;
|
||||
}
|
||||
|
||||
export class EstimateFromPhotosDto {
|
||||
@ApiProperty({ type: [PhotoEstimateItemDto], description: 'Items to assess from photos' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PhotoEstimateItemDto)
|
||||
@ArrayMaxSize(5, { message: 'Tối đa 5 items mỗi lần đánh giá' })
|
||||
items!: PhotoEstimateItemDto[];
|
||||
}
|
||||
Reference in New Issue
Block a user