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,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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user