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>
703 lines
23 KiB
Markdown
703 lines
23 KiB
Markdown
# 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**
|