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

@@ -1,2 +1,3 @@
export { PROPERTY_REPOSITORY, type IPropertyRepository } from './property.repository';
export { LISTING_REPOSITORY, type IListingRepository, type ListingSearchParams, type PaginatedResult } from './listing.repository';
export type { ListingDetailData, ListingSearchItem, ListingSellerItem, ListingMediaData } from './listing-read.dto';

View File

@@ -0,0 +1,98 @@
import { type ListingStatus, type TransactionType, type PropertyType, type Direction } from '@prisma/client';
/** Returned by findByIdWithProperty — full listing detail with property, seller, agent */
export interface ListingDetailData {
id: string;
status: ListingStatus;
transactionType: TransactionType;
priceVND: string;
pricePerM2: number | null;
rentPriceMonthly: string | null;
commissionPct: number | null;
viewCount: number;
saveCount: number;
inquiryCount: number;
publishedAt: string | null;
createdAt: string;
property: {
id: string;
propertyType: PropertyType;
title: string;
description: string;
address: string;
ward: string;
district: string;
city: string;
areaM2: number;
bedrooms: number | null;
bathrooms: number | null;
floors: number | null;
direction: Direction | null;
yearBuilt: number | null;
legalStatus: string | null;
amenities: unknown;
projectName: string | null;
media: ListingMediaData[];
};
seller: {
id: string;
fullName: string;
phone: string;
};
agent: {
id: string;
userId: string;
agency: string | null;
} | null;
}
export interface ListingMediaData {
id: string;
url: string;
type: string;
order: number;
caption: string | null;
}
/** Returned by search / findByStatus — listing summary with thumbnail */
export interface ListingSearchItem {
id: string;
status: ListingStatus;
transactionType: TransactionType;
priceVND: string;
pricePerM2: number | null;
viewCount: number;
publishedAt: string | null;
property: {
id: string;
propertyType: PropertyType;
title: string;
address: string;
district: string;
city: string;
areaM2: number;
bedrooms: number | null;
bathrooms: number | null;
thumbnail: string | null;
};
seller: {
id: string;
fullName: string;
};
}
/** Returned by findBySellerId — compact listing for seller dashboard */
export interface ListingSellerItem {
id: string;
status: ListingStatus;
transactionType: TransactionType;
priceVND: string;
property: {
id: string;
title: string;
district: string;
city: string;
areaM2: number;
thumbnail: string | null;
};
}

View File

@@ -1,12 +1,13 @@
import { type ListingStatus } from '@prisma/client';
import { type ListingStatus, type TransactionType, type PropertyType } from '@prisma/client';
import { type ListingEntity } from '../entities/listing.entity';
import { type ListingDetailData, type ListingSearchItem, type ListingSellerItem } from './listing-read.dto';
export const LISTING_REPOSITORY = Symbol('LISTING_REPOSITORY');
export interface ListingSearchParams {
status?: ListingStatus;
transactionType?: string;
propertyType?: string;
transactionType?: TransactionType;
propertyType?: PropertyType;
city?: string;
district?: string;
minPrice?: bigint;
@@ -28,10 +29,10 @@ export interface PaginatedResult<T> {
export interface IListingRepository {
findById(id: string): Promise<ListingEntity | null>;
findByIdWithProperty(id: string): Promise<{ listing: ListingEntity; property: any } | null>;
findByIdWithProperty(id: string): Promise<ListingDetailData | null>;
save(listing: ListingEntity): Promise<void>;
update(listing: ListingEntity): Promise<void>;
search(params: ListingSearchParams): Promise<PaginatedResult<any>>;
findByStatus(status: ListingStatus, page: number, limit: number): Promise<PaginatedResult<any>>;
findBySellerId(sellerId: string, page: number, limit: number): Promise<PaginatedResult<any>>;
search(params: ListingSearchParams): Promise<PaginatedResult<ListingSearchItem>>;
findByStatus(status: ListingStatus, page: number, limit: number): Promise<PaginatedResult<ListingSearchItem>>;
findBySellerId(sellerId: string, page: number, limit: number): Promise<PaginatedResult<ListingSellerItem>>;
}