agent and category Dto creation

This commit is contained in:
pradeepkumar
2026-01-11 20:59:12 +05:30
parent 14ca59f02c
commit c72be7d2ac
35 changed files with 3665 additions and 317 deletions

View File

@@ -0,0 +1,176 @@
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
UseGuards,
ParseUUIDPipe,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
} from '@nestjs/swagger';
import { AgentsService } from './agents.service';
import {
CreateAgentProfileDto,
UpdateAgentProfileDto,
SearchAgentsDto,
} from './dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
import { Roles } from '../auth/decorators/roles.decorator';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { UserRole } from '@prisma/client';
@ApiTags('agents')
@Controller('agents')
export class AgentsController {
constructor(private readonly agentsService: AgentsService) {}
// Public endpoints
@Get()
@ApiOperation({ summary: 'Search and list agents' })
@ApiResponse({ status: 200, description: 'Agents retrieved successfully' })
async searchAgents(@Query() dto: SearchAgentsDto) {
return this.agentsService.searchAgents(dto);
}
@Get('featured')
@ApiOperation({ summary: 'Get featured agents' })
@ApiResponse({
status: 200,
description: 'Featured agents retrieved successfully',
})
async getFeaturedAgents(@Query('limit') limit?: number) {
return this.agentsService.getFeaturedAgents(limit || 10);
}
@Get('top-rated')
@ApiOperation({ summary: 'Get top rated agents' })
@ApiResponse({
status: 200,
description: 'Top rated agents retrieved successfully',
})
async getTopRatedAgents(@Query('limit') limit?: number) {
return this.agentsService.getTopRatedAgents(limit || 10);
}
// Authenticated endpoints - must come before :id route
@Get('profile/me')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Get current user agent profile' })
@ApiResponse({ status: 200, description: 'Profile retrieved successfully' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async getMyProfile(@CurrentUser('id') userId: string) {
return this.agentsService.getProfile(userId);
}
// Parameterized route - must come after specific routes
@Get(':id')
@ApiOperation({ summary: 'Get agent profile by ID' })
@ApiResponse({ status: 200, description: 'Agent profile retrieved successfully' })
@ApiResponse({ status: 404, description: 'Agent profile not found' })
async getAgentById(@Param('id', ParseUUIDPipe) id: string) {
return this.agentsService.getProfileById(id);
}
@Post('profile')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Create agent profile' })
@ApiResponse({ status: 201, description: 'Profile created successfully' })
@ApiResponse({ status: 409, description: 'Profile already exists' })
async createProfile(
@CurrentUser('id') userId: string,
@Body() dto: CreateAgentProfileDto,
) {
return this.agentsService.createProfile(userId, dto);
}
@Put('profile')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Update current user agent profile' })
@ApiResponse({ status: 200, description: 'Profile updated successfully' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async updateProfile(
@CurrentUser('id') userId: string,
@Body() dto: UpdateAgentProfileDto,
) {
return this.agentsService.updateProfile(userId, dto);
}
// Admin endpoints
@Get('admin/list')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
@ApiBearerAuth()
@ApiOperation({ summary: 'Admin: List all agents' })
@ApiResponse({ status: 200, description: 'Agents retrieved successfully' })
async adminListAgents(@Query() dto: SearchAgentsDto) {
return this.agentsService.getAllAgentsAdmin(dto);
}
@Put('admin/:id')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
@ApiBearerAuth()
@ApiOperation({ summary: 'Admin: Update agent profile' })
@ApiResponse({ status: 200, description: 'Profile updated successfully' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async adminUpdateProfile(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateAgentProfileDto,
) {
return this.agentsService.adminUpdateProfile(id, dto);
}
@Delete('admin/:id')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
@ApiBearerAuth()
@ApiOperation({ summary: 'Admin: Delete agent profile' })
@ApiResponse({ status: 200, description: 'Profile deleted successfully' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async adminDeleteProfile(@Param('id', ParseUUIDPipe) id: string) {
return this.agentsService.adminDeleteProfile(id);
}
@Put('admin/:id/verify')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
@ApiBearerAuth()
@ApiOperation({ summary: 'Admin: Verify/unverify agent' })
@ApiResponse({ status: 200, description: 'Verification status updated' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async adminVerifyAgent(
@Param('id', ParseUUIDPipe) id: string,
@Body('isVerified') isVerified: boolean,
) {
return this.agentsService.adminVerifyAgent(id, isVerified);
}
@Put('admin/:id/featured')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
@ApiBearerAuth()
@ApiOperation({ summary: 'Admin: Set agent as featured' })
@ApiResponse({ status: 200, description: 'Featured status updated' })
@ApiResponse({ status: 404, description: 'Profile not found' })
async adminSetFeatured(
@Param('id', ParseUUIDPipe) id: string,
@Body('isFeatured') isFeatured: boolean,
) {
return this.agentsService.adminSetFeatured(id, isFeatured);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AgentsController } from './agents.controller';
import { AgentsService } from './agents.service';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [AgentsController],
providers: [AgentsService],
exports: [AgentsService],
})
export class AgentsModule {}

View File

@@ -0,0 +1,509 @@
import {
Injectable,
NotFoundException,
ForbiddenException,
ConflictException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import {
CreateAgentProfileDto,
UpdateAgentProfileDto,
SearchAgentsDto,
} from './dto';
import { Prisma } from '@prisma/client';
function generateSlug(firstName: string, lastName: string): string {
const base = `${firstName}-${lastName}`
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
const uniqueSuffix = Math.random().toString(36).substring(2, 8);
return `${base}-${uniqueSuffix}`;
}
@Injectable()
export class AgentsService {
constructor(private prisma: PrismaService) {}
async createProfile(userId: string, dto: CreateAgentProfileDto) {
// Check if user already has an agent profile
const existingProfile = await this.prisma.agentProfile.findUnique({
where: { userId },
});
if (existingProfile) {
throw new ConflictException('User already has an agent profile');
}
const { specializationIds, ...profileData } = dto;
const slug = generateSlug(dto.firstName, dto.lastName);
const profile = await this.prisma.agentProfile.create({
data: {
...profileData,
userId,
slug,
specializations: specializationIds?.length
? {
create: specializationIds.map((specId) => ({
specializationId: specId,
})),
}
: undefined,
},
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
specializations: {
include: {
specialization: {
include: {
category: true,
},
},
},
},
},
});
return profile;
}
async getProfile(userId: string) {
const profile = await this.prisma.agentProfile.findUnique({
where: { userId },
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
specializations: {
include: {
specialization: {
include: {
category: true,
},
},
},
},
verificationRequests: {
orderBy: { createdAt: 'desc' },
take: 5,
},
},
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
return profile;
}
async getProfileById(profileId: string) {
const profile = await this.prisma.agentProfile.findUnique({
where: { id: profileId },
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
specializations: {
include: {
specialization: {
include: {
category: true,
},
},
},
},
},
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
return profile;
}
async updateProfile(userId: string, dto: UpdateAgentProfileDto) {
const profile = await this.prisma.agentProfile.findUnique({
where: { userId },
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
const { specializationIds, ...profileData } = dto;
// If specializationIds is provided, update the specializations
if (specializationIds !== undefined) {
// Delete existing specializations
await this.prisma.agentSpecialization.deleteMany({
where: { agentProfileId: profile.id },
});
// Create new specializations
if (specializationIds.length > 0) {
await this.prisma.agentSpecialization.createMany({
data: specializationIds.map((specId) => ({
agentProfileId: profile.id,
specializationId: specId,
})),
});
}
}
const updatedProfile = await this.prisma.agentProfile.update({
where: { userId },
data: profileData,
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
specializations: {
include: {
specialization: {
include: {
category: true,
},
},
},
},
},
});
return updatedProfile;
}
async searchAgents(dto: SearchAgentsDto) {
const {
search,
city,
state,
country,
categoryId,
specializationIds,
isVerified,
page = 1,
limit = 10,
sortBy = 'createdAt',
sortOrder = 'desc',
} = dto;
const skip = (page - 1) * limit;
// Build where clause
const where: Prisma.AgentProfileWhereInput = {};
if (search) {
where.OR = [
{ firstName: { contains: search, mode: 'insensitive' } },
{ lastName: { contains: search, mode: 'insensitive' } },
{ bio: { contains: search, mode: 'insensitive' } },
{ headline: { contains: search, mode: 'insensitive' } },
{ companyName: { contains: search, mode: 'insensitive' } },
];
}
if (city) {
where.city = { contains: city, mode: 'insensitive' };
}
if (state) {
where.state = { contains: state, mode: 'insensitive' };
}
if (country) {
where.country = { contains: country, mode: 'insensitive' };
}
if (isVerified !== undefined) {
where.isVerified = isVerified;
}
// Filter by category
if (categoryId) {
where.specializations = {
some: {
specialization: {
categoryId,
},
},
};
}
// Filter by specializations
if (specializationIds?.length) {
where.specializations = {
some: {
specializationId: { in: specializationIds },
},
};
}
// Build orderBy
const orderBy: Prisma.AgentProfileOrderByWithRelationInput = {};
if (sortBy === 'averageRating') {
orderBy.averageRating = sortOrder;
} else if (sortBy === 'totalReviews') {
orderBy.totalReviews = sortOrder;
} else if (sortBy === 'firstName') {
orderBy.firstName = sortOrder;
} else {
orderBy.createdAt = sortOrder;
}
const [agents, total] = await Promise.all([
this.prisma.agentProfile.findMany({
where,
skip,
take: limit,
orderBy,
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
specializations: {
include: {
specialization: {
include: {
category: true,
},
},
},
},
},
}),
this.prisma.agentProfile.count({ where }),
]);
return {
data: agents,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
async getFeaturedAgents(limit = 10) {
const agents = await this.prisma.agentProfile.findMany({
where: {
isVerified: true,
isFeatured: true,
},
take: limit,
orderBy: [{ averageRating: 'desc' }, { totalReviews: 'desc' }],
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
specializations: {
include: {
specialization: {
include: {
category: true,
},
},
},
},
},
});
return agents;
}
async getTopRatedAgents(limit = 10) {
const agents = await this.prisma.agentProfile.findMany({
where: {
isVerified: true,
totalReviews: { gte: 5 },
},
take: limit,
orderBy: [{ averageRating: 'desc' }, { totalReviews: 'desc' }],
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
specializations: {
include: {
specialization: {
include: {
category: true,
},
},
},
},
},
});
return agents;
}
// Admin methods
async adminUpdateProfile(profileId: string, dto: UpdateAgentProfileDto) {
const profile = await this.prisma.agentProfile.findUnique({
where: { id: profileId },
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
const { specializationIds, ...profileData } = dto;
if (specializationIds !== undefined) {
await this.prisma.agentSpecialization.deleteMany({
where: { agentProfileId: profileId },
});
if (specializationIds.length > 0) {
await this.prisma.agentSpecialization.createMany({
data: specializationIds.map((specId) => ({
agentProfileId: profileId,
specializationId: specId,
})),
});
}
}
const updatedProfile = await this.prisma.agentProfile.update({
where: { id: profileId },
data: profileData,
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
specializations: {
include: {
specialization: {
include: {
category: true,
},
},
},
},
},
});
return updatedProfile;
}
async adminDeleteProfile(profileId: string) {
const profile = await this.prisma.agentProfile.findUnique({
where: { id: profileId },
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
// Delete related records first
await this.prisma.agentSpecialization.deleteMany({
where: { agentProfileId: profileId },
});
await this.prisma.verificationRequest.deleteMany({
where: { agentProfileId: profileId },
});
await this.prisma.agentProfile.delete({
where: { id: profileId },
});
return { message: 'Agent profile deleted successfully' };
}
async adminVerifyAgent(profileId: string, isVerified: boolean) {
const profile = await this.prisma.agentProfile.findUnique({
where: { id: profileId },
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
const updatedProfile = await this.prisma.agentProfile.update({
where: { id: profileId },
data: { isVerified },
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
},
});
return updatedProfile;
}
async adminSetFeatured(profileId: string, isFeatured: boolean) {
const profile = await this.prisma.agentProfile.findUnique({
where: { id: profileId },
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
const updatedProfile = await this.prisma.agentProfile.update({
where: { id: profileId },
data: { isFeatured },
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
},
});
return updatedProfile;
}
async getAllAgentsAdmin(dto: SearchAgentsDto) {
// Same as searchAgents but returns all data for admin
return this.searchAgents(dto);
}
}

View File

@@ -0,0 +1,128 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
IsString,
IsOptional,
IsArray,
IsInt,
IsUrl,
IsUUID,
MinLength,
MaxLength,
Min,
Max,
} from 'class-validator';
export class CreateAgentProfileDto {
@ApiProperty({ example: 'John' })
@IsString()
@MinLength(1)
@MaxLength(100)
firstName: string;
@ApiProperty({ example: 'Doe' })
@IsString()
@MinLength(1)
@MaxLength(100)
lastName: string;
@ApiPropertyOptional({ example: '+1234567890' })
@IsOptional()
@IsString()
@MaxLength(20)
phone?: string;
@ApiPropertyOptional({ example: 'Experienced real estate agent...' })
@IsOptional()
@IsString()
@MaxLength(2000)
bio?: string;
@ApiPropertyOptional({ example: 'Top Real Estate Agent in California' })
@IsOptional()
@IsString()
@MaxLength(200)
headline?: string;
@ApiPropertyOptional({ example: 'Los Angeles' })
@IsOptional()
@IsString()
@MaxLength(100)
city?: string;
@ApiPropertyOptional({ example: 'California' })
@IsOptional()
@IsString()
@MaxLength(100)
state?: string;
@ApiPropertyOptional({ example: 'USA' })
@IsOptional()
@IsString()
@MaxLength(100)
country?: string;
@ApiPropertyOptional({ example: '123 Main St' })
@IsOptional()
@IsString()
@MaxLength(200)
address?: string;
@ApiPropertyOptional({ example: '90001' })
@IsOptional()
@IsString()
@MaxLength(20)
zipCode?: string;
@ApiPropertyOptional({ example: 10 })
@IsOptional()
@IsInt()
@Min(0)
@Max(100)
yearsOfExperience?: number;
@ApiPropertyOptional({ example: 'LIC-12345' })
@IsOptional()
@IsString()
@MaxLength(50)
licenseNumber?: string;
@ApiPropertyOptional({ example: 'ABC Realty' })
@IsOptional()
@IsString()
@MaxLength(200)
companyName?: string;
@ApiPropertyOptional({ example: 'https://mywebsite.com' })
@IsOptional()
@IsUrl()
website?: string;
@ApiPropertyOptional({ example: 'https://facebook.com/johndoe' })
@IsOptional()
@IsUrl()
facebookUrl?: string;
@ApiPropertyOptional({ example: 'https://twitter.com/johndoe' })
@IsOptional()
@IsUrl()
twitterUrl?: string;
@ApiPropertyOptional({ example: 'https://linkedin.com/in/johndoe' })
@IsOptional()
@IsUrl()
linkedinUrl?: string;
@ApiPropertyOptional({ example: 'https://instagram.com/johndoe' })
@IsOptional()
@IsUrl()
instagramUrl?: string;
@ApiPropertyOptional({
example: ['uuid-1', 'uuid-2'],
description: 'Array of specialization IDs',
})
@IsOptional()
@IsArray()
@IsUUID('4', { each: true })
specializationIds?: string[];
}

3
src/agents/dto/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './create-agent-profile.dto';
export * from './update-agent-profile.dto';
export * from './search-agents.dto';

View File

@@ -0,0 +1,83 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import {
IsString,
IsOptional,
IsBoolean,
IsInt,
IsUUID,
IsArray,
Min,
Max,
} from 'class-validator';
import { Transform, Type } from 'class-transformer';
export class SearchAgentsDto {
@ApiPropertyOptional({ example: 'John' })
@IsOptional()
@IsString()
search?: string;
@ApiPropertyOptional({ example: 'Los Angeles' })
@IsOptional()
@IsString()
city?: string;
@ApiPropertyOptional({ example: 'California' })
@IsOptional()
@IsString()
state?: string;
@ApiPropertyOptional({ example: 'USA' })
@IsOptional()
@IsString()
country?: string;
@ApiPropertyOptional({ example: 'uuid-of-category' })
@IsOptional()
@IsUUID()
categoryId?: string;
@ApiPropertyOptional({
example: ['uuid-1', 'uuid-2'],
description: 'Filter by specialization IDs',
})
@IsOptional()
@IsArray()
@IsUUID('4', { each: true })
@Transform(({ value }) => (typeof value === 'string' ? [value] : value))
specializationIds?: string[];
@ApiPropertyOptional({ example: true })
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean()
isVerified?: boolean;
@ApiPropertyOptional({ example: 1, default: 1 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@ApiPropertyOptional({ example: 10, default: 10 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 10;
@ApiPropertyOptional({
example: 'createdAt',
enum: ['createdAt', 'averageRating', 'totalReviews', 'firstName'],
})
@IsOptional()
@IsString()
sortBy?: string = 'createdAt';
@ApiPropertyOptional({ example: 'desc', enum: ['asc', 'desc'] })
@IsOptional()
@IsString()
sortOrder?: 'asc' | 'desc' = 'desc';
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateAgentProfileDto } from './create-agent-profile.dto';
export class UpdateAgentProfileDto extends PartialType(CreateAgentProfileDto) {}

4
src/agents/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './agents.module';
export * from './agents.service';
export * from './agents.controller';
export * from './dto';