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:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user