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:
Ho Ngoc Hai
2026-04-21 16:29:24 +07:00
parent 912121cf09
commit 08b96f9c2d
39 changed files with 15129 additions and 562 deletions

View 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**