diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0f887fa..5622ee9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -49,6 +49,13 @@ enum FieldType { REPEATER // Repeatable group of fields (e.g., certification + years) } +enum VerificationStatus { + NONE // Agent hasn't uploaded documents + PENDING_REVIEW // Documents uploaded, awaiting admin review + APPROVED // Admin approved verification + REJECTED // Admin rejected verification +} + // =========================================== // USER - Authentication Only @@ -156,8 +163,11 @@ model AgentProfile { instagramUrl String? // Verification Status - isVerified Boolean @default(false) - verifiedAt DateTime? + isVerified Boolean @default(false) + verificationStatus VerificationStatus @default(NONE) + verificationNote String? // Admin note (rejection reason) + verifiedAt DateTime? + verifiedBy String? // Admin user ID who verified // Profile Status isProfileComplete Boolean @default(false) @@ -183,6 +193,7 @@ model AgentProfile { @@index([slug]) @@index([city, state]) @@index([isVerified]) + @@index([verificationStatus]) @@index([isPublic]) @@index([isFeatured]) @@map("agent_profiles") diff --git a/src/agents/agents.controller.ts b/src/agents/agents.controller.ts index 8e8ef79..883258f 100644 --- a/src/agents/agents.controller.ts +++ b/src/agents/agents.controller.ts @@ -28,7 +28,7 @@ import { RolesGuard } from '../auth/guards/roles.guard'; import { Roles } from '../auth/decorators/roles.decorator'; import { Public } from '../auth/decorators/public.decorator'; import { CurrentUser } from '../auth/decorators/current-user.decorator'; -import { UserRole } from '@prisma/client'; +import { UserRole, VerificationStatus } from '@prisma/client'; @ApiTags('agents') @Controller('agents') @@ -193,14 +193,19 @@ export class AgentsController { @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) @ApiBearerAuth() - @ApiOperation({ summary: 'Admin: Verify/unverify agent' }) + @ApiOperation({ summary: 'Admin: Update agent verification status' }) @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, + @Body() data: { status: VerificationStatus; note?: string }, + @CurrentUser() admin: { id: string }, ) { - return this.agentsService.adminVerifyAgent(id, isVerified); + return this.agentsService.adminVerifyAgent(id, { + status: data.status, + note: data.note, + adminId: admin.id, + }); } @Put('admin/:id/featured') diff --git a/src/agents/agents.service.ts b/src/agents/agents.service.ts index 7b8ceb1..ddce2b5 100644 --- a/src/agents/agents.service.ts +++ b/src/agents/agents.service.ts @@ -10,7 +10,7 @@ import { SearchAgentsDto, SaveFieldValuesDto, } from './dto'; -import { Prisma, Prisma as PrismaTypes } from '@prisma/client'; +import { Prisma, Prisma as PrismaTypes, VerificationStatus } from '@prisma/client'; function generateSlug(firstName: string, lastName: string): string { const base = `${firstName}-${lastName}` @@ -312,7 +312,14 @@ export class AgentsService { return { message: 'Agent profile deleted successfully' }; } - async adminVerifyAgent(profileId: string, isVerified: boolean) { + async adminVerifyAgent( + profileId: string, + data: { + status: VerificationStatus; + note?: string; + adminId: string; + }, + ) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, }); @@ -321,9 +328,24 @@ export class AgentsService { throw new NotFoundException('Agent profile not found'); } + const updateData: Prisma.AgentProfileUpdateInput = { + verificationStatus: data.status, + verificationNote: data.note || null, + }; + + if (data.status === VerificationStatus.APPROVED) { + updateData.isVerified = true; + updateData.verifiedAt = new Date(); + updateData.verifiedBy = data.adminId; + } else if (data.status === VerificationStatus.REJECTED) { + updateData.isVerified = false; + updateData.verifiedAt = null; + updateData.verifiedBy = null; + } + const updatedProfile = await this.prisma.agentProfile.update({ where: { id: profileId }, - data: { isVerified }, + data: updateData, include: { user: { select: { @@ -338,6 +360,26 @@ export class AgentsService { return updatedProfile; } + async getVerificationDocuments(agentProfileId: string) { + // Find the upload-documents section field + const field = await this.prisma.profileField.findFirst({ + where: { + section: { slug: 'upload-documents' }, + slug: 'documents', + }, + }); + + if (!field) return []; + + const fieldValue = await this.prisma.agentProfileFieldValue.findUnique({ + where: { + agentProfileId_fieldId: { agentProfileId, fieldId: field.id }, + }, + }); + + return fieldValue?.jsonValue || []; + } + async adminSetFeatured(profileId: string, isFeatured: boolean) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index a7de9d2..6294e43 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -17,8 +17,8 @@ import { } from '@nestjs/swagger'; import { UsersService } from './users.service'; import { JwtAuthGuard, RolesGuard } from '../auth/guards'; -import { Roles } from '../auth/decorators'; -import { UserRole, UserStatus } from '@prisma/client'; +import { Roles, CurrentUser } from '../auth/decorators'; +import { UserRole, UserStatus, VerificationStatus } from '@prisma/client'; @ApiTags('Users') @Controller('users') @@ -82,6 +82,19 @@ export class UsersController { return this.usersService.findAll(pageNum, limitNum, role); } + @Get(':id/verification-documents') + @Roles(UserRole.ADMIN) + @ApiOperation({ summary: 'Get verification documents for an agent user (Admin only)' }) + @ApiResponse({ + status: 200, + description: 'Verification documents retrieved', + }) + @ApiResponse({ status: 400, description: 'User is not an agent' }) + @ApiResponse({ status: 404, description: 'User not found' }) + async getVerificationDocuments(@Param('id') id: string) { + return this.usersService.getVerificationDocuments(id); + } + @Get(':id') @Roles(UserRole.ADMIN) @ApiOperation({ summary: 'Get user by ID (Admin only)' }) @@ -132,4 +145,21 @@ export class UsersController { ) { return this.usersService.updateAgentType(id, agentTypeId); } + + @Patch(':id/verification') + @Roles(UserRole.ADMIN) + @ApiOperation({ summary: 'Update verification status for an agent user (Admin only)' }) + @ApiResponse({ + status: 200, + description: 'Verification status updated', + }) + @ApiResponse({ status: 400, description: 'User is not an agent or invalid status' }) + @ApiResponse({ status: 404, description: 'User not found' }) + async updateVerification( + @Param('id') id: string, + @Body() data: { status: VerificationStatus; note?: string }, + @CurrentUser() admin: { id: string }, + ) { + return this.usersService.updateVerification(id, data, admin.id); + } } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 635f277..c9ae66d 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,5 +1,5 @@ import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; -import { UserStatus, UserRole, Prisma } from '@prisma/client'; +import { UserStatus, UserRole, Prisma, VerificationStatus } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; type UserWithProfiles = Prisma.UserGetPayload<{ @@ -123,6 +123,12 @@ export class UsersService { profileCompleteness: user.agentProfile.profileCompleteness, agentTypeId: user.agentProfile.agentTypeId, agentType: user.agentProfile.agentType, + // Verification fields + isVerified: user.agentProfile.isVerified, + verificationStatus: user.agentProfile.verificationStatus, + verificationNote: user.agentProfile.verificationNote, + verifiedAt: user.agentProfile.verifiedAt, + verifiedBy: user.agentProfile.verifiedBy, }, }; } @@ -193,4 +199,106 @@ export class UsersService { message: 'Agent type updated successfully', }; } + + async getVerificationDocuments(userId: string) { + // Get user to verify they are an agent + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + include: { agentProfile: true }, + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + if (user.role !== UserRole.AGENT) { + throw new BadRequestException('User is not an agent'); + } + + if (!user.agentProfile) { + throw new NotFoundException('Agent profile not found'); + } + + // Find the upload-documents section field + const field = await this.prisma.profileField.findFirst({ + where: { + section: { slug: 'upload-documents' }, + slug: 'documents', + }, + }); + + if (!field) return []; + + const fieldValue = await this.prisma.agentProfileFieldValue.findUnique({ + where: { + agentProfileId_fieldId: { + agentProfileId: user.agentProfile.id, + fieldId: field.id, + }, + }, + }); + + return fieldValue?.jsonValue || []; + } + + async updateVerification( + userId: string, + data: { + status: VerificationStatus; + note?: string; + }, + adminId: string, + ) { + // Get user to verify they are an agent + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + include: { agentProfile: true }, + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + if (user.role !== UserRole.AGENT) { + throw new BadRequestException('User is not an agent'); + } + + if (!user.agentProfile) { + throw new NotFoundException('Agent profile not found'); + } + + const updateData: Prisma.AgentProfileUpdateInput = { + verificationStatus: data.status, + verificationNote: data.note || null, + }; + + if (data.status === VerificationStatus.APPROVED) { + updateData.isVerified = true; + updateData.verifiedAt = new Date(); + updateData.verifiedBy = adminId; + } else if (data.status === VerificationStatus.REJECTED) { + updateData.isVerified = false; + updateData.verifiedAt = null; + updateData.verifiedBy = null; + } + + const updatedProfile = await this.prisma.agentProfile.update({ + where: { id: user.agentProfile.id }, + data: updateData, + include: { + agentType: true, + }, + }); + + return { + id: user.id, + agentProfileId: updatedProfile.id, + isVerified: updatedProfile.isVerified, + verificationStatus: updatedProfile.verificationStatus, + verificationNote: updatedProfile.verificationNote, + verifiedAt: updatedProfile.verifiedAt, + verifiedBy: updatedProfile.verifiedBy, + message: `Verification ${data.status.toLowerCase()} successfully`, + }; + } }