feat(api): add inquiries, leads, and agents modules for Agent Portal
Build three new DDD modules following existing CQRS patterns: - Inquiries: CRUD endpoints for buyer consultation requests with agent notification support - Leads: Full lead lifecycle management with status state machine and conversion tracking - Agents: Quality score calculation (event-driven on review changes) and dashboard stats API All modules include unit tests (14 test files, all 797 tests pass). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { type CommandBus, type QueryBus } from '@nestjs/cqrs';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
ApiParam,
|
||||
} from '@nestjs/swagger';
|
||||
import { type JwtPayload, CurrentUser, JwtAuthGuard, RolesGuard, Roles } from '@modules/auth';
|
||||
import { CreateLeadCommand } from '../../application/commands/create-lead/create-lead.command';
|
||||
import { type CreateLeadResult } from '../../application/commands/create-lead/create-lead.handler';
|
||||
import { DeleteLeadCommand } from '../../application/commands/delete-lead/delete-lead.command';
|
||||
import { UpdateLeadStatusCommand } from '../../application/commands/update-lead-status/update-lead-status.command';
|
||||
import { GetLeadStatsQuery } from '../../application/queries/get-lead-stats/get-lead-stats.query';
|
||||
import { GetLeadsByAgentQuery } from '../../application/queries/get-leads-by-agent/get-leads-by-agent.query';
|
||||
import type { LeadReadDto } from '../../domain/repositories/lead-read.dto';
|
||||
import type { LeadStatsData, PaginatedResult } from '../../domain/repositories/lead.repository';
|
||||
import type { CreateLeadDto } from '../dto/create-lead.dto';
|
||||
import type { ListLeadsDto } from '../dto/list-leads.dto';
|
||||
import type { UpdateLeadStatusDto } from '../dto/update-lead-status.dto';
|
||||
|
||||
@ApiTags('leads')
|
||||
@ApiBearerAuth('JWT')
|
||||
@Controller('leads')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles('AGENT')
|
||||
export class LeadsController {
|
||||
constructor(
|
||||
private readonly commandBus: CommandBus,
|
||||
private readonly queryBus: QueryBus,
|
||||
) {}
|
||||
|
||||
@ApiOperation({ summary: 'Tạo lead mới' })
|
||||
@ApiResponse({ status: 201, description: 'Lead đã được tạo thành công' })
|
||||
@ApiResponse({ status: 400, description: 'Lỗi validation' })
|
||||
@ApiResponse({ status: 401, description: 'Chưa xác thực' })
|
||||
@ApiResponse({ status: 403, description: 'Không có quyền' })
|
||||
@Post()
|
||||
async createLead(
|
||||
@Body() dto: CreateLeadDto,
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<CreateLeadResult> {
|
||||
return this.commandBus.execute(
|
||||
new CreateLeadCommand(
|
||||
user.sub,
|
||||
dto.name,
|
||||
dto.phone,
|
||||
dto.email ?? null,
|
||||
dto.source,
|
||||
dto.score ?? null,
|
||||
dto.notes ?? null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Danh sách lead của agent' })
|
||||
@ApiResponse({ status: 200, description: 'Danh sách lead phân trang' })
|
||||
@Get()
|
||||
async getLeads(
|
||||
@Query() dto: ListLeadsDto,
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<PaginatedResult<LeadReadDto>> {
|
||||
return this.queryBus.execute(
|
||||
new GetLeadsByAgentQuery(
|
||||
user.sub,
|
||||
dto.status ?? null,
|
||||
dto.page ?? 1,
|
||||
dto.limit ?? 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Thống kê lead của agent' })
|
||||
@ApiResponse({ status: 200, description: 'Thống kê lead' })
|
||||
@Get('stats')
|
||||
async getStats(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<LeadStatsData> {
|
||||
return this.queryBus.execute(
|
||||
new GetLeadStatsQuery(user.sub),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Cập nhật trạng thái lead' })
|
||||
@ApiParam({ name: 'id', description: 'Lead ID' })
|
||||
@ApiResponse({ status: 200, description: 'Trạng thái đã được cập nhật' })
|
||||
@ApiResponse({ status: 400, description: 'Chuyển trạng thái không hợp lệ' })
|
||||
@ApiResponse({ status: 403, description: 'Không có quyền' })
|
||||
@ApiResponse({ status: 404, description: 'Không tìm thấy lead' })
|
||||
@Patch(':id/status')
|
||||
async updateStatus(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateLeadStatusDto,
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<{ updated: boolean }> {
|
||||
await this.commandBus.execute(
|
||||
new UpdateLeadStatusCommand(id, user.sub, dto.status),
|
||||
);
|
||||
return { updated: true };
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Xóa lead' })
|
||||
@ApiParam({ name: 'id', description: 'Lead ID' })
|
||||
@ApiResponse({ status: 200, description: 'Lead đã được xóa' })
|
||||
@ApiResponse({ status: 403, description: 'Không có quyền' })
|
||||
@ApiResponse({ status: 404, description: 'Không tìm thấy lead' })
|
||||
@Delete(':id')
|
||||
async deleteLead(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: JwtPayload,
|
||||
): Promise<{ deleted: boolean }> {
|
||||
await this.commandBus.execute(new DeleteLeadCommand(id, user.sub));
|
||||
return { deleted: true };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user