feat(api): add inquiries, leads, and agents modules for Agent Portal
Build three new DDD modules following existing CQRS patterns: - Inquiries: CRUD endpoints for buyer consultation requests with agent notification support - Leads: Full lead lifecycle management with status state machine and conversion tracking - Agents: Quality score calculation (event-driven on review changes) and dashboard stats API All modules include unit tests (14 test files, all 797 tests pass). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { Inquiry as PrismaInquiry } from '@prisma/client';
|
||||
import { type PrismaService } from '@modules/shared';
|
||||
import { InquiryEntity } from '../../domain/entities/inquiry.entity';
|
||||
import type { InquiryReadDto } from '../../domain/repositories/inquiry-read.dto';
|
||||
import type { IInquiryRepository, PaginatedResult } from '../../domain/repositories/inquiry.repository';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaInquiryRepository implements IInquiryRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findById(id: string): Promise<InquiryEntity | null> {
|
||||
const inquiry = await this.prisma.inquiry.findUnique({ where: { id } });
|
||||
return inquiry ? this.toDomain(inquiry) : null;
|
||||
}
|
||||
|
||||
async save(entity: InquiryEntity): Promise<void> {
|
||||
await this.prisma.inquiry.create({
|
||||
data: {
|
||||
id: entity.id,
|
||||
listingId: entity.listingId,
|
||||
userId: entity.userId,
|
||||
message: entity.message,
|
||||
phone: entity.phone,
|
||||
isRead: entity.isRead,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async markAsRead(id: string): Promise<void> {
|
||||
await this.prisma.inquiry.update({
|
||||
where: { id },
|
||||
data: { isRead: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findByListing(
|
||||
listingId: string,
|
||||
page: number,
|
||||
limit: number,
|
||||
): Promise<PaginatedResult<InquiryReadDto>> {
|
||||
const take = Math.min(limit, 100);
|
||||
const skip = (page - 1) * take;
|
||||
const where = { listingId };
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
this.prisma.inquiry.findMany({
|
||||
where,
|
||||
skip,
|
||||
take,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
listing: { select: { id: true, property: { select: { title: true } } } },
|
||||
user: { select: { id: true, fullName: true, phone: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.inquiry.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: data.map((r) => ({
|
||||
id: r.id,
|
||||
listingId: r.listingId,
|
||||
listingTitle: r.listing.property.title,
|
||||
userId: r.userId,
|
||||
userName: r.user.fullName,
|
||||
userPhone: r.user.phone,
|
||||
message: r.message,
|
||||
phone: r.phone,
|
||||
isRead: r.isRead,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
limit: take,
|
||||
totalPages: Math.ceil(total / take),
|
||||
};
|
||||
}
|
||||
|
||||
async findByAgent(
|
||||
agentId: string,
|
||||
page: number,
|
||||
limit: number,
|
||||
): Promise<PaginatedResult<InquiryReadDto>> {
|
||||
const take = Math.min(limit, 100);
|
||||
const skip = (page - 1) * take;
|
||||
const where = { listing: { agentId } };
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
this.prisma.inquiry.findMany({
|
||||
where,
|
||||
skip,
|
||||
take,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
listing: { select: { id: true, property: { select: { title: true } } } },
|
||||
user: { select: { id: true, fullName: true, phone: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.inquiry.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: data.map((r) => ({
|
||||
id: r.id,
|
||||
listingId: r.listingId,
|
||||
listingTitle: r.listing.property.title,
|
||||
userId: r.userId,
|
||||
userName: r.user.fullName,
|
||||
userPhone: r.user.phone,
|
||||
message: r.message,
|
||||
phone: r.phone,
|
||||
isRead: r.isRead,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
limit: take,
|
||||
totalPages: Math.ceil(total / take),
|
||||
};
|
||||
}
|
||||
|
||||
async countUnreadByAgent(agentId: string): Promise<number> {
|
||||
return this.prisma.inquiry.count({
|
||||
where: {
|
||||
isRead: false,
|
||||
listing: { agentId },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private toDomain(raw: PrismaInquiry): InquiryEntity {
|
||||
return new InquiryEntity(
|
||||
raw.id,
|
||||
{
|
||||
listingId: raw.listingId,
|
||||
userId: raw.userId,
|
||||
message: raw.message,
|
||||
phone: raw.phone,
|
||||
isRead: raw.isRead,
|
||||
},
|
||||
raw.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user