destinyebuka commited on
Commit
ee3d3d4
Β·
1 Parent(s): 338cfea
app/__pycache__/config.cpython-313.pyc CHANGED
Binary files a/app/__pycache__/config.cpython-313.pyc and b/app/__pycache__/config.cpython-313.pyc differ
 
app/ai/agent/nodes/listing_publish.py CHANGED
@@ -302,12 +302,53 @@ async def listing_publish_handler(state: AgentState) -> AgentState:
302
  # ============================================================
303
  # STEP 4: Generate success message & UI Update
304
  # ============================================================
305
-
306
  # Re-build UI for the published state
307
  from app.ai.agent.nodes.listing_validate import build_draft_ui
308
  draft_ui = build_draft_ui(draft)
309
  draft_ui["status"] = "published"
310
  draft_ui["title"] = f"βœ… {draft.title}" # Add checkmark to title
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
  # Generate personalized success message using LLM
313
  from langchain_openai import ChatOpenAI
 
302
  # ============================================================
303
  # STEP 4: Generate success message & UI Update
304
  # ============================================================
305
+
306
  # Re-build UI for the published state
307
  from app.ai.agent.nodes.listing_validate import build_draft_ui
308
  draft_ui = build_draft_ui(draft)
309
  draft_ui["status"] = "published"
310
  draft_ui["title"] = f"βœ… {draft.title}" # Add checkmark to title
311
+ draft_ui["listing_id"] = listing_id # Include the new listing ID
312
+
313
+ # βœ… STEP 4.1: Enrich with owner rating/reviews for proper display
314
+ # Fetch owner's rating and reviews_count so the listing card shows correct data
315
+ try:
316
+ owner = await db.users.find_one({"_id": ObjectId(draft.user_id)})
317
+ if owner:
318
+ listing_type = draft.listing_type
319
+
320
+ # Owner info
321
+ first_name = owner.get("firstName", "")
322
+ last_name = owner.get("lastName", "")
323
+ draft_ui["owner_name"] = f"{first_name} {last_name}".strip() or "Unknown"
324
+ draft_ui["owner_profile_picture"] = owner.get("profilePicture")
325
+ draft_ui["user_id"] = draft.user_id
326
+
327
+ # Rating - for short-stay use listing's own rating (new listing = 0)
328
+ # For other types, use owner's rating
329
+ if listing_type == "short-stay":
330
+ draft_ui["rating"] = 0.0 # New listing has no reviews yet
331
+ draft_ui["reviews_count"] = 0
332
+ else:
333
+ # Use owner's rating for rent/sale/roommate listings
334
+ draft_ui["rating"] = owner.get("rating", 0.0)
335
+ draft_ui["reviews_count"] = owner.get("reviews_count", 0)
336
+
337
+ logger.info(
338
+ "Draft UI enriched with owner data",
339
+ owner_name=draft_ui["owner_name"],
340
+ rating=draft_ui["rating"],
341
+ reviews_count=draft_ui["reviews_count"]
342
+ )
343
+ else:
344
+ # Fallback if owner not found
345
+ draft_ui["owner_name"] = "Unknown"
346
+ draft_ui["rating"] = 0.0
347
+ draft_ui["reviews_count"] = 0
348
+ except Exception as enrich_err:
349
+ logger.warning("Failed to enrich draft_ui with owner data", error=str(enrich_err))
350
+ draft_ui["rating"] = 0.0
351
+ draft_ui["reviews_count"] = 0
352
 
353
  # Generate personalized success message using LLM
354
  from langchain_openai import ChatOpenAI
app/models/user.py CHANGED
@@ -51,7 +51,8 @@ class User:
51
  "languages": [],
52
  "preferredLanguage": preferred_language, # User's preferred language for emails/notifications
53
  "isVerified": False,
54
- "reviews_count": 0,
 
55
  "lastLogin": None,
56
  "createdAt": now,
57
  "updatedAt": now,
 
51
  "languages": [],
52
  "preferredLanguage": preferred_language, # User's preferred language for emails/notifications
53
  "isVerified": False,
54
+ "rating": 0.0, # User's average rating from reviews
55
+ "reviews_count": 0, # Number of reviews received
56
  "lastLogin": None,
57
  "createdAt": now,
58
  "updatedAt": now,
