# 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.
