# Listings Module Exploration - Summary

**Exploration Date:** April 21, 2026  
**Scope:** Complete understanding of listings, properties, AVM, agents, inquiries, and caching

## 📋 Documents Generated

This exploration has produced **3 comprehensive reference documents**:

1. **LISTINGS_MODULE_EXPLORATION.md** (965 lines)
   - Detailed, section-by-section breakdown of all 7 components
   - Code snippets from actual source files
   - Full interface definitions and data flow diagrams

2. **LISTINGS_QUICK_REFERENCE.md** (Quick lookup)
   - Visual flow diagrams using ASCII art
   - 1-page reference for each major component
   - Key formulas, algorithms, and cache configuration

3. **LISTINGS_DATA_SCHEMA.md** (Database focus)
   - Table definitions in SQL
   - Foreign key relationships
   - Index strategy and query patterns
   - Denormalization strategy & eventual consistency model

---

## 🎯 Key Findings

### 1. GET /listings/:id Architecture
- **Handler:** `ListingsController.getListing()` → `GetListingQuery` → `GetListingHandler`
- **Cache:** Redis cache-aside, 300s TTL, envelope-based storage with metadata
- **Not-found behavior:** Uses internal signal to avoid caching null; allows new listings to be discoverable immediately
- **Response:** `ListingDetailData` with nested property, seller, agent info

### 2. Response DTO (ListingDetailData)
- Contains **full property details** (40+ fields)
- Includes **engagement metrics**: `viewCount`, `saveCount`, **`inquiryCount`** (denormalized)
- Includes **featured status**: `isFeatured`, `featuredUntil` (computed at runtime)
- Media: Up to 10 images/videos with order & captions
- Seller & agent info with contact details

### 3. AVM Service Integration
- **Primary:** Python AI microservice (v1 or v2 model)
- **Fallback:** PostGIS-based comparables if AI service down
- **Batch concurrency:** Max 5 concurrent requests
- **Input:** Property attributes (area, district, bedrooms, etc.)
- **Output:** Estimated price + confidence + comparables + model version

### 4. Agent Quality Score
- **Formula:** Weighted average of 4 metrics (40% reviews, 30% response time, 20% conversion, 10% listing activity)
- **Storage:** `Agent.qualityScore` field (denormalized aggregate)
- **Update trigger:** Review and inquiry events
- **Result:** Score rounded to 1 decimal, displayed in agent profiles

### 5. Inquiries Tracking
- **Creation:** `InquiryEntity` persisted, `InquiryCreatedEvent` published
- **Denormalization:** Event listener increments `Listing.inquiryCount`
- **Display:** `inquiryCount` returned in `ListingDetailData`
- **Data:** Inquirer, message (HTML-sanitized), optional phone, read status, timestamp

### 6. Similar Listings Algorithm
- **Rule-based matcher** (not ML-based)
- **Criteria:** Same property type & district, price ±10%, area ±20%, ACTIVE status
- **Sorting:** By price delta (closest comparable first)
- **Performance:** Fetches 3x limit, sorts by delta, returns top N

### 7. Redis Caching
- **Pattern:** Cache-aside with envelope metadata
- **TTLs:** 300s (listing detail), 120s (search), 60s (quota), 86400s (reference data)
- **Graceful degradation:** System works offline; metrics track all failures
- **Invalidation:** Single key or prefix-based (SCAN-based for prefix)
- **Metrics:** Hit/miss/degradation counters by resource

---

## 🗂️ File Organization

### Listings Module (`apps/api/src/modules/listings/`)
```
listings/
├── presentation/
│   ├── controllers/listings.controller.ts       ← HTTP handler
│   └── dto/                                     ← Request/response DTOs
├── application/
│   ├── queries/get-listing/                     ← Query handler + cache logic
│   └── commands/                                ← Create, update, delete
├── domain/
│   ├── entities/listing.entity.ts               ← Domain model
│   ├── repositories/listing-read.dto.ts         ← Response DTOs
│   └── events/                                  ← Domain events
└── infrastructure/
    ├── repositories/listing-read.queries.ts    ← SQL queries (including similar listings)
    └── services/
```

