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:
Ho Ngoc Hai
2026-04-09 10:01:16 +07:00
parent a1a44ef8fb
commit d64bbe97e2
69 changed files with 3420 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
import {
Body,
Controller,
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, Roles, JwtAuthGuard, RolesGuard } from '@modules/auth';
import { CreateInquiryCommand } from '../../application/commands/create-inquiry/create-inquiry.command';
import { type CreateInquiryResult } from '../../application/commands/create-inquiry/create-inquiry.handler';
import { MarkInquiryReadCommand } from '../../application/commands/mark-inquiry-read/mark-inquiry-read.command';
import { GetInquiriesByAgentQuery } from '../../application/queries/get-inquiries-by-agent/get-inquiries-by-agent.query';
import { GetInquiriesByListingQuery } from '../../application/queries/get-inquiries-by-listing/get-inquiries-by-listing.query';
import type { InquiryReadDto } from '../../domain/repositories/inquiry-read.dto';
import type { PaginatedResult } from '../../domain/repositories/inquiry.repository';
import type { CreateInquiryDto } from '../dto/create-inquiry.dto';
import type { ListInquiriesDto } from '../dto/list-inquiries.dto';
@ApiTags('inquiries')
@Controller('inquiries')
export class InquiriesController {
constructor(
private readonly commandBus: CommandBus,
private readonly queryBus: QueryBus,
) {}
@ApiBearerAuth('JWT')
@ApiOperation({ summary: 'Create an inquiry for a listing' })
@ApiResponse({ status: 201, description: 'Inquiry created successfully' })
@ApiResponse({ status: 400, description: 'Validation error' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 404, description: 'Listing not found' })
@UseGuards(JwtAuthGuard)
@Post()
async createInquiry(
@Body() dto: CreateInquiryDto,
@CurrentUser() user: JwtPayload,
): Promise<CreateInquiryResult> {
return this.commandBus.execute(
new CreateInquiryCommand(
user.sub,
dto.listingId,
dto.message,
dto.phone ?? null,
),
);
}
@ApiBearerAuth('JWT')
@ApiOperation({ summary: 'List inquiries by listing' })
@ApiParam({ name: 'listingId', description: 'Listing ID' })
@ApiResponse({ status: 200, description: 'Paginated list of inquiries' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@UseGuards(JwtAuthGuard)
@Get('listing/:listingId')
async getByListing(
@Param('listingId') listingId: string,
@Query() dto: ListInquiriesDto,
): Promise<PaginatedResult<InquiryReadDto>> {
return this.queryBus.execute(
new GetInquiriesByListingQuery(
listingId,
dto.page ?? 1,
dto.limit ?? 20,
),
);
}
@ApiBearerAuth('JWT')
@ApiOperation({ summary: 'List inquiries for current agent' })
@ApiResponse({ status: 200, description: 'Paginated list of inquiries for agent' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden — not an agent' })
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('AGENT')
@Get('agent/me')
async getMyInquiries(
@CurrentUser() user: JwtPayload,
@Query() dto: ListInquiriesDto,
): Promise<PaginatedResult<InquiryReadDto>> {
return this.queryBus.execute(
new GetInquiriesByAgentQuery(
user.sub,
dto.page ?? 1,
dto.limit ?? 20,
),
);
}
@ApiBearerAuth('JWT')
@ApiOperation({ summary: 'Mark inquiry as read' })
@ApiParam({ name: 'id', description: 'Inquiry ID' })
@ApiResponse({ status: 200, description: 'Inquiry marked as read' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 403, description: 'Forbidden — not the listing agent' })
@ApiResponse({ status: 404, description: 'Inquiry not found' })
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('AGENT')
@Patch(':id/read')
async markAsRead(
@Param('id') id: string,
@CurrentUser() user: JwtPayload,
): Promise<{ success: boolean }> {
await this.commandBus.execute(
new MarkInquiryReadCommand(id, user.sub),
);
return { success: true };
}
}