From e5e18886153de69a408432fabd6943ec84d0bb6d Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Tue, 31 Mar 2026 22:39:10 +0530 Subject: [PATCH] ifx --- prisma/schema.prisma | 1 + src/users/users.controller.ts | 16 ++++ src/users/users.service.ts | 160 +++++++++++++++++++++++++++++++++- 3 files changed, 176 insertions(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 182da03..5ce7733 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -783,6 +783,7 @@ model VerificationHistory { status VerificationStatus note String? adminId String? + submittedData Json? // Snapshot of agent profile + documents at submission time createdAt DateTime @default(now()) agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade) diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index c3c4287..31897b1 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, + Post, Put, Delete, Param, @@ -9,6 +10,8 @@ import { Query, UseGuards, NotFoundException, + HttpCode, + HttpStatus, } from '@nestjs/common'; import { ApiTags, @@ -190,6 +193,19 @@ export class UsersController { return this.usersService.deleteAccount(user.id); } + // ========================================== + // AGENT VERIFICATION SUBMISSION + // ========================================== + + @Post('me/verification/submit') + @Roles(UserRole.AGENT) + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Agent: Submit or resubmit profile for verification' }) + @ApiResponse({ status: 200, description: 'Verification submitted' }) + async submitForVerification(@CurrentUser('id') userId: string) { + return this.usersService.submitForVerification(userId); + } + // ============================================= // ADMIN ENDPOINTS // ============================================= diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 0ca3edb..f2a3d89 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -425,13 +425,15 @@ export class UsersService { }, }); - // Save verification history + // Save verification history with profile snapshot + const profileSnapshot = await this.captureProfileSnapshot(user.agentProfile.id); await this.prisma.verificationHistory.create({ data: { agentProfileId: user.agentProfile.id, status: data.status, note: data.note || null, adminId, + submittedData: profileSnapshot ?? undefined, }, }); @@ -466,6 +468,162 @@ export class UsersService { }); } + // Submit or resubmit for verification (agent-facing) + async submitForVerification(userId: string) { + 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 currentStatus = user.agentProfile.verificationStatus; + if (currentStatus === VerificationStatus.PENDING_REVIEW) { + throw new BadRequestException('Verification is already pending review'); + } + if (currentStatus === VerificationStatus.APPROVED) { + throw new BadRequestException('Profile is already verified'); + } + + // Validate required fields are filled + const missingFields = await this.getRequiredFieldsMissing(user.agentProfile.id, user.agentProfile.agentTypeId); + if (missingFields.length > 0) { + throw new BadRequestException({ + message: 'Please fill in all required fields before submitting', + missingFields, + }); + } + + // Capture snapshot of current profile data + documents + const profileSnapshot = await this.captureProfileSnapshot(user.agentProfile.id); + + // Update status to PENDING_REVIEW + await this.prisma.agentProfile.update({ + where: { id: user.agentProfile.id }, + data: { + verificationStatus: VerificationStatus.PENDING_REVIEW, + verificationNote: null, + }, + }); + + // Create history entry with submitted data snapshot + await this.prisma.verificationHistory.create({ + data: { + agentProfileId: user.agentProfile.id, + status: VerificationStatus.PENDING_REVIEW, + note: currentStatus === VerificationStatus.REJECTED ? 'Resubmitted after rejection' : 'Initial submission', + submittedData: profileSnapshot ?? undefined, + }, + }); + + return { message: 'Verification submitted successfully', status: 'PENDING_REVIEW' }; + } + + // Capture a snapshot of agent's profile data and documents for history + private async captureProfileSnapshot(agentProfileId: string) { + const profile = await this.prisma.agentProfile.findUnique({ + where: { id: agentProfileId }, + include: { + user: { select: { email: true } }, + agentType: { select: { id: true, name: true } }, + fieldValues: { + include: { + field: { + select: { slug: true, name: true, fieldType: true, section: { select: { slug: true, name: true } } }, + }, + }, + }, + }, + }); + + if (!profile) return null; + + // Organize field values by section + const sections: Record> = {}; + for (const fv of profile.fieldValues) { + const sectionSlug = fv.field.section.slug; + if (!sections[sectionSlug]) { + sections[sectionSlug] = { _name: fv.field.section.name }; + } + const value = fv.textValue ?? fv.numberValue ?? fv.booleanValue ?? fv.jsonValue ?? fv.dateValue; + sections[sectionSlug][fv.field.slug] = { + name: fv.field.name, + type: fv.field.fieldType, + value, + }; + } + + return { + firstName: profile.firstName, + lastName: profile.lastName, + email: profile.user?.email, + phone: profile.phone, + avatar: profile.avatar, + bio: profile.bio, + agentType: profile.agentType?.name, + sections, + capturedAt: new Date().toISOString(), + }; + } + + // Check which required fields are missing for an agent + private async getRequiredFieldsMissing(agentProfileId: string, agentTypeId: string | null) { + // Get all sections for this agent type (or global sections) + const sectionFilter: any = { isActive: true }; + if (agentTypeId) { + sectionFilter.OR = [ + { isGlobal: true }, + { agentTypeSections: { some: { agentTypeId } } }, + ]; + } else { + sectionFilter.isGlobal = true; + } + + // Get all required fields from applicable sections + const requiredFields = await this.prisma.profileField.findMany({ + where: { + isRequired: true, + isActive: true, + section: sectionFilter, + }, + select: { id: true, name: true, slug: true, section: { select: { name: true, slug: true } } }, + }); + + if (requiredFields.length === 0) return []; + + // Get agent's filled field values + const filledValues = await this.prisma.agentProfileFieldValue.findMany({ + where: { + agentProfileId, + fieldId: { in: requiredFields.map((f) => f.id) }, + }, + select: { fieldId: true, textValue: true, numberValue: true, booleanValue: true, jsonValue: true, dateValue: true }, + }); + + const filledMap = new Map(filledValues.map((fv) => [fv.fieldId, fv])); + + // Find missing or empty required fields + const missing: { fieldName: string; sectionName: string }[] = []; + for (const field of requiredFields) { + const value = filledMap.get(field.id); + if (!value) { + missing.push({ fieldName: field.name, sectionName: field.section.name }); + continue; + } + // Check if value is actually empty + const hasValue = value.textValue || value.numberValue !== null || value.booleanValue !== null || + (value.jsonValue && (Array.isArray(value.jsonValue) ? (value.jsonValue as any[]).length > 0 : true)) || + value.dateValue; + if (!hasValue) { + missing.push({ fieldName: field.name, sectionName: field.section.name }); + } + } + + return missing; + } + // ============================================= // NOTIFICATION PREFERENCES // =============================================