# 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](#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)

---

## 📁 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**

```typescript
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
```typescript
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
```typescript
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
```typescript
class GetInquiriesByListingHandler implements IQueryHandler<GetInquiriesByListingQuery> {
  async execute(query: GetInquiriesByListingQuery): Promise<PaginatedResult<InquiryReadDto>> {
    return this.inquiryRepo.findByListing(
      query.listingId,
      query.page,
      query.limit
    )
  }
}
```

#### GetInquiriesByAgentHandler
```typescript
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**
