import { Injectable, BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common'; import { UserStatus, UserRole, Prisma, VerificationStatus } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; import * as argon2 from 'argon2'; type UserWithProfiles = Prisma.UserGetPayload<{ include: { userProfile: true; agentProfile: { include: { agentType: true } } }; }>; @Injectable() export class UsersService { constructor( private readonly prisma: PrismaService, private readonly eventEmitter: EventEmitter2, ) {} // ============================================= // 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 // ============================================= async findAll(page = 1, limit = 10, role?: UserRole, search?: string, verificationStatus?: string) { const skip = (page - 1) * limit; // Build where clause const where: Prisma.UserWhereInput = {}; if (role) where.role = role; if (search) { 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' } } }, ]; } if (verificationStatus) { where.agentProfile = { ...((where.agentProfile as object) || {}), verificationStatus: verificationStatus as any }; } const [users, total] = await Promise.all([ this.prisma.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, include: { userProfile: true, agentProfile: { include: { agentType: true, }, }, }, }) as Promise, this.prisma.user.count({ where }), ]); return { 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, 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, } : null, }; }), total, page, limit, totalPages: Math.ceil(total / limit), }; } async findOne(id: string) { const user = (await this.prisma.user.findUnique({ where: { id }, include: { userProfile: true, agentProfile: { include: { agentType: true, }, }, }, })) as UserWithProfiles | null; if (!user) { return null; } // Base user data const baseData = { id: user.id, email: user.email, role: user.role, status: user.status, emailVerified: user.emailVerified, emailVerifiedAt: user.emailVerifiedAt, authProvider: user.authProvider, createdAt: user.createdAt, lastLoginAt: user.lastLoginAt, }; // 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, // Verification fields isVerified: user.agentProfile.isVerified, verificationStatus: user.agentProfile.verificationStatus, verificationNote: user.agentProfile.verificationNote, verifiedAt: user.agentProfile.verifiedAt, verifiedBy: user.agentProfile.verifiedBy, }, }; } // For regular users return { ...baseData, profile: user.userProfile ? { 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, } : null, }; } async updateStatus(id: string, status: UserStatus) { return this.prisma.user.update({ where: { id }, data: { status }, }); } 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', }; } 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, }, }); // 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, }, }); // Emit notification to the agent this.eventEmitter.emit('notification.verification', { recipientUserId: userId, status: data.status, note: data.note, agentName: `${updatedProfile.firstName || ''} ${updatedProfile.lastName || ''}`.trim(), }); 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`, }; } // 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' }, }); } // 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> = {}; 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 // ============================================= 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; push?: Record }, ) { 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; } // ============================================= // 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; dataSettings?: Record; }, ) { 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; } // ============================================= // 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' }; } // ============================================= // 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; } }