app/services/listing_service.py CHANGED
@@ -56,9 +56,14 @@ async def enrich_listing_with_owner_and_reviews(listing: dict, db) -> dict:
56
  logger.debug(f"Short-stay listing {listing.get('_id')}: using listing rating")
57
  else:
58
  # Use owner's rating (landlord/seller reviews)
59
- listing["rating"] = owner.get("rating", 0.0)
60
- listing["reviews_count"] = owner.get("reviews_count", 0)
61
- logger.debug(f"Non-short-stay listing {listing.get('_id')}: using owner rating")
 
 
 
 
 
62
  else:
63
  logger.warning(f"Owner {user_id} not found for listing {listing.get('_id')}")
64
 
@@ -126,8 +131,15 @@ async def enrich_listings_batch(listings: List[dict], db) -> List[dict]:
126
  listing["rating"] = listing.get("rating", 0.0)
127
  listing["reviews_count"] = listing.get("reviews_count", 0)
128
  else:
129
- listing["rating"] = owner.get("rating", 0.0)
130
- listing["reviews_count"] = owner.get("reviews_count", 0)
 
 
 
 
 
 
 
131
  except Exception as e:
132
  logger.warning(f"Failed to enrich listing {listing.get('_id')} specific data: {e}")
133
 
 
56
  logger.debug(f"Short-stay listing {listing.get('_id')}: using listing rating")
57
  else:
58
  # Use owner's rating (landlord/seller reviews)
59
+ owner_rating = owner.get("rating", 0.0)
60
+ owner_reviews = owner.get("reviews_count", 0)
61
+ listing["rating"] = owner_rating
62
+ listing["reviews_count"] = owner_reviews
63
+ logger.info(
64
+ f"Enriched listing {listing.get('_id')} ({listing_type}): "
65
+ f"owner={user_id}, rating={owner_rating}, reviews={owner_reviews}"
66
+ )
67
  else:
68
  logger.warning(f"Owner {user_id} not found for listing {listing.get('_id')}")
69
 
 
131
  listing["rating"] = listing.get("rating", 0.0)
132
  listing["reviews_count"] = listing.get("reviews_count", 0)
133
  else:
134
+ # Use owner's rating for non-short-stay listings
135
+ owner_rating = owner.get("rating", 0.0)
136
+ owner_reviews = owner.get("reviews_count", 0)
137
+ listing["rating"] = owner_rating
138
+ listing["reviews_count"] = owner_reviews
139
+ logger.debug(
140
+ f"Batch enrich: listing {listing.get('_id')} "
141
+ f"owner={user_id}, rating={owner_rating}, reviews={owner_reviews}"
142
+ )
143
  except Exception as e:
144
  logger.warning(f"Failed to enrich listing {listing.get('_id')} specific data: {e}")
145
 
