From 3df8ea067aa7f5799250aca034a8b1c3764dc0d6 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Tue, 20 Jan 2026 12:16:01 +0530 Subject: [PATCH] feat: Introduce agent types module, replacing categories and verification modules, and updating agent-related DTOs, services, and Prisma schema. --- prisma/schema.prisma | 126 +------ prisma/seed.ts | 55 ++- src/agent-types/agent-types.controller.ts | 80 +++++ src/agent-types/agent-types.module.ts | 12 + src/agent-types/agent-types.service.ts | 118 ++++++ src/agent-types/dto/create-agent-type.dto.ts | 31 ++ src/agent-types/dto/index.ts | 2 + src/agent-types/dto/update-agent-type.dto.ts | 4 + src/agent-types/index.ts | 4 + src/agents/agents.service.ts | 169 +-------- src/agents/dto/create-agent-profile.dto.ts | 18 +- src/agents/dto/search-agents.dto.ts | 16 +- src/app.module.ts | 6 +- src/categories/categories.controller.ts | 197 ---------- src/categories/categories.module.ts | 12 - src/categories/categories.service.ts | 328 ----------------- src/categories/dto/create-category.dto.ts | 43 --- .../dto/create-specialization.dto.ts | 38 -- src/categories/dto/index.ts | 4 - src/categories/dto/update-category.dto.ts | 4 - .../dto/update-specialization.dto.ts | 6 - src/categories/index.ts | 4 - .../dto/create-verification-request.dto.ts | 30 -- src/verification/dto/index.ts | 3 - .../dto/review-verification-request.dto.ts | 31 -- .../dto/search-verification-requests.dto.ts | 57 --- src/verification/index.ts | 4 - src/verification/verification.controller.ts | 154 -------- src/verification/verification.module.ts | 12 - src/verification/verification.service.ts | 336 ------------------ 30 files changed, 345 insertions(+), 1559 deletions(-) create mode 100644 src/agent-types/agent-types.controller.ts create mode 100644 src/agent-types/agent-types.module.ts create mode 100644 src/agent-types/agent-types.service.ts create mode 100644 src/agent-types/dto/create-agent-type.dto.ts create mode 100644 src/agent-types/dto/index.ts create mode 100644 src/agent-types/dto/update-agent-type.dto.ts create mode 100644 src/agent-types/index.ts delete mode 100644 src/categories/categories.controller.ts delete mode 100644 src/categories/categories.module.ts delete mode 100644 src/categories/categories.service.ts delete mode 100644 src/categories/dto/create-category.dto.ts delete mode 100644 src/categories/dto/create-specialization.dto.ts delete mode 100644 src/categories/dto/index.ts delete mode 100644 src/categories/dto/update-category.dto.ts delete mode 100644 src/categories/dto/update-specialization.dto.ts delete mode 100644 src/categories/index.ts delete mode 100644 src/verification/dto/create-verification-request.dto.ts delete mode 100644 src/verification/dto/index.ts delete mode 100644 src/verification/dto/review-verification-request.dto.ts delete mode 100644 src/verification/dto/search-verification-requests.dto.ts delete mode 100644 src/verification/index.ts delete mode 100644 src/verification/verification.controller.ts delete mode 100644 src/verification/verification.module.ts delete mode 100644 src/verification/verification.service.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b05c034..4ea4915 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,18 +33,6 @@ enum AuthProvider { TWITTER } -enum VerificationStatus { - PENDING - APPROVED - REJECTED -} - -enum VerificationDocType { - LICENSE - ID_CARD - CERTIFICATE - OTHER -} // =========================================== // USER - Authentication Only @@ -120,6 +108,7 @@ model AgentProfile { id String @id @default(uuid()) userId String @unique slug String @unique + agentTypeId String? // Basic Info firstName String? @@ -170,10 +159,10 @@ model AgentProfile { // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) - specializations AgentSpecialization[] - verificationRequests VerificationRequest[] + agentType AgentType? @relation(fields: [agentTypeId], references: [id]) @@index([userId]) + @@index([agentTypeId]) @@index([slug]) @@index([city, state]) @@index([isVerified]) @@ -183,111 +172,26 @@ model AgentProfile { } // =========================================== -// CATEGORY - Main categories for agents +// AGENT TYPE - Dynamic agent types (Professional, Lender, etc.) // =========================================== -model Category { - id String @id @default(uuid()) - name String @unique - slug String @unique - description String? - icon String? - image String? - isActive Boolean @default(true) - sortOrder Int @default(0) +model AgentType { + id String @id @default(uuid()) + name String @unique + description String? + icon String? + isActive Boolean @default(true) + sortOrder Int @default(0) // Timestamps - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt // Relations - specializations Specialization[] + agents AgentProfile[] - @@index([slug]) @@index([isActive]) - @@map("categories") -} - -// =========================================== -// SPECIALIZATION - Sub-categories under Category -// =========================================== - -model Specialization { - id String @id @default(uuid()) - categoryId String - name String - slug String @unique - description String? - isActive Boolean @default(true) - sortOrder Int @default(0) - - // Timestamps - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relations - category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) - agents AgentSpecialization[] - - @@unique([categoryId, name]) - @@index([categoryId]) - @@index([slug]) - @@map("specializations") -} - -// =========================================== -// AGENT SPECIALIZATION - Many-to-Many pivot -// =========================================== - -model AgentSpecialization { - id String @id @default(uuid()) - agentProfileId String - specializationId String - - // Timestamps - createdAt DateTime @default(now()) - - // Relations - agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade) - specialization Specialization @relation(fields: [specializationId], references: [id], onDelete: Cascade) - - @@unique([agentProfileId, specializationId]) - @@index([agentProfileId]) - @@index([specializationId]) - @@map("agent_specializations") -} - -// =========================================== -// VERIFICATION REQUEST - Agent verification -// =========================================== - -model VerificationRequest { - id String @id @default(uuid()) - agentProfileId String - - // Document Info - documentType VerificationDocType - documentUrl String - documentName String? - notes String? @db.Text - - // Status - status VerificationStatus @default(PENDING) - reviewedAt DateTime? - reviewedBy String? // Admin user ID - adminNotes String? @db.Text - rejectionReason String? - - // Timestamps - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relations - agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade) - - @@index([agentProfileId]) - @@index([status]) - @@map("verification_requests") + @@map("agent_types") } // =========================================== diff --git a/prisma/seed.ts b/prisma/seed.ts index fe4d200..2b67ccb 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -12,7 +12,48 @@ async function main() { console.log('🌱 Starting database seeding...\n'); - // Admin credentials + // ============================================= + // Seed Agent Types + // ============================================= + console.log('šŸ“‹ Seeding Agent Types...'); + + const agentTypes = [ + { + name: 'Professional', + description: 'Licensed real estate professionals', + icon: 'briefcase', + sortOrder: 1, + }, + { + name: 'Lender', + description: 'Mortgage and loan specialists', + icon: 'bank', + sortOrder: 2, + }, + ]; + + for (const agentType of agentTypes) { + const existing = await prisma.agentType.findUnique({ + where: { name: agentType.name }, + }); + + if (existing) { + console.log(` āš ļø Agent type "${agentType.name}" already exists`); + } else { + await prisma.agentType.create({ + data: agentType, + }); + console.log(` āœ… Created agent type: ${agentType.name}`); + } + } + + console.log(''); + + // ============================================= + // Seed Admin User + // ============================================= + console.log('šŸ‘¤ Seeding Admin User...'); + const adminEmail = process.env.ADMIN_EMAIL || 'admin@realestate.com'; const adminPassword = process.env.ADMIN_PASSWORD || 'Admin@123456'; @@ -25,7 +66,7 @@ async function main() { }); if (existingAdmin) { - console.log(`āš ļø Admin user already exists: ${adminEmail}`); + console.log(` āš ļø Admin user already exists: ${adminEmail}`); } else { // Create admin user const admin = await prisma.user.create({ @@ -40,7 +81,7 @@ async function main() { }, }); - console.log('āœ… Admin user created successfully!'); + console.log(' āœ… Admin user created successfully!'); console.log(' ───────────────────────────────'); console.log(` šŸ“§ Email: ${adminEmail}`); console.log(` šŸ”‘ Password: ${adminPassword}`); @@ -49,13 +90,17 @@ async function main() { console.log(' ───────────────────────────────\n'); } + // ============================================= // Summary + // ============================================= const userCount = await prisma.user.count(); const adminCount = await prisma.user.count({ where: { role: UserRole.ADMIN } }); + const agentTypeCount = await prisma.agentType.count(); console.log('šŸ“Š Database Summary:'); - console.log(` Total Users: ${userCount}`); - console.log(` Admins: ${adminCount}`); + console.log(` Total Users: ${userCount}`); + console.log(` Admins: ${adminCount}`); + console.log(` Agent Types: ${agentTypeCount}`); console.log('\n🌱 Seeding completed!\n'); await prisma.$disconnect(); diff --git a/src/agent-types/agent-types.controller.ts b/src/agent-types/agent-types.controller.ts new file mode 100644 index 0000000..400349b --- /dev/null +++ b/src/agent-types/agent-types.controller.ts @@ -0,0 +1,80 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + Query, + UseGuards, + ParseUUIDPipe, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger'; +import { AgentTypesService } from './agent-types.service'; +import { CreateAgentTypeDto, UpdateAgentTypeDto } from './dto'; +import { JwtAuthGuard, RolesGuard } from '../auth/guards'; +import { Roles, Public } from '../auth/decorators'; +import { UserRole } from '@prisma/client'; + +@ApiTags('Agent Types') +@Controller('agent-types') +export class AgentTypesController { + constructor(private readonly agentTypesService: AgentTypesService) {} + + @Post() + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new agent type (Admin only)' }) + @ApiResponse({ status: 201, description: 'Agent type created successfully' }) + @ApiResponse({ status: 409, description: 'Agent type already exists' }) + create(@Body() createAgentTypeDto: CreateAgentTypeDto) { + return this.agentTypesService.create(createAgentTypeDto); + } + + @Get() + @Public() + @ApiOperation({ summary: 'Get all agent types' }) + @ApiQuery({ name: 'includeInactive', required: false, type: Boolean }) + @ApiResponse({ status: 200, description: 'List of agent types' }) + findAll(@Query('includeInactive') includeInactive?: string) { + return this.agentTypesService.findAll(includeInactive === 'true'); + } + + @Get(':id') + @Public() + @ApiOperation({ summary: 'Get a single agent type by ID' }) + @ApiResponse({ status: 200, description: 'Agent type found' }) + @ApiResponse({ status: 404, description: 'Agent type not found' }) + findOne(@Param('id', ParseUUIDPipe) id: string) { + return this.agentTypesService.findOne(id); + } + + @Patch(':id') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @ApiBearerAuth() + @ApiOperation({ summary: 'Update an agent type (Admin only)' }) + @ApiResponse({ status: 200, description: 'Agent type updated successfully' }) + @ApiResponse({ status: 404, description: 'Agent type not found' }) + @ApiResponse({ status: 409, description: 'Agent type with this name already exists' }) + update( + @Param('id', ParseUUIDPipe) id: string, + @Body() updateAgentTypeDto: UpdateAgentTypeDto, + ) { + return this.agentTypesService.update(id, updateAgentTypeDto); + } + + @Delete(':id') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(UserRole.ADMIN) + @ApiBearerAuth() + @ApiOperation({ summary: 'Delete an agent type (Admin only)' }) + @ApiResponse({ status: 200, description: 'Agent type deleted successfully' }) + @ApiResponse({ status: 404, description: 'Agent type not found' }) + @ApiResponse({ status: 409, description: 'Cannot delete - agents are using this type' }) + remove(@Param('id', ParseUUIDPipe) id: string) { + return this.agentTypesService.remove(id); + } +} diff --git a/src/agent-types/agent-types.module.ts b/src/agent-types/agent-types.module.ts new file mode 100644 index 0000000..10ae2c2 --- /dev/null +++ b/src/agent-types/agent-types.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AgentTypesController } from './agent-types.controller'; +import { AgentTypesService } from './agent-types.service'; +import { PrismaModule } from '../prisma'; + +@Module({ + imports: [PrismaModule], + controllers: [AgentTypesController], + providers: [AgentTypesService], + exports: [AgentTypesService], +}) +export class AgentTypesModule {} diff --git a/src/agent-types/agent-types.service.ts b/src/agent-types/agent-types.service.ts new file mode 100644 index 0000000..9e3a574 --- /dev/null +++ b/src/agent-types/agent-types.service.ts @@ -0,0 +1,118 @@ +import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; +import { PrismaService } from '../prisma'; +import { CreateAgentTypeDto, UpdateAgentTypeDto } from './dto'; + +@Injectable() +export class AgentTypesService { + constructor(private readonly prisma: PrismaService) {} + + /** + * Create a new agent type + */ + async create(createAgentTypeDto: CreateAgentTypeDto) { + const { name, ...rest } = createAgentTypeDto; + + // Check if name already exists + const existing = await this.prisma.agentType.findUnique({ + where: { name }, + }); + + if (existing) { + throw new ConflictException('Agent type with this name already exists'); + } + + return this.prisma.agentType.create({ + data: { + name, + ...rest, + }, + }); + } + + /** + * Get all agent types (optionally filter by active status) + */ + async findAll(includeInactive = false) { + const where = includeInactive ? {} : { isActive: true }; + + return this.prisma.agentType.findMany({ + where, + orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }], + include: { + _count: { + select: { agents: true }, + }, + }, + }); + } + + /** + * Get a single agent type by ID + */ + async findOne(id: string) { + const agentType = await this.prisma.agentType.findUnique({ + where: { id }, + include: { + _count: { + select: { agents: true }, + }, + }, + }); + + if (!agentType) { + throw new NotFoundException(`Agent type with ID ${id} not found`); + } + + return agentType; + } + + /** + * Update an agent type + */ + async update(id: string, updateAgentTypeDto: UpdateAgentTypeDto) { + // Check if agent type exists + await this.findOne(id); + + const { name, ...rest } = updateAgentTypeDto; + + // If name is being updated, check for conflicts + if (name) { + const existing = await this.prisma.agentType.findFirst({ + where: { + AND: [{ id: { not: id } }, { name }], + }, + }); + + if (existing) { + throw new ConflictException('Agent type with this name already exists'); + } + } + + return this.prisma.agentType.update({ + where: { id }, + data: { + ...(name && { name }), + ...rest, + }, + }); + } + + /** + * Delete an agent type + */ + async remove(id: string) { + // Check if agent type exists + const agentType = await this.findOne(id); + + // Check if any agents are using this type + if (agentType._count.agents > 0) { + throw new ConflictException( + `Cannot delete agent type. ${agentType._count.agents} agent(s) are using this type.`, + ); + } + + return this.prisma.agentType.delete({ + where: { id }, + }); + } +} diff --git a/src/agent-types/dto/create-agent-type.dto.ts b/src/agent-types/dto/create-agent-type.dto.ts new file mode 100644 index 0000000..2ed9dd6 --- /dev/null +++ b/src/agent-types/dto/create-agent-type.dto.ts @@ -0,0 +1,31 @@ +import { IsString, IsOptional, IsBoolean, IsInt, MinLength, MaxLength } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateAgentTypeDto { + @ApiProperty({ description: 'Agent type name', example: 'Professional' }) + @IsString() + @MinLength(2) + @MaxLength(50) + name: string; + + @ApiPropertyOptional({ description: 'Agent type description' }) + @IsOptional() + @IsString() + @MaxLength(500) + description?: string; + + @ApiPropertyOptional({ description: 'Icon URL or icon name' }) + @IsOptional() + @IsString() + icon?: string; + + @ApiPropertyOptional({ description: 'Whether the agent type is active', default: true }) + @IsOptional() + @IsBoolean() + isActive?: boolean; + + @ApiPropertyOptional({ description: 'Sort order for display', default: 0 }) + @IsOptional() + @IsInt() + sortOrder?: number; +} diff --git a/src/agent-types/dto/index.ts b/src/agent-types/dto/index.ts new file mode 100644 index 0000000..60ce748 --- /dev/null +++ b/src/agent-types/dto/index.ts @@ -0,0 +1,2 @@ +export * from './create-agent-type.dto'; +export * from './update-agent-type.dto'; diff --git a/src/agent-types/dto/update-agent-type.dto.ts b/src/agent-types/dto/update-agent-type.dto.ts new file mode 100644 index 0000000..1c0f669 --- /dev/null +++ b/src/agent-types/dto/update-agent-type.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateAgentTypeDto } from './create-agent-type.dto'; + +export class UpdateAgentTypeDto extends PartialType(CreateAgentTypeDto) {} diff --git a/src/agent-types/index.ts b/src/agent-types/index.ts new file mode 100644 index 0000000..6bb9423 --- /dev/null +++ b/src/agent-types/index.ts @@ -0,0 +1,4 @@ +export * from './agent-types.module'; +export * from './agent-types.service'; +export * from './agent-types.controller'; +export * from './dto'; diff --git a/src/agents/agents.service.ts b/src/agents/agents.service.ts index 451709e..6ed02fa 100644 --- a/src/agents/agents.service.ts +++ b/src/agents/agents.service.ts @@ -1,7 +1,6 @@ import { Injectable, NotFoundException, - ForbiddenException, ConflictException, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; @@ -35,21 +34,13 @@ export class AgentsService { 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, + ...dto, userId, slug, - specializations: specializationIds?.length - ? { - create: specializationIds.map((specId) => ({ - specializationId: specId, - })), - } - : undefined, }, include: { user: { @@ -59,15 +50,7 @@ export class AgentsService { avatar: true, }, }, - specializations: { - include: { - specialization: { - include: { - category: true, - }, - }, - }, - }, + agentType: true, }, }); @@ -85,19 +68,7 @@ export class AgentsService { avatar: true, }, }, - specializations: { - include: { - specialization: { - include: { - category: true, - }, - }, - }, - }, - verificationRequests: { - orderBy: { createdAt: 'desc' }, - take: 5, - }, + agentType: true, }, }); @@ -119,15 +90,7 @@ export class AgentsService { avatar: true, }, }, - specializations: { - include: { - specialization: { - include: { - category: true, - }, - }, - }, - }, + agentType: true, }, }); @@ -147,29 +110,9 @@ export class AgentsService { 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, + data: dto, include: { user: { select: { @@ -178,15 +121,7 @@ export class AgentsService { avatar: true, }, }, - specializations: { - include: { - specialization: { - include: { - category: true, - }, - }, - }, - }, + agentType: true, }, }); @@ -199,8 +134,7 @@ export class AgentsService { city, state, country, - categoryId, - specializationIds, + agentTypeId, isVerified, page = 1, limit = 10, @@ -239,24 +173,9 @@ export class AgentsService { 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 }, - }, - }; + // Filter by agent type + if (agentTypeId) { + where.agentTypeId = agentTypeId; } // Build orderBy @@ -285,15 +204,7 @@ export class AgentsService { avatar: true, }, }, - specializations: { - include: { - specialization: { - include: { - category: true, - }, - }, - }, - }, + agentType: true, }, }), this.prisma.agentProfile.count({ where }), @@ -326,15 +237,7 @@ export class AgentsService { avatar: true, }, }, - specializations: { - include: { - specialization: { - include: { - category: true, - }, - }, - }, - }, + agentType: true, }, }); @@ -357,15 +260,7 @@ export class AgentsService { avatar: true, }, }, - specializations: { - include: { - specialization: { - include: { - category: true, - }, - }, - }, - }, + agentType: true, }, }); @@ -382,26 +277,9 @@ export class AgentsService { 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, + data: dto, include: { user: { select: { @@ -410,15 +288,7 @@ export class AgentsService { avatar: true, }, }, - specializations: { - include: { - specialization: { - include: { - category: true, - }, - }, - }, - }, + agentType: true, }, }); @@ -434,15 +304,6 @@ export class AgentsService { 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 }, }); diff --git a/src/agents/dto/create-agent-profile.dto.ts b/src/agents/dto/create-agent-profile.dto.ts index 281eb4c..0d39152 100644 --- a/src/agents/dto/create-agent-profile.dto.ts +++ b/src/agents/dto/create-agent-profile.dto.ts @@ -2,7 +2,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsString, IsOptional, - IsArray, IsInt, IsUrl, IsUUID, @@ -13,6 +12,14 @@ import { } from 'class-validator'; export class CreateAgentProfileDto { + @ApiPropertyOptional({ + example: 'uuid-agent-type-id', + description: 'Agent type ID (Professional, Lender, Advisor, etc.)', + }) + @IsOptional() + @IsUUID('4') + agentTypeId?: string; + @ApiProperty({ example: 'John' }) @IsString() @MinLength(1) @@ -116,13 +123,4 @@ export class CreateAgentProfileDto { @IsOptional() @IsUrl() instagramUrl?: string; - - @ApiPropertyOptional({ - example: ['uuid-1', 'uuid-2'], - description: 'Array of specialization IDs', - }) - @IsOptional() - @IsArray() - @IsUUID('4', { each: true }) - specializationIds?: string[]; } diff --git a/src/agents/dto/search-agents.dto.ts b/src/agents/dto/search-agents.dto.ts index 3aa2b44..c62fb76 100644 --- a/src/agents/dto/search-agents.dto.ts +++ b/src/agents/dto/search-agents.dto.ts @@ -5,7 +5,6 @@ import { IsBoolean, IsInt, IsUUID, - IsArray, Min, Max, } from 'class-validator'; @@ -32,20 +31,13 @@ export class SearchAgentsDto { @IsString() country?: string; - @ApiPropertyOptional({ example: 'uuid-of-category' }) - @IsOptional() - @IsUUID() - categoryId?: string; - @ApiPropertyOptional({ - example: ['uuid-1', 'uuid-2'], - description: 'Filter by specialization IDs', + example: 'uuid-of-agent-type', + description: 'Filter by agent type ID', }) @IsOptional() - @IsArray() - @IsUUID('4', { each: true }) - @Transform(({ value }) => (typeof value === 'string' ? [value] : value)) - specializationIds?: string[]; + @IsUUID() + agentTypeId?: string; @ApiPropertyOptional({ example: true }) @IsOptional() diff --git a/src/app.module.ts b/src/app.module.ts index da76567..9cccffe 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,9 +10,8 @@ import { PrismaModule } from './prisma'; import { AuthModule, JwtAuthGuard, RolesGuard } from './auth'; import { UsersModule } from './users'; import { EmailModule } from './email'; -import { CategoriesModule } from './categories'; +import { AgentTypesModule } from './agent-types'; import { AgentsModule } from './agents'; -import { VerificationModule } from './verification'; import { UploadModule } from './upload'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @@ -47,9 +46,8 @@ import { AppService } from './app.service'; AuthModule, UsersModule, EmailModule, - CategoriesModule, + AgentTypesModule, AgentsModule, - VerificationModule, UploadModule, ], controllers: [AppController], diff --git a/src/categories/categories.controller.ts b/src/categories/categories.controller.ts deleted file mode 100644 index 22f1a6a..0000000 --- a/src/categories/categories.controller.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { - Controller, - Get, - Post, - Put, - Delete, - Body, - Param, - Query, - UseGuards, - HttpCode, - HttpStatus, -} from '@nestjs/common'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiBearerAuth, - ApiQuery, -} from '@nestjs/swagger'; -import { CategoriesService } from './categories.service'; -import { - CreateCategoryDto, - UpdateCategoryDto, - CreateSpecializationDto, - UpdateSpecializationDto, -} from './dto'; -import { JwtAuthGuard, RolesGuard } from '../auth/guards'; -import { Public, Roles } from '../auth/decorators'; -import { UserRole } from '@prisma/client'; - -@ApiTags('Categories') -@Controller('categories') -export class CategoriesController { - constructor(private readonly categoriesService: CategoriesService) {} - - // =========================================== - // CATEGORY ENDPOINTS - // =========================================== - - @Public() - @Get() - @ApiOperation({ summary: 'Get all categories with specializations' }) - @ApiQuery({ - name: 'includeInactive', - required: false, - type: Boolean, - description: 'Include inactive categories (admin only)', - }) - @ApiResponse({ - status: 200, - description: 'List of categories', - schema: { - example: { - success: true, - data: [ - { - id: 'uuid', - name: 'Real Estate', - slug: 'real-estate', - description: 'Real estate agents', - icon: 'home-icon', - isActive: true, - specializations: [ - { - id: 'uuid', - name: 'Residential Sales', - slug: 'real-estate-residential-sales', - }, - ], - }, - ], - }, - }, - }) - async findAll(@Query('includeInactive') includeInactive?: string) { - return this.categoriesService.findAllCategories( - includeInactive === 'true', - ); - } - - @Public() - @Get(':id') - @ApiOperation({ summary: 'Get category by ID' }) - @ApiResponse({ status: 200, description: 'Category details' }) - @ApiResponse({ status: 404, description: 'Category not found' }) - async findOne(@Param('id') id: string) { - return this.categoriesService.findCategoryById(id); - } - - @Public() - @Get('slug/:slug') - @ApiOperation({ summary: 'Get category by slug' }) - @ApiResponse({ status: 200, description: 'Category details' }) - @ApiResponse({ status: 404, description: 'Category not found' }) - async findBySlug(@Param('slug') slug: string) { - return this.categoriesService.findCategoryBySlug(slug); - } - - @Post() - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Create a new category (Admin only)' }) - @ApiResponse({ status: 201, description: 'Category created' }) - @ApiResponse({ status: 409, description: 'Category already exists' }) - async create(@Body() dto: CreateCategoryDto) { - return this.categoriesService.createCategory(dto); - } - - @Put(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Update a category (Admin only)' }) - @ApiResponse({ status: 200, description: 'Category updated' }) - @ApiResponse({ status: 404, description: 'Category not found' }) - async update(@Param('id') id: string, @Body() dto: UpdateCategoryDto) { - return this.categoriesService.updateCategory(id, dto); - } - - @Delete(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @HttpCode(HttpStatus.OK) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Delete a category (Admin only)' }) - @ApiResponse({ status: 200, description: 'Category deleted' }) - @ApiResponse({ status: 404, description: 'Category not found' }) - async remove(@Param('id') id: string) { - return this.categoriesService.deleteCategory(id); - } - - // =========================================== - // SPECIALIZATION ENDPOINTS - // =========================================== - - @Public() - @Get('specializations/all') - @ApiOperation({ summary: 'Get all specializations' }) - @ApiQuery({ - name: 'categoryId', - required: false, - description: 'Filter by category ID', - }) - @ApiResponse({ status: 200, description: 'List of specializations' }) - async findAllSpecializations(@Query('categoryId') categoryId?: string) { - return this.categoriesService.findAllSpecializations(categoryId); - } - - @Public() - @Get('specializations/:id') - @ApiOperation({ summary: 'Get specialization by ID' }) - @ApiResponse({ status: 200, description: 'Specialization details' }) - @ApiResponse({ status: 404, description: 'Specialization not found' }) - async findSpecialization(@Param('id') id: string) { - return this.categoriesService.findSpecializationById(id); - } - - @Post('specializations') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Create a new specialization (Admin only)' }) - @ApiResponse({ status: 201, description: 'Specialization created' }) - @ApiResponse({ status: 404, description: 'Category not found' }) - @ApiResponse({ status: 409, description: 'Specialization already exists' }) - async createSpecialization(@Body() dto: CreateSpecializationDto) { - return this.categoriesService.createSpecialization(dto); - } - - @Put('specializations/:id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Update a specialization (Admin only)' }) - @ApiResponse({ status: 200, description: 'Specialization updated' }) - @ApiResponse({ status: 404, description: 'Specialization not found' }) - async updateSpecialization( - @Param('id') id: string, - @Body() dto: UpdateSpecializationDto, - ) { - return this.categoriesService.updateSpecialization(id, dto); - } - - @Delete('specializations/:id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @HttpCode(HttpStatus.OK) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: 'Delete a specialization (Admin only)' }) - @ApiResponse({ status: 200, description: 'Specialization deleted' }) - @ApiResponse({ status: 404, description: 'Specialization not found' }) - async removeSpecialization(@Param('id') id: string) { - return this.categoriesService.deleteSpecialization(id); - } -} diff --git a/src/categories/categories.module.ts b/src/categories/categories.module.ts deleted file mode 100644 index 0339875..0000000 --- a/src/categories/categories.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { CategoriesController } from './categories.controller'; -import { CategoriesService } from './categories.service'; -import { PrismaModule } from '../prisma/prisma.module'; - -@Module({ - imports: [PrismaModule], - controllers: [CategoriesController], - providers: [CategoriesService], - exports: [CategoriesService], -}) -export class CategoriesModule {} diff --git a/src/categories/categories.service.ts b/src/categories/categories.service.ts deleted file mode 100644 index f58fa81..0000000 --- a/src/categories/categories.service.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { - Injectable, - NotFoundException, - ConflictException, -} from '@nestjs/common'; -import { PrismaService } from '../prisma/prisma.service'; -import { - CreateCategoryDto, - UpdateCategoryDto, - CreateSpecializationDto, - UpdateSpecializationDto, -} from './dto'; - -@Injectable() -export class CategoriesService { - constructor(private prisma: PrismaService) {} - - // =========================================== - // CATEGORY METHODS - // =========================================== - - private generateSlug(name: string): string { - return name - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, ''); - } - - async createCategory(dto: CreateCategoryDto) { - const slug = this.generateSlug(dto.name); - - // Check if category with same name or slug exists - const existing = await this.prisma.category.findFirst({ - where: { - OR: [{ name: dto.name }, { slug }], - }, - }); - - if (existing) { - throw new ConflictException('Category with this name already exists'); - } - - const category = await this.prisma.category.create({ - data: { - ...dto, - slug, - }, - include: { - specializations: true, - }, - }); - - return { - success: true, - data: category, - }; - } - - async findAllCategories(includeInactive = false) { - const categories = await this.prisma.category.findMany({ - where: includeInactive ? {} : { isActive: true }, - include: { - specializations: { - where: includeInactive ? {} : { isActive: true }, - orderBy: { sortOrder: 'asc' }, - }, - }, - orderBy: { sortOrder: 'asc' }, - }); - - return { - success: true, - data: categories, - }; - } - - async findCategoryById(id: string) { - const category = await this.prisma.category.findUnique({ - where: { id }, - include: { - specializations: { - orderBy: { sortOrder: 'asc' }, - }, - }, - }); - - if (!category) { - throw new NotFoundException('Category not found'); - } - - return { - success: true, - data: category, - }; - } - - async findCategoryBySlug(slug: string) { - const category = await this.prisma.category.findUnique({ - where: { slug }, - include: { - specializations: { - where: { isActive: true }, - orderBy: { sortOrder: 'asc' }, - }, - }, - }); - - if (!category) { - throw new NotFoundException('Category not found'); - } - - return { - success: true, - data: category, - }; - } - - async updateCategory(id: string, dto: UpdateCategoryDto) { - const category = await this.prisma.category.findUnique({ - where: { id }, - }); - - if (!category) { - throw new NotFoundException('Category not found'); - } - - // If name is being updated, generate new slug - let slug = category.slug; - if (dto.name && dto.name !== category.name) { - slug = this.generateSlug(dto.name); - - // Check if new name/slug conflicts - const existing = await this.prisma.category.findFirst({ - where: { - OR: [{ name: dto.name }, { slug }], - NOT: { id }, - }, - }); - - if (existing) { - throw new ConflictException('Category with this name already exists'); - } - } - - const updated = await this.prisma.category.update({ - where: { id }, - data: { - ...dto, - slug, - }, - include: { - specializations: true, - }, - }); - - return { - success: true, - data: updated, - }; - } - - async deleteCategory(id: string) { - const category = await this.prisma.category.findUnique({ - where: { id }, - }); - - if (!category) { - throw new NotFoundException('Category not found'); - } - - await this.prisma.category.delete({ - where: { id }, - }); - - return { - success: true, - data: { message: 'Category deleted successfully' }, - }; - } - - // =========================================== - // SPECIALIZATION METHODS - // =========================================== - - async createSpecialization(dto: CreateSpecializationDto) { - // Check if category exists - const category = await this.prisma.category.findUnique({ - where: { id: dto.categoryId }, - }); - - if (!category) { - throw new NotFoundException('Category not found'); - } - - const slug = this.generateSlug(`${category.slug}-${dto.name}`); - - // Check if specialization with same name exists in category - const existing = await this.prisma.specialization.findFirst({ - where: { - categoryId: dto.categoryId, - name: dto.name, - }, - }); - - if (existing) { - throw new ConflictException( - 'Specialization with this name already exists in this category', - ); - } - - const specialization = await this.prisma.specialization.create({ - data: { - ...dto, - slug, - }, - include: { - category: true, - }, - }); - - return { - success: true, - data: specialization, - }; - } - - async findAllSpecializations(categoryId?: string) { - const specializations = await this.prisma.specialization.findMany({ - where: categoryId ? { categoryId, isActive: true } : { isActive: true }, - include: { - category: true, - }, - orderBy: { sortOrder: 'asc' }, - }); - - return { - success: true, - data: specializations, - }; - } - - async findSpecializationById(id: string) { - const specialization = await this.prisma.specialization.findUnique({ - where: { id }, - include: { - category: true, - }, - }); - - if (!specialization) { - throw new NotFoundException('Specialization not found'); - } - - return { - success: true, - data: specialization, - }; - } - - async updateSpecialization(id: string, dto: UpdateSpecializationDto) { - const specialization = await this.prisma.specialization.findUnique({ - where: { id }, - include: { category: true }, - }); - - if (!specialization) { - throw new NotFoundException('Specialization not found'); - } - - // If name is being updated, generate new slug - let slug = specialization.slug; - if (dto.name && dto.name !== specialization.name) { - slug = this.generateSlug( - `${specialization.category.slug}-${dto.name}`, - ); - - // Check if new name conflicts - const existing = await this.prisma.specialization.findFirst({ - where: { - categoryId: specialization.categoryId, - name: dto.name, - NOT: { id }, - }, - }); - - if (existing) { - throw new ConflictException( - 'Specialization with this name already exists in this category', - ); - } - } - - const updated = await this.prisma.specialization.update({ - where: { id }, - data: { - ...dto, - slug, - }, - include: { - category: true, - }, - }); - - return { - success: true, - data: updated, - }; - } - - async deleteSpecialization(id: string) { - const specialization = await this.prisma.specialization.findUnique({ - where: { id }, - }); - - if (!specialization) { - throw new NotFoundException('Specialization not found'); - } - - await this.prisma.specialization.delete({ - where: { id }, - }); - - return { - success: true, - data: { message: 'Specialization deleted successfully' }, - }; - } -} diff --git a/src/categories/dto/create-category.dto.ts b/src/categories/dto/create-category.dto.ts deleted file mode 100644 index 2b27117..0000000 --- a/src/categories/dto/create-category.dto.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { - IsString, - IsOptional, - IsBoolean, - IsInt, - MinLength, - MaxLength, -} from 'class-validator'; - -export class CreateCategoryDto { - @ApiProperty({ example: 'Real Estate' }) - @IsString() - @MinLength(2) - @MaxLength(100) - name: string; - - @ApiPropertyOptional({ example: 'Agents specializing in real estate' }) - @IsOptional() - @IsString() - @MaxLength(500) - description?: string; - - @ApiPropertyOptional({ example: 'home-icon' }) - @IsOptional() - @IsString() - icon?: string; - - @ApiPropertyOptional({ example: 'https://example.com/image.jpg' }) - @IsOptional() - @IsString() - image?: string; - - @ApiPropertyOptional({ example: true, default: true }) - @IsOptional() - @IsBoolean() - isActive?: boolean; - - @ApiPropertyOptional({ example: 0 }) - @IsOptional() - @IsInt() - sortOrder?: number; -} diff --git a/src/categories/dto/create-specialization.dto.ts b/src/categories/dto/create-specialization.dto.ts deleted file mode 100644 index 1cc0a97..0000000 --- a/src/categories/dto/create-specialization.dto.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { - IsString, - IsOptional, - IsBoolean, - IsInt, - IsUUID, - MinLength, - MaxLength, -} from 'class-validator'; - -export class CreateSpecializationDto { - @ApiProperty({ example: 'uuid-of-category' }) - @IsUUID() - categoryId: string; - - @ApiProperty({ example: 'Residential Sales' }) - @IsString() - @MinLength(2) - @MaxLength(100) - name: string; - - @ApiPropertyOptional({ example: 'Specializing in residential property sales' }) - @IsOptional() - @IsString() - @MaxLength(500) - description?: string; - - @ApiPropertyOptional({ example: true, default: true }) - @IsOptional() - @IsBoolean() - isActive?: boolean; - - @ApiPropertyOptional({ example: 0 }) - @IsOptional() - @IsInt() - sortOrder?: number; -} diff --git a/src/categories/dto/index.ts b/src/categories/dto/index.ts deleted file mode 100644 index 0ab907d..0000000 --- a/src/categories/dto/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './create-category.dto'; -export * from './update-category.dto'; -export * from './create-specialization.dto'; -export * from './update-specialization.dto'; diff --git a/src/categories/dto/update-category.dto.ts b/src/categories/dto/update-category.dto.ts deleted file mode 100644 index d713b9b..0000000 --- a/src/categories/dto/update-category.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreateCategoryDto } from './create-category.dto'; - -export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {} diff --git a/src/categories/dto/update-specialization.dto.ts b/src/categories/dto/update-specialization.dto.ts deleted file mode 100644 index 65f120f..0000000 --- a/src/categories/dto/update-specialization.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PartialType, OmitType } from '@nestjs/swagger'; -import { CreateSpecializationDto } from './create-specialization.dto'; - -export class UpdateSpecializationDto extends PartialType( - OmitType(CreateSpecializationDto, ['categoryId'] as const), -) {} diff --git a/src/categories/index.ts b/src/categories/index.ts deleted file mode 100644 index 6d82bce..0000000 --- a/src/categories/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './categories.module'; -export * from './categories.service'; -export * from './categories.controller'; -export * from './dto'; diff --git a/src/verification/dto/create-verification-request.dto.ts b/src/verification/dto/create-verification-request.dto.ts deleted file mode 100644 index 19a9586..0000000 --- a/src/verification/dto/create-verification-request.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsString, IsEnum, IsOptional, IsUrl, MaxLength } from 'class-validator'; -import { VerificationDocType } from '@prisma/client'; - -export class CreateVerificationRequestDto { - @ApiProperty({ - example: 'LICENSE', - enum: VerificationDocType, - description: 'Type of verification document', - }) - @IsEnum(VerificationDocType) - documentType: VerificationDocType; - - @ApiProperty({ - example: 'https://s3.amazonaws.com/bucket/license.pdf', - description: 'URL of the uploaded document', - }) - @IsString() - @IsUrl() - documentUrl: string; - - @ApiPropertyOptional({ - example: 'My real estate license from California', - description: 'Additional notes about the verification request', - }) - @IsOptional() - @IsString() - @MaxLength(500) - notes?: string; -} diff --git a/src/verification/dto/index.ts b/src/verification/dto/index.ts deleted file mode 100644 index 014cdd2..0000000 --- a/src/verification/dto/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './create-verification-request.dto'; -export * from './review-verification-request.dto'; -export * from './search-verification-requests.dto'; diff --git a/src/verification/dto/review-verification-request.dto.ts b/src/verification/dto/review-verification-request.dto.ts deleted file mode 100644 index f441757..0000000 --- a/src/verification/dto/review-verification-request.dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsString, IsEnum, IsOptional, MaxLength } from 'class-validator'; -import { VerificationStatus } from '@prisma/client'; - -export class ReviewVerificationRequestDto { - @ApiProperty({ - example: 'APPROVED', - enum: ['APPROVED', 'REJECTED'], - description: 'New status for the verification request', - }) - @IsEnum(['APPROVED', 'REJECTED'] as const) - status: 'APPROVED' | 'REJECTED'; - - @ApiPropertyOptional({ - example: 'License verified successfully', - description: 'Admin notes about the review decision', - }) - @IsOptional() - @IsString() - @MaxLength(500) - adminNotes?: string; - - @ApiPropertyOptional({ - example: 'Document is blurry, please resubmit', - description: 'Rejection reason (required if status is REJECTED)', - }) - @IsOptional() - @IsString() - @MaxLength(500) - rejectionReason?: string; -} diff --git a/src/verification/dto/search-verification-requests.dto.ts b/src/verification/dto/search-verification-requests.dto.ts deleted file mode 100644 index a15c5c2..0000000 --- a/src/verification/dto/search-verification-requests.dto.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ApiPropertyOptional } from '@nestjs/swagger'; -import { IsOptional, IsEnum, IsInt, Min, Max, IsString } from 'class-validator'; -import { Type, Transform } from 'class-transformer'; -import { VerificationStatus, VerificationDocType } from '@prisma/client'; - -export class SearchVerificationRequestsDto { - @ApiPropertyOptional({ - example: 'PENDING', - enum: VerificationStatus, - description: 'Filter by status', - }) - @IsOptional() - @IsEnum(VerificationStatus) - status?: VerificationStatus; - - @ApiPropertyOptional({ - example: 'LICENSE', - enum: VerificationDocType, - description: 'Filter by document type', - }) - @IsOptional() - @IsEnum(VerificationDocType) - documentType?: VerificationDocType; - - @ApiPropertyOptional({ example: 'uuid-of-agent-profile' }) - @IsOptional() - @IsString() - agentProfileId?: string; - - @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', 'updatedAt', 'status'], - }) - @IsOptional() - @IsString() - sortBy?: string = 'createdAt'; - - @ApiPropertyOptional({ example: 'desc', enum: ['asc', 'desc'] }) - @IsOptional() - @IsString() - sortOrder?: 'asc' | 'desc' = 'desc'; -} diff --git a/src/verification/index.ts b/src/verification/index.ts deleted file mode 100644 index 23a17ec..0000000 --- a/src/verification/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './verification.module'; -export * from './verification.service'; -export * from './verification.controller'; -export * from './dto'; diff --git a/src/verification/verification.controller.ts b/src/verification/verification.controller.ts deleted file mode 100644 index ac6e507..0000000 --- a/src/verification/verification.controller.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { - Controller, - Get, - Post, - Put, - Delete, - Body, - Param, - Query, - UseGuards, - ParseUUIDPipe, -} from '@nestjs/common'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiBearerAuth, -} from '@nestjs/swagger'; -import { VerificationService } from './verification.service'; -import { - CreateVerificationRequestDto, - ReviewVerificationRequestDto, - SearchVerificationRequestsDto, -} 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('verification') -@Controller('verification') -export class VerificationController { - constructor(private readonly verificationService: VerificationService) {} - - // Agent-only endpoints - requires AGENT role - - @Post('request') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.AGENT) - @ApiBearerAuth() - @ApiOperation({ summary: 'Submit a verification request' }) - @ApiResponse({ status: 201, description: 'Request submitted successfully' }) - @ApiResponse({ status: 400, description: 'Pending request already exists' }) - @ApiResponse({ status: 404, description: 'Agent profile not found' }) - async createRequest( - @CurrentUser('id') userId: string, - @Body() dto: CreateVerificationRequestDto, - ) { - return this.verificationService.createRequest(userId, dto); - } - - @Get('my-requests') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.AGENT) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get my verification requests' }) - @ApiResponse({ status: 200, description: 'Requests retrieved successfully' }) - async getMyRequests(@CurrentUser('id') userId: string) { - return this.verificationService.getMyRequests(userId); - } - - @Get('request/:id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.AGENT) - @ApiBearerAuth() - @ApiOperation({ summary: 'Get verification request by ID' }) - @ApiResponse({ status: 200, description: 'Request retrieved successfully' }) - @ApiResponse({ status: 404, description: 'Request not found' }) - @ApiResponse({ status: 403, description: 'Access denied' }) - async getRequestById( - @Param('id', ParseUUIDPipe) id: string, - @CurrentUser('id') userId: string, - ) { - return this.verificationService.getRequestById(id, userId); - } - - @Delete('request/:id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.AGENT) - @ApiBearerAuth() - @ApiOperation({ summary: 'Cancel a pending verification request' }) - @ApiResponse({ status: 200, description: 'Request cancelled successfully' }) - @ApiResponse({ status: 400, description: 'Request is not pending' }) - @ApiResponse({ status: 404, description: 'Request not found' }) - async cancelRequest( - @Param('id', ParseUUIDPipe) id: string, - @CurrentUser('id') userId: string, - ) { - return this.verificationService.cancelRequest(id, userId); - } - - // Admin endpoints - - @Get('admin/list') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Admin: List all verification requests' }) - @ApiResponse({ status: 200, description: 'Requests retrieved successfully' }) - async searchRequests(@Query() dto: SearchVerificationRequestsDto) { - return this.verificationService.searchRequests(dto); - } - - @Get('admin/pending') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Admin: Get all pending verification requests' }) - @ApiResponse({ - status: 200, - description: 'Pending requests retrieved successfully', - }) - async getPendingRequests() { - return this.verificationService.getPendingRequests(); - } - - @Get('admin/stats') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Admin: Get verification statistics' }) - @ApiResponse({ status: 200, description: 'Stats retrieved successfully' }) - async getStats() { - return this.verificationService.getStats(); - } - - @Get('admin/:id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Admin: Get verification request by ID' }) - @ApiResponse({ status: 200, description: 'Request retrieved successfully' }) - @ApiResponse({ status: 404, description: 'Request not found' }) - async adminGetRequestById(@Param('id', ParseUUIDPipe) id: string) { - return this.verificationService.getRequestById(id); - } - - @Put('admin/:id/review') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(UserRole.ADMIN) - @ApiBearerAuth() - @ApiOperation({ summary: 'Admin: Review a verification request' }) - @ApiResponse({ status: 200, description: 'Request reviewed successfully' }) - @ApiResponse({ status: 400, description: 'Request already reviewed' }) - @ApiResponse({ status: 404, description: 'Request not found' }) - async reviewRequest( - @Param('id', ParseUUIDPipe) id: string, - @CurrentUser('id') adminId: string, - @Body() dto: ReviewVerificationRequestDto, - ) { - return this.verificationService.reviewRequest(id, adminId, dto); - } -} diff --git a/src/verification/verification.module.ts b/src/verification/verification.module.ts deleted file mode 100644 index 8cbae9a..0000000 --- a/src/verification/verification.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { VerificationController } from './verification.controller'; -import { VerificationService } from './verification.service'; -import { PrismaModule } from '../prisma/prisma.module'; - -@Module({ - imports: [PrismaModule], - controllers: [VerificationController], - providers: [VerificationService], - exports: [VerificationService], -}) -export class VerificationModule {} diff --git a/src/verification/verification.service.ts b/src/verification/verification.service.ts deleted file mode 100644 index 23b647f..0000000 --- a/src/verification/verification.service.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { - Injectable, - NotFoundException, - ForbiddenException, - BadRequestException, -} from '@nestjs/common'; -import { PrismaService } from '../prisma/prisma.service'; -import { - CreateVerificationRequestDto, - ReviewVerificationRequestDto, - SearchVerificationRequestsDto, -} from './dto'; -import { Prisma, VerificationStatus } from '@prisma/client'; - -@Injectable() -export class VerificationService { - constructor(private prisma: PrismaService) {} - - async createRequest(userId: string, dto: CreateVerificationRequestDto) { - // Get the agent profile for this user - const agentProfile = await this.prisma.agentProfile.findUnique({ - where: { userId }, - }); - - if (!agentProfile) { - throw new NotFoundException( - 'Agent profile not found. Create an agent profile first.', - ); - } - - // Check if there's already a pending request for this document type - const existingRequest = await this.prisma.verificationRequest.findFirst({ - where: { - agentProfileId: agentProfile.id, - documentType: dto.documentType, - status: VerificationStatus.PENDING, - }, - }); - - if (existingRequest) { - throw new BadRequestException( - `A pending verification request for ${dto.documentType} already exists`, - ); - } - - const request = await this.prisma.verificationRequest.create({ - data: { - agentProfileId: agentProfile.id, - documentType: dto.documentType, - documentUrl: dto.documentUrl, - notes: dto.notes, - }, - include: { - agentProfile: { - include: { - user: { - select: { - id: true, - email: true, - }, - }, - }, - }, - }, - }); - - return request; - } - - async getMyRequests(userId: string) { - const agentProfile = await this.prisma.agentProfile.findUnique({ - where: { userId }, - }); - - if (!agentProfile) { - throw new NotFoundException('Agent profile not found'); - } - - const requests = await this.prisma.verificationRequest.findMany({ - where: { agentProfileId: agentProfile.id }, - orderBy: { createdAt: 'desc' }, - }); - - return requests; - } - - async getRequestById(id: string, userId?: string) { - const request = await this.prisma.verificationRequest.findUnique({ - where: { id }, - include: { - agentProfile: { - include: { - user: { - select: { - id: true, - email: true, - avatar: true, - }, - }, - }, - }, - }, - }); - - if (!request) { - throw new NotFoundException('Verification request not found'); - } - - // If userId is provided, check ownership (for non-admin access) - if (userId) { - const agentProfile = await this.prisma.agentProfile.findUnique({ - where: { userId }, - }); - - if (!agentProfile || agentProfile.id !== request.agentProfileId) { - throw new ForbiddenException( - 'You do not have access to this verification request', - ); - } - } - - return request; - } - - async cancelRequest(id: string, userId: string) { - const request = await this.getRequestById(id, userId); - - if (request.status !== VerificationStatus.PENDING) { - throw new BadRequestException( - 'Only pending requests can be cancelled', - ); - } - - await this.prisma.verificationRequest.delete({ - where: { id }, - }); - - return { message: 'Verification request cancelled successfully' }; - } - - // Admin methods - - async searchRequests(dto: SearchVerificationRequestsDto) { - const { - status, - documentType, - agentProfileId, - page = 1, - limit = 10, - sortBy = 'createdAt', - sortOrder = 'desc', - } = dto; - - const skip = (page - 1) * limit; - - const where: Prisma.VerificationRequestWhereInput = {}; - - if (status) { - where.status = status; - } - - if (documentType) { - where.documentType = documentType; - } - - if (agentProfileId) { - where.agentProfileId = agentProfileId; - } - - const orderBy: Prisma.VerificationRequestOrderByWithRelationInput = {}; - if (sortBy === 'status') { - orderBy.status = sortOrder; - } else if (sortBy === 'updatedAt') { - orderBy.updatedAt = sortOrder; - } else { - orderBy.createdAt = sortOrder; - } - - const [requests, total] = await Promise.all([ - this.prisma.verificationRequest.findMany({ - where, - skip, - take: limit, - orderBy, - include: { - agentProfile: { - include: { - user: { - select: { - id: true, - email: true, - avatar: true, - }, - }, - }, - }, - }, - }), - this.prisma.verificationRequest.count({ where }), - ]); - - return { - data: requests, - meta: { - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }, - }; - } - - async getPendingRequests() { - const requests = await this.prisma.verificationRequest.findMany({ - where: { status: VerificationStatus.PENDING }, - orderBy: { createdAt: 'asc' }, - include: { - agentProfile: { - include: { - user: { - select: { - id: true, - email: true, - avatar: true, - }, - }, - }, - }, - }, - }); - - return requests; - } - - async reviewRequest( - id: string, - adminId: string, - dto: ReviewVerificationRequestDto, - ) { - const request = await this.prisma.verificationRequest.findUnique({ - where: { id }, - include: { - agentProfile: true, - }, - }); - - if (!request) { - throw new NotFoundException('Verification request not found'); - } - - if (request.status !== VerificationStatus.PENDING) { - throw new BadRequestException( - 'This verification request has already been reviewed', - ); - } - - if (dto.status === 'REJECTED' && !dto.rejectionReason) { - throw new BadRequestException( - 'Rejection reason is required when rejecting a request', - ); - } - - const updatedRequest = await this.prisma.verificationRequest.update({ - where: { id }, - data: { - status: dto.status as VerificationStatus, - reviewedBy: adminId, - reviewedAt: new Date(), - adminNotes: dto.adminNotes, - rejectionReason: dto.rejectionReason, - }, - include: { - agentProfile: { - include: { - user: { - select: { - id: true, - email: true, - }, - }, - }, - }, - }, - }); - - // If approved, check if all required documents are verified - // and auto-verify the agent profile - if (dto.status === 'APPROVED') { - await this.checkAndUpdateAgentVerification(request.agentProfileId); - } - - return updatedRequest; - } - - private async checkAndUpdateAgentVerification(agentProfileId: string) { - // Get all verification requests for this agent - const requests = await this.prisma.verificationRequest.findMany({ - where: { agentProfileId }, - }); - - // Check if at least one LICENSE document is approved - const hasApprovedLicense = requests.some( - (r) => - r.documentType === 'LICENSE' && r.status === VerificationStatus.APPROVED, - ); - - if (hasApprovedLicense) { - await this.prisma.agentProfile.update({ - where: { id: agentProfileId }, - data: { isVerified: true }, - }); - } - } - - async getStats() { - const [pending, approved, rejected, total] = await Promise.all([ - this.prisma.verificationRequest.count({ - where: { status: VerificationStatus.PENDING }, - }), - this.prisma.verificationRequest.count({ - where: { status: VerificationStatus.APPROVED }, - }), - this.prisma.verificationRequest.count({ - where: { status: VerificationStatus.REJECTED }, - }), - this.prisma.verificationRequest.count(), - ]); - - return { - pending, - approved, - rejected, - total, - }; - } -}