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

92
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,92 @@
# Contributing Guide
## Error Handling Convention
### Overview
All application-layer error handling uses **domain exceptions** from `@modules/shared/domain/domain-exception`. Never import exception classes from `@nestjs/common` in handlers — use the project's own domain exceptions instead.
The `GlobalExceptionFilter` catches all exceptions and normalizes them into a consistent JSON response with structured error codes.
### Domain Exceptions
| Exception | HTTP Status | When to use |
|---|---|---|
| `NotFoundException(entity, id?)` | 404 | Entity not found in database |
| `ValidationException(message, details?)` | 400 | Invalid input, business rule violation, value object creation failure |
| `ConflictException(message)` | 409 | Duplicate resource, idempotency violation |
| `UnauthorizedException(message?)` | 401 | Invalid/expired credentials or tokens |
| `ForbiddenException(message?)` | 403 | Authenticated but not authorized for the action |
Import from:
```typescript
import { NotFoundException, ValidationException } from '@modules/shared/domain/domain-exception';
```
### Patterns by Layer
#### Command/Query Handlers
Handlers throw domain exceptions directly. No try-catch wrapping needed — the `GlobalExceptionFilter` handles uncaught exceptions.
```typescript
// Good: domain exception with entity context
const user = await this.userRepo.findById(id);
if (!user) throw new NotFoundException('User', id);
// Good: Result pattern from value objects
const phoneResult = Phone.create(command.phone);
if (phoneResult.isErr) {
throw new ValidationException(phoneResult.unwrapErr());
}
const phone = phoneResult.unwrap();
// Good: business rule validation
if (subscription.status === 'CANCELLED') {
throw new ValidationException('Subscription da bi huy');
}
```
#### Controllers
Controllers are thin delegation layers — they dispatch to the command/query bus and return the result. No error handling needed at the controller level.
#### Domain Services / Value Objects
Use the `Result<T, E>` pattern from `@modules/shared/domain/result`:
```typescript
static create(value: string): Result<Phone, string> {
if (!isValid(value)) return Result.err('So dien thoai khong hop le');
return Result.ok(new Phone({ value }));
}
```
Handlers consume `Result` by checking `.isErr` and throwing a `ValidationException`.
### What NOT to Do
```typescript
// Bad: NestJS built-in exceptions (missing errorCode in response)
import { NotFoundException } from '@nestjs/common';
// Bad: object-style exceptions (inconsistent with domain exception API)
throw new NotFoundException({ code: ErrorCode.NOT_FOUND, message: '...' });
// Bad: generic try-catch swallowing infrastructure errors as domain errors
try {
await this.prisma.plan.findFirst({ where: { tier } });
} catch {
throw new NotFoundException('Plan not found'); // hides DB connection errors
}
// Bad: returning Result from handlers to controllers
// Handlers should unwrap Result and throw on error
```
### Repository Return Types
All repository read methods must return explicitly typed DTOs — never `Promise<any>` or `PaginatedResult<any>`. Define read DTOs in the domain layer alongside the repository interface.
See `listing-read.dto.ts` for the canonical example.