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
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:
@@ -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 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)
|
||||
|
||||
---
|
||||
|
||||
## 📁 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 APPLICATION** (8 file)
|
||||
|
||||
#### Commands (4 files)
|
||||
#### 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 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 files)
|
||||
#### 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ê 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 |
|
||||
|
||||
#### Application Tests (4 files)
|
||||
#### Test Tầng Application (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 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 |
|
||||
|
||||
---
|
||||
|
||||
### **DOMAIN LAYER** (6 files)
|
||||
### **TẦNG DOMAIN** (6 file)
|
||||
|
||||
#### 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 | Entity domain cốt lõi với id, listingId, userId, message, phone, isRead; các phương thức: createNew(), markAsRead() |
|
||||
|
||||
#### Events (2 files)
|
||||
#### 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 | 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 files)
|
||||
#### Repositories (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 queries - bao gồm tiêu đề listing, thông tin người dùng, timestamps |
|
||||
|
||||
#### Domain Tests (1 file)
|
||||
#### Test Tầng 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, domain events, markAsRead |
|
||||
|
||||
---
|
||||
|
||||
### **INFRASTRUCTURE LAYER** (1 file)
|
||||
### **TẦNG INFRASTRUCTURE** (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 dùng Prisma; 6 phương thức: findById, save, markAsRead, findByListing, findByAgent, countUnreadByAgent |
|
||||
|
||||
---
|
||||
|
||||
### **PRESENTATION LAYER** (5 files)
|
||||
### **TẦNG PRESENTATION** (5 file)
|
||||
|
||||
#### Controller (1 file)
|
||||
|
||||
| File Path | Type | Routes | Auth |
|
||||
| Đườ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 |
|
||||
|
||||
**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 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 files)
|
||||
#### Data Transfer Objects (2 file)
|
||||
|
||||
| File Path | Type | Properties | Validation |
|
||||
| Đường Dẫn File | Loại | Thuộc Tính | Validation |
|
||||
|-----------|------|-----------|------------|
|
||||
| `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, tối thiểu 1, tối đa 100, mặc định 1/20 |
|
||||
|
||||
#### Presentation Tests (1 file)
|
||||
#### Test Tầng Presentation (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 | Cả 4 endpoint, xử lý phone null, giá trị mặc định phân trang |
|
||||
|
||||
---
|
||||
|
||||
### **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 | Exports: InquiriesController; Providers: PrismaInquiryRepository, 2 command handler, 2 query handler |
|
||||
| `index.ts` | Barrel Export | Exports: 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 Inquiry:**
|
||||
```
|
||||
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 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 }
|
||||
```
|
||||
|
||||
**Getting Inquiries:**
|
||||
**Lấy Danh Sách Inquiry:**
|
||||
```
|
||||
GET /inquiries/listing/:id or /agent/me (with pagination)
|
||||
→ InquiriesController.getByListing() or getMyInquiries()
|
||||
→ QueryBus.execute(GetInquiriesByListingQuery or GetInquiriesByAgentQuery)
|
||||
→ Handler delegates to repository
|
||||
→ Repository.findByListing() or findByAgent()
|
||||
- Queries Prisma with joins
|
||||
- Maps to InquiryReadDto
|
||||
- Returns PaginatedResult
|
||||
→ Return paginated data
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 KEY CLASSES & HANDLERS
|
||||
## 🔑 CÁC LỚP & 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 inquiry 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 qua factory
|
||||
const inquiry = InquiryEntity.createNew(...)
|
||||
|
||||
// 3. Persist to database
|
||||
// 3. Lưu vào database
|
||||
await inquiryRepo.save(inquiry)
|
||||
|
||||
// 4. Publish domain events
|
||||
// 4. Phát domain events
|
||||
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 quyền hạ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. Tra cứu 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 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
|
||||
|
||||
**Prisma Relations Used:**
|
||||
- `inquiry.listing` → property (for title)
|
||||
**Quan Hệ Prisma 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
|
||||
- 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`
|
||||
- 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 và audit trail
|
||||
- Lớp 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 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
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
### **APPLICATION LAYER** (`application/`)
|
||||
### **TẦNG APPLICATION** (`application/`)
|
||||
|
||||
**Purpose:** Use cases & coordination
|
||||
**Mục Đích:** Use case và điều phối
|
||||
|
||||
**Contains:**
|
||||
- **Commands** - Mutable operations
|
||||
**Bao Gồm:**
|
||||
- **Commands** - Các thao tác thay đổi dữ liệu
|
||||
- `CreateInquiryCommand` - Input DTO
|
||||
- `MarkInquiryReadCommand` - Input DTO
|
||||
- Handlers orchestrate domain operations
|
||||
- Các handler điều phối thao tác domain
|
||||
|
||||
- **Queries** - Immutable reads
|
||||
- **Queries** - Các đọc bất biến
|
||||
- `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:**
|
||||
- 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.)
|
||||
|
||||
**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 INFRASTRUCTURE** (`infrastructure/`)
|
||||
|
||||
**Purpose:** Database & persistence details
|
||||
**Mục Đích:** Chi tiết database và persistence
|
||||
|
||||
**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 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
|
||||
|
||||
**Responsibilities:**
|
||||
- Query building
|
||||
- Result mapping
|
||||
- Pagination calculation
|
||||
- Join relationships
|
||||
- Database-specific optimizations
|
||||
**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
|
||||
|
||||
**Isolation:**
|
||||
- Swappable implementations (could use TypeORM, MongoDB, etc.)
|
||||
- Domain code unaffected by database changes
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
### **PRESENTATION LAYER** (`presentation/`)
|
||||
### **TẦNG PRESENTATION** (`presentation/`)
|
||||
|
||||
**Purpose:** HTTP interface & I/O
|
||||
**Mục Đích:** Giao diện HTTP và I/O
|
||||
|
||||
**Contains:**
|
||||
**Bao Gồm:**
|
||||
- **Controllers** - `inquiries.controller.ts`
|
||||
- NestJS `@Controller` decorator
|
||||
- HTTP route handlers
|
||||
- Dispatch to CQRS bus
|
||||
- Return HTTP responses
|
||||
- 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`
|
||||
- Input validation (class-validator)
|
||||
- Swagger documentation (@ApiProperty)
|
||||
- Separate from domain entities
|
||||
- Xác thực đầ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
|
||||
- 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
|
||||
|
||||
**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** | **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() có 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 inquiry thành công
|
||||
✓ Ném NotFoundException khi listing không tồn tại
|
||||
✓ Phát domain events 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 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
|
||||
```
|
||||
|
||||
#### 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ó inquiry
|
||||
```
|
||||
|
||||
#### 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 agent
|
||||
```
|
||||
|
||||
#### 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 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 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, 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 Endpoints** | 4 |
|
||||
| **HTTP Endpoint** | 4 |
|
||||
| **DTOs** | 2 |
|
||||
| **Interfaces** | 3 (IInquiryRepository, PaginatedResult, InquiryReadDto) |
|
||||
| **Test Suites** | 6 |
|
||||
| **Test Cases** | 24 |
|
||||
| **Test Suite** | 6 |
|
||||
| **Test Case** | 24 |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ KEY DEPENDENCIES
|
||||
## 🛠️ CÁC DEPENDENCY 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` - Xác thực DTO
|
||||
- `@nestjs/swagger` - Tài liệu API
|
||||
|
||||
**Internal Modules:**
|
||||
**Module Nội Bộ:**
|
||||
- `@modules/shared` - AggregateRoot, DomainEvent, exceptions
|
||||
- `@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 Danh Tính:**
|
||||
- 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` → á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
|
||||
|
||||
---
|
||||
|
||||
## 📝 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 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 đọ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ó
|
||||
|
||||
### **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 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. **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. **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 handle mutations with authorization
|
||||
- Queries handle reads with filtering
|
||||
- Both independent, can be optimized separately
|
||||
- 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. **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ả 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á**
|
||||
|
||||
Reference in New Issue
Block a user