refactor(api): split 3 oversized files to comply with 200 LOC convention

Extract shared logic from postgres-search.repository.ts (361→105),
prisma-agent.repository.ts (298→179), and prisma-avm.service.ts (224→143)
into focused helper modules. All existing tests (92/92) pass unchanged.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-12 21:12:56 +07:00
parent 97a9541fde
commit aca4fd37cb
9 changed files with 511 additions and 545 deletions

View File

@@ -0,0 +1,112 @@
import { type PrismaService } from '@modules/shared';
import {
type AgentPublicProfileData,
type AgentPublicListingItem,
} from '../../domain/repositories/agent.repository';
/** Fetch active listings for an agent's public profile. */
export async function getActiveListingsForAgent(
prisma: PrismaService,
agentId: string,
): Promise<AgentPublicListingItem[]> {
const listings = await prisma.listing.findMany({
where: { agentId, status: 'ACTIVE' },
take: 12,
orderBy: { createdAt: 'desc' },
select: {
id: true,
transactionType: true,
priceVND: true,
status: true,
property: {
select: {
id: true,
title: true,
propertyType: true,
address: true,
district: true,
city: true,
areaM2: true,
bedrooms: true,
bathrooms: true,
media: {
where: { type: 'image' },
take: 1,
orderBy: { order: 'asc' },
select: { url: true },
},
},
},
},
});
return listings.map((l) => ({
id: l.id,
transactionType: l.transactionType,
priceVND: l.priceVND.toString(),
status: l.status,
property: {
id: l.property.id,
title: l.property.title,
propertyType: l.property.propertyType,
address: l.property.address,
district: l.property.district,
city: l.property.city,
areaM2: l.property.areaM2,
bedrooms: l.property.bedrooms,
bathrooms: l.property.bathrooms,
imageUrl: l.property.media[0]?.url ?? null,
},
}));
}
/** Build the full public profile data for an agent. */
export async function buildPublicProfile(
prisma: PrismaService,
agentId: string,
): Promise<AgentPublicProfileData | null> {
const agent = await prisma.agent.findUnique({
where: { id: agentId },
include: {
user: {
select: {
fullName: true,
avatarUrl: true,
phone: true,
email: true,
createdAt: true,
},
},
},
});
if (!agent) return null;
const [listings, reviewStats] = await Promise.all([
getActiveListingsForAgent(prisma, agentId),
prisma.review.aggregate({
where: { targetType: 'AGENT', targetId: agentId },
_avg: { rating: true },
_count: { rating: true },
}),
]);
return {
id: agent.id,
fullName: agent.user.fullName,
avatarUrl: agent.user.avatarUrl,
phone: agent.user.phone,
email: agent.user.email,
agency: agent.agency,
licenseNumber: agent.licenseNumber,
bio: agent.bio,
qualityScore: agent.qualityScore,
totalDeals: agent.totalDeals,
isVerified: agent.isVerified,
serviceAreas: (agent.serviceAreas as string[]) ?? [],
memberSince: agent.createdAt.toISOString(),
activeListings: listings,
avgReviewRating: Math.round((reviewStats._avg.rating ?? 0) * 10) / 10,
totalReviews: reviewStats._count.rating,
};
}

View File

