chore: update project documentation, audit reports, and initialize IDE configuration files
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

This commit is contained in:
Ho Ngoc Hai
2026-04-19 03:12:54 +07:00
parent 3be106074d
commit 11f2bf26e6
101 changed files with 21312 additions and 20672 deletions

View File

@@ -1,24 +1,24 @@
# Inquiries Module - Complete Exploration
# Module Inquiries - Khám Phá Toàn Diện
**GoodGo Platform Backend API**
**Module Location:** `apps/api/src/modules/inquiries/`
**Total Lines of Code:** 1,212 lines
**Date Explored:** April 11, 2026
**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
---
## 📋 TABLE OF CONTENTS
## 📋 MỤC LỤC
1. [Directory Structure](#directory-structure)
2. [Complete File Listing](#complete-file-listing)
3. [Module Architecture](#module-architecture)
4. [Key Classes & Handlers](#key-classes--handlers)
5. [DDD Layer Analysis](#ddd-layer-analysis)
6. [Test Files Summary](#test-files-summary)
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)
---
## 📁 DIRECTORY STRUCTURE
## 📁 CẤU TRÚC THƯ MỤC
```
apps/api/src/modules/inquiries/
@@ -70,120 +70,120 @@ apps/api/src/modules/inquiries/
---
## 📄 COMPLETE FILE LISTING
## 📄 DANH SÁCH FILE ĐẦY ĐỦ
### **APPLICATION LAYER** (8 files)
### **TẦNG ỨNG DỤNG (APPLICATION LAYER)** (8 file)
#### Commands (4 files)
#### Lệnh (Commands) (4 file)
| File Path | Type | Description |
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `application/commands/create-inquiry/create-inquiry.command.ts` | Command | DTO for creating inquiry - contains userId, listingId, message, phone |
| `application/commands/create-inquiry/create-inquiry.handler.ts` | Command Handler | Executes CreateInquiryCommand; validates listing exists, creates entity, publishes event |
| `application/commands/mark-inquiry-read/mark-inquiry-read.command.ts` | Command | DTO for marking inquiry as read - contains inquiryId, agentUserId |
| `application/commands/mark-inquiry-read/mark-inquiry-read.handler.ts` | Command Handler | Executes MarkInquiryReadCommand; validates permissions, marks inquiry read, publishes event |
| `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 |
#### Queries (4 files)
#### Truy Vấn (Queries) (4 file)
| File Path | Type | Description |
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `application/queries/get-inquiries-by-agent/get-inquiries-by-agent.query.ts` | Query | DTO for listing agent's inquiries - contains agentUserId, page, limit |
| `application/queries/get-inquiries-by-agent/get-inquiries-by-agent.handler.ts` | Query Handler | Resolves agent by userId, delegates to repository findByAgent |
| `application/queries/get-inquiries-by-listing/get-inquiries-by-listing.query.ts` | Query | DTO for listing inquiries by listing - contains listingId, page, limit |
| `application/queries/get-inquiries-by-listing/get-inquiries-by-listing.handler.ts` | Query Handler | Delegates directly to repository findByListing |
| `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 |
#### Application Tests (4 files)
#### Test Tầng Ứng Dụng (4 file)
| File Path | Type | Test Count | Coverage |
| Đường Dẫn File | Loại | Số Test | Phạm Vi |
|-----------|------|-----------|----------|
| `application/__tests__/create-inquiry.handler.spec.ts` | Jest | 4 tests | Happy path, missing listing, domain event publishing |
| `application/__tests__/mark-inquiry-read.handler.spec.ts` | Jest | 5 tests | Happy path, missing inquiry, missing listing, forbidden access, agent not found |
| `application/__tests__/get-inquiries-by-listing.handler.spec.ts` | Jest | 2 tests | Paginated results, empty results |
| `application/__tests__/get-inquiries-by-agent.handler.spec.ts` | Jest | 2 tests | Paginated results, agent not found |
| `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 |
---
### **DOMAIN LAYER** (6 files)
### **TẦNG DOMAIN** (6 file)
#### Entities (1 file)
#### Thực Thể (Entities) (1 file)
| File Path | Type | Description |
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `domain/entities/inquiry.entity.ts` | Aggregate Root | Core domain entity with id, listingId, userId, message, phone, isRead; methods: createNew(), markAsRead() |
| `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() |
#### Events (2 files)
#### Sự Kiện (Events) (2 file)
| File Path | Type | Description |
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `domain/events/inquiry-created.event.ts` | Domain Event | Published when inquiry is created - contains aggregateId, listingId, userId |
| `domain/events/inquiry-read.event.ts` | Domain Event | Published when inquiry is marked read - contains aggregateId, listingId, userId |
| `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 |
#### Repositories (2 files)
#### Repository (2 file)
| File Path | Type | Description |
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `domain/repositories/inquiry.repository.ts` | Interface + Symbol | IInquiryRepository interface with 6 methods; INQUIRY_REPOSITORY symbol for DI; PaginatedResult interface |
| `domain/repositories/inquiry-read.dto.ts` | Interface | Read DTO for queries - includes listing title, user details, timestamps |
| `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 |
#### Domain Tests (1 file)
#### Test Domain (1 file)
| File Path | Type | Test Count | Coverage |
| Đường Dẫn File | Loại | Số Test | Phạm Vi |
|-----------|------|-----------|----------|
| `domain/__tests__/inquiry-domain.spec.ts` | Jest | 5 tests | Entity creation, null phone, domain events, markAsRead |
| `domain/__tests__/inquiry-domain.spec.ts` | Jest | 5 test | Tạo entity, phone null, sự kiện domain, markAsRead |
---
### **INFRASTRUCTURE LAYER** (1 file)
### **TẦNG CƠ SỞ HẠ TẦNG (INFRASTRUCTURE LAYER)** (1 file)
#### Repository Implementation
#### Triển Khai Repository
| File Path | Type | Description |
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `infrastructure/repositories/prisma-inquiry.repository.ts` | Service | Implements IInquiryRepository using Prisma; 6 methods: findById, save, markAsRead, findByListing, findByAgent, countUnreadByAgent |
| `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 |
---
### **PRESENTATION LAYER** (5 files)
### **TẦNG TRÌNH BÀY (PRESENTATION LAYER)** (5 file)
#### Controller (1 file)
| File Path | Type | Routes | Auth |
| Đườ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 |
**Endpoints:**
- `POST /inquiries` - Create inquiry (BUYER)
- `GET /inquiries/listing/:listingId` - Get inquiries by listing
- `GET /inquiries/agent/me` - Get inquiries for logged-in agent (AGENT only)
- `PATCH /inquiries/:id/read` - Mark inquiry as read (AGENT only)
**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)
#### Data Transfer Objects (2 files)
#### Đối Tượng Truyền Dữ Liệu - Data Transfer Objects (2 file)
| File Path | Type | Properties | Validation |
| Đườ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? | Required string, max 2000 char message |
| `presentation/dto/list-inquiries.dto.ts` | DTO | page?, limit? | Int, min 1, max 100, defaults 1/20 |
| `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 |
#### Presentation Tests (1 file)
#### Test Tầng Trình Bày (1 file)
| File Path | Type | Test Count | Coverage |
| Đường Dẫn File | Loại | Số Test | Phạm Vi |
|-----------|------|-----------|----------|
| `presentation/__tests__/inquiries.controller.spec.ts` | Jest | 6 tests | All 4 endpoints, null phone handling, pagination defaults |
| `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 |
---
### **MODULE FILES** (2 files)
### **FILE MODULE** (2 file)
| File Path | Type | Description |
| Đường Dẫn File | Loại | Mô Tả |
|-----------|------|-------------|
| `inquiries.module.ts` | NestJS Module | Exports: InquiriesController; Providers: PrismaInquiryRepository, 2 command handlers, 2 query handlers |
| `index.ts` | Barrel Export | Exports: InquiriesModule, INQUIRY_REPOSITORY symbol, IInquiryRepository interface, InquiryEntity |
| `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 |
---
## 🏗️ MODULE ARCHITECTURE
## 🏗️ KIẾN TRÚC MODULE
### **Design Pattern: CQRS + Event Sourcing + DDD**
### **Mẫu Thiết Kế: CQRS + Event Sourcing + DDD**
```
┌─────────────────────────────────────────────────────────────┐
@@ -227,57 +227,57 @@ apps/api/src/modules/inquiries/
└──────────────────────┘
```
### **Data Flow**
### **Luồng Dữ Liệu**
**Creating Inquiry:**
**Tạo Yêu Cầu:**
```
POST /inquiries (CreateInquiryDto)
→ InquiriesController.createInquiry()
→ CommandBus.execute(CreateInquiryCommand)
→ CreateInquiryHandler.execute()
- Validate listing exists (Prisma)
- Create InquiryEntity
- Save to repository (Prisma)
- Publish InquiryCreatedEvent via EventBus
Return { id, listingId, createdAt }
- 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 }
```
**Marking as Read:**
**Đánh Dấu Đã Đọc:**
```
PATCH /inquiries/:id/read (agent only)
PATCH /inquiries/:id/read (chỉ agent)
→ InquiriesController.markAsRead()
→ CommandBus.execute(MarkInquiryReadCommand)
→ MarkInquiryReadHandler.execute()
- Find inquiry entity
- Verify agent is listing owner
- Call entity.markAsRead()
- Update in repository
- Publish InquiryReadEvent via EventBus
Return { success: true }
- 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 }
```
**Getting Inquiries:**
**Lấy Danh Sách Yêu Cầu:**
```
GET /inquiries/listing/:id or /agent/me (with pagination)
GET /inquiries/listing/:id or /agent/me (có phân trang)
→ InquiriesController.getByListing() or getMyInquiries()
→ QueryBus.execute(GetInquiriesByListingQuery or GetInquiriesByAgentQuery)
→ Handler delegates to repository
→ Handler ủy quyền cho repository
→ Repository.findByListing() or findByAgent()
- Queries Prisma with joins
- Maps to InquiryReadDto
- Returns PaginatedResult
Return paginated data
- Truy vấn Prisma với join
- Map sang InquiryReadDto
- Trả về PaginatedResult
Trả về dữ liệu phân trang
```
---
## 🔑 KEY CLASSES & HANDLERS
## 🔑 CÁC CLASS & HANDLER CHÍNH
### **Domain Entity: InquiryEntity**
```typescript
export class InquiryEntity extends AggregateRoot<string> {
// Properties
// Thuộc tính
private _listingId: string;
private _userId: string;
private _message: string;
@@ -286,13 +286,13 @@ export class InquiryEntity extends AggregateRoot<string> {
// Factory Method
static createNew(id, listingId, userId, message, phone): InquiryEntity
Creates new inquiry with isRead=false
Emits InquiryCreatedEvent
Tạo yêu cầu mới với isRead=false
Phát InquiryCreatedEvent
// Business Logic
markAsRead(): void
Sets isRead to true
Emits InquiryReadEvent
Đt isRead thành true
Phát InquiryReadEvent
}
```
@@ -302,17 +302,17 @@ export class InquiryEntity extends AggregateRoot<string> {
```typescript
class CreateInquiryHandler implements ICommandHandler<CreateInquiryCommand> {
async execute(command: CreateInquiryCommand): Promise<CreateInquiryResult> {
// 1. Validate listing exists
// 1. Xác thực listing tồn tại
const listing = await prisma.listing.findUnique(...)
if (!listing) throw NotFoundException
// 2. Create entity with factory
// 2. Tạo entity bằng factory
const inquiry = InquiryEntity.createNew(...)
// 3. Persist to database
// 3. Lưu vào cơ sở dữ liệu
await inquiryRepo.save(inquiry)
// 4. Publish domain events
// 4. Phát các sự kiện domain
const events = inquiry.clearDomainEvents()
events.forEach(e => eventBus.publish(e))
@@ -325,22 +325,22 @@ class CreateInquiryHandler implements ICommandHandler<CreateInquiryCommand> {
```typescript
class MarkInquiryReadHandler implements ICommandHandler<MarkInquiryReadCommand> {
async execute(command: MarkInquiryReadCommand): Promise<void> {
// 1. Load aggregate root
// 1. Tải aggregate root
const inquiry = await inquiryRepo.findById(...)
if (!inquiry) throw NotFoundException
// 2. Verify authorization
// 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. Update domain state
// 3. Cập nhật trạng thái domain
inquiry.markAsRead()
// 4. Persist state
// 4. Lưu trạng thái
await inquiryRepo.markAsRead(...)
// 5. Publish events
// 5. Phát sự kiện
const events = inquiry.clearDomainEvents()
events.forEach(e => eventBus.publish(e))
}
@@ -366,155 +366,155 @@ class GetInquiriesByListingHandler implements IQueryHandler<GetInquiriesByListin
```typescript
class GetInquiriesByAgentHandler implements IQueryHandler<GetInquiriesByAgentQuery> {
async execute(query: GetInquiriesByAgentQuery): Promise<PaginatedResult<InquiryReadDto>> {
// 1. Resolve agent ID from user ID
// 1. Phân giải agent ID từ user ID
const agent = await prisma.agent.findUnique({ where: { userId } })
if (!agent) throw NotFoundException
// 2. Delegate to repository
// 2. Ủy quyền cho repository
return this.inquiryRepo.findByAgent(agent.id, page, limit)
}
}
```
### **Repository Implementation**
### **Triển Khai Repository**
#### PrismaInquiryRepository
**Methods:**
1. `findById(id)` - Single inquiry lookup
2. `save(entity)` - Create inquiry
3. `markAsRead(id)` - Update isRead flag
4. `findByListing(listingId, page, limit)` - Paginated search with joins
5. `findByAgent(agentId, page, limit)` - Paginated search via listing agent
6. `countUnreadByAgent(agentId)` - Unread count aggregation
**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
**Prisma Relations Used:**
- `inquiry.listing` → property (for title)
**Quan Hệ Prisma Được Sử Dụng:**
- `inquiry.listing` → property (để lấy tiêu đề)
- `inquiry.user` → fullName, phone
- `listing.agentId`agent filter
- Pagination: skip/take with orderBy descending
- `listing.agentId`lọc theo agent
- Phân trang: skip/take với orderBy giảm dần
---
## 🎯 DDD LAYER STRUCTURE
## 🎯 CẤU TRÚC TẦNG DDD
### **DOMAIN LAYER** (`domain/`)
### **TẦNG DOMAIN** (`domain/`)
**Purpose:** Pure business logic, independent of frameworks
**Mục Đích:** Logic nghiệp vụ thuần túy, độc lập với framework
**Contains:**
**Bao Gồm:**
- **Entities** - `inquiry.entity.ts` (Aggregate Root)
- Pure TypeScript class
- Encapsulates business rules (isRead flag, field validation)
- Factory method for creation
- Methods for state transitions
- Domain events collection
- 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`
- Record significant business occurrences
- Used for event sourcing & audit trails
- Plain data classes
- Ghi lại các sự kiện nghiệp vụ quan trọng
- Dùng cho event sourcing audit trail
- Các class dữ liệu đơn giản
- **Repositories** - `inquiry.repository.ts`, `inquiry-read.dto.ts`
- Interface defines contract (dependency inversion)
- Symbol for DI token
- Read DTO separate from write entity
- Pagination result interface
- 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
**Isolation:**
- Zero NestJS dependencies
- Zero database dependencies
- Zero external service dependencies
**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
---
### **APPLICATION LAYER** (`application/`)
### **TẦNG ỨNG DỤNG** (`application/`)
**Purpose:** Use cases & coordination
**Mục Đích:** Các use case & điều phối
**Contains:**
- **Commands** - Mutable operations
**Bao Gồm:**
- **Commands** - Các thao tác thay đổi trạng thái
- `CreateInquiryCommand` - Input DTO
- `MarkInquiryReadCommand` - Input DTO
- Handlers orchestrate domain operations
- Các handler điều phối các thao tác domain
- **Queries** - Immutable reads
- **Queries** - Các thao tác đọc không thay đổi trạng thái
- `GetInquiriesByListingQuery`
- `GetInquiriesByAgentQuery`
- Handlers delegate to repository
- Các handler ủy quyền cho repository
**Responsibilities:**
- Validate preconditions (listing exists, agent authorized)
- Coordinate domain entity operations
- Publish domain events
- Handle cross-cutting concerns (logging, etc.)
**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.)
**NestJS Integration:**
- `@CommandHandler()` decorator
- `@QueryHandler()` decorator
- Dependency injection via constructor
**Tích Hợp NestJS:**
- Decorator `@CommandHandler()`
- Decorator `@QueryHandler()`
- Dependency injection qua constructor
---
### **INFRASTRUCTURE LAYER** (`infrastructure/`)
### **TẦNG CƠ SỞ HẠ TẦNG** (`infrastructure/`)
**Purpose:** Database & persistence details
**Mục Đích:** Chi tiết cơ sở dữ liệu & lưu trữ
**Contains:**
**Bao Gồm:**
- **Repositories** - `prisma-inquiry.repository.ts`
- Implements domain repository interface
- Uses Prisma client for queries
- Maps database records ↔ domain entities
- Handles pagination logic
- 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
**Responsibilities:**
- Query building
- Result mapping
- Pagination calculation
- Join relationships
- Database-specific optimizations
**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
**Isolation:**
- Swappable implementations (could use TypeORM, MongoDB, etc.)
- Domain code unaffected by database changes
**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
---
### **PRESENTATION LAYER** (`presentation/`)
### **TẦNG TRÌNH BÀY** (`presentation/`)
**Purpose:** HTTP interface & I/O
**Mục Đích:** Giao diện HTTP & I/O
**Contains:**
**Bao Gồm:**
- **Controllers** - `inquiries.controller.ts`
- NestJS `@Controller` decorator
- HTTP route handlers
- Dispatch to CQRS bus
- Return HTTP responses
- 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`
- Input validation (class-validator)
- Swagger documentation (@ApiProperty)
- Separate from domain entities
- 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
**Responsibilities:**
- Route handling
- Request validation
- Authentication/Authorization
- Response formatting
- API documentation
**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
**NestJS Integration:**
- Decorators: `@Post`, `@Get`, `@Patch`
- Guards: `JwtAuthGuard`, `RolesGuard`
**Tích Hợp NestJS:**
- Decorator: `@Post`, `@Get`, `@Patch`
- Guard: `JwtAuthGuard`, `RolesGuard`
- Middleware: `@CurrentUser`, `@Roles`
---
## 📊 TEST FILES SUMMARY
## 📊 TÓM TẮT FILE TEST
### **Test Statistics**
### **Thống Kê Test**
| Layer | File | Tests | Type |
| 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 |
@@ -522,114 +522,114 @@ class GetInquiriesByAgentHandler implements IQueryHandler<GetInquiriesByAgentQue
| 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 |
| **TOTAL** | **6 files** | **24 tests** | Jest + Vitest |
| **TỔNG CỘNG** | **6 file** | **24 test** | Jest + Vitest |
### **Test Coverage by File**
### **Phạm Vi Test Theo File**
#### Domain Tests (`inquiry-domain.spec.ts`) - 5 tests
#### Test Domain (`inquiry-domain.spec.ts`) - 5 test
```
✓ InquiryEntity.createNew() with phone
✓ InquiryEntity.createNew() with null phone
✓ createNew() emits InquiryCreatedEvent
✓ markAsRead() sets flag to true
✓ markAsRead() emits InquiryReadEvent
✓ 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
```
#### CreateInquiryHandler Tests - 4 tests
#### Test CreateInquiryHandler - 4 test
```
Creates inquiry successfully
Throws NotFoundException for missing listing
✓ Publishes domain events after saving
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
```
#### MarkInquiryReadHandler Tests - 5 tests
#### Test MarkInquiryReadHandler - 5 test
```
Marks inquiry as read successfully
Throws NotFoundException when inquiry not found
Throws NotFoundException when listing not found
Throws ForbiddenException when user not agent
Throws ForbiddenException when agent not found
Đá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ý
```
#### GetInquiriesByListingHandler Tests - 2 tests
#### Test GetInquiriesByListingHandler - 2 test
```
Returns paginated results
Returns empty data when no inquiries
Trả về kết quả phân trang
Trả về dữ liệu rỗng khi không có yêu cầu
```
#### GetInquiriesByAgentHandler Tests - 2 tests
#### Test GetInquiriesByAgentHandler - 2 test
```
Returns paginated results
Throws NotFoundException for non-agent user
Trả về kết quả phân trang
Ném NotFoundException cho người dùng không phải đại lý
```
#### InquiriesController Tests - 6 tests
#### Test InquiriesController - 6 test
```
✓ POST creates inquiry with command dispatch
✓ POST passes null phone when not provided
✓ GET /listing dispatches query with defaults
✓ GET /listing passes custom pagination
✓ GET /agent/me dispatches agent query
✓ PATCH marks inquiry and returns success
✓ 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
```
---
## 🔍 SUMMARY STATISTICS
## 🔍 THỐNG KÊ TÓM TẮT
| Metric | Count |
| Chỉ Số | Số Lượng |
|--------|-------|
| **Total Files** | 25 |
| **Source Files (.ts, excluding tests)** | 19 |
| **Test Files** | 6 |
| **Total Lines of Code** | 1,212 |
| **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 Handlers** | 2 |
| **Query Handlers** | 2 |
| **Domain Events** | 2 |
| **HTTP Endpoints** | 4 |
| **DTOs** | 2 |
| **Interfaces** | 3 (IInquiryRepository, PaginatedResult, InquiryReadDto) |
| **Test Suites** | 6 |
| **Test Cases** | 24 |
| **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 |
---
## 🛠️ KEY DEPENDENCIES
## 🛠️ CÁC PHỤ THUỘC CHÍNH
**External Packages:**
**Gói Bên Ngoài:**
- `@nestjs/common` - Framework
- `@nestjs/cqrs` - CQRS bus
- `@paralleldrive/cuid2` - ID generation
- `@paralleldrive/cuid2` - Tạo ID
- `@prisma/client` - ORM
- `class-validator` - DTO validation
- `@nestjs/swagger` - API documentation
- `class-validator` - Kiểm tra hợp lệ DTO
- `@nestjs/swagger` - Tài liệu API
**Internal Modules:**
- `@modules/shared` - AggregateRoot, DomainEvent, exceptions
**Module Nội Bộ:**
- `@modules/shared` - AggregateRoot, DomainEvent, các ngoại lệ
- `@modules/auth` - JwtPayload, JwtAuthGuard, RolesGuard
---
## 🔐 Security & Authorization
## 🔐 Bảo Mật & Phân Quyền
**Authentication:**
- All endpoints require `@UseGuards(JwtAuthGuard)`
- JWT token extracted via `@CurrentUser()` decorator
**Xác Thực:**
- Tất cả endpoint yêu cầu `@UseGuards(JwtAuthGuard)`
- JWT token được trích xuất qua decorator `@CurrentUser()`
**Authorization:**
- `GET /agent/me``@Roles('AGENT')` enforced
- `PATCH /:id/read``@Roles('AGENT')` enforced
- **MarkInquiryReadHandler** verifies:
- Inquiry exists
- Listing exists
- User is registered as agent
- Agent owns the listing
**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
---
## 📝 API CONTRACTS
## 📝 HỢP ĐỒNG API
### POST /inquiries
```
@@ -654,49 +654,49 @@ Status: 200 OK | 401 Unauthorized | 403 Forbidden
### PATCH /inquiries/:id/read
```
Request: (no body)
Request: (không có body)
Response: { success: boolean }
Status: 200 OK | 401 Unauthorized | 403 Forbidden | 404 Not Found
```
---
## 🎓 ARCHITECTURAL INSIGHTS
## 🎓 NHẬN XÉT VỀ KIẾN TRÚC
### **Strengths**
### **Điểm Mạnh**
1. **Clean Architecture** - Clear separation of concerns across layers
2. **CQRS Pattern** - Separate read/write paths enable scalability
3. **Domain-Driven Design** - Business logic in entities, not anemic models
4. **Event-Driven** - Domain events enable audit trails and event sourcing
5. **Testability** - Each layer independently testable with mocks
6. **Type Safety** - Full TypeScript with interfaces and strict types
7. **DI Framework** - NestJS provides dependency injection out of box
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 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ó
### **Design Decisions**
### **Quyết Định Thiết Kế**
1. **InquiryEntity as Aggregate Root**
- Encapsulates inquiry business rules
- Controls state transitions via methods (createNew, markAsRead)
- Collects domain events
1. **InquiryEntity 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. **Repository Pattern**
- Interface in domain, implementation in infrastructure
- Allows swapping data sources without affecting business logic
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. **Separate Read/Write DTOs**
- `CreateInquiryDto` (input) vs `InquiryReadDto` (output)
- Enables flexible API evolution
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 Handlers**
- Commands handle mutations with authorization
- Queries handle reads with filtering
- Both independent, can be optimized separately
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. **Pagination Interface**
- Consistent pagination across all list endpoints
- Page + limit model with calculated totalPages
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
---
**End of Exploration Report**
**Kết Thúc Báo Cáo Khám Phá**