feat: Implement agent verification system with a new status enum, admin update functionality, and document retrieval.
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user