Files
goodgo-platform/docs/audits/INQUIRIES_MODULE_EXPLORATION_2.md
Ho Ngoc Hai 25b22ea9bd docs: move additional exploration docs to docs/audits/
Move 6 recently generated inquiry and MCP exploration documents
to the centralized audit directory.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-11 01:41:23 +07:00

23 KiB

Inquiries Module - Complete Exploration

GoodGo Platform Backend API
Module Location: apps/api/src/modules/inquiries/
Total Lines of Code: 1,212 lines
Date Explored: April 11, 2026


📋 TABLE OF CONTENTS

  1. Directory Structure
  2. Complete File Listing
  3. Module Architecture
  4. Key Classes & Handlers
  5. DDD Layer Analysis
  6. Test Files Summary

📁 DIRECTORY STRUCTURE

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

📄 COMPLETE FILE LISTING

APPLICATION LAYER (8 files)

Commands (4 files)

File Path Type Description
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

Queries (4 files)

File Path Type Description
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 Tests (4 files)

File Path Type Test Count Coverage
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

DOMAIN LAYER (6 files)

Entities (1 file)

File Path Type Description
domain/entities/inquiry.entity.ts Aggregate Root Core domain entity with id, listingId, userId, message, phone, isRead; methods: createNew(), markAsRead()

Events (2 files)

File Path Type Description
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

Repositories (2 files)

File Path Type Description
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 Tests (1 file)

File Path Type Test Count Coverage
domain/__tests__/inquiry-domain.spec.ts Jest 5 tests Entity creation, null phone, domain events, markAsRead

INFRASTRUCTURE LAYER (1 file)

Repository Implementation

File Path Type Description
infrastructure/repositories/prisma-inquiry.repository.ts Service Implements IInquiryRepository using Prisma; 6 methods: findById, save, markAsRead, findByListing, findByAgent, countUnreadByAgent

PRESENTATION LAYER (5 files)

Controller (1 file)

File Path Type 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)

Data Transfer Objects (2 files)

File Path Type Properties 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 Tests (1 file)

File Path Type Test Count Coverage
presentation/__tests__/inquiries.controller.spec.ts Jest 6 tests All 4 endpoints, null phone handling, pagination defaults

MODULE FILES (2 files)

File Path Type Description
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

🏗️ MODULE ARCHITECTURE

Design Pattern: 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     │
    └──────────────────────┘

Data Flow

Creating 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 }

Marking as Read:

PATCH /inquiries/:id/read (agent only)
  → 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 }

Getting Inquiries:

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

🔑 KEY CLASSES & HANDLERS

Domain Entity: InquiryEntity

export class InquiryEntity extends AggregateRoot<string> {
  // Properties
  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
     Creates new inquiry with isRead=false
     Emits InquiryCreatedEvent

  // Business Logic
  markAsRead(): void
     Sets isRead to true
     Emits InquiryReadEvent
}

Command Handlers

CreateInquiryHandler

class CreateInquiryHandler implements ICommandHandler<CreateInquiryCommand> {
  async execute(command: CreateInquiryCommand): Promise<CreateInquiryResult> {
    // 1. Validate listing exists
    const listing = await prisma.listing.findUnique(...)
    if (!listing) throw NotFoundException

    // 2. Create entity with factory
    const inquiry = InquiryEntity.createNew(...)

    // 3. Persist to database
    await inquiryRepo.save(inquiry)

    // 4. Publish domain events
    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. Load aggregate root
    const inquiry = await inquiryRepo.findById(...)
    if (!inquiry) throw NotFoundException

    // 2. Verify authorization
    const listing = await prisma.listing.findUnique(...)
    const agent = await prisma.agent.findUnique(...)
    if (agent.id !== listing.agentId) throw ForbiddenException

    // 3. Update domain state
    inquiry.markAsRead()

    // 4. Persist state
    await inquiryRepo.markAsRead(...)

    // 5. Publish events
    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. Resolve agent ID from user ID
    const agent = await prisma.agent.findUnique({ where: { userId } })
    if (!agent) throw NotFoundException

    // 2. Delegate to repository
    return this.inquiryRepo.findByAgent(agent.id, page, limit)
  }
}

Repository Implementation

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

Prisma Relations Used:

  • inquiry.listing → property (for title)
  • inquiry.user → fullName, phone
  • listing.agentId → agent filter
  • Pagination: skip/take with orderBy descending

🎯 DDD LAYER STRUCTURE

DOMAIN LAYER (domain/)

Purpose: Pure business logic, independent of frameworks

Contains:

  • 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
  • Events - inquiry-created.event.ts, inquiry-read.event.ts

    • Record significant business occurrences
    • Used for event sourcing & audit trails
    • Plain data classes
  • 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

Isolation:

  • Zero NestJS dependencies
  • Zero database dependencies
  • Zero external service dependencies

