Files
goodgo-platform/docs/audits/INQUIRIES_MODULE_EXPLORATION.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

25 KiB

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
  2. Danh Sách File Đầy Đủ
  3. Kiến Trúc Module
  4. Các Class & Handler Chính
  5. Phân Tích Tầng DDD
  6. Tóm Tắt File Test

📁 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 ỨNG DỤNG (APPLICATION LAYER) (8 file)

Lệnh (Commands) (4 file)

Đường Dẫn File Loại Mô Tả
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

Truy Vấ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ê 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

Test Tầng Ứng Dụng (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 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

TẦNG DOMAIN (6 file)

Thực Thể (Entities) (1 file)

Đường Dẫn File Loại Mô Tả
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()

Sự Kiện (Events) (2 file)

Đường Dẫn File Loại Mô Tả
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

Repository (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 các truy vấn - bao gồm tiêu đề listing, thông tin người dùng, dấu thời gian

Test 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, sự kiện domain, markAsRead

TẦNG CƠ SỞ HẠ TẦNG (INFRASTRUCTURE LAYER) (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 sử dụng Prisma; 6 phương thức: findById, save, markAsRead, findByListing, findByAgent, countUnreadByAgent

TẦNG TRÌNH BÀY (PRESENTATION LAYER) (5 file)

Controller (1 file)

Đườ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

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)

Đối Tượng Truyền Dữ Liệu - Data Transfer Objects (2 file)

Đườ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? 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

Test Tầng Trình Bày (1 file)

Đường Dẫn File Loại Số Test Phạm Vi
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

FILE MODULE (2 file)

Đường Dẫn File Loại Mô Tả
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

🏗️ 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 Yêu Cầu:

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 đạ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 }

Lấy Danh Sách Yêu Cầu:

GET /inquiries/listing/:id or /agent/me (có phân trang)
  → InquiriesController.getByListing() or getMyInquiries()
  → QueryBus.execute(GetInquiriesByListingQuery or GetInquiriesByAgentQuery)
  → Handler ủy quyền cho repository
  → Repository.findByListing() or findByAgent()
    - Truy vấn Prisma với join
    - Map sang InquiryReadDto
    - Trả về PaginatedResult
  → Trả về dữ liệu phân trang

🔑 CÁC CLASS & HANDLER CHÍNH

Domain Entity: InquiryEntity

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 yêu cầu mới với isRead=false
     Phát InquiryCreatedEvent

  // Business Logic
  markAsRead(): void
     Đặt isRead thành true
     Phát InquiryReadEvent
}

Command Handlers

CreateInquiryHandler

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 bằng factory
    const inquiry = InquiryEntity.createNew(...)

    // 3. Lưu vào cơ sở dữ liệu
    await inquiryRepo.save(inquiry)

    // 4. Phát các sự kiện domain
    const events = inquiry.clearDomainEvents()
    events.forEach(e => eventBus.publish(e))

    return { id, listingId, createdAt }
  }
}

MarkInquiryReadHandler

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 phân quyề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

class GetInquiriesByListingHandler implements IQueryHandler<GetInquiriesByListingQuery> {
  async execute(query: GetInquiriesByListingQuery): Promise<PaginatedResult<InquiryReadDto>> {
    return this.inquiryRepo.findByListing(
      query.listingId,
      query.page,
      query.limit
    )
  }
}

GetInquiriesByAgentHandler

class GetInquiriesByAgentHandler implements IQueryHandler<GetInquiriesByAgentQuery> {
  async execute(query: GetInquiriesByAgentQuery): Promise<PaginatedResult<InquiryReadDto>> {
    // 1. Phân giải 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 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

Quan Hệ Prisma Được 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)

    • 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

    • Ghi lại các sự kiện nghiệp vụ quan trọng
    • Dùng cho event sourcing và audit trail
    • Các class 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 dùng cho DI token
    • Read DTO tách biệt với write entity
    • Interface kết quả phân trang

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

TẦNG ỨNG DỤNG (application/)

Mục Đích: Các use case & điều phối

Bao Gồm:

  • Commands - Các thao tác thay đổi trạng thái

    • CreateInquiryCommand - Input DTO
    • MarkInquiryReadCommand - Input DTO
    • Các handler điều phối các thao tác domain
  • Queries - Các thao tác đọc không thay đổi trạng thái

    • GetInquiriesByListingQuery
    • GetInquiriesByAgentQuery
    • Các handler ủy quyền cho repository

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.)

Tích Hợp NestJS:

  • Decorator @CommandHandler()
  • Decorator @QueryHandler()
  • Dependency injection qua constructor

TẦNG CƠ SỞ HẠ TẦNG (infrastructure/)

Mục Đích: Chi tiết cơ sở dữ liệu & lưu trữ

Bao Gồm:

  • Repositories - prisma-inquiry.repository.ts
    • 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

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

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

TẦNG TRÌNH BÀY (presentation/)

Mục Đích: Giao diện HTTP & I/O

Bao Gồm:

  • Controllers - inquiries.controller.ts

    • 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

    • 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

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

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 CỘNG 6 file 24 test Jest + Vitest

Phạm Vi Test Theo File

Test Domain (inquiry-domain.spec.ts) - 5 test

✓ 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

Test CreateInquiryHandler - 4 test

✓ 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

Test MarkInquiryReadHandler - 5 test

✓ Đá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ý

Test GetInquiriesByListingHandler - 2 test

✓ Trả về kết quả phân trang
✓ Trả về dữ liệu rỗng khi không có yêu cầu

Test GetInquiriesByAgentHandler - 2 test

✓ Trả về kết quả phân trang
✓ Ném NotFoundException cho người dùng không phải đại lý

Test InquiriesController - 6 test

✓ 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

🔍 THỐNG KÊ TÓM TẮT

Chỉ Số Số Lượng
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 Handler 2
Query Handler 2
Domain Event 2
HTTP Endpoint 4
DTO 2
Interface 3 (IInquiryRepository, PaginatedResult, InquiryReadDto)
Test Suite 6
Test Case 24

🛠️ CÁC PHỤ THUỘC 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 - Kiểm tra hợp lệ DTO
  • @nestjs/swagger - Tài liệu API

Module Nội Bộ:

  • @modules/shared - AggregateRoot, DomainEvent, các ngoại lệ
  • @modules/auth - JwtPayload, JwtAuthGuard, RolesGuard

🔐 Bảo Mật & Phân Quyền

Xác Thực:

  • 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 → 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

📝 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 VỀ 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 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 và 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ó

Quyết Định Thiết Kế

  1. InquiryEntity là 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. 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. 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 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. 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

Kết Thúc Báo Cáo Khám Phá