ifx
This commit is contained in:
@@ -783,6 +783,7 @@ model VerificationHistory {
|
|||||||
status VerificationStatus
|
status VerificationStatus
|
||||||
note String?
|
note String?
|
||||||
adminId String?
|
adminId String?
|
||||||
|
submittedData Json? // Snapshot of agent profile + documents at submission time
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade)
|
agentProfile AgentProfile @relation(fields: [agentProfileId], references: [id], onDelete: Cascade)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Delete,
|
Delete,
|
||||||
Param,
|
Param,
|
||||||
@@ -9,6 +10,8 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
@@ -190,6 +193,19 @@ export class UsersController {
|
|||||||
return this.usersService.deleteAccount(user.id);
|
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
|
// 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({
|
await this.prisma.verificationHistory.create({
|
||||||
data: {
|
data: {
|
||||||
agentProfileId: user.agentProfile.id,
|
agentProfileId: user.agentProfile.id,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
note: data.note || null,
|
note: data.note || null,
|
||||||
adminId,
|
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
|
// NOTIFICATION PREFERENCES
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|||||||
Reference in New Issue
Block a user