ifx
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
// =============================================
|
||||
|
||||
@@ -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<string, Record<string, any>> = {};
|
||||
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
|
||||
// =============================================
|
||||
|
||||
Reference in New Issue
Block a user