### Analytics Module (`apps/api/src/modules/analytics/`)
```
analytics/
├── domain/services/avm-service.ts               ← AVM interface
└── infrastructure/services/
    ├── http-avm.service.ts                      ← Primary implementation
    ├── ai-service.client.ts                     ← AI microservice HTTP client
    └── prisma-avm.service.ts                    ← Fallback (PostGIS)
```

### Agents Module (`apps/api/src/modules/agents/`)
```
agents/
├── domain/services/quality-score.service.ts     ← Calculator (pure domain)
├── application/commands/recalculate-quality-score/
└── infrastructure/repositories/agent-profile.queries.ts
```

### Inquiries Module (`apps/api/src/modules/inquiries/`)
```
inquiries/
├── application/commands/create-inquiry/         ← Creates + publishes event
├── domain/entities/inquiry.entity.ts
└── domain/events/inquiry-created.event.ts
```

### Cache Service (`apps/api/src/modules/shared/`)
```
shared/
└── infrastructure/cache.service.ts              ← Cache-aside implementation
```

---

## 🔗 Data Flow Diagram

```
Client HTTP Request: GET /listings/{id}
    ↓
ListingsController.getListing(id)
    ↓
GetListingHandler.execute(GetListingQuery)
    ├─ CacheService.getOrSet(cache:listing:{id}, loader, 300s)
    │  ├─ Redis hit? → return ListingDetailData
    │  ├─ Redis miss → call loader()
    │  │   ├─ prisma.listing.findUnique({id}, include: {...})
    │  │   ├─ Extract PostGIS geometry (lat/lng)
    │  │   ├─ Map to ListingDetailData
    │  │   └─ Store in Redis (envelope + metadata)
    │  └─ Not found? → throw ListingNotFoundSignal (don't cache)
    ├─ Exception handling
    └─ Return ListingDetailData | null
    ↓
Controller: null → throw NotFoundException (404)
    ↓
Response: 200 OK + JSON(ListingDetailData)
```

---

## 📊 Component Dependencies

```
ListingsController
├─ CommandBus (for mutations)
└─ QueryBus (for queries)
    ├─ GetListingHandler
    │   ├─ ListingRepository
    │   ├─ CacheService
    │   └─ LoggerService
    ├─ GetSimilarListingsHandler
    │   └─ ListingRepository
    │       └─ Prisma (for SQL queries)
    └─ SearchListingsHandler
        └─ ListingRepository
            ├─ Prisma
            └─ PostGIS (geometry extraction)

AgentsModule
├─ QualityScoreCalculator (pure domain service)
├─ RecalculateQualityScoreHandler
│   └─ Triggered by ReviewCreatedEvent, InquiryCreatedEvent
└─ AgentProfileQueries
    ├─ Prisma (fetch agent + reviews + listings)
    └─ ReviewAggregation

InquiriesModule
├─ CreateInquiryHandler
│   ├─ InquiryRepository
│   ├─ EventBus
│   └─ InquiryCreatedEvent (published)
└─ ListingModule receives event
    └─ Increments Listing.inquiryCount

AnalyticsModule (AVM)
├─ HttpAVMService
│   ├─ IAiServiceClient (HTTP to Python service)
│   ├─ PrismaAVMService (fallback)
│   └─ Prometheus metrics
└─ Used by: Listing valuation, Comparable search

SharedModule (Cache)
├─ CacheService
│   ├─ RedisService
│   └─ Metrics (hit/miss/degradation counters)
└─ Used by: GetListingHandler, SearchListingsHandler, etc.
```

---

## 🛠️ Important Implementation Details

### Cache Envelope Format
```json
{
  "__v": { /* actual data */ },
  "cachedAt": "2026-04-21T12:00:00Z",
  "ttlSeconds": 300
}
```
- Allows frontend to display cache freshness
- Backward-compatible with legacy plain-JSON entries
- Metadata stored in async storage (`cacheMetaStorage`)

