# 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 ``` ### 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 ``` --- ## ๐Ÿ”‘ 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 save(inquiry: InquiryEntity): Promise markAsRead(id: string): Promise findByListing(listingId, page, limit): Promise> findByAgent(agentId, page, limit): Promise> countUnreadByAgent(agentId): Promise } // 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 { 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