APPLICATION LAYER (application/)

Purpose: Use cases & coordination

Contains:

  • Commands - Mutable operations

    • CreateInquiryCommand - Input DTO
    • MarkInquiryReadCommand - Input DTO
    • Handlers orchestrate domain operations
  • Queries - Immutable reads

    • GetInquiriesByListingQuery
    • GetInquiriesByAgentQuery
    • Handlers delegate to repository

Responsibilities:

  • Validate preconditions (listing exists, agent authorized)
  • Coordinate domain entity operations
  • Publish domain events
  • Handle cross-cutting concerns (logging, etc.)

NestJS Integration:

  • @CommandHandler() decorator
  • @QueryHandler() decorator
  • Dependency injection via constructor

INFRASTRUCTURE LAYER (infrastructure/)

Purpose: Database & persistence details

Contains:

  • Repositories - prisma-inquiry.repository.ts
    • Implements domain repository interface
    • Uses Prisma client for queries
    • Maps database records ↔ domain entities
    • Handles pagination logic

Responsibilities:

  • Query building
  • Result mapping
  • Pagination calculation
  • Join relationships
  • Database-specific optimizations

Isolation:

  • Swappable implementations (could use TypeORM, MongoDB, etc.)
  • Domain code unaffected by database changes

PRESENTATION LAYER (presentation/)

Purpose: HTTP interface & I/O

Contains:

  • Controllers - inquiries.controller.ts

    • NestJS @Controller decorator
    • HTTP route handlers
    • Dispatch to CQRS bus
    • Return HTTP responses
  • DTOs - create-inquiry.dto.ts, list-inquiries.dto.ts

    • Input validation (class-validator)
    • Swagger documentation (@ApiProperty)
    • Separate from domain entities

Responsibilities:

  • Route handling
  • Request validation
  • Authentication/Authorization
  • Response formatting
  • API documentation

NestJS Integration:

  • Decorators: @Post, @Get, @Patch
  • Guards: JwtAuthGuard, RolesGuard
  • Middleware: @CurrentUser, @Roles

📊 TEST FILES SUMMARY

Test Statistics

Layer File Tests Type
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
TOTAL 6 files 24 tests Jest + Vitest

Test Coverage by File

Domain Tests (inquiry-domain.spec.ts) - 5 tests

✓ InquiryEntity.createNew() with phone
✓ InquiryEntity.createNew() with null phone
✓ createNew() emits InquiryCreatedEvent
✓ markAsRead() sets flag to true
✓ markAsRead() emits InquiryReadEvent

CreateInquiryHandler Tests - 4 tests

✓ Creates inquiry successfully
✓ Throws NotFoundException for missing listing
✓ Publishes domain events after saving

MarkInquiryReadHandler Tests - 5 tests

✓ 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

GetInquiriesByListingHandler Tests - 2 tests

✓ Returns paginated results
✓ Returns empty data when no inquiries

GetInquiriesByAgentHandler Tests - 2 tests

✓ Returns paginated results
✓ Throws NotFoundException for non-agent user

InquiriesController Tests - 6 tests

✓ 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

🔍 SUMMARY STATISTICS

Metric Count
Total Files 25
Source Files (.ts, excluding tests) 19
Test Files 6
Total Lines of 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

🛠️ KEY DEPENDENCIES

External Packages:

  • @nestjs/common - Framework
  • @nestjs/cqrs - CQRS bus
  • @paralleldrive/cuid2 - ID generation
  • @prisma/client - ORM
  • class-validator - DTO validation
  • @nestjs/swagger - API documentation

Internal Modules:

  • @modules/shared - AggregateRoot, DomainEvent, exceptions
  • @modules/auth - JwtPayload, JwtAuthGuard, RolesGuard

🔐 Security & Authorization

Authentication:

  • All endpoints require @UseGuards(JwtAuthGuard)
  • JWT token extracted via @CurrentUser() decorator

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

📝 API CONTRACTS

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:  (no body)
Response: { success: boolean }
Status:   200 OK | 401 Unauthorized | 403 Forbidden | 404 Not Found

🎓 ARCHITECTURAL INSIGHTS

Strengths

  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

Design Decisions

  1. InquiryEntity as Aggregate Root

    • Encapsulates inquiry business rules
    • Controls state transitions via methods (createNew, markAsRead)
    • Collects domain events
  2. Repository Pattern

    • Interface in domain, implementation in infrastructure
    • Allows swapping data sources without affecting business logic
  3. Separate Read/Write DTOs

    • CreateInquiryDto (input) vs InquiryReadDto (output)
    • Enables flexible API evolution
  4. CQRS Handlers

    • Commands handle mutations with authorization
    • Queries handle reads with filtering
    • Both independent, can be optimized separately
  5. Pagination Interface

    • Consistent pagination across all list endpoints
    • Page + limit model with calculated totalPages

End of Exploration Report