### Denormalization Pattern
```
Event Publish (InquiryCreatedEvent)
    ↓
Event Listener (in Listings or Inquiry module)
    ├─ Count inquiries for listing: 
    │   await prisma.inquiry.count({ where: { listingId } })
    ├─ Update listing counter:
    │   await prisma.listing.update({
    │       where: { id: listingId },
    │       data: { inquiryCount: count }
    │   })
    └─ Cache invalidation (optional): cache.invalidate(cache:listing:{id})
```
- Eventual consistency (may lag seconds)
- Avoids expensive COUNT() on every read
- Event-driven guarantees eventual correctness

### AVM Fallback Chain
```
estimateValue(params)
    ├─ Try: aiClient.predict(AiPredictRequest)
    │        → HTTP POST to Python service
    │        → Return AI-based ValuationResult
    └─ Catch (any error):
        └─ fallback.estimateValue(params)
            → PostGIS spatial query
            → Find comparable sales nearby
            → Calculate median price
            → Return comparables-based ValuationResult
```

### Quality Score Formula
```
Score = 
  (avgRating / 5) * 100 * 0.40 +
  MAX(0, 100 - (responseTimeAvg / 3600) * 100) * 0.30 +
  (conversionRate * 100) * 0.20 +
  (activeListingRatio * 100) * 0.10

Rounded to 1 decimal place
```

---

## ⚠️ Potential Issues & Edge Cases

1. **Cache invalidation on null:** Not caching null results is smart but requires careful coordination—ensure all listing creation/updates invalidate appropriately.

2. **PostGIS geometry extraction:** Always requires raw SQL queries (`$queryRaw`) for lat/lng extraction. Prisma's ORM doesn't map PostGIS types directly.

3. **Denormalized inquiryCount:** May lag behind actual count if event listener fails. Should have reconciliation job.

4. **AVM service downtime:** Falls back gracefully, but confidence score from fallback service may be lower. Frontend should handle confidence thresholds.

5. **Similar listings boundary cases:** Price ±10% is exact mathematical boundary—no tolerance. Two listings with 10.1% price difference won't be marked as similar.

6. **Agent quality score with no reviews:** Defaults to 50 (neutral). May want business logic to penalize agents with 0 reviews.

7. **Cache stampede potential:** If Redis is slow, multiple concurrent requests could trigger parallel loader calls. Implement request coalescing if needed.

---

## 📈 Recommended Next Steps

1. **Verify inquiryCount event listener:** Check listings module for event handler that increments inquiryCount on `InquiryCreatedEvent`.

2. **Audit cache invalidation:** Ensure all listing mutations (status change, price update, media upload) invalidate cache appropriately.

3. **Test AVM fallback:** Simulate Python service downtime; verify graceful fallback to PostGIS.

4. **Performance review:** Profile queries with EXPLAIN ANALYZE:
   - Listing detail with media fetch
   - Similar listings (price/area bounds)
   - Inquiry count aggregation
   - Agent quality score recalculation

5. **Document cache TTL rationale:** Why 300s for listing detail but only 120s for search? May need adjustment based on data freshness requirements.

6. **Implement reconciliation job:** Periodic job to recount inquiries per listing, detect denormalization drift.

---

## 📖 References

**All source files are organized in the 3 generated documents:**
- Detailed exploration: `LISTINGS_MODULE_EXPLORATION.md`
- Quick reference: `LISTINGS_QUICK_REFERENCE.md`
- Database schema: `LISTINGS_DATA_SCHEMA.md`

**Key base paths:**
- Listings module: `apps/api/src/modules/listings/`
- Analytics (AVM): `apps/api/src/modules/analytics/`
- Agents: `apps/api/src/modules/agents/`
- Inquiries: `apps/api/src/modules/inquiries/`
- Cache service: `apps/api/src/modules/shared/infrastructure/cache.service.ts`

---

**End of Exploration Summary**
