2026-03-20 17:05:40 +05:30
|
|
|
import { Injectable, BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
2026-01-28 13:48:14 +05:30
|
|
|
import { UserStatus, UserRole, Prisma, VerificationStatus } from '@prisma/client';
|
2025-12-20 19:22:30 +05:30
|
|
|
import { PrismaService } from '../prisma/prisma.service';
|
2026-03-21 08:55:57 +05:30
|
|
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
2026-03-20 17:05:40 +05:30
|
|
|
import * as argon2 from 'argon2';
|
2025-12-20 19:22:30 +05:30
|
|
|
|
2025-12-22 13:26:39 +05:30
|
|
|
type UserWithProfiles = Prisma.UserGetPayload<{
|
2026-01-26 23:12:22 +05:30
|
|
|
include: { userProfile: true; agentProfile: { include: { agentType: true } } };
|
2025-12-20 19:22:30 +05:30
|
|
|
}>;
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class UsersService {
|
2026-03-21 08:55:57 +05:30
|
|
|
constructor(
|
|
|
|
|
private readonly prisma: PrismaService,
|
|
|
|
|
private readonly eventEmitter: EventEmitter2,
|
|
|
|
|
) {}
|
2025-12-20 19:22:30 +05:30
|
|
|
|
2026-02-01 00:50:46 +05:30
|
|
|
// =============================================
|
|
|
|
|
// USER PROFILE METHODS (for regular users)
|
|
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
async getMyUserProfile(userId: string) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
include: { userProfile: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.role !== UserRole.USER) {
|
|
|
|
|
throw new BadRequestException('This endpoint is for regular users only');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create user profile if it doesn't exist
|
|
|
|
|
let profile = user.userProfile;
|
|
|
|
|
if (!profile) {
|
|
|
|
|
profile = await this.prisma.userProfile.create({
|
|
|
|
|
data: {
|
|
|
|
|
userId: user.id,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: profile.id,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
email: user.email,
|
|
|
|
|
firstName: profile.firstName,
|
|
|
|
|
lastName: profile.lastName,
|
|
|
|
|
phone: profile.phone,
|
|
|
|
|
avatar: profile.avatar,
|
|
|
|
|
city: profile.city,
|
|
|
|
|
state: profile.state,
|
|
|
|
|
country: profile.country,
|
|
|
|
|
createdAt: profile.createdAt,
|
|
|
|
|
updatedAt: profile.updatedAt,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateMyUserProfile(
|
|
|
|
|
userId: string,
|
|
|
|
|
data: {
|
|
|
|
|
firstName?: string;
|
|
|
|
|
lastName?: string;
|
|
|
|
|
phone?: string;
|
|
|
|
|
avatar?: string | null;
|
|
|
|
|
city?: string;
|
|
|
|
|
state?: string;
|
|
|
|
|
country?: string;
|
|
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
include: { userProfile: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.role !== UserRole.USER) {
|
|
|
|
|
throw new BadRequestException('This endpoint is for regular users only');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create or update user profile
|
|
|
|
|
const profile = await this.prisma.userProfile.upsert({
|
|
|
|
|
where: { userId },
|
|
|
|
|
create: {
|
|
|
|
|
userId,
|
|
|
|
|
...data,
|
|
|
|
|
},
|
|
|
|
|
update: data,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: profile.id,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
email: user.email,
|
|
|
|
|
firstName: profile.firstName,
|
|
|
|
|
lastName: profile.lastName,
|
|
|
|
|
phone: profile.phone,
|
|
|
|
|
avatar: profile.avatar,
|
|
|
|
|
city: profile.city,
|
|
|
|
|
state: profile.state,
|
|
|
|
|
country: profile.country,
|
|
|
|
|
createdAt: profile.createdAt,
|
|
|
|
|
updatedAt: profile.updatedAt,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =============================================
|
|
|
|
|
// ADMIN METHODS
|
|
|
|
|
// =============================================
|
|
|
|
|
|
2026-03-27 12:00:04 +05:30
|
|
|
async findAll(page = 1, limit = 10, role?: UserRole, search?: string, verificationStatus?: string) {
|
2025-12-20 19:22:30 +05:30
|
|
|
const skip = (page - 1) * limit;
|
|
|
|
|
|
2026-03-27 12:00:04 +05:30
|
|
|
// Build where clause
|
|
|
|
|
const where: Prisma.UserWhereInput = {};
|
|
|
|
|
if (role) where.role = role;
|
|
|
|
|
if (search) {
|
2026-04-08 10:26:19 +05:30
|
|
|
const searchParts = search.trim().split(/\s+/).filter(Boolean);
|
|
|
|
|
|
|
|
|
|
if (searchParts.length > 1) {
|
|
|
|
|
// Multi-word search (e.g. "Sophie Brown") — match across firstName + lastName
|
|
|
|
|
where.OR = [
|
|
|
|
|
// Match all parts across firstName/lastName for userProfile
|
|
|
|
|
{
|
|
|
|
|
userProfile: {
|
|
|
|
|
AND: searchParts.map((part) => ({
|
|
|
|
|
OR: [
|
|
|
|
|
{ firstName: { contains: part, mode: 'insensitive' as const } },
|
|
|
|
|
{ lastName: { contains: part, mode: 'insensitive' as const } },
|
|
|
|
|
],
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Match all parts across firstName/lastName for agentProfile
|
|
|
|
|
{
|
|
|
|
|
agentProfile: {
|
|
|
|
|
AND: searchParts.map((part) => ({
|
|
|
|
|
OR: [
|
|
|
|
|
{ firstName: { contains: part, mode: 'insensitive' as const } },
|
|
|
|
|
{ lastName: { contains: part, mode: 'insensitive' as const } },
|
|
|
|
|
],
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// Also match the full search string against email
|
|
|
|
|
{ email: { contains: search, mode: 'insensitive' } },
|
|
|
|
|
];
|
|
|
|
|
} else {
|
|
|
|
|
// Single word — match against any field
|
|
|
|
|
where.OR = [
|
|
|
|
|
{ email: { contains: search, mode: 'insensitive' } },
|
|
|
|
|
{ userProfile: { firstName: { contains: search, mode: 'insensitive' } } },
|
|
|
|
|
{ userProfile: { lastName: { contains: search, mode: 'insensitive' } } },
|
|
|
|
|
{ agentProfile: { firstName: { contains: search, mode: 'insensitive' } } },
|
|
|
|
|
{ agentProfile: { lastName: { contains: search, mode: 'insensitive' } } },
|
|
|
|
|
];
|
|
|
|
|
}
|
2026-03-27 12:00:04 +05:30
|
|
|
}
|
|
|
|
|
if (verificationStatus) {
|
|
|
|
|
where.agentProfile = { ...((where.agentProfile as object) || {}), verificationStatus: verificationStatus as any };
|
|
|
|
|
}
|
2025-12-22 13:26:39 +05:30
|
|
|
|
2025-12-20 19:22:30 +05:30
|
|
|
const [users, total] = await Promise.all([
|
|
|
|
|
this.prisma.user.findMany({
|
2025-12-22 13:26:39 +05:30
|
|
|
where,
|
2025-12-20 19:22:30 +05:30
|
|
|
skip,
|
|
|
|
|
take: limit,
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
include: {
|
|
|
|
|
userProfile: true,
|
2026-01-26 23:12:22 +05:30
|
|
|
agentProfile: {
|
|
|
|
|
include: {
|
|
|
|
|
agentType: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-12-20 19:22:30 +05:30
|
|
|
},
|
2025-12-22 13:26:39 +05:30
|
|
|
}) as Promise<UserWithProfiles[]>,
|
|
|
|
|
this.prisma.user.count({ where }),
|
2025-12-20 19:22:30 +05:30
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return {
|
2025-12-22 13:26:39 +05:30
|
|
|
users: users.map((user) => {
|
|
|
|
|
// Get the appropriate profile based on role
|
|
|
|
|
const profile = user.role === UserRole.AGENT ? user.agentProfile : user.userProfile;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: user.id,
|
|
|
|
|
email: user.email,
|
|
|
|
|
role: user.role,
|
|
|
|
|
status: user.status,
|
|
|
|
|
emailVerified: user.emailVerified,
|
|
|
|
|
authProvider: user.authProvider,
|
|
|
|
|
createdAt: user.createdAt,
|
|
|
|
|
lastLoginAt: user.lastLoginAt,
|
|
|
|
|
profile: profile
|
|
|
|
|
? {
|
|
|
|
|
firstName: profile.firstName,
|
|
|
|
|
lastName: profile.lastName,
|
|
|
|
|
avatar: profile.avatar,
|
|
|
|
|
phone: profile.phone,
|
|
|
|
|
city: profile.city,
|
|
|
|
|
state: profile.state,
|
|
|
|
|
country: profile.country,
|
|
|
|
|
}
|
|
|
|
|
: null,
|
2026-03-29 03:31:44 +05:30
|
|
|
agentProfile: user.agentProfile
|
|
|
|
|
? {
|
|
|
|
|
id: user.agentProfile.id,
|
|
|
|
|
slug: user.agentProfile.slug,
|
|
|
|
|
headline: user.agentProfile.headline,
|
|
|
|
|
bio: user.agentProfile.bio,
|
|
|
|
|
companyName: user.agentProfile.companyName,
|
|
|
|
|
licenseNumber: user.agentProfile.licenseNumber,
|
|
|
|
|
yearsOfExperience: user.agentProfile.yearsOfExperience,
|
|
|
|
|
isProfileComplete: user.agentProfile.isProfileComplete,
|
|
|
|
|
profileCompleteness: user.agentProfile.profileCompleteness,
|
|
|
|
|
agentTypeId: user.agentProfile.agentTypeId,
|
|
|
|
|
agentType: user.agentProfile.agentType,
|
|
|
|
|
isVerified: user.agentProfile.isVerified,
|
|
|
|
|
verificationStatus: user.agentProfile.verificationStatus,
|
|
|
|
|
verificationNote: user.agentProfile.verificationNote,
|
2026-04-08 04:12:44 +05:30
|
|
|
averageRating: user.agentProfile.averageRating,
|
|
|
|
|
totalReviews: user.agentProfile.totalReviews,
|
2026-03-29 03:31:44 +05:30
|
|
|
}
|
|
|
|
|
: null,
|
2025-12-22 13:26:39 +05:30
|
|
|
};
|
|
|
|
|
}),
|
2025-12-20 19:22:30 +05:30
|
|
|
total,
|
|
|
|
|
page,
|
|
|
|
|
limit,
|
|
|
|
|
totalPages: Math.ceil(total / limit),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async findOne(id: string) {
|
|
|
|
|
const user = (await this.prisma.user.findUnique({
|
|
|
|
|
where: { id },
|
|
|
|
|
include: {
|
|
|
|
|
userProfile: true,
|
2026-01-26 23:12:22 +05:30
|
|
|
agentProfile: {
|
|
|
|
|
include: {
|
|
|
|
|
agentType: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-12-20 19:22:30 +05:30
|
|
|
},
|
2025-12-22 13:26:39 +05:30
|
|
|
})) as UserWithProfiles | null;
|
2025-12-20 19:22:30 +05:30
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 23:12:22 +05:30
|
|
|
// Base user data
|
|
|
|
|
const baseData = {
|
2025-12-20 19:22:30 +05:30
|
|
|
id: user.id,
|
|
|
|
|
email: user.email,
|
|
|
|
|
role: user.role,
|
|
|
|
|
status: user.status,
|
|
|
|
|
emailVerified: user.emailVerified,
|
2026-01-26 23:12:22 +05:30
|
|
|
emailVerifiedAt: user.emailVerifiedAt,
|
2025-12-20 19:22:30 +05:30
|
|
|
authProvider: user.authProvider,
|
|
|
|
|
createdAt: user.createdAt,
|
|
|
|
|
lastLoginAt: user.lastLoginAt,
|
2026-01-26 23:12:22 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// For agents, return full agent profile
|
|
|
|
|
if (user.role === UserRole.AGENT && user.agentProfile) {
|
|
|
|
|
return {
|
|
|
|
|
...baseData,
|
|
|
|
|
profile: {
|
|
|
|
|
firstName: user.agentProfile.firstName,
|
|
|
|
|
lastName: user.agentProfile.lastName,
|
|
|
|
|
avatar: user.agentProfile.avatar,
|
|
|
|
|
phone: user.agentProfile.phone,
|
|
|
|
|
city: user.agentProfile.city,
|
|
|
|
|
state: user.agentProfile.state,
|
|
|
|
|
country: user.agentProfile.country,
|
|
|
|
|
},
|
|
|
|
|
agentProfile: {
|
|
|
|
|
id: user.agentProfile.id,
|
|
|
|
|
slug: user.agentProfile.slug,
|
|
|
|
|
bio: user.agentProfile.bio,
|
|
|
|
|
headline: user.agentProfile.headline,
|
|
|
|
|
companyName: user.agentProfile.companyName,
|
|
|
|
|
licenseNumber: user.agentProfile.licenseNumber,
|
|
|
|
|
yearsOfExperience: user.agentProfile.yearsOfExperience,
|
|
|
|
|
isProfileComplete: user.agentProfile.isProfileComplete,
|
|
|
|
|
profileCompleteness: user.agentProfile.profileCompleteness,
|
|
|
|
|
agentTypeId: user.agentProfile.agentTypeId,
|
|
|
|
|
agentType: user.agentProfile.agentType,
|
2026-01-28 13:48:14 +05:30
|
|
|
// Verification fields
|
|
|
|
|
isVerified: user.agentProfile.isVerified,
|
|
|
|
|
verificationStatus: user.agentProfile.verificationStatus,
|
|
|
|
|
verificationNote: user.agentProfile.verificationNote,
|
|
|
|
|
verifiedAt: user.agentProfile.verifiedAt,
|
|
|
|
|
verifiedBy: user.agentProfile.verifiedBy,
|
2026-01-26 23:12:22 +05:30
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For regular users
|
|
|
|
|
return {
|
|
|
|
|
...baseData,
|
|
|
|
|
profile: user.userProfile
|
2025-12-22 13:26:39 +05:30
|
|
|
? {
|
2026-01-26 23:12:22 +05:30
|
|
|
firstName: user.userProfile.firstName,
|
|
|
|
|
lastName: user.userProfile.lastName,
|
|
|
|
|
avatar: user.userProfile.avatar,
|
|
|
|
|
phone: user.userProfile.phone,
|
|
|
|
|
city: user.userProfile.city,
|
|
|
|
|
state: user.userProfile.state,
|
|
|
|
|
country: user.userProfile.country,
|
2025-12-22 13:26:39 +05:30
|
|
|
}
|
|
|
|
|
: null,
|
2025-12-20 19:22:30 +05:30
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateStatus(id: string, status: UserStatus) {
|
|
|
|
|
return this.prisma.user.update({
|
|
|
|
|
where: { id },
|
|
|
|
|
data: { status },
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-26 23:12:22 +05:30
|
|
|
|
|
|
|
|
async updateAgentType(userId: string, agentTypeId: 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');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify agent type exists
|
|
|
|
|
const agentType = await this.prisma.agentType.findUnique({
|
|
|
|
|
where: { id: agentTypeId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!agentType) {
|
|
|
|
|
throw new BadRequestException('Invalid agent type');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update agent profile with new agent type
|
|
|
|
|
const updatedProfile = await this.prisma.agentProfile.update({
|
|
|
|
|
where: { id: user.agentProfile.id },
|
|
|
|
|
data: { agentTypeId },
|
|
|
|
|
include: { agentType: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: user.id,
|
|
|
|
|
agentTypeId: updatedProfile.agentTypeId,
|
|
|
|
|
agentType: updatedProfile.agentType,
|
|
|
|
|
message: 'Agent type updated successfully',
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-01-28 13:48:14 +05:30
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-31 22:39:10 +05:30
|
|
|
// Save verification history with profile snapshot
|
|
|
|
|
const profileSnapshot = await this.captureProfileSnapshot(user.agentProfile.id);
|
2026-03-21 08:55:57 +05:30
|
|
|
await this.prisma.verificationHistory.create({
|
|
|
|
|
data: {
|
|
|
|
|
agentProfileId: user.agentProfile.id,
|
|
|
|
|
status: data.status,
|
|
|
|
|
note: data.note || null,
|
|
|
|
|
adminId,
|
2026-03-31 22:39:10 +05:30
|
|
|
submittedData: profileSnapshot ?? undefined,
|
2026-03-21 08:55:57 +05:30
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Emit notification to the agent
|
|
|
|
|
this.eventEmitter.emit('notification.verification', {
|
|
|
|
|
recipientUserId: userId,
|
|
|
|
|
status: data.status,
|
|
|
|
|
note: data.note,
|
|
|
|
|
agentName: `${updatedProfile.firstName || ''} ${updatedProfile.lastName || ''}`.trim(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-28 13:48:14 +05:30
|
|
|
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`,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-02-09 00:33:54 +05:30
|
|
|
|
2026-03-21 08:55:57 +05:30
|
|
|
// Get verification history for an agent
|
|
|
|
|
async getVerificationHistory(agentProfileId: string) {
|
|
|
|
|
return this.prisma.verificationHistory.findMany({
|
|
|
|
|
where: { agentProfileId },
|
|
|
|
|
include: {
|
|
|
|
|
admin: { select: { id: true, email: true } },
|
|
|
|
|
},
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 22:39:10 +05:30
|
|
|
// 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');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 00:33:54 +05:30
|
|
|
// =============================================
|
|
|
|
|
// NOTIFICATION PREFERENCES
|
|
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
async getNotificationPreferences(userId: string) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
select: { notificationPreferences: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return preferences or default empty object
|
|
|
|
|
return user.notificationPreferences || { email: {}, push: {} };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateNotificationPreferences(
|
|
|
|
|
userId: string,
|
|
|
|
|
preferences: { email?: Record<string, boolean>; push?: Record<string, boolean> },
|
|
|
|
|
) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatedUser = await this.prisma.user.update({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
data: {
|
|
|
|
|
notificationPreferences: preferences,
|
|
|
|
|
},
|
|
|
|
|
select: { notificationPreferences: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return updatedUser.notificationPreferences;
|
|
|
|
|
}
|
2026-02-09 00:55:55 +05:30
|
|
|
|
|
|
|
|
// =============================================
|
|
|
|
|
// PRIVACY PREFERENCES
|
|
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
async getPrivacyPreferences(userId: string) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
select: { privacyPreferences: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return preferences or default empty object
|
|
|
|
|
return user.privacyPreferences || { privacySettings: {}, dataSettings: {} };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updatePrivacyPreferences(
|
|
|
|
|
userId: string,
|
|
|
|
|
preferences: {
|
|
|
|
|
privacySettings?: Record<string, string>;
|
|
|
|
|
dataSettings?: Record<string, boolean>;
|
|
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updatedUser = await this.prisma.user.update({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
data: {
|
|
|
|
|
privacyPreferences: preferences,
|
|
|
|
|
},
|
|
|
|
|
select: { privacyPreferences: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return updatedUser.privacyPreferences;
|
|
|
|
|
}
|
2026-02-09 01:15:45 +05:30
|
|
|
|
|
|
|
|
// =============================================
|
|
|
|
|
// DELETE ACCOUNT
|
|
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
async deleteAccount(userId: string) {
|
|
|
|
|
const user = await this.prisma.user.findUnique({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
select: { id: true, role: true },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new NotFoundException('User not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (user.role !== UserRole.USER) {
|
|
|
|
|
throw new BadRequestException('This endpoint is for regular users only');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete in a transaction to ensure data consistency
|
|
|
|
|
await this.prisma.$transaction(async (tx) => {
|
|
|
|
|
// Delete user profile if it exists
|
|
|
|
|
await tx.userProfile.deleteMany({
|
|
|
|
|
where: { userId },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Delete the user account
|
|
|
|
|
await tx.user.delete({
|
|
|
|
|
where: { id: userId },
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { message: 'Account deleted successfully' };
|
|
|
|
|
}
|
2026-03-20 17:05:40 +05:30
|
|
|
|
|
|
|
|
// =============================================
|
|
|
|
|
// ADMIN MANAGEMENT (SUPER_ADMIN only)
|
|
|
|
|
// =============================================
|
|
|
|
|
|
|
|
|
|
async getAdminUsers() {
|
|
|
|
|
return this.prisma.user.findMany({
|
|
|
|
|
where: { role: { in: ['ADMIN', 'SUPER_ADMIN'] } },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
email: true,
|
|
|
|
|
role: true,
|
|
|
|
|
status: true,
|
|
|
|
|
createdAt: true,
|
|
|
|
|
lastLoginAt: true,
|
|
|
|
|
},
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createAdminUser(email: string, password: string) {
|
|
|
|
|
const existing = await this.prisma.user.findUnique({ where: { email } });
|
|
|
|
|
if (existing) {
|
|
|
|
|
throw new BadRequestException('A user with this email already exists');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hashedPassword = await argon2.hash(password);
|
|
|
|
|
return this.prisma.user.create({
|
|
|
|
|
data: {
|
|
|
|
|
email,
|
|
|
|
|
password: hashedPassword,
|
|
|
|
|
role: 'ADMIN',
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
emailVerified: true,
|
|
|
|
|
emailVerifiedAt: new Date(),
|
|
|
|
|
authProvider: 'LOCAL',
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
email: true,
|
|
|
|
|
role: true,
|
|
|
|
|
status: true,
|
|
|
|
|
createdAt: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteAdminUser(id: string, requesterId: string) {
|
|
|
|
|
if (id === requesterId) {
|
|
|
|
|
throw new BadRequestException('You cannot delete your own account');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
|
|
|
|
if (!user) throw new NotFoundException('User not found');
|
|
|
|
|
if (user.role === 'SUPER_ADMIN') {
|
|
|
|
|
throw new ForbiddenException('Cannot delete a super admin');
|
|
|
|
|
}
|
|
|
|
|
if (user.role !== 'ADMIN') {
|
|
|
|
|
throw new BadRequestException('This user is not an admin');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.prisma.user.delete({ where: { id } });
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-12-20 19:22:30 +05:30
|
|
|
}
|