Files
backend/src/agents/agents.service.ts

897 lines
22 KiB
TypeScript
Raw Normal View History

2026-01-11 20:59:12 +05:30
import {
Injectable,
NotFoundException,
ConflictException,
BadRequestException,
2026-01-11 20:59:12 +05:30
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import {
CreateAgentProfileDto,
UpdateAgentProfileDto,
SearchAgentsDto,
SaveFieldValuesDto,
2026-01-11 20:59:12 +05:30
} from './dto';
import { Prisma, Prisma as PrismaTypes, VerificationStatus } from '@prisma/client';
2026-01-11 20:59:12 +05:30
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) {}
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,
2026-01-11 20:59:12 +05:30
userId,
slug,
},
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
agentType: true,
2026-01-11 20:59:12 +05:30
},
});
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,
2026-01-11 20:59:12 +05:30
},
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
return profile;
}
async getProfileById(profileId: string) {
const profile = await this.prisma.agentProfile.findUnique({
where: { id: profileId },
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
agentType: true,
2026-01-11 20:59:12 +05:30
},
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
return profile;
}
async updateProfile(userId: string, dto: UpdateAgentProfileDto) {
const profile = await this.prisma.agentProfile.findUnique({
where: { userId },
});
if (!profile) {
throw new NotFoundException('Agent profile not found');
}
const updatedProfile = await this.prisma.agentProfile.update({
where: { userId },
data: dto,
2026-01-11 20:59:12 +05:30
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
agentType: true,
2026-01-11 20:59:12 +05:30
},
});
return updatedProfile;
}
async searchAgents(dto: SearchAgentsDto) {
const {
search,
city,
state,
country,
agentTypeId,
2026-01-11 20:59:12 +05:30
isVerified,
page = 1,
limit = 10,
sortBy = 'createdAt',
sortOrder = 'desc',
filters,
2026-01-11 20:59:12 +05:30
} = dto;
const skip = (page - 1) * limit;
// Build where clause
const where: Prisma.AgentProfileWhereInput = {};
if (search) {
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' } },
];
}
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;
2026-01-11 20:59:12 +05:30
}
// Process dynamic profile field filters
if (filters) {
try {
const parsedFilters: Record<string, string[]> = 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 },
});
const slugToFieldId = new Map(fields.map((f) => [f.slug, f.id]));
// For each filter, find agents that have ANY of the selected values
// Use AND logic between different fields
const filterConditions: string[][] = [];
for (const [slug, selectedValues] of Object.entries(parsedFilters)) {
const fieldId = slugToFieldId.get(slug);
if (!fieldId || !selectedValues || selectedValues.length === 0) continue;
// Find agentProfileIds where jsonValue contains ANY of the selected values
const matchingValues = await this.prisma.agentProfileFieldValue.findMany({
where: {
fieldId,
// jsonValue contains any of the selected values (for MULTI_SELECT, CHECKBOX_GROUP)
// We need to check if the jsonValue array contains any of the selected values
},
select: {
agentProfileId: true,
jsonValue: true,
},
});
// Filter in memory - check if jsonValue array contains any selected value
const matchingAgentIds = 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);
filterConditions.push(matchingAgentIds);
}
// 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
}
}
2026-01-11 20:59:12 +05:30
// 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,
},
},
agentType: true,
fieldValues: {
include: {
field: {
select: {
slug: true,
name: true,
fieldType: true,
},
},
},
},
2026-01-11 20:59:12 +05:30
},
}),
this.prisma.agentProfile.count({ where }),
]);
return {
data: agents,
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,
},
take: limit,
orderBy: [{ averageRating: 'desc' }, { totalReviews: 'desc' }],
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
agentType: true,
2026-01-11 20:59:12 +05:30
},
});
return agents;
}
async getTopRatedAgents(limit = 10) {
const agents = await this.prisma.agentProfile.findMany({
where: {
isVerified: true,
totalReviews: { gte: 5 },
},
take: limit,
orderBy: [{ averageRating: 'desc' }, { totalReviews: 'desc' }],
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
agentType: true,
2026-01-11 20:59:12 +05:30
},
});
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,
2026-01-11 20:59:12 +05:30
include: {
user: {
select: {
id: true,
email: true,
avatar: true,
},
},
agentType: true,
2026-01-11 20:59:12 +05:30
},
});
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;
},
) {
2026-01-11 20:59:12 +05:30
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;
}
2026-01-11 20:59:12 +05:30
const updatedProfile = await this.prisma.agentProfile.update({
where: { id: profileId },
data: updateData,
2026-01-11 20:59:12 +05:30
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 || [];
}
2026-01-11 20:59:12 +05:30
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
const field = await this.prisma.profileField.findFirst({
where: { slug: fieldValue.fieldSlug },
include: { section: true },
});
if (!field) {
// Skip fields that don't exist
continue;
}
// 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);
}
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
2026-01-24 23:10:05 +05:30
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<string, unknown> | 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':
2026-01-24 22:31:12 +05:30
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':
2026-01-24 22:31:12 +05:30
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<string, boolean>; push?: Record<string, boolean> },
) {
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<string, string>;
dataSettings?: Record<string, boolean>;
},
) {
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;
}
2026-01-11 20:59:12 +05:30
}