# Module Yêu Cầu - Tài Liệu Tham Khảo Nhanh

## 📊 Tổng Quan Module

**Đường dẫn:** `apps/api/src/modules/inquiries/`  
**Kiến trúc:** CQRS + DDD  
**Tổng số tệp:** 25  
**Tổng số dòng mã:** 1,212  
**Độ phủ kiểm thử:** 6 bộ kiểm thử, 24 bài kiểm thử  

---

## 📁 CÁC TỆP THEO TẦNG

### TRÌNH BÀY (5 tệp)
```
presentation/
├── controllers/inquiries.controller.ts        [4 endpoints]
├── dto/create-inquiry.dto.ts                 [3 properties]
├── dto/list-inquiries.dto.ts                 [2 properties]
└── __tests__/inquiries.controller.spec.ts    [6 tests]
```

### ỨNG DỤNG (8 tệp)
```
application/
├── commands/create-inquiry/
│   ├── create-inquiry.command.ts
│   └── create-inquiry.handler.ts
├── commands/mark-inquiry-read/
│   ├── mark-inquiry-read.command.ts
│   └── mark-inquiry-read.handler.ts
├── queries/get-inquiries-by-agent/
│   ├── get-inquiries-by-agent.query.ts
│   └── get-inquiries-by-agent.handler.ts
├── queries/get-inquiries-by-listing/
│   ├── get-inquiries-by-listing.query.ts
│   └── get-inquiries-by-listing.handler.ts
└── __tests__/                                 [4 test files, 13 tests]
```

### MIỀN (6 tệp)
```
domain/
├── entities/inquiry.entity.ts                [Aggregate Root]
├── events/
│   ├── inquiry-created.event.ts
│   └── inquiry-read.event.ts
├── repositories/
│   ├── inquiry.repository.ts                 [Interface + Symbol]
│   └── inquiry-read.dto.ts                   [Read DTO]
└── __tests__/inquiry-domain.spec.ts          [5 tests]
```

### HẠ TẦNG (1 tệp)
```
infrastructure/
└── repositories/
    └── prisma-inquiry.repository.ts          [6 methods]
```

### MODULE (2 tệp)
```
inquiries.module.ts                           [NestJS Module]
index.ts                                      [Barrel export]
```

---

## 🔄 LUỒNG YÊU CẦU

### TẠO YÊU CẦU
```
POST /inquiries { listingId, message, phone? }
  ↓
InquiriesController.createInquiry()
  ↓
CommandBus.execute(CreateInquiryCommand)
  ↓
CreateInquiryHandler
  1. Validate listing exists (Prisma)
  2. Create InquiryEntity
  3. Save to PrismaInquiryRepository
  4. Publish InquiryCreatedEvent
  ↓
Response: { id, listingId, createdAt }
```

### ĐÁNH DẤU ĐÃ ĐỌC
```
PATCH /inquiries/:id/read (AGENT only)
  ↓
InquiriesController.markAsRead()
  ↓
CommandBus.execute(MarkInquiryReadCommand)
  ↓
MarkInquiryReadHandler
  1. Load inquiry entity
  2. Verify agent owns listing
  3. Call inquiry.markAsRead()
  4. Update in repository
  5. Publish InquiryReadEvent
  ↓
Response: { success: true }
```

### DANH SÁCH THEO TIN ĐĂNG
```
GET /inquiries/listing/:listingId?page=1&limit=20
  ↓
InquiriesController.getByListing()
  ↓
QueryBus.execute(GetInquiriesByListingQuery)
  ↓
GetInquiriesByListingHandler
  ↓
PrismaInquiryRepository.findByListing()
  ↓
Response: PaginatedResult<InquiryReadDto>
```

### DANH SÁCH THEO MÔI GIỚI
```
GET /inquiries/agent/me?page=1&limit=20 (AGENT only)
  ↓
InquiriesController.getMyInquiries()
  ↓
QueryBus.execute(GetInquiriesByAgentQuery)
  ↓
GetInquiriesByAgentHandler
  1. Resolve agent from userId
  2. Delegate to repository
  ↓
PrismaInquiryRepository.findByAgent()
  ↓
Response: PaginatedResult<InquiryReadDto>
```

---

