Files
goodgo-platform/docs/audits/INQUIRIES_MODULE_EXPLORATION_2.md
Ho Ngoc Hai 11f2bf26e6
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled
chore: update project documentation, audit reports, and initialize IDE configuration files
2026-04-19 03:12:54 +07:00

703 lines
24 KiB
Markdown

# 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 Lớp & 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 APPLICATION** (8 file)
#### Commands (4 file)
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `application/commands/create-inquiry/create-inquiry.command.ts` | Command | DTO để tạo inquiry - 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 inquiry đã đọ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 hạn, đánh dấu inquiry đã đọc, phát sự kiệ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ê inquiry của agent - chứa agentUserId, page, limit |
| `application/queries/get-inquiries-by-agent/get-inquiries-by-agent.handler.ts` | Query Handler | Tra cứu agent 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ê inquiry 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 Application (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 domain event |
| `application/__tests__/mark-inquiry-read.handler.spec.ts` | Jest | 5 test | Happy path, inquiry không tồn tại, listing không tồn tại, truy cập bị từ chối, agent không tồn tại |
| `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, agent không tồn tại |
---
### **TẦNG DOMAIN** (6 file)
#### Entities (1 file)
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `domain/entities/inquiry.entity.ts` | Aggregate Root | Entity domain cốt lõi với id, listingId, userId, message, phone, isRead; các phương thức: createNew(), markAsRead() |
#### Events (2 file)
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `domain/events/inquiry-created.event.ts` | Domain Event | Phát khi inquiry được tạo - chứa aggregateId, listingId, userId |
| `domain/events/inquiry-read.event.ts` | Domain Event | Phát khi inquiry được đánh dấu đã đọc - chứa aggregateId, listingId, userId |
#### Repositories (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 queries - bao gồm tiêu đề listing, thông tin người dùng, timestamps |
#### Test Tầng 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, domain events, markAsRead |
---
### **TẦNG INFRASTRUCTURE** (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 dùng Prisma; 6 phương thức: findById, save, markAsRead, findByListing, findByAgent, countUnreadByAgent |
---
### **TẦNG PRESENTATION** (5 file)
#### Controller (1 file)
| Đường Dẫn File | Loại | Routes | Auth |
|-----------|------|--------|------|
| `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 inquiry (BUYER)
- `GET /inquiries/listing/:listingId` - Lấy inquiry theo listing
- `GET /inquiries/agent/me` - Lấy inquiry của agent đang đăng nhập (chỉ AGENT)
- `PATCH /inquiries/:id/read` - Đánh dấu inquiry đã đọc (chỉ AGENT)
#### Data Transfer Objects (2 file)
| Đường Dẫn File | Loại | Thuộc Tính | Validation |
|-----------|------|-----------|------------|
| `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, tối thiểu 1, tối đa 100, mặc định 1/20 |
#### Test Tầng Presentation (1 file)
| Đường Dẫn File | Loại | Số Test | Phạm Vi |
|-----------|------|-----------|----------|
| `presentation/__tests__/inquiries.controller.spec.ts` | Jest | 6 test | Cả 4 endpoint, xử lý phone null, giá trị mặc định phân trang |
---
### **FILE MODULE** (2 file)
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `inquiries.module.ts` | NestJS Module | Exports: InquiriesController; Providers: PrismaInquiryRepository, 2 command handler, 2 query handler |
| `index.ts` | Barrel Export | Exports: 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 Inquiry:**
```
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 agent 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 Inquiry:**
```
GET /inquiries/listing/:id hoặc /agent/me (có phân trang)
→ InquiriesController.getByListing() hoặc getMyInquiries()
→ QueryBus.execute(GetInquiriesByListingQuery hoặc GetInquiriesByAgentQuery)
→ Handler ủy quyền cho repository
→ Repository.findByListing() hoặc findByAgent()
- Truy vấn Prisma với join
- Ánh xạ sang InquiryReadDto
- Trả về PaginatedResult
→ Trả về dữ liệu phân trang
```
---
## 🔑 CÁC LỚP & 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 inquiry 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 qua factory
const inquiry = InquiryEntity.createNew(...)
// 3. Lưu vào database
await inquiryRepo.save(inquiry)
// 4. Phát domain events
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 quyền hạ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. Tra cứu 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 inquiry đơn lẻ
2. `save(entity)` - Tạo inquiry
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 agent của listing
6. `countUnreadByAgent(agentId)` - Tổng hợp số lượng chưa đọc
**Quan Hệ Prisma 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)
- Lớp TypeScript thuần túy
- Đóng gói các quy tắc nghiệp vụ (cờ isRead, xác thực trường)
- Factory method để tạo mới
- Các phương thức chuyển đổi trạng thái
- Tập hợp domain events
- **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
- Lớp 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 cho DI token
- Read DTO tách biệt với write entity
- Interface kết quả phân trang
**Sự Cô Lập:**
- Không có dependency NestJS
- Không có dependency database
- Không có dependency dịch vụ bên ngoài
---
### **TẦNG APPLICATION** (`application/`)
**Mục Đích:** Use case và điều phối
**Bao Gồm:**
- **Commands** - Các thao tác thay đổi dữ liệu
- `CreateInquiryCommand` - Input DTO
- `MarkInquiryReadCommand` - Input DTO
- Các handler điều phối thao tác domain
- **Queries** - Các đọc bất biến
- `GetInquiriesByListingQuery`
- `GetInquiriesByAgentQuery`
- Các handler ủy quyền cho repository
**Trách Nhiệm:**
- Xác thực điều kiện tiên quyết (listing tồn tại, agent được phép)
- Điều phối các thao tác domain entity
- Phát domain events
- 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 INFRASTRUCTURE** (`infrastructure/`)
**Mục Đích:** Chi tiết database và persistence
**Bao Gồm:**
- **Repositories** - `prisma-inquiry.repository.ts`
- Triển khai interface repository của domain
- Dùng Prisma client cho các truy vấn
- Ánh xạ bản ghi database ↔ domain entity
- Xử lý logic phân trang
**Trách Nhiệm:**
- Xây dựng truy vấn
- Ánh xạ kết quả
- Tính toán phân trang
- Quan hệ join
- Tối ưu hóa riêng cho database
**Sự Cô Lập:**
- Triển khai có thể hoán đổi (có thể dùng TypeORM, MongoDB, v.v.)
- Code domain không bị ảnh hưởng khi thay đổi database
---
### **TẦNG PRESENTATION** (`presentation/`)
**Mục Đích:** Giao diện HTTP và I/O
**Bao Gồm:**
- **Controllers** - `inquiries.controller.ts`
- Decorator NestJS `@Controller`
- Các handler route HTTP
- Phân phối đến CQRS bus
- Trả về HTTP response
- **DTOs** - `create-inquiry.dto.ts`, `list-inquiries.dto.ts`
- Xác thực đầ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
- Xác thực request
- Xác thực danh tính/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** | **6 file** | **24 test** | Jest + Vitest |
### **Phạm Vi Test Theo File**
#### Test Domain (`inquiry-domain.spec.ts`) - 5 test
```
✓ InquiryEntity.createNew() có 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 inquiry thành công
✓ Ném NotFoundException khi listing không tồn tại
✓ Phát domain events sau khi lưu
```
#### Test MarkInquiryReadHandler - 5 test
```
✓ Đánh dấu inquiry đã đọc thành công
✓ Ném NotFoundException khi không tìm thấy inquiry
✓ Ném NotFoundException khi không tìm thấy listing
✓ Ném ForbiddenException khi người dùng không phải agent
✓ Ném ForbiddenException khi không tìm thấy agent
```
#### Test GetInquiriesByListingHandler - 2 test
```
✓ Trả về kết quả phân trang
✓ Trả về dữ liệu rỗng khi không có inquiry
```
#### Test GetInquiriesByAgentHandler - 2 test
```
✓ Trả về kết quả phân trang
✓ Ném NotFoundException cho người dùng không phải agent
```
#### Test InquiriesController - 6 test
```
✓ POST tạo inquiry với command dispatch
✓ POST truyền phone null khi không cung cấp
✓ GET /listing phân phối query với giá trị mặc định
✓ GET /listing truyền phân trang tùy chỉnh
✓ GET /agent/me phân phối agent query
✓ PATCH đánh dấu inquiry 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, trừ test)** | 19 |
| **File Test** | 6 |
| **Tổng Số Dòng Code** | 1.212 |
| **Commands** | 2 |
| **Queries** | 2 |
| **Command Handlers** | 2 |
| **Query Handlers** | 2 |
| **Domain Events** | 2 |
| **HTTP Endpoint** | 4 |
| **DTOs** | 2 |
| **Interfaces** | 3 (IInquiryRepository, PaginatedResult, InquiryReadDto) |
| **Test Suite** | 6 |
| **Test Case** | 24 |
---
## 🛠️ CÁC DEPENDENCY 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` - Xác thực DTO
- `@nestjs/swagger` - Tài liệu API
**Module Nội Bộ:**
- `@modules/shared` - AggregateRoot, DomainEvent, exceptions
- `@modules/auth` - JwtPayload, JwtAuthGuard, RolesGuard
---
## 🔐 Bảo Mật & Phân Quyền
**Xác Thực Danh Tính:**
- 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` → áp dụng `@Roles('AGENT')`
- `PATCH /:id/read` → áp dụng `@Roles('AGENT')`
- **MarkInquiryReadHandler** xác minh:
- Inquiry tồn tại
- Listing tồn tại
- Người dùng đã đăng ký là agent
- Agent 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 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 đọc/ghi riêng biệt giúp tăng khả năng mở rộng
3. **Thiết Kế Hướng Domain** - Logic nghiệp vụ nằm trong entity, không phải mô hình thiếu máu
4. **Hướng Sự Kiện** - Domain events cho phép audit trail và event sourcing
5. **Khả Năng Kiểm Thử** - Mỗi tầng có thể kiểm thử độ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ụ inquiry
- Kiểm soát chuyển đổi trạng thái qua phương thức (createNew, markAsRead)
- Thu thập domain events
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. **DTO Đọc/Ghi Riêng Biệt**
- `CreateInquiryDto` (đầu vào) so với `InquiryReadDto` (đầu ra)
- Cho phép tiến hóa API linh hoạt
4. **CQRS Handlers**
- Commands xử lý thay đổi dữ liệu với phân quyền
- Queries xử lý đọc với lọc
- Cả hai độc lập, có thể tối ưu riêng biệt
5. **Interface Phân Trang**
- Phân trang nhất quán trên tất 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á**