docs: consolidate exploration & audit reports under docs/ (TEC-3094)
- Move 8 stray .md (+5 .txt) from ~/Desktop into docs/explorations/from-desktop/ - Reorganize 27 .md/.txt at workspace root: - audit reports -> docs/audits/ - exploration reports -> docs/explorations/ - design system -> docs/design-system/ - Keep only README/CHANGELOG/CONTRIBUTING/CLAUDE at repo root - Refresh docs/README.md as canonical index with links to all groups - Note: pre-existing docs/audits/AUDIT_INDEX.md and AUDIT_SUMMARY.md were overwritten by the newer root-level versions during the move Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
320
docs/explorations/EXPLORATION_SUMMARY_LISTINGS.md
Normal file
320
docs/explorations/EXPLORATION_SUMMARY_LISTINGS.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# 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**
|
||||
Reference in New Issue
Block a user