Files
goodgo-platform/docs/audits/INQUIRIES_MODULE_QUICK_REFERENCE.md
Ho Ngoc Hai b8512ebff4 docs: consolidate audit and analysis reports into docs/audits/
Move 36 root-level audit/analysis documents and 7 web app audit documents
into docs/audits/ directory to declutter the project root. Remove stale
EXPLORATION_SUMMARY.txt.

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

9.3 KiB

Inquiries Module - Quick Reference

📊 Module at a Glance

Path: apps/api/src/modules/inquiries/
Pattern: CQRS + DDD
Total Files: 25
Total LOC: 1,212
Test Coverage: 6 suites, 24 tests


📁 FILES BY LAYER

PRESENTATION (5 files)

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]

APPLICATION (8 files)

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]

DOMAIN (6 files)

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]

INFRASTRUCTURE (1 file)

infrastructure/
└── repositories/
    └── prisma-inquiry.repository.ts          [6 methods]

MODULE (2 files)

inquiries.module.ts                           [NestJS Module]
index.ts                                      [Barrel export]

🔄 REQUEST FLOWS

CREATE INQUIRY

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 }

MARK AS READ

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 }

LIST BY LISTING

GET /inquiries/listing/:listingId?page=1&limit=20
  ↓
InquiriesController.getByListing()
  ↓
QueryBus.execute(GetInquiriesByListingQuery)
  ↓
GetInquiriesByListingHandler
  ↓
PrismaInquiryRepository.findByListing()
  ↓
Response: PaginatedResult<InquiryReadDto>

LIST BY AGENT

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<InquiryReadDto>

🔑 KEY CLASSES

Class Location Purpose
InquiryEntity domain/entities/ Aggregate root with business logic
CreateInquiryHandler application/commands/create-inquiry/ Executes create command
MarkInquiryReadHandler application/commands/mark-inquiry-read/ Executes mark read command
GetInquiriesByListingHandler application/queries/get-inquiries-by-listing/ Resolves listing inquiries
GetInquiriesByAgentHandler application/queries/get-inquiries-by-agent/ Resolves agent inquiries
PrismaInquiryRepository infrastructure/repositories/ Implements persistence
InquiriesController presentation/controllers/ HTTP endpoints

📝 KEY INTERFACES

// Domain interface (repository contract)
interface IInquiryRepository {
  findById(id: string): Promise<InquiryEntity | null>
  save(inquiry: InquiryEntity): Promise<void>
  markAsRead(id: string): Promise<void>
  findByListing(listingId, page, limit): Promise<PaginatedResult<InquiryReadDto>>
  findByAgent(agentId, page, limit): Promise<PaginatedResult<InquiryReadDto>>
  countUnreadByAgent(agentId): Promise<number>
}

// 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<T> {
  data: T[]
  total: number
  page: number
  limit: number
  totalPages: number
}

🧪 TEST FILES AT A GLANCE

Test File Tests Focus
domain/__tests__/inquiry-domain.spec.ts 5 Entity creation, events
application/__tests__/create-inquiry.handler.spec.ts 4 Handler success, validation
application/__tests__/mark-inquiry-read.handler.spec.ts 5 Handler success, auth checks
application/__tests__/get-inquiries-by-listing.handler.spec.ts 2 Query results, empty state
application/__tests__/get-inquiries-by-agent.handler.spec.ts 2 Query results, agent lookup
presentation/__tests__/inquiries.controller.spec.ts 6 All endpoints, defaults
TOTAL 24 Comprehensive coverage

🔐 Authorization Matrix

Endpoint Auth Role Query
POST /inquiries JWT Any -
GET /listing/:id JWT Any page, limit
GET /agent/me JWT AGENT page, limit
PATCH /:id/read JWT AGENT -

Permission Checks:

  • MarkInquiryReadHandler: Verifies user is agent, agent owns listing

🎯 DDD PRINCIPLES

Domain Entity Encapsulation

// 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

Repository Pattern

  • Interface in DomainIInquiryRepository
  • Implementation in InfrastructurePrismaInquiryRepository
  • Dependency InjectionINQUIRY_REPOSITORY symbol

Separate Read/Write Models

  • Write Model: InquiryEntity (aggregate)
  • Read Model: InquiryReadDto (query DTO)

🔄 Domain Events

Event When Data
InquiryCreatedEvent Inquiry created aggregateId, listingId, userId
InquiryReadEvent Marked as read aggregateId, listingId, userId

💾 Database Operations

Prisma Models Used:

  • inquiry - Main entity
  • listing - For foreign key & agent lookup
  • property - For listing title
  • user - For buyer name & phone

Key Queries:

  • inquiry.create() - New inquiry
  • inquiry.update() - Mark read
  • inquiry.findMany() - Pagination
  • inquiry.count() - Total count

🚀 Entry Points

// Module export
export { InquiriesModule }

// Exported interfaces
export { INQUIRY_REPOSITORY, type IInquiryRepository }
export { InquiryEntity }

// Usage in other modules
import { InquiriesModule } from '@modules/inquiries'

🎓 Architecture Summary

CLEAN ARCHITECTURE with CQRS + DDD

Presentation Layer (Controllers + DTOs)
         ↓
Application Layer (CQRS Handlers)
         ↓
Domain Layer (Entities + Events + Interfaces)
         ↓
Infrastructure Layer (Prisma Repository)
         ↓
Database

Key Characteristics: Dependency Inversion - Domain defines contracts
Separation of Concerns - Each layer has clear responsibility
Testability - Mock implementations at each layer
Event-Driven - Domain events for audit & integration
CQRS - Separate commands & queries for scalability
Type Safety - Full TypeScript with strict interfaces


📌 Common Patterns

Command Pattern:

// Send command
commandBus.execute(new CreateInquiryCommand(...))
// Handler processes
@CommandHandler(CreateInquiryCommand)
class CreateInquiryHandler { ... }

Query Pattern:

// Send query
queryBus.execute(new GetInquiriesByListingQuery(...))
// Handler processes
@QueryHandler(GetInquiriesByListingQuery)
class GetInquiriesByListingHandler { ... }

Dependency Injection Pattern:

@Injectable()
export class Handler {
  constructor(
    @Inject(INQUIRY_REPOSITORY) private repo: IInquiryRepository,
    private prisma: PrismaService,
  ) {}
}

🔍 Where to Look For...

Need File
Add new endpoint presentation/controllers/inquiries.controller.ts
Add command application/commands/[name]/[name].command.ts + [name].handler.ts
Add query application/queries/[name]/[name].query.ts + [name].handler.ts
Business logic domain/entities/inquiry.entity.ts
New domain event domain/events/[name].event.ts
Database queries infrastructure/repositories/prisma-inquiry.repository.ts
Input validation presentation/dto/*.ts
Write tests [layer]/__tests__/*

Last Updated: April 11, 2026