import { Injectable, NotFoundException, ConflictException, BadRequestException, ForbiddenException, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateAgentProfileDto, UpdateAgentProfileDto, SearchAgentsDto, SaveFieldValuesDto, } from './dto'; import { Prisma, Prisma as PrismaTypes, VerificationStatus } from '@prisma/client'; import { ConnectionRequestsService } from '../connection-requests/connection-requests.service'; function generateSlug(firstName: string, lastName: string): string { const base = `${firstName}-${lastName}` .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); const uniqueSuffix = Math.random().toString(36).substring(2, 8); return `${base}-${uniqueSuffix}`; } @Injectable() export class AgentsService { constructor( private prisma: PrismaService, private connectionRequestsService: ConnectionRequestsService, ) {} /** * Get profile_visibility setting from user's privacy preferences */ private getProfileVisibility(privacyPreferences: any): string { return privacyPreferences?.privacySettings?.profile_visibility || 'public'; } /** * Get activity_status setting from user's privacy preferences */ private getActivityStatus(privacyPreferences: any): string { return privacyPreferences?.privacySettings?.activity_status || 'public'; } async createProfile(userId: string, dto: CreateAgentProfileDto) { // Check if user already has an agent profile const existingProfile = await this.prisma.agentProfile.findUnique({ where: { userId }, }); if (existingProfile) { throw new ConflictException('User already has an agent profile'); } const slug = generateSlug(dto.firstName, dto.lastName); const profile = await this.prisma.agentProfile.create({ data: { ...dto, userId, slug, }, include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); return profile; } async getProfile(userId: string) { const profile = await this.prisma.agentProfile.findUnique({ where: { userId }, include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } // Fetch serviceAreas from field values (not a direct column) const serviceAreasField = await this.prisma.profileField.findFirst({ where: { slug: { in: ['service_areas', 'serviceAreas', 'city'] } }, }); let serviceAreas: string[] = []; if (serviceAreasField) { const fv = await this.prisma.agentProfileFieldValue.findUnique({ where: { agentProfileId_fieldId: { agentProfileId: profile.id, fieldId: serviceAreasField.id, }, }, }); if (fv?.jsonValue && Array.isArray(fv.jsonValue)) { serviceAreas = (fv.jsonValue as string[]); } else if (fv?.textValue) { serviceAreas = [fv.textValue]; } } return { ...profile, serviceAreas }; } async getProfileById(profileId: string, requestingUserId?: string) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, include: { user: { select: { id: true, email: true, avatar: true, privacyPreferences: true, }, }, agentType: true, }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } const visibility = this.getProfileVisibility(profile.user.privacyPreferences); // Own profile — always visible if (requestingUserId && requestingUserId === profile.user.id) { const { privacyPreferences, ...userWithoutPrefs } = profile.user; return { ...profile, user: userWithoutPrefs }; } if (visibility === 'private') { throw new ForbiddenException('This profile is not available'); } if (visibility === 'connections' && requestingUserId) { const connected = await this.connectionRequestsService.isConnected(requestingUserId, profile.user.id); if (!connected) { throw new ForbiddenException('This profile is only visible to connections'); } } else if (visibility === 'connections' && !requestingUserId) { throw new ForbiddenException('This profile is only visible to connections'); } // Strip privacyPreferences from response const { privacyPreferences, ...userWithoutPrefs } = profile.user; return { ...profile, user: userWithoutPrefs }; } async updateProfile(userId: string, dto: UpdateAgentProfileDto) { const profile = await this.prisma.agentProfile.findUnique({ where: { userId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } // Filter out fields that don't exist in the database schema // serviceAreas is accepted by DTO but stored in field values, not direct column // eslint-disable-next-line @typescript-eslint/no-unused-vars const { serviceAreas, ...profileData } = dto as UpdateAgentProfileDto & { serviceAreas?: string[]; }; const updatedProfile = await this.prisma.agentProfile.update({ where: { userId }, data: profileData, include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); // Sync phone to field values if phone was updated // This keeps AgentProfile.phone and AgentProfileFieldValue in sync if (dto.phone !== undefined) { await this.syncPhoneToFieldValues(profile.id, dto.phone); } // Sync serviceAreas to field values if provided if (serviceAreas && serviceAreas.length > 0) { await this.syncServiceAreasToFieldValues(profile.id, serviceAreas); } return updatedProfile; } /** * Sync phone from AgentProfile to AgentProfileFieldValue * This ensures both storage locations have the same phone value */ private async syncPhoneToFieldValues( agentProfileId: string, phone: string | null, ) { // Find any phone-related field definitions const phoneField = await this.prisma.profileField.findFirst({ where: { slug: { in: ['phone', 'phone_number', 'cell_number', 'office_number'] }, }, }); if (phoneField) { await this.prisma.agentProfileFieldValue.upsert({ where: { agentProfileId_fieldId: { agentProfileId, fieldId: phoneField.id, }, }, create: { agentProfileId, fieldId: phoneField.id, textValue: phone, }, update: { textValue: phone, }, }); } } /** * Sync serviceAreas to AgentProfileFieldValue */ private async syncServiceAreasToFieldValues( agentProfileId: string, serviceAreas: string[], ) { const serviceAreasField = await this.prisma.profileField.findFirst({ where: { slug: { in: ['service_areas', 'serviceAreas', 'city'] }, }, }); if (serviceAreasField) { await this.prisma.agentProfileFieldValue.upsert({ where: { agentProfileId_fieldId: { agentProfileId, fieldId: serviceAreasField.id, }, }, create: { agentProfileId, fieldId: serviceAreasField.id, jsonValue: serviceAreas, }, update: { jsonValue: serviceAreas, }, }); } } async searchAgents(dto: SearchAgentsDto) { const { search, location, city, state, country, agentTypeId, isVerified, page = 1, limit = 10, sortBy = 'createdAt', sortOrder = 'desc', filters, } = dto; const skip = (page - 1) * limit; // Build where clause — only show admin-approved profiles in search const where: Prisma.AgentProfileWhereInput = { verificationStatus: 'APPROVED', }; if (search) { const searchParts = search.trim().split(/\s+/); if (searchParts.length > 1) { // Multi-word search (e.g. "John Smith") — match first+last name combination // plus still try the full string against other fields where.OR = [ // Match firstName + lastName across parts { 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 other fields { bio: { contains: search, mode: 'insensitive' } }, { headline: { contains: search, mode: 'insensitive' } }, { companyName: { contains: search, mode: 'insensitive' } }, ]; } else { // Single word search — match against any field where.OR = [ { firstName: { contains: search, mode: 'insensitive' } }, { lastName: { contains: search, mode: 'insensitive' } }, { bio: { contains: search, mode: 'insensitive' } }, { headline: { contains: search, mode: 'insensitive' } }, { companyName: { contains: search, mode: 'insensitive' } }, ]; } } // General location search - searches across legacy columns AND dynamic field values if (location) { // Find agent IDs that have matching city/state in dynamic field values // City and State are CHECKBOX_GROUP fields stored as JSON arrays in jsonValue const locationFields = await this.prisma.profileField.findMany({ where: { slug: { in: ['city', 'state', 'service_areas'] } }, select: { id: true }, }); const fieldIds = locationFields.map((f) => f.id); let matchingAgentIds: string[] = []; if (fieldIds.length > 0) { // Search jsonValue (JSON arrays) and textValue using raw SQL for case-insensitive JSON content matching const locationLower = location.toLowerCase(); // Also match snake_case version (e.g., "New York" -> "new_york") const locationSnakeCase = locationLower.replace(/\s+/g, '_'); const matchingFieldValues = await this.prisma.agentProfileFieldValue.findMany({ where: { fieldId: { in: fieldIds }, OR: [ { textValue: { contains: location, mode: 'insensitive' }, }, ], }, select: { agentProfileId: true, jsonValue: true }, }); // Also fetch all field values with jsonValue for these fields to search within JSON arrays const jsonFieldValues = await this.prisma.agentProfileFieldValue.findMany({ where: { fieldId: { in: fieldIds }, jsonValue: { not: Prisma.JsonNull }, }, select: { agentProfileId: true, jsonValue: true }, }); // Filter JSON array values that contain the location string const jsonMatchIds = jsonFieldValues .filter((fv) => { const val = fv.jsonValue; if (Array.isArray(val)) { return val.some((v: unknown) => { if (typeof v !== 'string') return false; const vLower = v.toLowerCase(); return ( vLower.includes(locationLower) || vLower.includes(locationSnakeCase) || locationLower.includes(vLower) || locationSnakeCase.includes(vLower) ); }); } if (typeof val === 'string') { return val.toLowerCase().includes(locationLower); } return false; }) .map((fv) => fv.agentProfileId); const textMatchIds = matchingFieldValues.map( (fv) => fv.agentProfileId, ); matchingAgentIds = [ ...new Set([...textMatchIds, ...jsonMatchIds]), ]; } // Search legacy columns OR dynamic field values where.OR = [ ...(where.OR || []), { city: { contains: location, mode: 'insensitive' } }, { state: { contains: location, mode: 'insensitive' } }, { country: { contains: location, mode: 'insensitive' } }, ...(matchingAgentIds.length > 0 ? [{ id: { in: matchingAgentIds } }] : []), ]; } if (city) { where.city = { contains: city, mode: 'insensitive' }; } if (state) { where.state = { contains: state, mode: 'insensitive' }; } if (country) { where.country = { contains: country, mode: 'insensitive' }; } if (isVerified !== undefined) { where.isVerified = isVerified; } // Filter by agent type if (agentTypeId) { where.agentTypeId = agentTypeId; } // Process dynamic profile field filters if (filters) { try { const parsedFilters: Record = JSON.parse(filters); const filterSlugs = Object.keys(parsedFilters); if (filterSlugs.length > 0) { // Get field IDs for the filter slugs const fields = await this.prisma.profileField.findMany({ where: { slug: { in: filterSlugs } }, select: { id: true, slug: true, }, }); // Group fields by slug (same slug can exist in multiple type-specific sections) const slugToFields = new Map(); for (const f of fields) { const existing = slugToFields.get(f.slug) || []; existing.push(f); slugToFields.set(f.slug, existing); } // For each filter, find agents that have ANY of the selected values // Use AND logic between different filter slugs, OR logic within same slug const filterConditions: string[][] = []; for (const [slug, selectedValues] of Object.entries(parsedFilters)) { const slugFields = slugToFields.get(slug); if (!slugFields || slugFields.length === 0 || !selectedValues || selectedValues.length === 0) continue; // Search across ALL fields with this slug (e.g. loan_type in both Professional and Lender sections) const allMatchingAgentIds: string[] = []; const fieldIds = slugFields.map((f) => f.id); const matchingValues = await this.prisma.agentProfileFieldValue.findMany({ where: { fieldId: { in: fieldIds } }, select: { agentProfileId: true, jsonValue: true, }, }); const matchingIds = matchingValues .filter((fv) => { const jsonVal = fv.jsonValue as string[] | null; if (!jsonVal || !Array.isArray(jsonVal)) return false; return selectedValues.some((sv) => jsonVal.includes(sv)); }) .map((fv) => fv.agentProfileId); allMatchingAgentIds.push(...matchingIds); filterConditions.push([...new Set(allMatchingAgentIds)]); } // AND all filter conditions - find intersection of all matching agent IDs if (filterConditions.length > 0) { const intersection = filterConditions.reduce((acc, curr) => { const currSet = new Set(curr); return acc.filter((id) => currSet.has(id)); }); // If no agents match all filters, return empty result if (intersection.length === 0) { return { data: [], meta: { total: 0, page, limit, totalPages: 0, }, }; } where.id = { in: intersection }; } } } catch { // Invalid JSON, ignore filters } } // Build orderBy const orderBy: Prisma.AgentProfileOrderByWithRelationInput = {}; if (sortBy === 'averageRating') { orderBy.averageRating = sortOrder; } else if (sortBy === 'totalReviews') { orderBy.totalReviews = sortOrder; } else if (sortBy === 'firstName') { orderBy.firstName = sortOrder; } else { orderBy.createdAt = sortOrder; } const [agents, total] = await Promise.all([ this.prisma.agentProfile.findMany({ where, skip, take: limit, orderBy, include: { user: { select: { id: true, email: true, avatar: true, privacyPreferences: true, }, }, agentType: true, fieldValues: { include: { field: { select: { slug: true, name: true, fieldType: true, }, }, }, }, }, }), this.prisma.agentProfile.count({ where }), ]); // Filter out private profiles (safe for null privacyPreferences) const visibleAgents = agents.filter((agent: any) => { const visibility = this.getProfileVisibility(agent.user?.privacyPreferences); return visibility !== 'private'; }); // Compute match score for each agent based on search context const agentsWithScore = visibleAgents.map((agent: any) => { let score = 0; let maxScore = 0; // 1. Profile completeness (15 points) maxScore += 15; score += Math.round(((agent.profileCompleteness || 0) / 100) * 15); // 2. Verification (15 points) maxScore += 15; if (agent.isVerified) score += 15; // 3. Rating (15 points) - normalized from 0-5 scale maxScore += 15; if (agent.averageRating) { score += Math.round((agent.averageRating / 5) * 15); } // 4. Location match (15 points) maxScore += 15; if (city && agent.city?.toLowerCase() === city.toLowerCase()) { score += 15; } else if (state && agent.state?.toLowerCase() === state.toLowerCase()) { score += 10; } else if (country && agent.country?.toLowerCase() === country.toLowerCase()) { score += 5; } else if (location) { const loc = location.toLowerCase(); if (agent.city?.toLowerCase().includes(loc) || agent.state?.toLowerCase().includes(loc)) { score += 10; } } else { // No location filter — give base points score += 8; } // 5. Filter match (40 points) - how well dynamic field values match search filters maxScore += 40; if (filters) { try { const parsedFilters: Record = JSON.parse(filters); const filterKeys = Object.keys(parsedFilters).filter(k => parsedFilters[k]?.length > 0); if (filterKeys.length > 0) { let matchedFilters = 0; for (const filterSlug of filterKeys) { const selectedValues = parsedFilters[filterSlug]; const agentFieldValues = (agent.fieldValues || []) as any[]; const fieldMatch = agentFieldValues.some((fv: any) => { if (fv.field?.slug !== filterSlug) return false; const jsonVal = fv.jsonValue as string[] | null; if (jsonVal && Array.isArray(jsonVal)) { return selectedValues.some(sv => jsonVal.includes(sv)); } return false; }); if (fieldMatch) matchedFilters++; } score += Math.round((matchedFilters / filterKeys.length) * 40); } else { score += 20; // No specific filters, give base } } catch { score += 20; } } else { score += 20; // No filters applied, give base } // Calculate percentage (minimum 50% for any approved agent) const rawPercentage = maxScore > 0 ? Math.round((score / maxScore) * 100) : 50; const matchPercentage = Math.max(50, Math.min(99, rawPercentage)); // Strip privacyPreferences from response const { user, ...agentWithoutUser } = agent; const { privacyPreferences, ...userWithoutPrefs } = user || {}; return { ...agentWithoutUser, user: userWithoutPrefs, matchPercentage }; }); // Sort by match percentage (highest first) agentsWithScore.sort((a: any, b: any) => b.matchPercentage - a.matchPercentage); return { data: agentsWithScore, meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; } async getFeaturedAgents(limit = 10) { const agents = await this.prisma.agentProfile.findMany({ where: { isVerified: true, isFeatured: true, verificationStatus: 'APPROVED', }, take: limit, orderBy: [{ averageRating: 'desc' }, { totalReviews: 'desc' }], include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); return agents; } async getTopRatedAgents(limit = 10) { const agents = await this.prisma.agentProfile.findMany({ where: { isVerified: true, totalReviews: { gte: 5 }, verificationStatus: 'APPROVED', }, take: limit, orderBy: [{ averageRating: 'desc' }, { totalReviews: 'desc' }], include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); return agents; } // Admin methods async adminUpdateProfile(profileId: string, dto: UpdateAgentProfileDto) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } const updatedProfile = await this.prisma.agentProfile.update({ where: { id: profileId }, data: dto, include: { user: { select: { id: true, email: true, avatar: true, }, }, agentType: true, }, }); return updatedProfile; } async adminDeleteProfile(profileId: string) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } await this.prisma.agentProfile.delete({ where: { id: profileId }, }); return { message: 'Agent profile deleted successfully' }; } async adminVerifyAgent( profileId: string, data: { status: VerificationStatus; note?: string; adminId: string; }, ) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, }); if (!profile) { 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 = data.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: profileId }, data: updateData, include: { user: { select: { id: true, email: true, avatar: true, }, }, }, }); return updatedProfile; } async getVerificationDocuments(agentProfileId: string) { // 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, fieldId: field.id }, }, }); return fieldValue?.jsonValue || []; } async adminSetFeatured(profileId: string, isFeatured: boolean) { const profile = await this.prisma.agentProfile.findUnique({ where: { id: profileId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } const updatedProfile = await this.prisma.agentProfile.update({ where: { id: profileId }, data: { isFeatured }, include: { user: { select: { id: true, email: true, avatar: true, }, }, }, }); return updatedProfile; } async getAllAgentsAdmin(dto: SearchAgentsDto) { // Same as searchAgents but returns all data for admin return this.searchAgents(dto); } // Field Values methods async saveFieldValues(userId: string, dto: SaveFieldValuesDto) { // Get agent profile const profile = await this.prisma.agentProfile.findUnique({ where: { userId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } // Process each field value const results: Array<{ id: string; agentProfileId: string; fieldId: string; textValue: string | null; numberValue: number | null; booleanValue: boolean | null; jsonValue: Prisma.JsonValue; dateValue: Date | null; createdAt: Date; updatedAt: Date; field: { id: string; slug: string; name: string; fieldType: string; }; }> = []; for (const fieldValue of dto.fieldValues) { // Find the field by slug, preferring the one from a section assigned to this agent's type const candidateFields = await this.prisma.profileField.findMany({ where: { slug: fieldValue.fieldSlug }, include: { section: { include: { agentTypeSections: { select: { agentTypeId: true } }, }, }, }, }); if (candidateFields.length === 0) { // Skip fields that don't exist continue; } // Pick the field from a section assigned to this agent's type, or global, or fallback to first const field = candidateFields.find( (f) => f.section.agentTypeSections.some( (ats) => ats.agentTypeId === profile.agentTypeId, ), ) || candidateFields.find((f) => f.section.isGlobal) || candidateFields[0]; // Determine which column to use based on value type and field type const valueData = this.getValueData(field.fieldType, fieldValue.value); // Upsert the field value const result = await this.prisma.agentProfileFieldValue.upsert({ where: { agentProfileId_fieldId: { agentProfileId: profile.id, fieldId: field.id, }, }, create: { agentProfileId: profile.id, fieldId: field.id, ...valueData, }, update: valueData, include: { field: { select: { id: true, slug: true, name: true, fieldType: true, }, }, }, }); results.push(result); } // Sync phone from field values to AgentProfile.phone // This keeps both storage locations in sync const phoneSlugs = ['phone', 'phone_number', 'cell_number', 'office_number']; const phoneFieldValue = dto.fieldValues.find((fv) => phoneSlugs.includes(fv.fieldSlug), ); if (phoneFieldValue !== undefined) { const phoneValue = phoneFieldValue.value; await this.prisma.agentProfile.update({ where: { id: profile.id }, data: { phone: phoneValue === '' || phoneValue === null ? null : String(phoneValue), }, }); } return { message: 'Field values saved successfully', savedCount: results.length, data: results, }; } async getFieldValues(userId: string) { // Get agent profile const profile = await this.prisma.agentProfile.findUnique({ where: { userId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } // Get all field values for this profile const fieldValues = await this.prisma.agentProfileFieldValue.findMany({ where: { agentProfileId: profile.id }, include: { field: { select: { id: true, slug: true, name: true, fieldType: true, section: { select: { id: true, slug: true, name: true, }, }, }, }, }, }); // Transform to a more usable format const transformedValues = fieldValues.map((fv) => ({ fieldSlug: fv.field.slug, fieldName: fv.field.name, fieldType: fv.field.fieldType, sectionSlug: fv.field.section.slug, sectionName: fv.field.section.name, value: this.extractValue(fv), })); return { agentProfileId: profile.id, fieldValues: transformedValues, }; } async getFieldValuesByAgentId(agentId: string) { // Get agent profile by ID const profile = await this.prisma.agentProfile.findUnique({ where: { id: agentId }, }); if (!profile) { throw new NotFoundException('Agent profile not found'); } // Get all field values for this profile const fieldValues = await this.prisma.agentProfileFieldValue.findMany({ where: { agentProfileId: profile.id }, include: { field: { select: { id: true, slug: true, name: true, fieldType: true, section: { select: { id: true, slug: true, name: true, }, }, }, }, }, }); // Transform to a more usable format const transformedValues = fieldValues.map((fv) => ({ fieldSlug: fv.field.slug, fieldName: fv.field.name, fieldType: fv.field.fieldType, sectionSlug: fv.field.section.slug, sectionName: fv.field.section.name, value: this.extractValue(fv), })); return { agentProfileId: profile.id, fieldValues: transformedValues, }; } private getValueData( fieldType: string, value: string | number | boolean | string[] | Record | null, ): { textValue: string | null; numberValue: number | null; booleanValue: boolean | null; jsonValue: PrismaTypes.InputJsonValue | typeof PrismaTypes.DbNull; dateValue: Date | null; } { // Reset all value columns const valueData: { textValue: string | null; numberValue: number | null; booleanValue: boolean | null; jsonValue: PrismaTypes.InputJsonValue | typeof PrismaTypes.DbNull; dateValue: Date | null; } = { textValue: null, numberValue: null, booleanValue: null, jsonValue: PrismaTypes.DbNull, dateValue: null, }; if (value === null || value === undefined) { return valueData; } switch (fieldType) { case 'TEXT': case 'TEXTAREA': valueData.textValue = String(value); break; case 'NUMBER': case 'RANGE': valueData.numberValue = typeof value === 'number' ? value : parseFloat(String(value)); break; case 'CHECKBOX': valueData.booleanValue = typeof value === 'boolean' ? value : value === 'true'; break; case 'DATE': valueData.dateValue = new Date(String(value)); break; case 'SELECT': case 'RADIO': // Single selection - store as text valueData.textValue = String(value); break; case 'MULTI_SELECT': case 'CHECKBOX_GROUP': case 'TAG_INPUT': case 'FILE': case 'FILE_UPLOAD': case 'REPEATER': // Arrays and complex objects - store as JSON valueData.jsonValue = value as PrismaTypes.InputJsonValue; break; default: // Default to JSON for unknown types if (typeof value === 'object') { valueData.jsonValue = value as PrismaTypes.InputJsonValue; } else { valueData.textValue = String(value); } } return valueData; } private extractValue(fieldValue: { textValue: string | null; numberValue: number | null; booleanValue: boolean | null; jsonValue: unknown; dateValue: Date | null; field: { fieldType: string }; }): unknown { const { fieldType } = fieldValue.field; switch (fieldType) { case 'TEXT': case 'TEXTAREA': case 'SELECT': case 'RADIO': return fieldValue.textValue; case 'NUMBER': case 'RANGE': return fieldValue.numberValue; case 'CHECKBOX': return fieldValue.booleanValue; case 'DATE': return fieldValue.dateValue?.toISOString(); case 'MULTI_SELECT': case 'CHECKBOX_GROUP': case 'TAG_INPUT': case 'FILE': case 'FILE_UPLOAD': case 'REPEATER': return fieldValue.jsonValue; default: // Return first non-null value return ( fieldValue.jsonValue ?? fieldValue.textValue ?? fieldValue.numberValue ?? fieldValue.booleanValue ?? fieldValue.dateValue?.toISOString() ); } } // ============================================= // NOTIFICATION PREFERENCES // ============================================= async getNotificationPreferences(userId: string) { const user = await this.prisma.user.findUnique({ where: { id: userId }, select: { notificationPreferences: true, role: true }, }); if (!user) { throw new NotFoundException('User not found'); } if (user.role !== 'AGENT') { throw new BadRequestException('This endpoint is for agents only'); } // 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 }, select: { role: true }, }); if (!user) { throw new NotFoundException('User not found'); } if (user.role !== 'AGENT') { throw new BadRequestException('This endpoint is for agents only'); } 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, role: true }, }); if (!user) { throw new NotFoundException('User not found'); } if (user.role !== 'AGENT') { throw new BadRequestException('This endpoint is for agents only'); } // 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 }, select: { role: true }, }); if (!user) { throw new NotFoundException('User not found'); } if (user.role !== 'AGENT') { throw new BadRequestException('This endpoint is for agents only'); } 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 !== 'AGENT') { throw new BadRequestException('This endpoint is for agents only'); } // Delete in a transaction to ensure data consistency await this.prisma.$transaction(async (tx) => { // First, delete the agent profile and related data const agentProfile = await tx.agentProfile.findUnique({ where: { userId }, }); if (agentProfile) { // Delete agent profile field values await tx.agentProfileFieldValue.deleteMany({ where: { agentProfileId: agentProfile.id }, }); // Delete the agent profile await tx.agentProfile.delete({ where: { userId }, }); } // Finally, delete the user account await tx.user.delete({ where: { id: userId }, }); }); return { message: 'Account deleted successfully' }; } }