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>
364 lines
9.3 KiB
Markdown
364 lines
9.3 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// 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 Domain** → `IInquiryRepository`
|
|
- **Implementation in Infrastructure** → `PrismaInquiryRepository`
|
|
- **Dependency Injection** → `INQUIRY_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
|
|
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// Send command
|
|
commandBus.execute(new CreateInquiryCommand(...))
|
|
// Handler processes
|
|
@CommandHandler(CreateInquiryCommand)
|
|
class CreateInquiryHandler { ... }
|
|
```
|
|
|
|
**Query Pattern:**
|
|
```typescript
|
|
// Send query
|
|
queryBus.execute(new GetInquiriesByListingQuery(...))
|
|
// Handler processes
|
|
@QueryHandler(GetInquiriesByListingQuery)
|
|
class GetInquiriesByListingHandler { ... }
|
|
```
|
|
|
|
**Dependency Injection Pattern:**
|
|
```typescript
|
|
@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
|