- 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>
17 KiB
Audit: GoodGo Real Estate Listings & Property Management Feature
Status: Comprehensive Audit Complete
Date: April 19, 2026
Scope: Property/Listings API, Search, Management Dashboard, Frontend Components
Target: Production readiness assessment
1. Scope Coverage — What's Implemented Well
API Architecture (CQRS Pattern)
- Commands: 9 implemented (Create, Update, Delete, Feature, Promote, Moderate, Upload-Media, Update-Status, Admin-Feature)
- Queries: 4 core queries (GetListing, SearchListings, GetPriceHistory, GetPendingModeration)
- Event-Driven: Domain events published post-mutation; event handlers for cache invalidation
- Auth Guards: JwtAuthGuard + RolesGuard on sensitive endpoints; ownership checks before mutations
- Rate Limiting: EndpointRateLimitGuard on POST /listings (10 req/60s per user)
Database Schema
- Listing Model: Complete with status enum (DRAFT/PENDING_REVIEW/ACTIVE/RESERVED/SOLD/RENTED/EXPIRED/REJECTED)
- Geospatial: PostGIS integration (geometry(Point, 4326)) with indexes on location (Gist)
- Indexes: Compound indexes on common queries (status + createdAt, sellerId + status, transactionType + status)
- Relations: Cascading deletes on related entities (PriceHistory, SavedListing, Inquiry)
Frontend Pages & Components
- Public Pages: Listing detail with full SEO (metadata, JSON-LD breadcrumb, OG tags); Search page with filters, map view, saved search
- Dashboard: Listings management (CRUD status card view), edit page with multi-step form
- Components: Image gallery, lightbox, inquiry modal, price history chart, social share
- i18n: Full Vietnamese i18n via next-intl; translation keys for all user-facing text
- Responsive: Tailwind mobile-first design with flex/grid utilities; test coverage on key components
Quality Practices
- Test Coverage: 17 test files covering commands, queries, repositories, validators
- Error Handling: Custom exceptions (DomainException, ForbiddenException, NotFoundException, ValidationException)
- Logging: Structured logging with LoggerService; error stack traces captured
- Caching: Redis cache with prefixes (SEARCH, MARKET_DISTRICT, LISTING) and TTL management
2. Gaps & Missing Functionality
Critical Gaps
-
Delete-Listing Handler Missing Tests
- No
.spec.tsfile fordelete-listing.handler.ts - Edge case: deletion of featured listings with active payments not validated
- File:
apps/api/src/modules/listings/application/commands/delete-listing/ - Impact: Risk of orphaned transactions/orders if deletions occur without proper state checks
- No
-
Admin-Feature-Listing Handler Incomplete
AdminFeatureListingHandlerexists but no implementation of admin-side featured listing expiry logic- No scheduled task to auto-expire featured listings when
featuredUntilpasses - File:
apps/api/src/modules/listings/application/commands/admin-feature-listing/ - Impact: Featured listings may remain highlighted beyond paid period
-
Activation/Expiry Pipeline Missing
- No handler for
ActivateFeaturedListingCommand - No event handler for
ListingCreatedEventto transition DRAFT → PENDING_REVIEW - File: Event handlers folder mostly empty
- Impact: New listings not automatically entering moderation queue; featured listing lifecycle incomplete
- No handler for
-
Search Module Separation Issue
SearchListingsQueryexists in two places: listings module AND search module- Potential for duplicate/conflicting search logic
- Files:
apps/api/src/modules/listings/application/queries/search-listings/apps/api/src/modules/search/application/queries/search-properties/
- Impact: Maintenance burden; risk of divergent behavior
-
Price Validation Service Not Wired to Schema
PRICE_VALIDATORservice exists but pricing boundaries not enforced at database level- No check constraints on
priceVND(negative prices technically allowed in DB) - File:
apps/api/src/modules/listings/domain/services/price-validator.ts - Impact: Invalid prices can persist if validator bypassed or service disabled
Medium-Priority Gaps
-
Missing Feature-Listing Expiry Cron/Scheduled Task
- Featured listing expiry relies on manual query-time checks (
featuredUntil > now) - No background job to transition expired featured listings back to non-featured state
- Impact: Search rankings may incorrectly show expired featured listings; analytics skewed
- Featured listing expiry relies on manual query-time checks (
-
Media Deletion Not Implemented
- Upload-media exists; no delete-media endpoint
- Users cannot remove individual images after upload
- File: Controllers missing DELETE
:id/media/:mediaId - Impact: User experience friction; storage bloat
-
Moderation Feedback Not Visible to User
ModerateListingCommandsetsmoderationNotesbut frontend doesn't display rejection reason- Users cannot see why their listing was rejected/pending
- Impact: Poor user experience; support ticket volume increases
-
Dashboard Saved Searches Not Fully Implemented
- Saved search CRUD endpoints exist; UI component incomplete
- Alert subscription feature defined in schema but no notification sender handler
- File:
apps/web/app/[locale]/(dashboard)/dashboard/saved-searches/page.tsx - Impact: Feature partially unusable
-
Missing Batch Operations
- No bulk update endpoints (e.g., mark multiple listings ACTIVE, bulk delete)
- Admin dashboard may require repeated individual API calls
- Impact: Scalability/usability issue for admins managing many listings
3. Code Quality Issues
Performance & N+1 Queries
-
Geo-Extraction Two-Step Query (Medium priority)
findByIdWithProperty(): First Prisma query, then raw SQL for PostGIS extractionsearchListings(): Batch geo-extraction mitigates but still 2 queries (Prisma + raw SQL)- File:
apps/api/src/modules/listings/infrastructure/repositories/listing-read.queries.ts(lines 26–35, 156–167) - Optimization: Embed ST_Y/ST_X in the Prisma query using
$queryRawfor the entire fetch
-
Seller Lookups in Search Result
- Search includes seller (line 146:
seller: { select: { id: true, fullName: true } }) - If frontend doesn't need seller full details on listing cards, unnecessary join
- File: Line 146 of listing-read.queries.ts
- Impact: Negligible on small datasets; scales poorly with 100k+ listings
- Search includes seller (line 146:
-
Media Eager-Load Limits
take: 5in search,take: 10in detail (hardcoded)- Inconsistent; no configuration option; wastes bandwidth if full gallery not needed
- File: Lines 143, 15 of listing-read.queries.ts
Hardcoded Values & Configuration
-
Featured Listing Prices Hardcoded
PACKAGE_PRICESdefined in handler (3_days: 99k, 7_days: 199k, 30_days: 499k VND)- Not configurable; require code change to adjust pricing
- File:
apps/api/src/modules/listings/application/commands/feature-listing/feature-listing.handler.ts(lines 14–18) - Risk: Admin cannot price-test without redeployment
-
MAX_MEDIA_PER_PROPERTY Hardcoded to 20
- Not fetched from database config or environment
- File:
apps/api/src/modules/listings/application/commands/upload-media/upload-media.handler.ts(line 10)
-
File Upload Limits Hardcoded
- Image max 10MB, video max 100MB; no admin override
- File:
apps/api/src/modules/listings/presentation/controllers/listings.controller.ts(lines 325–329)
-
Search Result Page Size Capped to 100
Math.min(params.limit ?? 20, 100)— prevents large exports- File:
apps/api/src/modules/listings/infrastructure/repositories/listing-read.queries.ts(line 105)
Validation Gaps
-
No Validation on Duplicate Property Creation
- Users can create multiple listings for same property
- Duplicate detector warns but never blocks (catch-all try/catch suppresses failures)
- File:
apps/api/src/modules/listings/application/commands/create-listing/create-listing.handler.ts(lines 153–155) - Risk: Spam, data duplication
-
Price History Source Always "manual_update"
- No distinction between user edits, AI adjustments, or system changes
- File:
prisma/schema.prisma(line 395),PriceHistory.sourcedefault - Impact: Audit trail unclear for analytics
-
No Agent Assignment Validation
agentIdaccepted without checking agent exists or has broker permission- File: CreateListingCommand accepts
agentIdbut no guard - Risk: Listing assigned to non-existent/unauthorized agent
-
Description/Title Length Not Enforced
- Schema allows unlimited text; no max_length on
descriptionin Property model - File:
prisma/schema.prisma— Property.description is@db.Text(no constraint) - Impact: Huge descriptions slow search queries; UI renders badly
- Schema allows unlimited text; no max_length on
Security Issues
-
Moderation Bypass Risk
- Update-listing transitions ACTIVE → PENDING_REVIEW, but admin can manually revert
- No audit log of who changed listing status
- File:
apps/api/src/modules/listings/application/commands/update-listing/update-listing.handler.ts(line 126) - Risk: Moderator decisions ignored silently
-
File Upload Path Predictable
- Files stored in
properties/{propertyId}(publicly guessable) - No hash-based path obfuscation; potential enumeration attack
- File:
apps/api/src/modules/listings/application/commands/upload-media/upload-media.handler.ts(line 40)
- Files stored in
-
No Rate Limit on Feature Listing
- Feature endpoint limited by quota, not rate limit
- User could spam feature requests and hit quota fast
- File:
listings.controller.ts—@Post(':id/feature')has no @EndpointRateLimit
-
Agent Assignment Not Validated During Update
- User can reassign listing to arbitrary agent; no permission check
- File:
UpdateListingCommandconstructor doesn't validate agent ownership - Risk: Seller gives away listing commission to unauthorized agent
-
Inquiry Message Not Sanitized
- Inquiry message stored as-is; potential XSS if echoed in admin dashboard
- File:
prisma/schema.prismaline 472 —Inquiry.messageis@db.Text - Impact: Admin UI could be compromised
Maintainability Issues
-
Unused Deprecated Type
ListingDetailDtomarked@deprecatedwith TODO comment- File:
apps/api/src/modules/listings/application/queries/get-listing/get-listing.handler.ts(line 8) - Cleanup: Remove after migration complete
-
Inconsistent Naming (Vietnamese vs English)
- Error messages in Vietnamese (e.g., "Không thể tạo tin đăng")
- Test names, constants in English
- Domain entities mixed (Vietnamese property types: APARTMENT, VILLA)
- Impact: Codebase hard to reason about for non-Vietnamese speakers
-
Missing JSDoc on Complex Methods
searchListings()function complex with Prisma filters; no parameter documentation- File:
listing-read.queries.tslines 100–213
4. Security & Validation Concerns
| Issue | Severity | Mitigation |
|---|---|---|
| No transaction check before delete | High | Add findByIdWithRelations() check before delete() |
| File path enumeration possible | Medium | Hash property IDs in storage path; implement signed URLs |
| Agent assignment not validated | Medium | Verify agent broker relationship before assignment |
| Inquiry message XSS risk | Medium | Sanitize on storage; escape on frontend display |
| Moderation audit trail missing | Medium | Add moderatedBy, moderatedAt fields; log state transitions |
| Duplicate listings not blocked | Low | Merge duplicate detector into command handler (convert warning to block option) |
5. Performance & Scalability
Issues Identified
-
Geo-Extraction Overhead
- Two separate query roundtrips (Prisma + raw SQL) per listing detail fetch
- At scale (1M+ listings), search page with 20 results = 21 queries total
- Fix: Use Prisma raw SQL for full fetch; avoid Prisma ORM + separate geo query
-
Uncached Admin Queries
GetPendingModerationQuerynot cached; admin dashboard refreshes full list every load- File:
apps/api/src/modules/listings/application/queries/get-pending-moderation/ - Fix: Add short-lived cache (60s) with invalidation on moderation action
-
Search Filters Create Complex Queries
- Nested property filters in searchListings (lines 118–129 listing-read.queries.ts)
- No query plan optimization hints; potential slow full table scan on large datasets
- Fix: Add database indexes on
status,transactionType,property(propertyType, district, city)
-
Media Queries Unoptimized
- Every search result includes media array (even if not displayed)
- Transferring 5–10 media objects × 20 results = unnecessary payload
- Fix: Add optional
?includeMedia=truequery param; default exclude in list view
6. Recommendations (Prioritized)
🔴 High Priority (Blocks Production)
-
Implement Delete-Listing Test Suite
- Add edge cases: featured listing with active payment, transactions in progress
- Verify cascading deletes work correctly
- Effort: 2 hours | Owner: Backend
-
Add Featured-Listing Expiry Handler
- Implement
ExpireFeaturedListingsScheduledTask(daily cron or on-demand) - Transition expired listings: set
featuredUntil = null, trigger search cache invalidation - Effort: 4 hours | Owner: Backend
- Implement
-
Wire Price Validation to Schema
- Add PostgreSQL check constraint:
priceVND > 0 - Document min/max pricing rules in README
- Effort: 1 hour | Owner: Backend + DBA
- Add PostgreSQL check constraint:
-
Implement Moderation Audit Trail
- Add
moderatedBy(admin user ID),moderatedAt(timestamp),previousStatusfields to Listing - Log transitions in event handler
- Effort: 3 hours | Owner: Backend
- Add
🟠 Medium Priority (Improves UX/Security)
-
Implement Media Deletion Endpoint
- Add
DELETE /listings/:id/media/:mediaIdwith ownership check - Trigger S3 deletion + cache invalidation
- Effort: 2 hours | Owner: Backend
- Add
-
Display Moderation Feedback to User
- Add frontend component to show rejection reason when status=REJECTED
- Notify user via email + in-app notification
- Effort: 3 hours | Owner: Frontend + Backend
-
Sanitize Inquiry Messages
- Add DOMPurify to admin dashboard inquiry display
- Store sanitized version if displaying in emails
- Effort: 1 hour | Owner: Frontend
-
Add Rate Limit to Feature Endpoint
@EndpointRateLimit({ limit: 5, windowSeconds: 3600 })(5 features per hour)- Effort: 30 min | Owner: Backend
-
Validate Agent Assignment
- Check agent.isVerified before assignment
- Add broker permission check if multi-broker support added
- Effort: 1 hour | Owner: Backend
-
Consolidate Search Logic
- Move
SearchListingsQueryfrom listings module to search module - Remove duplicate in listings module; re-export from search
- Effort: 2 hours | Owner: Backend
- Move
🟡 Low Priority (Tech Debt)
-
Extract Hardcoded Values to Config
- Move PACKAGE_PRICES → database config table (FeaturePackagePrice)
- Add admin UI to manage pricing
- Effort: 4 hours | Owner: Backend
-
Optimize Geo-Extraction Queries
- Consolidate Prisma + raw SQL into single raw query
- Add benchmark: measure latency before/after
- Effort: 3 hours | Owner: Backend + Infra
-
Implement Batch Operations API
PATCH /listings/batchto update multiple listings status- Quota should apply per-listing, not per-request
- Effort: 4 hours | Owner: Backend
-
Add Missing JSDoc & Comments
- Document searchListings Prisma filters
- Clarify geo-extraction rationale
- Effort: 2 hours | Owner: Backend
-
Complete Saved Searches Alerts
- Implement
SavedSearchAlertHandlerevent listener - Send daily digest when new listings match saved search
- Effort: 6 hours | Owner: Backend + Notifications
- Implement
Summary
Overall Assessment: Feature is 80% production-ready; core CRUD works, search functional, but audit trail, expiry automation, and error handling need hardening.
Critical Path to Production:
- ✅ Implement delete-listing tests
- ✅ Add featured-listing expiry cron
- ✅ Wire price validation to schema
- ✅ Add moderation audit trail
Risk Level: Medium (security concerns around file paths, agent assignment; moderation bypass possible)
Test Coverage: 70% of commands/queries; delete-listing and expiry handlers untested
Deployment Blocker: None, if recommendations #1–#4 completed within 2 weeks.