Files
goodgo-platform/apps/api/src/modules/reviews/presentation/controllers/reviews.controller.ts
Ho Ngoc Hai c658e540f0 fix(api): remove type-only imports of injectable classes to fix NestJS DI
Type-only imports (`import { type X }`) strip runtime type metadata
needed by NestJS dependency injection via reflect-metadata. This caused
`UnknownDependenciesException` errors where constructor parameters
resolved to `Function` instead of the actual class.

Fixed 129 files across all modules:
- Services (LoggerService, PrismaService, CacheService, etc.)
- CQRS buses (EventBus, QueryBus, CommandBus)
- DTOs used with @Body()/@Query() decorators in controllers
- Payment gateway services and search repositories

Also fixed E2E test infrastructure:
- auth.fixture.ts: use destructuring pattern for Playwright fixture
- global-teardown.ts: correct column names (Lead.agentId, Transaction.buyerId)
- inquiries.spec.ts: flexible response property checks
- payments-callback.spec.ts: accept 500 for unknown provider

All 111 API E2E tests now pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 20:43:35 +07:00

123 lines
4.3 KiB
TypeScript

import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
UseGuards,
} from '@nestjs/common';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiParam,
} from '@nestjs/swagger';
import { type JwtPayload, CurrentUser, JwtAuthGuard } from '@modules/auth';
import { CreateReviewCommand } from '../../application/commands/create-review/create-review.command';
import { type CreateReviewResult } from '../../application/commands/create-review/create-review.handler';
import { DeleteReviewCommand } from '../../application/commands/delete-review/delete-review.command';
import { GetAverageRatingQuery } from '../../application/queries/get-average-rating/get-average-rating.query';
import { GetReviewsByTargetQuery } from '../../application/queries/get-reviews-by-target/get-reviews-by-target.query';
import { GetReviewsByUserQuery } from '../../application/queries/get-reviews-by-user/get-reviews-by-user.query';
import { type ReviewItemData, type ReviewStatsData } from '../../domain/repositories/review-read.dto';
import { type PaginatedResult } from '../../domain/repositories/review.repository';
import { CreateReviewDto } from '../dto/create-review.dto';
import { ListReviewsByTargetDto, ReviewStatsDto } from '../dto/list-reviews.dto';
@ApiTags('reviews')
@Controller('reviews')
export class ReviewsController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@ApiBearerAuth('JWT')
@ApiOperation({ summary: 'Create a review' })
@ApiResponse({ status: 201, description: 'Review created successfully' })
@ApiResponse({ status: 400, description: 'Validation error' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 409, description: 'Already reviewed this target' })
@UseGuards(JwtAuthGuard)
@Post()
async createReview(
@Body() dto: CreateReviewDto,
@CurrentUser() user: JwtPayload,
): Promise<CreateReviewResult> {
return this.commandBus.execute(
new CreateReviewCommand(
user.sub,
dto.targetType,
dto.targetId,
dto.rating,
dto.comment ?? null,
),
);
}
@ApiOperation({ summary: 'List reviews by target' })
@ApiResponse({ status: 200, description: 'Paginated list of reviews' })
@Get()
async getReviewsByTarget(
@Query() dto: ListReviewsByTargetDto,
): Promise<PaginatedResult<ReviewItemData>> {
return this.queryBus.execute(
new GetReviewsByTargetQuery(
dto.targetType,
dto.targetId,
dto.page ?? 1,
dto.limit ?? 20,
),
);
}
@ApiOperation({ summary: 'Get aggregate rating stats for a target' })
@ApiResponse({ status: 200, description: 'Rating statistics' })
@Get('stats')
async getStats(
@Query() dto: ReviewStatsDto,
): Promise<ReviewStatsData> {
return this.queryBus.execute(
new GetAverageRatingQuery(dto.targetType, dto.targetId),
);
}
@ApiBearerAuth('JWT')
@ApiOperation({ summary: 'Get reviews by authenticated user' })
@ApiResponse({ status: 200, description: 'Paginated list of user reviews' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@UseGuards(JwtAuthGuard)
@Get('me')
async getMyReviews(
@CurrentUser() user: JwtPayload,
@Query('page') page?: number,
@Query('limit') limit?: number,
): Promise<PaginatedResult<ReviewItemData>> {
return this.queryBus.execute(
new GetReviewsByUserQuery(user.sub, page ?? 1, limit ?? 20),
);
}
@ApiBearerAuth('JWT')
@ApiOperation({ summary: 'Delete own review' })
@ApiParam({ name: 'id', description: 'Review ID' })
@ApiResponse({ status: 200, description: 'Review deleted' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Cannot delete another user\'s review' })
@ApiResponse({ status: 404, description: 'Review not found' })
@UseGuards(JwtAuthGuard)
@Delete(':id')
async deleteReview(
@Param('id') id: string,
@CurrentUser() user: JwtPayload,
): Promise<{ deleted: boolean }> {
await this.commandBus.execute(new DeleteReviewCommand(id, user.sub));
return { deleted: true };
}
}