@@ -4,28 +4,24 @@ import { AgentEntity } from '../../domain/entities/agent.entity';
import {
type AgentDashboardData,
type AgentPublicProfileData,
type AgentPublicListingItem,
type IAgentRepository,
type QualityScoreInputData,
} from '../../domain/repositories/agent.repository';
import { QualityScore } from '../../domain/value-objects/quality-score.vo';
import { buildPublicProfile } from './agent-profile.queries';
@Injectable()
export class PrismaAgentRepository implements IAgentRepository {
constructor(private readonly prisma: PrismaService) {}
async findByUserId(userId: string): Promise<AgentEntity | null> {
const row = await this.prisma.agent.findUnique({
where: { userId },
});
const row = await this.prisma.agent.findUnique({ where: { userId } });
if (!row) return null;
return this.toDomain(row);
}
async findById(agentId: string): Promise<AgentEntity | null> {
const row = await this.prisma.agent.findUnique({
where: { id: agentId },
});
const row = await this.prisma.agent.findUnique({ where: { id: agentId } });
if (!row) return null;
return this.toDomain(row);
}
@@ -33,9 +29,7 @@ export class PrismaAgentRepository implements IAgentRepository {
async save(agent: AgentEntity): Promise<void> {
await this.prisma.agent.update({
where: { id: agent.id },
data: {
qualityScore: agent.qualityScore.value,
},
data: { qualityScore: agent.qualityScore.value },
});
}
@@ -45,11 +39,8 @@ export class PrismaAgentRepository implements IAgentRepository {
this.prisma.agent.findUniqueOrThrow({
where: { id: agentId },
select: {
id: true,
qualityScore: true,
totalDeals: true,
responseTimeAvg: true,
isVerified: true,
id: true, qualityScore: true, totalDeals: true,
responseTimeAvg: true, isVerified: true,
},
}),
this.prisma.lead.groupBy({
@@ -73,9 +64,7 @@ export class PrismaAgentRepository implements IAgentRepository {
for (const group of leads) {
leadsByStatus[group.status] = group._count.id;
totalLeads += group._count.id;
if (group.status === 'CONVERTED') {
convertedLeads = group._count.id;
}
if (group.status === 'CONVERTED') convertedLeads = group._count.id;
}
const conversionRate = totalLeads > 0 ? convertedLeads / totalLeads : 0;
@@ -88,62 +77,18 @@ export class PrismaAgentRepository implements IAgentRepository {
isVerified: agent.isVerified,
totalLeads,
leadsByStatus,
conversionRate: Math.round(conversionRate * 1000) / 1000, // 3 decimals
conversionRate: Math.round(conversionRate * 1000) / 1000,
totalInquiries: inquiryStats.total,
unreadInquiries: inquiryStats.unread,
totalListings: listingStats.total,
activeListings: listingStats.active,
avgReviewRating:
Math.round((reviewStats._avg.rating ?? 0) * 10) / 10,
avgReviewRating: Math.round((reviewStats._avg.rating ?? 0) * 10) / 10,
totalReviews: reviewStats._count.rating,
};
}
async getPublicProfile(agentId: string): Promise<AgentPublicProfileData | null> {
const agent = await this.prisma.agent.findUnique({
where: { id: agentId },
include: {
user: {
select: {
fullName: true,
avatarUrl: true,
phone: true,
email: true,
createdAt: true,
},
},
},
});
if (!agent) return null;
const [listings, reviewStats] = await Promise.all([
this.getActiveListingsForAgent(agentId),
this.prisma.review.aggregate({
where: { targetType: 'AGENT', targetId: agentId },
_avg: { rating: true },
_count: { rating: true },
}),
]);
return {
id: agent.id,
fullName: agent.user.fullName,
avatarUrl: agent.user.avatarUrl,
phone: agent.user.phone,
email: agent.user.email,
agency: agent.agency,
licenseNumber: agent.licenseNumber,
bio: agent.bio,
qualityScore: agent.qualityScore,
totalDeals: agent.totalDeals,
isVerified: agent.isVerified,
serviceAreas: (agent.serviceAreas as string[]) ?? [],
memberSince: agent.createdAt.toISOString(),
activeListings: listings,
avgReviewRating: Math.round((reviewStats._avg.rating ?? 0) * 10) / 10,
totalReviews: reviewStats._count.rating,
};
return buildPublicProfile(this.prisma, agentId);
}
async getQualityScoreInputs(agentId: string): Promise<QualityScoreInputData> {
@@ -156,15 +101,11 @@ export class PrismaAgentRepository implements IAgentRepository {
}),
Promise.all([
this.prisma.lead.count({ where: { agentId } }),
this.prisma.lead.count({
where: { agentId, status: 'CONVERTED' },
}),
this.prisma.lead.count({ where: { agentId, status: 'CONVERTED' } }),
]),
Promise.all([
this.prisma.listing.count({ where: { agentId } }),
this.prisma.listing.count({
where: { agentId, status: 'ACTIVE' },
}),
this.prisma.listing.count({ where: { agentId, status: 'ACTIVE' } }),
]),
this.prisma.agent.findUnique({
where: { id: agentId },
@@ -188,12 +129,8 @@ export class PrismaAgentRepository implements IAgentRepository {
agentId: string,
): Promise<{ total: number; unread: number }> {
const [total, unread] = await Promise.all([
this.prisma.inquiry.count({
where: { listing: { agentId } },
}),
this.prisma.inquiry.count({
where: { listing: { agentId }, isRead: false },
}),
this.prisma.inquiry.count({ where: { listing: { agentId } } }),
this.prisma.inquiry.count({ where: { listing: { agentId }, isRead: false } }),
]);
return { total, unread };
}
@@ -203,67 +140,11 @@ export class PrismaAgentRepository implements IAgentRepository {
): Promise<{ total: number; active: number }> {
const [total, active] = await Promise.all([
this.prisma.listing.count({ where: { agentId } }),
this.prisma.listing.count({
where: { agentId, status: 'ACTIVE' },
}),
this.prisma.listing.count({ where: { agentId, status: 'ACTIVE' } }),
]);
return { total, active };
}
private async getActiveListingsForAgent(
agentId: string,
): Promise<AgentPublicListingItem[]> {
const listings = await this.prisma.listing.findMany({
where: { agentId, status: 'ACTIVE' },
take: 12,
orderBy: { createdAt: 'desc' },
select: {
id: true,
transactionType: true,
priceVND: true,
status: true,
property: {
select: {
id: true,
title: true,
propertyType: true,
address: true,
district: true,
city: true,
areaM2: true,
bedrooms: true,
bathrooms: true,
media: {
where: { type: 'image' },
take: 1,
orderBy: { order: 'asc' },
select: { url: true },
},
},
},
},
});
return listings.map((l) => ({
id: l.id,
transactionType: l.transactionType,
priceVND: l.priceVND.toString(),
status: l.status,
property: {
id: l.property.id,
title: l.property.title,
propertyType: l.property.propertyType,
address: l.property.address,
district: l.property.district,
city: l.property.city,
areaM2: l.property.areaM2,
bedrooms: l.property.bedrooms,
bathrooms: l.property.bathrooms,
imageUrl: l.property.media[0]?.url ?? null,
},
}));
}
private toDomain(row: {
id: string;
userId: string;