# Module Yêu Cầu - Tài Liệu Tham Khảo Nhanh ## 📊 Tổng Quan Module **Đường dẫn:** `apps/api/src/modules/inquiries/` **Kiến trúc:** CQRS + DDD **Tổng số tệp:** 25 **Tổng số dòng mã:** 1,212 **Độ phủ kiểm thử:** 6 bộ kiểm thử, 24 bài kiểm thử --- ## 📁 CÁC TỆP THEO TẦNG ### TRÌNH BÀY (5 tệp) ``` presentation/ ├── controllers/inquiries.controller.ts [4 endpoints] ├── dto/create-inquiry.dto.ts [3 properties] ├── dto/list-inquiries.dto.ts [2 properties] └── __tests__/inquiries.controller.spec.ts [6 tests] ``` ### ỨNG DỤNG (8 tệp) ``` application/ ├── commands/create-inquiry/ │ ├── create-inquiry.command.ts │ └── create-inquiry.handler.ts ├── commands/mark-inquiry-read/ │ ├── mark-inquiry-read.command.ts │ └── mark-inquiry-read.handler.ts ├── queries/get-inquiries-by-agent/ │ ├── get-inquiries-by-agent.query.ts │ └── get-inquiries-by-agent.handler.ts ├── queries/get-inquiries-by-listing/ │ ├── get-inquiries-by-listing.query.ts │ └── get-inquiries-by-listing.handler.ts └── __tests__/ [4 test files, 13 tests] ``` ### MIỀN (6 tệp) ``` domain/ ├── entities/inquiry.entity.ts [Aggregate Root] ├── events/ │ ├── inquiry-created.event.ts │ └── inquiry-read.event.ts ├── repositories/ │ ├── inquiry.repository.ts [Interface + Symbol] │ └── inquiry-read.dto.ts [Read DTO] └── __tests__/inquiry-domain.spec.ts [5 tests] ``` ### HẠ TẦNG (1 tệp) ``` infrastructure/ └── repositories/ └── prisma-inquiry.repository.ts [6 methods] ``` ### MODULE (2 tệp) ``` inquiries.module.ts [NestJS Module] index.ts [Barrel export] ``` --- ## 🔄 LUỒNG YÊU CẦU ### TẠO YÊU CẦU ``` POST /inquiries { listingId, message, phone? } ↓ InquiriesController.createInquiry() ↓ CommandBus.execute(CreateInquiryCommand) ↓ CreateInquiryHandler 1. Validate listing exists (Prisma) 2. Create InquiryEntity 3. Save to PrismaInquiryRepository 4. Publish InquiryCreatedEvent ↓ Response: { id, listingId, createdAt } ``` ### ĐÁNH DẤU ĐÃ ĐỌC ``` PATCH /inquiries/:id/read (AGENT only) ↓ InquiriesController.markAsRead() ↓ CommandBus.execute(MarkInquiryReadCommand) ↓ MarkInquiryReadHandler 1. Load inquiry entity 2. Verify agent owns listing 3. Call inquiry.markAsRead() 4. Update in repository 5. Publish InquiryReadEvent ↓ Response: { success: true } ``` ### DANH SÁCH THEO TIN ĐĂNG ``` GET /inquiries/listing/:listingId?page=1&limit=20 ↓ InquiriesController.getByListing() ↓ QueryBus.execute(GetInquiriesByListingQuery) ↓ GetInquiriesByListingHandler ↓ PrismaInquiryRepository.findByListing() ↓ Response: PaginatedResult ``` ### DANH SÁCH THEO MÔI GIỚI ``` GET /inquiries/agent/me?page=1&limit=20 (AGENT only) ↓ InquiriesController.getMyInquiries() ↓ QueryBus.execute(GetInquiriesByAgentQuery) ↓ GetInquiriesByAgentHandler 1. Resolve agent from userId 2. Delegate to repository ↓ PrismaInquiryRepository.findByAgent() ↓ Response: PaginatedResult ``` --- ## 🔑 CÁC LỚP CHÍNH | Lớp | Vị trí | Mục đích | |-----|--------|----------| | **InquiryEntity** | domain/entities/ | Gốc tổng hợp với logic nghiệp vụ | | **CreateInquiryHandler** | application/commands/create-inquiry/ | Thực thi lệnh tạo mới | | **MarkInquiryReadHandler** | application/commands/mark-inquiry-read/ | Thực thi lệnh đánh dấu đã đọc | | **GetInquiriesByListingHandler** | application/queries/get-inquiries-by-listing/ | Phân giải yêu cầu theo tin đăng | | **GetInquiriesByAgentHandler** | application/queries/get-inquiries-by-agent/ | Phân giải yêu cầu theo môi giới | | **PrismaInquiryRepository** | infrastructure/repositories/ | Triển khai lưu trữ dữ liệu | | **InquiriesController** | presentation/controllers/ | Các endpoint HTTP | --- ## 📝 CÁC GIAO DIỆN CHÍNH ```typescript // Domain interface (repository contract) interface IInquiryRepository { findById(id: string): Promise save(inquiry: InquiryEntity): Promise markAsRead(id: string): Promise findByListing(listingId, page, limit): Promise> findByAgent(agentId, page, limit): Promise> countUnreadByAgent(agentId): Promise } // Read DTO (queries only) interface InquiryReadDto { id: string listingId: string listingTitle: string userId: string userName: string userPhone: string message: string phone: string | null isRead: boolean createdAt: string } // Pagination result interface PaginatedResult { data: T[] total: number page: number limit: number totalPages: number } ``` --- ## 🧪 CÁC TỆP KIỂM THỬ TỔNG QUAN | Tệp kiểm thử | Số bài | Nội dung | |--------------|--------|----------| | `domain/__tests__/inquiry-domain.spec.ts` | 5 | Tạo thực thể, sự kiện | | `application/__tests__/create-inquiry.handler.spec.ts` | 4 | Thành công của handler, kiểm tra đầu vào | | `application/__tests__/mark-inquiry-read.handler.spec.ts` | 5 | Thành công của handler, kiểm tra xác thực | | `application/__tests__/get-inquiries-by-listing.handler.spec.ts` | 2 | Kết quả truy vấn, trạng thái rỗng | | `application/__tests__/get-inquiries-by-agent.handler.spec.ts` | 2 | Kết quả truy vấn, tra cứu môi giới | | `presentation/__tests__/inquiries.controller.spec.ts` | 6 | Tất cả endpoint, giá trị mặc định | | **TỔNG** | **24** | **Độ phủ toàn diện** | --- ## 🔐 Ma Trận Phân Quyền | Endpoint | Xác thực | Vai trò | Truy vấn | |----------|----------|---------|----------| | `POST /inquiries` | JWT | Bất kỳ | - | | `GET /listing/:id` | JWT | Bất kỳ | page, limit | | `GET /agent/me` | JWT | AGENT | page, limit | | `PATCH /:id/read` | JWT | AGENT | - | **Kiểm tra quyền:** - MarkInquiryReadHandler: Xác minh người dùng là môi giới, môi giới sở hữu tin đăng --- ## 🎯 NGUYÊN TẮC DDD ### Đóng Gói Thực Thể Miền ```typescript // Factory method (controlled creation) static createNew(id, listingId, userId, message, phone): InquiryEntity → Creates entity with isRead=false → Emits InquiryCreatedEvent // Domain methods (state transitions) markAsRead(): void → Sets isRead=true → Emits InquiryReadEvent ``` ### Mẫu Repository - **Giao diện trong Miền** → `IInquiryRepository` - **Triển khai trong Hạ tầng** → `PrismaInquiryRepository` - **Tiêm phụ thuộc** → ký hiệu `INQUIRY_REPOSITORY` ### Mô Hình Đọc/Ghi Tách Biệt - **Mô hình Ghi:** `InquiryEntity` (tổng hợp) - **Mô hình Đọc:** `InquiryReadDto` (DTO truy vấn) --- ## 🔄 Sự Kiện Miền | Sự kiện | Thời điểm | Dữ liệu | |---------|-----------|---------| | **InquiryCreatedEvent** | Yêu cầu được tạo | aggregateId, listingId, userId | | **InquiryReadEvent** | Được đánh dấu đã đọc | aggregateId, listingId, userId | --- ## 💾 Thao Tác Cơ Sở Dữ Liệu **Các Prisma Model được sử dụng:** - `inquiry` - Thực thể chính - `listing` - Cho khóa ngoại & tra cứu môi giới - `property` - Cho tiêu đề tin đăng - `user` - Cho tên & số điện thoại người mua **Các Truy Vấn Chính:** - `inquiry.create()` - Tạo yêu cầu mới - `inquiry.update()` - Đánh dấu đã đọc - `inquiry.findMany()` - Phân trang - `inquiry.count()` - Đếm tổng số --- ## 🚀 Điểm Vào ```typescript // Module export export { InquiriesModule } // Exported interfaces export { INQUIRY_REPOSITORY, type IInquiryRepository } export { InquiryEntity } // Usage in other modules import { InquiriesModule } from '@modules/inquiries' ``` --- ## 🎓 Tóm Tắt Kiến Trúc ``` KIẾN TRÚC SẠCH với CQRS + DDD Tầng Trình Bày (Controllers + DTOs) ↓ Tầng Ứng Dụng (CQRS Handlers) ↓ Tầng Miền (Entities + Events + Interfaces) ↓ Tầng Hạ Tầng (Prisma Repository) ↓ Cơ Sở Dữ Liệu ``` **Đặc Điểm Chính:** ✅ Đảo Ngược Phụ Thuộc - Miền định nghĩa các hợp đồng ✅ Phân Tách Mối Quan Tâm - Mỗi tầng có trách nhiệm rõ ràng ✅ Khả Năng Kiểm Thử - Triển khai giả lập tại mỗi tầng ✅ Hướng Sự Kiện - Sự kiện miền cho kiểm toán & tích hợp ✅ CQRS - Tách biệt lệnh & truy vấn để mở rộng quy mô ✅ An Toàn Kiểu - TypeScript đầy đủ với giao diện nghiêm ngặt --- ## 📌 Các Mẫu Thường Dùng **Mẫu Command:** ```typescript // Send command commandBus.execute(new CreateInquiryCommand(...)) // Handler processes @CommandHandler(CreateInquiryCommand) class CreateInquiryHandler { ... } ``` **Mẫu Query:** ```typescript // Send query queryBus.execute(new GetInquiriesByListingQuery(...)) // Handler processes @QueryHandler(GetInquiriesByListingQuery) class GetInquiriesByListingHandler { ... } ``` **Mẫu Tiêm Phụ Thuộc:** ```typescript @Injectable() export class Handler { constructor( @Inject(INQUIRY_REPOSITORY) private repo: IInquiryRepository, private prisma: PrismaService, ) {} } ``` --- ## 🔍 Nơi Tìm Kiếm... | Nhu cầu | Tệp | |---------|-----| | Thêm endpoint mới | `presentation/controllers/inquiries.controller.ts` | | Thêm command | `application/commands/[name]/[name].command.ts` + `[name].handler.ts` | | Thêm query | `application/queries/[name]/[name].query.ts` + `[name].handler.ts` | | Logic nghiệp vụ | `domain/entities/inquiry.entity.ts` | | Sự kiện miền mới | `domain/events/[name].event.ts` | | Truy vấn cơ sở dữ liệu | `infrastructure/repositories/prisma-inquiry.repository.ts` | | Kiểm tra đầu vào | `presentation/dto/*.ts` | | Viết kiểm thử | `[layer]/__tests__/*` | --- **Cập Nhật Lần Cuối:** 11 tháng 4, 2026