scripts/fix_ratings.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Migration Script: Fix Ratings and Reviews Count
4
+
5
+ This script ensures all listings and users have correct rating/reviews_count:
6
+ 1. Users: Recalculates rating and reviews_count from actual reviews
7
+ 2. Short-stay listings: Recalculates rating and reviews_count from actual reviews
8
+ 3. Non-short-stay listings: Will use owner's rating (handled at fetch time)
9
+
10
+ Run with: python scripts/fix_ratings.py
11
+ """
12
+
13
+ import asyncio
14
+ import sys
15
+ import os
16
+
17
+ # Add parent directory to path for imports
18
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
19
+
20
+ from motor.motor_asyncio import AsyncIOMotorClient
21
+ from bson import ObjectId
22
+ from datetime import datetime
23
+
24
+ # Import app config to get correct MongoDB URL
25
+ from app.config import settings
26
+
27
+
28
+ async def get_database():
29
+ """Connect to MongoDB using app settings"""
30
+ client = AsyncIOMotorClient(
31
+ settings.MONGODB_URL,
32
+ serverSelectionTimeoutMS=5000,
33
+ )
34
+ # Test connection
35
+ await client.admin.command("ping")
36
+ print(f" πŸ“¦ Database: {settings.MONGODB_DATABASE}")
37
+ return client[settings.MONGODB_DATABASE]
38
+
39
+
40
+ async def recalculate_user_ratings(db):
41
+ """
42
+ Recalculate rating and reviews_count for all users
43
+ based on actual reviews in the reviews collection.
44
+ """
45
+ print("\n" + "="*60)
46
+ print("STEP 2: Recalculating User Ratings")
47
+ print("="*60)
48
+
49
+ # Get all users
50
+ users_cursor = db.users.find({})
51
+ users = await users_cursor.to_list(length=None)
52
+ print(f"Found {len(users)} users to process")
53
+
54
+ updated_count = 0
55
+
56
+ for user in users:
57
+ user_id = str(user["_id"])
58
+
59
+ # Find all reviews where this user is the target
60
+ reviews_cursor = db.reviews.find({
61
+ "target_type": "user",
62
+ "target_id": user_id
63
+ })
64
+ reviews = await reviews_cursor.to_list(length=None)
65
+
66
+ # Calculate new rating
67
+ if reviews:
68
+ total_rating = sum(r.get("rating", 0) for r in reviews)
69
+ reviews_count = len(reviews)
70
+ avg_rating = round(total_rating / reviews_count, 2)
71
+ else:
72
+ reviews_count = 0
73
+ avg_rating = 0.0
74
+
75
+ # Get current values
76
+ current_rating = user.get("rating", 0.0)
77
+ current_count = user.get("reviews_count", 0)
78
+
79
+ # Update if different or missing
80
+ if current_rating != avg_rating or current_count != reviews_count:
81
+ await db.users.update_one(
82
+ {"_id": user["_id"]},
83
+ {"$set": {
84
+ "rating": avg_rating,
85
+ "reviews_count": reviews_count
86
+ }}
87
+ )
88
+ updated_count += 1
89
+ print(f" βœ… User {user.get('firstName', 'Unknown')} {user.get('lastName', '')}: "
90
+ f"{current_rating} β†’ {avg_rating} ({reviews_count} reviews)")
91
+
92
+ print(f"\nπŸ“Š Updated {updated_count} users")
93
+ return updated_count
94
+
95
+
96
+ async def recalculate_listing_ratings(db):
97
+ """
98
+ Recalculate rating and reviews_count for all SHORT-STAY listings
99
+ based on actual reviews in the reviews collection.
100
+
101
+ Non-short-stay listings use the owner's rating (handled at fetch time).
102
+ """
103
+ print("\n" + "="*60)
104
+ print("STEP 3: Recalculating Short-Stay Listing Ratings")
105
+ print("="*60)
106
+
107
+ # Get all short-stay listings
108
+ listings_cursor = db.listings.find({"listing_type": "short-stay"})
109
+ listings = await listings_cursor.to_list(length=None)
110
+ print(f"Found {len(listings)} short-stay listings to process")
111
+
112
+ updated_count = 0
113
+
114
+ for listing in listings:
115
+ listing_id = str(listing["_id"])
116
+
117
+ # Find all reviews where this listing is the target
118
+ reviews_cursor = db.reviews.find({
119
+ "target_type": "listing",
120
+ "target_id": listing_id
121
+ })
122
+ reviews = await reviews_cursor.to_list(length=None)
123
+
124
+ # Calculate new rating
125
+ if reviews:
126
+ total_rating = sum(r.get("rating", 0) for r in reviews)
127
+ reviews_count = len(reviews)
128
+ avg_rating = round(total_rating / reviews_count, 2)
129
+ else:
130
+ reviews_count = 0
131
+ avg_rating = 0.0
132
+
133
+ # Get current values
134
+ current_rating = listing.get("rating", 0.0)
135
+ current_count = listing.get("reviews_count", 0)
136
+
137
+ # Update if different or missing
138
+ if current_rating != avg_rating or current_count != reviews_count:
139
+ await db.listings.update_one(
140
+ {"_id": listing["_id"]},
141
+ {"$set": {
142
+ "rating": avg_rating,
143
+ "reviews_count": reviews_count
144
+ }}
145
+ )
146
+ updated_count += 1
147
+ print(f" βœ… Listing '{listing.get('title', 'Untitled')[:40]}': "
148
+ f"{current_rating} β†’ {avg_rating} ({reviews_count} reviews)")
149
+
150
+ print(f"\nπŸ“Š Updated {updated_count} short-stay listings")
151
+ return updated_count
152
+
153
+
154
+ async def ensure_fields_exist(db):
155
+ """
156
+ Ensure all users and listings have the rating/reviews_count fields.
157
+ This initializes them to 0 if missing.
158
+ """
159
+ print("\n" + "="*60)
160
+ print("STEP 1: Ensuring Fields Exist on All Documents")
161
+ print("="*60)
162
+
163
+ # Update users missing rating field
164
+ users_result = await db.users.update_many(
165
+ {"rating": {"$exists": False}},
166
+ {"$set": {"rating": 0.0}}
167
+ )
168
+ print(f" βœ… Added 'rating' field to {users_result.modified_count} users")
169
+
170
+ # Update users missing reviews_count field
171
+ users_result2 = await db.users.update_many(
172
+ {"reviews_count": {"$exists": False}},
173
+ {"$set": {"reviews_count": 0}}
174
+ )
175
+ print(f" βœ… Added 'reviews_count' field to {users_result2.modified_count} users")
176
+
177
+ # Update short-stay listings missing rating field
178
+ listings_result = await db.listings.update_many(
179
+ {"listing_type": "short-stay", "rating": {"$exists": False}},
180
+ {"$set": {"rating": 0.0}}
181
+ )
182
+ print(f" βœ… Added 'rating' field to {listings_result.modified_count} short-stay listings")
183
+
184
+ # Update short-stay listings missing reviews_count field
185
+ listings_result2 = await db.listings.update_many(
186
+ {"listing_type": "short-stay", "reviews_count": {"$exists": False}},
187
+ {"$set": {"reviews_count": 0}}
188
+ )
189
+ print(f" βœ… Added 'reviews_count' field to {listings_result2.modified_count} short-stay listings")
190
+
191
+
192
+ async def print_summary(db):
193
+ """Print a summary of the current state"""
194
+ print("\n" + "="*60)
195
+ print("SUMMARY")
196
+ print("="*60)
197
+
198
+ # Count users with reviews
199
+ users_with_reviews = await db.users.count_documents({"reviews_count": {"$gt": 0}})
200
+ total_users = await db.users.count_documents({})
201
+ print(f" πŸ‘₯ Users with reviews: {users_with_reviews}/{total_users}")
202
+
203
+ # Count listings by type
204
+ for listing_type in ["rent", "sale", "short-stay", "roommate"]:
205
+ count = await db.listings.count_documents({"listing_type": listing_type})
206
+ if listing_type == "short-stay":
207
+ with_reviews = await db.listings.count_documents({
208
+ "listing_type": listing_type,
209
+ "reviews_count": {"$gt": 0}
210
+ })
211
+ print(f" 🏠 {listing_type}: {count} listings ({with_reviews} with reviews)")
212
+ else:
213
+ print(f" 🏠 {listing_type}: {count} listings (use owner's rating)")
214
+
215
+ # Count total reviews
216
+ total_reviews = await db.reviews.count_documents({})
217
+ user_reviews = await db.reviews.count_documents({"target_type": "user"})
218
+ listing_reviews = await db.reviews.count_documents({"target_type": "listing"})
219
+ print(f"\n ⭐ Total reviews: {total_reviews}")
220
+ print(f" - User reviews (for landlords): {user_reviews}")
221
+ print(f" - Listing reviews (for properties): {listing_reviews}")
222
+
223
+
224
+ async def main():
225
+ """Main migration function"""
226
+ print("\n" + "="*60)
227
+ print("πŸ”§ RATING MIGRATION SCRIPT")
228
+ print("="*60)
229
+ print(f"Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
230
+
231
+ try:
232
+ db = await get_database()
233
+ print(f"βœ… Connected to MongoDB successfully")
234
+
235
+ # Step 1: Ensure all fields exist
236
+ await ensure_fields_exist(db)
237
+
238
+ # Step 2: Recalculate user ratings
239
+ await recalculate_user_ratings(db)
240
+
241
+ # Step 3: Recalculate short-stay listing ratings
242
+ await recalculate_listing_ratings(db)
243
+
244
+ # Print summary
245
+ await print_summary(db)
246
+
247
+ print("\n" + "="*60)
248
+ print("βœ… MIGRATION COMPLETED SUCCESSFULLY")
249
+ print("="*60)
250
+ print(f"Finished at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
251
+
252
+ except Exception as e:
253
+ print(f"\n❌ ERROR: {e}")
254
+ import traceback
255
+ traceback.print_exc()
256
+ sys.exit(1)
257
+
258
+
259
+ if __name__ == "__main__":
260
+ asyncio.run(main())