fix: eliminate untyped repository returns and standardize DomainException usage across all handlers

- Create typed DTOs (ListingDetailData, ListingSearchItem, ListingSellerItem) for repository read methods
- Replace all Promise<any> and PaginatedResult<any> with concrete types in repository interface and implementation
- Remove `as any` casts in search params by using Prisma enum types (TransactionType, PropertyType)
- Migrate all 16 handlers from NestJS built-in exceptions to domain exceptions (NotFoundException, ValidationException, etc.)
- Add CONTRIBUTING.md documenting error handling convention
- All 230 tests pass, typecheck clean

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 06:25:44 +07:00
parent 6389dcf78e
commit e9889539ea
28 changed files with 288 additions and 247 deletions

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { PrismaService } from '@modules/shared/infrastructure/prisma.service';
import { type Listing as PrismaListing, type Prisma, type ListingStatus } from '@prisma/client';
import { type IListingRepository, type ListingSearchParams, type PaginatedResult } from '../../domain/repositories/listing.repository';
import { type ListingDetailData, type ListingSearchItem, type ListingSellerItem } from '../../domain/repositories/listing-read.dto';
import { ListingEntity, type ListingProps } from '../../domain/entities/listing.entity';
import { Price } from '../../domain/value-objects/price.vo';
@@ -14,7 +15,7 @@ export class PrismaListingRepository implements IListingRepository {
return listing ? this.toDomain(listing) : null;
}
async findByIdWithProperty(id: string): Promise<any | null> {
async findByIdWithProperty(id: string): Promise<ListingDetailData | null> {
const listing = await this.prisma.listing.findUnique({
where: { id },
include: {
@@ -122,7 +123,7 @@ export class PrismaListingRepository implements IListingRepository {
});
}
async search(params: ListingSearchParams): Promise<PaginatedResult<any>> {
async search(params: ListingSearchParams): Promise<PaginatedResult<ListingSearchItem>> {
const page = params.page ?? 1;
const limit = Math.min(params.limit ?? 20, 100);
const skip = (page - 1) * limit;
@@ -130,7 +131,7 @@ export class PrismaListingRepository implements IListingRepository {
const where: Prisma.ListingWhereInput = {};
if (params.status) where.status = params.status;
if (params.transactionType) where.transactionType = params.transactionType as any;
if (params.transactionType) where.transactionType = params.transactionType;
if (params.minPrice || params.maxPrice) {
where.priceVND = {};
if (params.minPrice) where.priceVND.gte = params.minPrice;
@@ -139,7 +140,7 @@ export class PrismaListingRepository implements IListingRepository {
if (params.propertyType || params.city || params.district || params.minArea || params.maxArea || params.bedrooms) {
where.property = {};
if (params.propertyType) where.property.propertyType = params.propertyType as any;
if (params.propertyType) where.property.propertyType = params.propertyType;
if (params.city) where.property.city = params.city;
if (params.district) where.property.district = params.district;
if (params.minArea || params.maxArea) {
@@ -198,11 +199,11 @@ export class PrismaListingRepository implements IListingRepository {
};
}
async findByStatus(status: ListingStatus, page: number, limit: number): Promise<PaginatedResult<any>> {
async findByStatus(status: ListingStatus, page: number, limit: number): Promise<PaginatedResult<ListingSearchItem>> {
return this.search({ status, page, limit });
}
async findBySellerId(sellerId: string, page: number, limit: number): Promise<PaginatedResult<any>> {
async findBySellerId(sellerId: string, page: number, limit: number): Promise<PaginatedResult<ListingSellerItem>> {
const skip = (page - 1) * limit;
const where: Prisma.ListingWhereInput = { sellerId };