# Module Inquiries - Khám Phá Toàn Diện

**GoodGo Platform Backend API**  
**Vị Trí Module:** `apps/api/src/modules/inquiries/`  
**Tổng Số Dòng Code:** 1.212 dòng  
**Ngày Khám Phá:** 11 tháng 4 năm 2026

---

## 📋 MỤC LỤC

1. [Cấu Trúc Thư Mục](#directory-structure)
2. [Danh Sách File Đầy Đủ](#complete-file-listing)
3. [Kiến Trúc Module](#module-architecture)
4. [Các Class & Handler Chính](#key-classes--handlers)
5. [Phân Tích Tầng DDD](#ddd-layer-analysis)
6. [Tóm Tắt File Test](#test-files-summary)

---

## 📁 CẤU TRÚC THƯ MỤC

```
apps/api/src/modules/inquiries/
├── application/
│   ├── __tests__/
│   │   ├── create-inquiry.handler.spec.ts
│   │   ├── get-inquiries-by-agent.handler.spec.ts
│   │   ├── get-inquiries-by-listing.handler.spec.ts
│   │   └── mark-inquiry-read.handler.spec.ts
│   ├── commands/
│   │   ├── create-inquiry/
│   │   │   ├── create-inquiry.command.ts
│   │   │   └── create-inquiry.handler.ts
│   │   └── mark-inquiry-read/
│   │       ├── mark-inquiry-read.command.ts
│   │       └── mark-inquiry-read.handler.ts
│   └── queries/
│       ├── get-inquiries-by-agent/
│       │   ├── get-inquiries-by-agent.handler.ts
│       │   └── get-inquiries-by-agent.query.ts
│       └── get-inquiries-by-listing/
│           ├── get-inquiries-by-listing.handler.ts
│           └── get-inquiries-by-listing.query.ts
├── domain/
│   ├── __tests__/
│   │   └── inquiry-domain.spec.ts
│   ├── entities/
│   │   └── inquiry.entity.ts
│   ├── events/
│   │   ├── inquiry-created.event.ts
│   │   └── inquiry-read.event.ts
│   └── repositories/
│       ├── inquiry.repository.ts
│       └── inquiry-read.dto.ts
├── infrastructure/
│   └── repositories/
│       └── prisma-inquiry.repository.ts
├── presentation/
│   ├── __tests__/
│   │   └── inquiries.controller.spec.ts
│   ├── controllers/
│   │   └── inquiries.controller.ts
│   └── dto/
│       ├── create-inquiry.dto.ts
│       └── list-inquiries.dto.ts
├── index.ts
└── inquiries.module.ts
```

---

## 📄 DANH SÁCH FILE ĐẦY ĐỦ

### **TẦNG ỨNG DỤNG (APPLICATION LAYER)** (8 file)

#### Lệnh (Commands) (4 file)

| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `application/commands/create-inquiry/create-inquiry.command.ts` | Command | DTO để tạo yêu cầu - chứa userId, listingId, message, phone |
| `application/commands/create-inquiry/create-inquiry.handler.ts` | Command Handler | Thực thi CreateInquiryCommand; xác thực listing tồn tại, tạo entity, phát sự kiện |
| `application/commands/mark-inquiry-read/mark-inquiry-read.command.ts` | Command | DTO để đánh dấu yêu cầu đã đọc - chứa inquiryId, agentUserId |
| `application/commands/mark-inquiry-read/mark-inquiry-read.handler.ts` | Command Handler | Thực thi MarkInquiryReadCommand; xác thực quyền, đánh dấu yêu cầu đã đọc, phát sự kiện |

#### Truy Vấn (Queries) (4 file)

| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `application/queries/get-inquiries-by-agent/get-inquiries-by-agent.query.ts` | Query | DTO để liệt kê yêu cầu của đại lý - chứa agentUserId, page, limit |
| `application/queries/get-inquiries-by-agent/get-inquiries-by-agent.handler.ts` | Query Handler | Phân giải đại lý theo userId, ủy quyền cho repository findByAgent |
| `application/queries/get-inquiries-by-listing/get-inquiries-by-listing.query.ts` | Query | DTO để liệt kê yêu cầu theo listing - chứa listingId, page, limit |
| `application/queries/get-inquiries-by-listing/get-inquiries-by-listing.handler.ts` | Query Handler | Ủy quyền trực tiếp cho repository findByListing |

#### Test Tầng Ứng Dụng (4 file)

| Đường Dẫn File | Loại | Số Test | Phạm Vi |
|-----------|------|-----------|----------|
| `application/__tests__/create-inquiry.handler.spec.ts` | Jest | 4 test | Happy path, listing không tồn tại, phát sự kiện domain |
| `application/__tests__/mark-inquiry-read.handler.spec.ts` | Jest | 5 test | Happy path, yêu cầu không tồn tại, listing không tồn tại, truy cập bị cấm, đại lý không tìm thấy |
| `application/__tests__/get-inquiries-by-listing.handler.spec.ts` | Jest | 2 test | Kết quả phân trang, kết quả rỗng |
| `application/__tests__/get-inquiries-by-agent.handler.spec.ts` | Jest | 2 test | Kết quả phân trang, đại lý không tìm thấy |

---

### **TẦNG DOMAIN** (6 file)

#### Thực Thể (Entities) (1 file)

| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `domain/entities/inquiry.entity.ts` | Aggregate Root | Thực thể domain cốt lõi với id, listingId, userId, message, phone, isRead; các phương thức: createNew(), markAsRead() |

#### Sự Kiện (Events) (2 file)

| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `domain/events/inquiry-created.event.ts` | Domain Event | Được phát khi yêu cầu được tạo - chứa aggregateId, listingId, userId |
| `domain/events/inquiry-read.event.ts` | Domain Event | Được phát khi yêu cầu được đánh dấu đã đọc - chứa aggregateId, listingId, userId |

#### Repository (2 file)

| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `domain/repositories/inquiry.repository.ts` | Interface + Symbol | Interface IInquiryRepository với 6 phương thức; symbol INQUIRY_REPOSITORY cho DI; interface PaginatedResult |
| `domain/repositories/inquiry-read.dto.ts` | Interface | Read DTO cho các truy vấn - bao gồm tiêu đề listing, thông tin người dùng, dấu thời gian |

#### Test Domain (1 file)

| Đường Dẫn File | Loại | Số Test | Phạm Vi |
|-----------|------|-----------|----------|
| `domain/__tests__/inquiry-domain.spec.ts` | Jest | 5 test | Tạo entity, phone null, sự kiện domain, markAsRead |

---

### **TẦNG CƠ SỞ HẠ TẦNG (INFRASTRUCTURE LAYER)** (1 file)

#### Triển Khai Repository

| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `infrastructure/repositories/prisma-inquiry.repository.ts` | Service | Triển khai IInquiryRepository sử dụng Prisma; 6 phương thức: findById, save, markAsRead, findByListing, findByAgent, countUnreadByAgent |

---

### **TẦNG TRÌNH BÀY (PRESENTATION LAYER)** (5 file)

#### Controller (1 file)

| Đường Dẫn File | Loại | Routes | Xác Thực |
|-----------|------|--------|------|
| `presentation/controllers/inquiries.controller.ts` | NestJS Controller | POST /, GET /listing/:id, GET /agent/me, PATCH /:id/read | JWT + RBAC |

**Các Endpoint:**
- `POST /inquiries` - Tạo yêu cầu (BUYER)
- `GET /inquiries/listing/:listingId` - Lấy yêu cầu theo listing
- `GET /inquiries/agent/me` - Lấy yêu cầu cho đại lý đang đăng nhập (chỉ AGENT)
- `PATCH /inquiries/:id/read` - Đánh dấu yêu cầu đã đọc (chỉ AGENT)

#### Đối Tượng Truyền Dữ Liệu - Data Transfer Objects (2 file)

| Đường Dẫn File | Loại | Thuộc Tính | Kiểm Tra Hợp Lệ |
|-----------|------|-----------|------------|
| `presentation/dto/create-inquiry.dto.ts` | DTO | listingId, message, phone? | Chuỗi bắt buộc, message tối đa 2000 ký tự |
| `presentation/dto/list-inquiries.dto.ts` | DTO | page?, limit? | Số nguyên, min 1, max 100, mặc định 1/20 |

#### Test Tầng Trình Bày (1 file)

| Đường Dẫn File | Loại | Số Test | Phạm Vi |
|-----------|------|-----------|----------|
| `presentation/__tests__/inquiries.controller.spec.ts` | Jest | 6 test | Tất cả 4 endpoint, xử lý phone null, giá trị phân trang mặc định |

---

### **FILE MODULE** (2 file)

| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `inquiries.module.ts` | NestJS Module | Xuất: InquiriesController; Provider: PrismaInquiryRepository, 2 command handler, 2 query handler |
| `index.ts` | Barrel Export | Xuất: InquiriesModule, symbol INQUIRY_REPOSITORY, interface IInquiryRepository, InquiryEntity |

---

## 🏗️ KIẾN TRÚC MODULE

### **Mẫu Thiết Kế: CQRS + Event Sourcing + DDD**

```
┌─────────────────────────────────────────────────────────────┐
│                    PRESENTATION LAYER                        │
│  Controllers + DTOs (inquiries.controller.ts)               │
└────────────────────────┬────────────────────────────────────┘
                         │
        ┌────────────────┼────────────────┐
        ↓                ↓                ↓
   ┌─────────┐    ┌─────────┐    ┌─────────┐
   │ Commands│    │ Queries │    │ Validation│
   └────┬────┘    └────┬────┘    └─────────┘
        │              │
        └──────┬───────┘
               ↓
    ┌──────────────────────┐
    │  APPLICATION LAYER   │
    │  Handlers + Services │
    └──────────┬───────────┘
               │
        ┌──────┴──────┐
        ↓             ↓
    ┌──────────┐  ┌──────────┐
    │ Commands │  │ Queries  │
    │ Handlers │  │ Handlers │
    └────┬─────┘  └────┬─────┘
         │             │
         └──────┬──────┘
                ↓
    ┌──────────────────────┐
    │   DOMAIN LAYER       │
    │ Entities + Events    │
    │ Repository Interface │
    └──────────┬───────────┘
               │
               ↓
    ┌──────────────────────┐
    │ INFRASTRUCTURE LAYER │
    │ Prisma Repository    │
    │ Database Queries     │
    └──────────────────────┘
```

### **Luồng Dữ Liệu**

**Tạo Yêu Cầu:**
```
POST /inquiries (CreateInquiryDto)
  → InquiriesController.createInquiry()
  → CommandBus.execute(CreateInquiryCommand)
  → CreateInquiryHandler.execute()
    - Xác thực listing tồn tại (Prisma)
    - Tạo InquiryEntity
    - Lưu vào repository (Prisma)
    - Phát InquiryCreatedEvent qua EventBus
  → Trả về { id, listingId, createdAt }
```

**Đánh Dấu Đã Đọc:**
```
PATCH /inquiries/:id/read (chỉ agent)
  → InquiriesController.markAsRead()
  → CommandBus.execute(MarkInquiryReadCommand)
  → MarkInquiryReadHandler.execute()
    - Tìm inquiry entity
    - Xác minh đại lý là chủ sở hữu listing
    - Gọi entity.markAsRead()
    - Cập nhật trong repository
    - Phát InquiryReadEvent qua EventBus
  → Trả về { success: true }
```

**Lấy Danh Sách Yêu Cầu:**
```
GET /inquiries/listing/:id or /agent/me (có phân trang)
  → InquiriesController.getByListing() or getMyInquiries()
  → QueryBus.execute(GetInquiriesByListingQuery or GetInquiriesByAgentQuery)
  → Handler ủy quyền cho repository
  → Repository.findByListing() or findByAgent()
    - Truy vấn Prisma với join
    - Map sang InquiryReadDto
    - Trả về PaginatedResult
  → Trả về dữ liệu phân trang
```

---

## 🔑 CÁC CLASS & HANDLER CHÍNH

### **Domain Entity: InquiryEntity**

```typescript
export class InquiryEntity extends AggregateRoot<string> {
  // Thuộc tính
  private _listingId: string;
  private _userId: string;
  private _message: string;
  private _phone: string | null;
  private _isRead: boolean;

  // Factory Method
  static createNew(id, listingId, userId, message, phone): InquiryEntity
    → Tạo yêu cầu mới với isRead=false
    → Phát InquiryCreatedEvent

  // Business Logic
  markAsRead(): void
    → Đặt isRead thành true
    → Phát InquiryReadEvent
}
```

### **Command Handlers**

#### CreateInquiryHandler
```typescript
class CreateInquiryHandler implements ICommandHandler<CreateInquiryCommand> {
  async execute(command: CreateInquiryCommand): Promise<CreateInquiryResult> {
    // 1. Xác thực listing tồn tại
    const listing = await prisma.listing.findUnique(...)
    if (!listing) throw NotFoundException

    // 2. Tạo entity bằng factory
    const inquiry = InquiryEntity.createNew(...)

    // 3. Lưu vào cơ sở dữ liệu
    await inquiryRepo.save(inquiry)

    // 4. Phát các sự kiện domain
    const events = inquiry.clearDomainEvents()
    events.forEach(e => eventBus.publish(e))

    return { id, listingId, createdAt }
  }
}
```

#### MarkInquiryReadHandler
```typescript
class MarkInquiryReadHandler implements ICommandHandler<MarkInquiryReadCommand> {
  async execute(command: MarkInquiryReadCommand): Promise<void> {
    // 1. Tải aggregate root
    const inquiry = await inquiryRepo.findById(...)
    if (!inquiry) throw NotFoundException

    // 2. Xác minh phân quyền
    const listing = await prisma.listing.findUnique(...)
    const agent = await prisma.agent.findUnique(...)
    if (agent.id !== listing.agentId) throw ForbiddenException

    // 3. Cập nhật trạng thái domain
    inquiry.markAsRead()

    // 4. Lưu trạng thái
    await inquiryRepo.markAsRead(...)

    // 5. Phát sự kiện
    const events = inquiry.clearDomainEvents()
    events.forEach(e => eventBus.publish(e))
  }
}
```

### **Query Handlers**

#### GetInquiriesByListingHandler
```typescript
class GetInquiriesByListingHandler implements IQueryHandler<GetInquiriesByListingQuery> {
  async execute(query: GetInquiriesByListingQuery): Promise<PaginatedResult<InquiryReadDto>> {
    return this.inquiryRepo.findByListing(
      query.listingId,
      query.page,
      query.limit
    )
  }
}
```

#### GetInquiriesByAgentHandler
```typescript
class GetInquiriesByAgentHandler implements IQueryHandler<GetInquiriesByAgentQuery> {
  async execute(query: GetInquiriesByAgentQuery): Promise<PaginatedResult<InquiryReadDto>> {
    // 1. Phân giải agent ID từ user ID
    const agent = await prisma.agent.findUnique({ where: { userId } })
    if (!agent) throw NotFoundException

    // 2. Ủy quyền cho repository
    return this.inquiryRepo.findByAgent(agent.id, page, limit)
  }
}
```

### **Triển Khai Repository**

#### PrismaInquiryRepository

**Các Phương Thức:**
1. `findById(id)` - Tra cứu một yêu cầu đơn lẻ
2. `save(entity)` - Tạo yêu cầu
3. `markAsRead(id)` - Cập nhật cờ isRead
4. `findByListing(listingId, page, limit)` - Tìm kiếm phân trang với join
5. `findByAgent(agentId, page, limit)` - Tìm kiếm phân trang qua listing agent
6. `countUnreadByAgent(agentId)` - Tổng hợp số lượng chưa đọc

**Quan Hệ Prisma Được Sử Dụng:**
- `inquiry.listing` → property (để lấy tiêu đề)
- `inquiry.user` → fullName, phone
- `listing.agentId` → lọc theo agent
- Phân trang: skip/take với orderBy giảm dần

---

## 🎯 CẤU TRÚC TẦNG DDD

### **TẦNG DOMAIN** (`domain/`)

**Mục Đích:** Logic nghiệp vụ thuần túy, độc lập với framework

**Bao Gồm:**
- **Entities** - `inquiry.entity.ts` (Aggregate Root)
  - Class TypeScript thuần túy
  - Đóng gói quy tắc nghiệp vụ (cờ isRead, kiểm tra hợp lệ trường dữ liệu)
  - Factory method để tạo đối tượng
  - Các phương thức chuyển đổi trạng thái
  - Thu thập sự kiện domain

- **Events** - `inquiry-created.event.ts`, `inquiry-read.event.ts`
  - Ghi lại các sự kiện nghiệp vụ quan trọng
  - Dùng cho event sourcing và audit trail
  - Các class dữ liệu đơn giản

- **Repositories** - `inquiry.repository.ts`, `inquiry-read.dto.ts`
  - Interface định nghĩa hợp đồng (đảo ngược phụ thuộc)
  - Symbol dùng cho DI token
  - Read DTO tách biệt với write entity
  - Interface kết quả phân trang

**Tính Cô Lập:**
- Không có phụ thuộc NestJS
- Không có phụ thuộc cơ sở dữ liệu
- Không có phụ thuộc dịch vụ bên ngoài

---

### **TẦNG ỨNG DỤNG** (`application/`)

**Mục Đích:** Các use case & điều phối

**Bao Gồm:**
- **Commands** - Các thao tác thay đổi trạng thái
  - `CreateInquiryCommand` - Input DTO
  - `MarkInquiryReadCommand` - Input DTO
  - Các handler điều phối các thao tác domain

- **Queries** - Các thao tác đọc không thay đổi trạng thái
  - `GetInquiriesByListingQuery`
  - `GetInquiriesByAgentQuery`
  - Các handler ủy quyền cho repository

**Trách Nhiệm:**
- Kiểm tra điều kiện tiên quyết (listing tồn tại, đại lý được phân quyền)
- Điều phối các thao tác entity domain
- Phát sự kiện domain
- Xử lý các mối quan tâm xuyên suốt (logging, v.v.)

**Tích Hợp NestJS:**
- Decorator `@CommandHandler()`
- Decorator `@QueryHandler()`
- Dependency injection qua constructor

---

### **TẦNG CƠ SỞ HẠ TẦNG** (`infrastructure/`)

**Mục Đích:** Chi tiết cơ sở dữ liệu & lưu trữ

**Bao Gồm:**
- **Repositories** - `prisma-inquiry.repository.ts`
  - Triển khai interface repository domain
  - Sử dụng Prisma client cho các truy vấn
  - Map bản ghi cơ sở dữ liệu ↔ domain entity
  - Xử lý logic phân trang

**Trách Nhiệm:**
- Xây dựng truy vấn
- Map kết quả
- Tính toán phân trang
- Quan hệ join
- Tối ưu hóa đặc thù cơ sở dữ liệu

**Tính Cô Lập:**
- Có thể hoán đổi triển khai (có thể dùng TypeORM, MongoDB, v.v.)
- Code domain không bị ảnh hưởng bởi thay đổi cơ sở dữ liệu

---

### **TẦNG TRÌNH BÀY** (`presentation/`)

**Mục Đích:** Giao diện HTTP & I/O

**Bao Gồm:**
- **Controllers** - `inquiries.controller.ts`
  - Decorator `@Controller` của NestJS
  - Các handler route HTTP
  - Điều phối đến CQRS bus
  - Trả về HTTP response

- **DTOs** - `create-inquiry.dto.ts`, `list-inquiries.dto.ts`
  - Kiểm tra hợp lệ đầu vào (class-validator)
  - Tài liệu Swagger (@ApiProperty)
  - Tách biệt với domain entity

**Trách Nhiệm:**
- Xử lý route
- Kiểm tra hợp lệ yêu cầu
- Xác thực/Phân quyền
- Định dạng response
- Tài liệu API

**Tích Hợp NestJS:**
- Decorator: `@Post`, `@Get`, `@Patch`
- Guard: `JwtAuthGuard`, `RolesGuard`
- Middleware: `@CurrentUser`, `@Roles`

---

## 📊 TÓM TẮT FILE TEST

### **Thống Kê Test**

| Tầng | File | Số Test | Loại |
|-------|------|-------|------|
| Domain | `domain/__tests__/inquiry-domain.spec.ts` | 5 | Jest |
| Application | `application/__tests__/create-inquiry.handler.spec.ts` | 4 | Jest |
| Application | `application/__tests__/mark-inquiry-read.handler.spec.ts` | 5 | Jest |
| Application | `application/__tests__/get-inquiries-by-listing.handler.spec.ts` | 2 | Jest |
| Application | `application/__tests__/get-inquiries-by-agent.handler.spec.ts` | 2 | Jest |
| Presentation | `presentation/__tests__/inquiries.controller.spec.ts` | 6 | Jest |
| **TỔNG CỘNG** | **6 file** | **24 test** | Jest + Vitest |

### **Phạm Vi Test Theo File**

#### Test Domain (`inquiry-domain.spec.ts`) - 5 test
```
✓ InquiryEntity.createNew() với phone
✓ InquiryEntity.createNew() với phone null
✓ createNew() phát InquiryCreatedEvent
✓ markAsRead() đặt cờ thành true
✓ markAsRead() phát InquiryReadEvent
```

#### Test CreateInquiryHandler - 4 test
```
✓ Tạo yêu cầu thành công
✓ Ném NotFoundException khi listing không tồn tại
✓ Phát sự kiện domain sau khi lưu
```

#### Test MarkInquiryReadHandler - 5 test
```
✓ Đánh dấu yêu cầu đã đọc thành công
✓ Ném NotFoundException khi không tìm thấy yêu cầu
✓ Ném NotFoundException khi không tìm thấy listing
✓ Ném ForbiddenException khi người dùng không phải đại lý
✓ Ném ForbiddenException khi không tìm thấy đại lý
```

#### Test GetInquiriesByListingHandler - 2 test
```
✓ Trả về kết quả phân trang
✓ Trả về dữ liệu rỗng khi không có yêu cầu
```

#### Test GetInquiriesByAgentHandler - 2 test
```
✓ Trả về kết quả phân trang
✓ Ném NotFoundException cho người dùng không phải đại lý
```

#### Test InquiriesController - 6 test
```
✓ POST tạo yêu cầu với command dispatch
✓ POST truyền phone null khi không được cung cấp
✓ GET /listing điều phối truy vấn với giá trị mặc định
✓ GET /listing truyền phân trang tùy chỉnh
✓ GET /agent/me điều phối truy vấn agent
✓ PATCH đánh dấu yêu cầu và trả về thành công
```

---

## 🔍 THỐNG KÊ TÓM TẮT

| Chỉ Số | Số Lượng |
|--------|-------|
| **Tổng Số File** | 25 |
| **File Nguồn (.ts, không tính test)** | 19 |
| **File Test** | 6 |
| **Tổng Số Dòng Code** | 1.212 |
| **Commands** | 2 |
| **Queries** | 2 |
| **Command Handler** | 2 |
| **Query Handler** | 2 |
| **Domain Event** | 2 |
| **HTTP Endpoint** | 4 |
| **DTO** | 2 |
| **Interface** | 3 (IInquiryRepository, PaginatedResult, InquiryReadDto) |
| **Test Suite** | 6 |
| **Test Case** | 24 |

---

## 🛠️ CÁC PHỤ THUỘC CHÍNH

**Gói Bên Ngoài:**
- `@nestjs/common` - Framework
- `@nestjs/cqrs` - CQRS bus
- `@paralleldrive/cuid2` - Tạo ID
- `@prisma/client` - ORM
- `class-validator` - Kiểm tra hợp lệ DTO
- `@nestjs/swagger` - Tài liệu API

**Module Nội Bộ:**
- `@modules/shared` - AggregateRoot, DomainEvent, các ngoại lệ
- `@modules/auth` - JwtPayload, JwtAuthGuard, RolesGuard

---

## 🔐 Bảo Mật & Phân Quyền

**Xác Thực:**
- Tất cả endpoint yêu cầu `@UseGuards(JwtAuthGuard)`
- JWT token được trích xuất qua decorator `@CurrentUser()`

**Phân Quyền:**
- `GET /agent/me` → bắt buộc `@Roles('AGENT')`
- `PATCH /:id/read` → bắt buộc `@Roles('AGENT')`
- **MarkInquiryReadHandler** xác minh:
  - Yêu cầu tồn tại
  - Listing tồn tại
  - Người dùng đã đăng ký là đại lý
  - Đại lý sở hữu listing

---

## 📝 HỢP ĐỒNG API

### POST /inquiries
```
Request:  { listingId, message, phone? }
Response: { id, listingId, createdAt }
Status:   201 Created | 400 Bad Request | 401 Unauthorized | 404 Not Found
```

### GET /inquiries/listing/:listingId
```
Request:  Query: page?, limit?
Response: PaginatedResult<InquiryReadDto>
Status:   200 OK | 401 Unauthorized
```

### GET /inquiries/agent/me
```
Request:  Query: page?, limit?
Response: PaginatedResult<InquiryReadDto>
Status:   200 OK | 401 Unauthorized | 403 Forbidden
```

### PATCH /inquiries/:id/read
```
Request:  (không có body)
Response: { success: boolean }
Status:   200 OK | 401 Unauthorized | 403 Forbidden | 404 Not Found
```

---

## 🎓 NHẬN XÉT VỀ KIẾN TRÚC

### **Điểm Mạnh**

1. **Kiến Trúc Sạch** - Phân tách mối quan tâm rõ ràng giữa các tầng
2. **Mẫu CQRS** - Đường dẫn đọc/ghi riêng biệt cho phép mở rộng quy mô
3. **Domain-Driven Design** - Logic nghiệp vụ trong entity, không phải mô hình thiếu máu
4. **Hướng Sự Kiện** - Sự kiện domain cho phép audit trail và event sourcing
5. **Khả Năng Test** - Mỗi tầng có thể test độc lập với mock
6. **An Toàn Kiểu** - TypeScript đầy đủ với interface và kiểu chặt chẽ
7. **Framework DI** - NestJS cung cấp dependency injection sẵn có

### **Quyết Định Thiết Kế**

1. **InquiryEntity là Aggregate Root**
   - Đóng gói quy tắc nghiệp vụ của yêu cầu
   - Kiểm soát chuyển đổi trạng thái qua các phương thức (createNew, markAsRead)
   - Thu thập sự kiện domain

2. **Mẫu Repository**
   - Interface trong domain, triển khai trong infrastructure
   - Cho phép hoán đổi nguồn dữ liệu mà không ảnh hưởng đến logic nghiệp vụ

3. **Read/Write DTO Riêng Biệt**
   - `CreateInquiryDto` (đầu vào) so với `InquiryReadDto` (đầu ra)
   - Cho phép phát triển API linh hoạt

4. **CQRS Handler**
   - Command xử lý các thao tác thay đổi trạng thái có phân quyền
   - Query xử lý các thao tác đọc có lọc
   - Cả hai độc lập, có thể tối ưu hóa riêng

5. **Interface Phân Trang**
   - Phân trang nhất quán trên tất cả các endpoint danh sách
   - Mô hình Page + limit với totalPages được tính toán

---

**Kết Thúc Báo Cáo Khám Phá**