## 🔑 CÁC LỚP CHÍNH

| Lớp | Vị trí | Mục đích |
|-----|--------|----------|
| **InquiryEntity** | domain/entities/ | Gốc tổng hợp với logic nghiệp vụ |
| **CreateInquiryHandler** | application/commands/create-inquiry/ | Thực thi lệnh tạo mới |
| **MarkInquiryReadHandler** | application/commands/mark-inquiry-read/ | Thực thi lệnh đánh dấu đã đọc |
| **GetInquiriesByListingHandler** | application/queries/get-inquiries-by-listing/ | Phân giải yêu cầu theo tin đăng |
| **GetInquiriesByAgentHandler** | application/queries/get-inquiries-by-agent/ | Phân giải yêu cầu theo môi giới |
| **PrismaInquiryRepository** | infrastructure/repositories/ | Triển khai lưu trữ dữ liệu |
| **InquiriesController** | presentation/controllers/ | Các endpoint HTTP |

---

## 📝 CÁC GIAO DIỆN CHÍNH

```typescript
// Domain interface (repository contract)
interface IInquiryRepository {
  findById(id: string): Promise<InquiryEntity | null>
  save(inquiry: InquiryEntity): Promise<void>
  markAsRead(id: string): Promise<void>
  findByListing(listingId, page, limit): Promise<PaginatedResult<InquiryReadDto>>
  findByAgent(agentId, page, limit): Promise<PaginatedResult<InquiryReadDto>>
  countUnreadByAgent(agentId): Promise<number>
}

// Read DTO (queries only)
interface InquiryReadDto {
  id: string
  listingId: string
  listingTitle: string
  userId: string
  userName: string
  userPhone: string
  message: string
  phone: string | null
  isRead: boolean
  createdAt: string
}

// Pagination result
interface PaginatedResult<T> {
  data: T[]
  total: number
  page: number
  limit: number
  totalPages: number
}
```

---

## 🧪 CÁC TỆP KIỂM THỬ TỔNG QUAN

| Tệp kiểm thử | Số bài | Nội dung |
|--------------|--------|----------|
| `domain/__tests__/inquiry-domain.spec.ts` | 5 | Tạo thực thể, sự kiện |
| `application/__tests__/create-inquiry.handler.spec.ts` | 4 | Thành công của handler, kiểm tra đầu vào |
| `application/__tests__/mark-inquiry-read.handler.spec.ts` | 5 | Thành công của handler, kiểm tra xác thực |
| `application/__tests__/get-inquiries-by-listing.handler.spec.ts` | 2 | Kết quả truy vấn, trạng thái rỗng |
| `application/__tests__/get-inquiries-by-agent.handler.spec.ts` | 2 | Kết quả truy vấn, tra cứu môi giới |
| `presentation/__tests__/inquiries.controller.spec.ts` | 6 | Tất cả endpoint, giá trị mặc định |
| **TỔNG** | **24** | **Độ phủ toàn diện** |

---

## 🔐 Ma Trận Phân Quyền

| Endpoint | Xác thực | Vai trò | Truy vấn |
|----------|----------|---------|----------|
| `POST /inquiries` | JWT | Bất kỳ | - |
| `GET /listing/:id` | JWT | Bất kỳ | page, limit |
| `GET /agent/me` | JWT | AGENT | page, limit |
| `PATCH /:id/read` | JWT | AGENT | - |

**Kiểm tra quyền:**
- MarkInquiryReadHandler: Xác minh người dùng là môi giới, môi giới sở hữu tin đăng

---

## 🎯 NGUYÊN TẮC DDD

### Đóng Gói Thực Thể Miền
```typescript
// Factory method (controlled creation)
static createNew(id, listingId, userId, message, phone): InquiryEntity
  → Creates entity with isRead=false
  → Emits InquiryCreatedEvent

// Domain methods (state transitions)
markAsRead(): void
  → Sets isRead=true
  → Emits InquiryReadEvent
```

### Mẫu Repository
- **Giao diện trong Miền** → `IInquiryRepository`
- **Triển khai trong Hạ tầng** → `PrismaInquiryRepository`
- **Tiêm phụ thuộc** → ký hiệu `INQUIRY_REPOSITORY`

### Mô Hình Đọc/Ghi Tách Biệt
- **Mô hình Ghi:** `InquiryEntity` (tổng hợp)
- **Mô hình Đọc:** `InquiryReadDto` (DTO truy vấn)

---

## 🔄 Sự Kiện Miền

| Sự kiện | Thời điểm | Dữ liệu |
|---------|-----------|---------|
| **InquiryCreatedEvent** | Yêu cầu được tạo | aggregateId, listingId, userId |
| **InquiryReadEvent** | Được đánh dấu đã đọc | aggregateId, listingId, userId |

---

## 💾 Thao Tác Cơ Sở Dữ Liệu

**Các Prisma Model được sử dụng:**
- `inquiry` - Thực thể chính
- `listing` - Cho khóa ngoại & tra cứu môi giới
- `property` - Cho tiêu đề tin đăng
- `user` - Cho tên & số điện thoại người mua

**Các Truy Vấn Chính:**
- `inquiry.create()` - Tạo yêu cầu mới
- `inquiry.update()` - Đánh dấu đã đọc
- `inquiry.findMany()` - Phân trang
- `inquiry.count()` - Đếm tổng số

---

## 🚀 Điểm Vào

```typescript
// Module export
export { InquiriesModule }

// Exported interfaces
export { INQUIRY_REPOSITORY, type IInquiryRepository }
export { InquiryEntity }

// Usage in other modules
import { InquiriesModule } from '@modules/inquiries'
```

---

## 🎓 Tóm Tắt Kiến Trúc

```
KIẾN TRÚC SẠCH với CQRS + DDD

Tầng Trình Bày (Controllers + DTOs)
         ↓
Tầng Ứng Dụng (CQRS Handlers)
         ↓
Tầng Miền (Entities + Events + Interfaces)
         ↓
Tầng Hạ Tầng (Prisma Repository)
         ↓
Cơ Sở Dữ Liệu
```

**Đặc Điểm Chính:**
✅ Đảo Ngược Phụ Thuộc - Miền định nghĩa các hợp đồng  
✅ Phân Tách Mối Quan Tâm - Mỗi tầng có trách nhiệm rõ ràng  
✅ Khả Năng Kiểm Thử - Triển khai giả lập tại mỗi tầng  
✅ Hướng Sự Kiện - Sự kiện miền cho kiểm toán & tích hợp  
✅ CQRS - Tách biệt lệnh & truy vấn để mở rộng quy mô  
✅ An Toàn Kiểu - TypeScript đầy đủ với giao diện nghiêm ngặt  

---

## 📌 Các Mẫu Thường Dùng

**Mẫu Command:**
```typescript
// Send command
commandBus.execute(new CreateInquiryCommand(...))
// Handler processes
@CommandHandler(CreateInquiryCommand)
class CreateInquiryHandler { ... }
```

**Mẫu Query:**
```typescript
// Send query
queryBus.execute(new GetInquiriesByListingQuery(...))
// Handler processes
@QueryHandler(GetInquiriesByListingQuery)
class GetInquiriesByListingHandler { ... }
```

**Mẫu Tiêm Phụ Thuộc:**
```typescript
@Injectable()
export class Handler {
  constructor(
    @Inject(INQUIRY_REPOSITORY) private repo: IInquiryRepository,
    private prisma: PrismaService,
  ) {}
}
```

---

## 🔍 Nơi Tìm Kiếm...

| Nhu cầu | Tệp |
|---------|-----|
| Thêm endpoint mới | `presentation/controllers/inquiries.controller.ts` |
| Thêm command | `application/commands/[name]/[name].command.ts` + `[name].handler.ts` |
| Thêm query | `application/queries/[name]/[name].query.ts` + `[name].handler.ts` |
| Logic nghiệp vụ | `domain/entities/inquiry.entity.ts` |
| Sự kiện miền mới | `domain/events/[name].event.ts` |
| Truy vấn cơ sở dữ liệu | `infrastructure/repositories/prisma-inquiry.repository.ts` |
| Kiểm tra đầu vào | `presentation/dto/*.ts` |
| Viết kiểm thử | `[layer]/__tests__/*` |

---

**Cập Nhật Lần Cuối:** 11 tháng 4, 2026
