hisham-cmd commited on
Commit
9c2ed25
·
verified ·
1 Parent(s): 2179b36

Update backend with high-quality catalog and fixes

Browse files
Files changed (2) hide show
  1. app/api/seed.py +51 -80
  2. seed_products.py +31 -7
app/api/seed.py CHANGED
@@ -17,60 +17,35 @@ from app.services.external_catalog import ExternalCatalogService
17
 
18
  router = APIRouter(prefix="/seed", tags=["Seed Data"])
19
 
20
- # Mapping distinct categories from Excel to a hierarchical structure
21
  CATEGORY_MAP = {
22
- # Parents (Keys) : Subcategories (Values)
23
- "الأزياء": ["الملابس الرجالية", "الملابس النسائية"],
24
- "الأحذية والحقائب": ["الأحذية", "الحقائب"],
25
- "الأطفال والألعاب": ["الأطفال"],
26
- "المنزل والمطبخ": ["المنزل والمطبخ", "المستلزمات المنزلية", "مستلزمات المطبخ", "ديكور المنزل", "الأثاث", "الإضاءة"],
27
- "العطور والتجميل": ["العطور", "التجميل والعناية", "الجمال", "العناية بالبشرة"],
28
  "الأجهزة الإلكترونية": ["الهواتف والإلكترونيات", "الهواتف الذكية", "أجهزة الكمبيوتر"],
 
 
 
29
  "الساعات والإكسسوارات": ["الساعات والاكسسوارات", "ساعات نسائية", "ساعات رجالية", "نظارات شمسية"],
30
  "الرياضة واللياقة": ["الرياضة واللياقة", "إكسسوارات رياضية"],
31
- "ملحقات السيارات": ["ملحقات السيارات", "إكسسوارات السيارات", "إكسسوارات الدراجات"],
32
- }
33
-
34
- # Emoji icons for frontend display
35
- PARENT_ICONS = {
36
- "الأزياء": "👗", "الأحذية والحقائب": "👟", "الأطفال والألعاب": "🧸",
37
- "المنزل والمطبخ": "🏠", "العطور والتجميل": "✨", "الأجهزة الإلكترونية": "📱",
38
- "الساعات والإكسسوارات": "⌚", "الرياضة واللياقة": "🏋️",
39
- }
40
- SUB_ICONS = {
41
- "الملابس الرجالية": "👔", "الملابس النسائية": "👗", "الأحذية": "👟",
42
- "الحقائب": "👜", "الأطفال": "🧸", "المنزل والمطبخ": "🏠",
43
- "المستلزمات المنزلية": "🧹", "العطور": "🌸", "التجميل والعناية": "💄",
44
- "الهواتف والإلكترونيات": "📱", "الساعات والاكسسوارات": "⌚",
45
- "الرياضة واللياقة": "🏋️", "مستلزمات المطبخ": "🍳", "ديكور المنزل": "🪑",
46
- "الأثاث": "🛋️", "الإضاءة": "💡", "الجمال": "💅", "العناية بالبشرة": "🧴",
47
- "الهواتف الذكية": "📱", "أجهزة الكمبيوتر": "💻", "ساعات نسائية": "⌚",
48
- "ساعات رجالية": "⌚", "نظارات شمسية": "🕶️", "إكسسوارات رياضية": "🏃",
49
- "إكسسوارات السيارات": "🚗", "إكسسوارات الدراجات": "🏍️",
50
  }
51
 
52
  @router.api_route("", methods=["GET", "POST"], response_model=AuthResponse)
53
  async def seed_database(clear: bool = False, db: Session = Depends(get_db)):
54
  """Populate database with proper product data, optionally clearing existing data first."""
55
  if clear:
56
- # Clear existing data using raw SQL to avoid materialization errors if enums changed
57
  db.execute(text("DELETE FROM product_images"))
58
  db.execute(text("DELETE FROM products"))
59
  db.execute(text("DELETE FROM categories"))
60
  db.execute(text("DELETE FROM coupons"))
61
- # Optional: clear users if you want a fresh start, or just fix the admin
62
- db.execute(text("UPDATE users SET role = 'ADMIN' WHERE role = 'admin'")) # Fix legacy enums
63
 
64
- # Patch schema: add specs column if missing (SQLite doesn't support IF NOT EXISTS in ALTER)
65
  try:
66
  db.execute(text("ALTER TABLE products ADD COLUMN specs JSON"))
67
  except Exception:
68
- pass # Already exists or other issue
69
 
70
  db.commit()
71
 
72
  # Always ensure admin user exists
73
- # Use raw SQL to check for admin to avoid LookupError if data is inconsistent
74
  admin_exists = db.execute(text("SELECT id FROM users WHERE email = 'admin@vortex.com'")).fetchone()
75
  if not admin_exists:
76
  admin_user = User(
@@ -83,16 +58,6 @@ async def seed_database(clear: bool = False, db: Session = Depends(get_db)):
83
  db.add(admin_user)
84
  db.commit()
85
 
86
- # Check if products are already seeded (Skip if clearing)
87
- if not clear:
88
- existing = db.query(Category).count()
89
- if existing > 0:
90
- return AuthResponse(
91
- isSuccess=True,
92
- value={"message": "Database already has data. Admin user ensured."},
93
- statusCode=200,
94
- )
95
-
96
  json_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "backend", "real_products_1000.json")
97
  if not os.path.exists(json_path):
98
  json_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "real_products_1000.json")
@@ -141,38 +106,50 @@ async def seed_database(clear: bool = False, db: Session = Depends(get_db)):
141
  db.refresh(sub_cat)
142
  sub_cats[sub_name] = {"model": sub_cat, "parent_name": parent_name}
143
 
 
 
 
 
144
  # 3. Create Products
145
  inserted_count = 0
146
- used_names = set() # Avoid absolute name duplicates in current seed run
147
 
148
  for item in products_data:
149
- # Handle both formats (real_products_500.json vs products_dump.json)
150
- cat_name = item.get("category_name") or item.get("الفئة")
151
  if not cat_name or cat_name not in sub_cats:
152
- continue
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
- product_name_ar = item.get("name_ar") or item.get("اسم المنتج", "").strip()
155
- if not product_name_ar or product_name_ar in used_names:
 
156
  continue
157
 
158
  sub_cat_info = sub_cats[cat_name]
159
- used_names.add(product_name_ar)
160
-
161
- price = float(item.get("price") or item.get("سعر البيع (ريال)", 0))
162
- compare_price = float(item.get("compare_price") or (price * 1.2))
163
 
164
  prod = Product(
165
  name_ar=product_name_ar,
166
- name_en=item.get("name_en") or product_name_ar,
167
- description_ar=item.get("description_ar") or item.get("الوصف التسويقي", ""),
168
- description_en=item.get("description_en") or item.get("الوصف التسويقي", ""),
169
- price=price,
170
- compare_price=round(compare_price, 2),
171
- stock=item.get("stock") or random.randint(10, 100),
172
  category_id=sub_cat_info["model"].id,
173
- rating=round(random.uniform(3.5, 5.0), 1),
174
- rating_count=random.randint(0, 50),
175
- is_featured=item.get("is_featured", random.choice([True, False])),
176
  is_active=item.get("is_active", True),
177
  specs=item.get("specs", {})
178
  )
@@ -180,38 +157,32 @@ async def seed_database(clear: bool = False, db: Session = Depends(get_db)):
180
  db.flush()
181
 
182
  # 4. Handle Images
183
- raw_images = item.get("images", [])
184
- for i, img_url in enumerate(raw_images):
185
- prod_img = ProductImage(
186
  product_id=prod.id,
187
  image_url=img_url,
188
  alt_text=prod.name_ar,
189
  sort_order=i
190
- )
191
- db.add(prod_img)
192
 
193
  inserted_count += 1
 
 
194
 
195
- # Seed Coupons
196
- DEMO_COUPONS = [
197
- {"code": "WELCOME10", "discount_percent": 10.0, "is_active": True},
198
- {"code": "VORTEX20", "discount_percent": 20.0, "is_active": True},
199
- {"code": "SUMMER50", "discount_percent": 50.0, "is_active": False},
200
- ]
201
-
202
- for coupon_data in DEMO_COUPONS:
203
- coupon = Coupon(**coupon_data)
204
- db.add(coupon)
205
 
206
  db.commit()
207
 
208
  return AuthResponse(
209
  isSuccess=True,
210
  value={
211
- "message": "تم تهيئة قاعدة البيانات بالمنتجات الحقيقية بنجاح",
212
- "categories_count": db.query(Category).count(),
213
- "total_products": inserted_count,
214
- "coupons_count": len(DEMO_COUPONS),
215
  },
216
  statusCode=201,
217
  )
 
17
 
18
  router = APIRouter(prefix="/seed", tags=["Seed Data"])
19
 
20
+ # Syncing with High-Quality Catalog Categories
21
  CATEGORY_MAP = {
 
 
 
 
 
 
22
  "الأجهزة الإلكترونية": ["الهواتف والإلكترونيات", "الهواتف الذكية", "أجهزة الكمبيوتر"],
23
+ "ملحقات السيارات": ["ملحقات السيارات", "إكسسوارات السيارات"],
24
+ "المنزل والمطبخ": ["المنزل والمطبخ", "المستلزمات المنزلية", "مستلزمات المطبخ", "ديكور المنزل", "الأثاث"],
25
+ "العطور والتجميل": ["العطور", "التجميل والعناية", "الجمال", "العناية بالبشرة"],
26
  "الساعات والإكسسوارات": ["الساعات والاكسسوارات", "ساعات نسائية", "ساعات رجالية", "نظارات شمسية"],
27
  "الرياضة واللياقة": ["الرياضة واللياقة", "إكسسوارات رياضية"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
 
30
  @router.api_route("", methods=["GET", "POST"], response_model=AuthResponse)
31
  async def seed_database(clear: bool = False, db: Session = Depends(get_db)):
32
  """Populate database with proper product data, optionally clearing existing data first."""
33
  if clear:
34
+ # Clear existing data
35
  db.execute(text("DELETE FROM product_images"))
36
  db.execute(text("DELETE FROM products"))
37
  db.execute(text("DELETE FROM categories"))
38
  db.execute(text("DELETE FROM coupons"))
39
+ db.execute(text("UPDATE users SET role = 'ADMIN' WHERE role = 'admin'"))
 
40
 
 
41
  try:
42
  db.execute(text("ALTER TABLE products ADD COLUMN specs JSON"))
43
  except Exception:
44
+ pass
45
 
46
  db.commit()
47
 
48
  # Always ensure admin user exists
 
49
  admin_exists = db.execute(text("SELECT id FROM users WHERE email = 'admin@vortex.com'")).fetchone()
50
  if not admin_exists:
51
  admin_user = User(
 
58
  db.add(admin_user)
59
  db.commit()
60
 
 
 
 
 
 
 
 
 
 
 
61
  json_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "backend", "real_products_1000.json")
62
  if not os.path.exists(json_path):
63
  json_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "real_products_1000.json")
 
106
  db.refresh(sub_cat)
107
  sub_cats[sub_name] = {"model": sub_cat, "parent_name": parent_name}
108
 
109
+ # Cache existing products to prevent duplicates when clear=False
110
+ existing_products_ar = set(r[0] for r in db.execute(text("SELECT name_ar FROM products")).fetchall())
111
+ existing_products_en = set(r[0] for r in db.execute(text("SELECT name_en FROM products")).fetchall())
112
+
113
  # 3. Create Products
114
  inserted_count = 0
115
+ skipped_count = 0
116
 
117
  for item in products_data:
118
+ cat_name = item.get("category_name")
 
119
  if not cat_name or cat_name not in sub_cats:
120
+ # Automatic fallback mapping for consistency
121
+ if not cat_name: continue
122
+ # Try to find best match
123
+ matched = False
124
+ for sub, info in sub_cats.items():
125
+ if cat_name in sub or sub in cat_name:
126
+ cat_name = sub
127
+ matched = True
128
+ break
129
+ if not matched: continue
130
+
131
+ product_name_ar = item.get("name_ar", "").strip()
132
+ product_name_en = item.get("name_en", "").strip()
133
 
134
+ # Smart Duplicate Prevention: Skip if name already exists in DB
135
+ if product_name_ar in existing_products_ar or product_name_en in existing_products_en:
136
+ skipped_count += 1
137
  continue
138
 
139
  sub_cat_info = sub_cats[cat_name]
 
 
 
 
140
 
141
  prod = Product(
142
  name_ar=product_name_ar,
143
+ name_en=product_name_en,
144
+ description_ar=item.get("description_ar", ""),
145
+ description_en=item.get("description_en", ""),
146
+ price=float(item.get("price", 0)),
147
+ compare_price=round(float(item.get("compare_price", price * 1.2)), 2),
148
+ stock=item.get("stock", random.randint(10, 100)),
149
  category_id=sub_cat_info["model"].id,
150
+ rating=item.get("rating", 4.5),
151
+ rating_count=item.get("rating_count", 20),
152
+ is_featured=item.get("is_featured", False),
153
  is_active=item.get("is_active", True),
154
  specs=item.get("specs", {})
155
  )
 
157
  db.flush()
158
 
159
  # 4. Handle Images
160
+ for i, img_url in enumerate(item.get("images", [])):
161
+ db.add(ProductImage(
 
162
  product_id=prod.id,
163
  image_url=img_url,
164
  alt_text=prod.name_ar,
165
  sort_order=i
166
+ ))
 
167
 
168
  inserted_count += 1
169
+ existing_products_ar.add(product_name_ar)
170
+ existing_products_en.add(product_name_en)
171
 
172
+ # Ensure Demo Coupons
173
+ for code, discount in [("WELCOME10", 10.0), ("VORTEX20", 20.0)]:
174
+ if not db.query(Coupon).filter(Coupon.code == code).first():
175
+ db.add(Coupon(code=code, discount_percent=discount, is_active=True))
 
 
 
 
 
 
176
 
177
  db.commit()
178
 
179
  return AuthResponse(
180
  isSuccess=True,
181
  value={
182
+ "message": "تمت عملية المزامنة بنجاح" if not clear else "تمت تهيئة قاعدة البيانات بنجاح",
183
+ "total_products": db.query(Product).count(),
184
+ "inserted_new": inserted_count,
185
+ "skipped_duplicates": skipped_count,
186
  },
187
  statusCode=201,
188
  )
seed_products.py CHANGED
@@ -2,6 +2,7 @@ import json
2
  import random
3
  import sys
4
  import os
 
5
  from sqlalchemy.orm import Session
6
  from app.db.base import SessionLocal, engine, Base
7
  from app.models.product import Product, Category, ProductImage
@@ -60,18 +61,35 @@ def seed_database(clear=False):
60
  db.refresh(sub_cat)
61
  cat_cache[parent_name] = sub_cat.id
62
 
 
 
 
 
63
  # 2. Insert Products
64
  print("Feeding database with Saudi trends...")
65
  inserted = 0
 
66
  for item in products_data:
 
 
 
 
 
 
 
67
  cat_id = cat_cache.get(item.get("category_name"))
68
  if not cat_id:
69
- # Fallback to first available if category mismatch
70
- cat_id = list(cat_cache.values())[0]
 
 
 
 
 
71
 
72
  prod = Product(
73
- name_ar=item.get("name_ar"),
74
- name_en=item.get("name_en"),
75
  description_ar=item.get("description_ar"),
76
  description_en=item.get("description_en"),
77
  price=item.get("price"),
@@ -82,7 +100,7 @@ def seed_database(clear=False):
82
  category_id=cat_id,
83
  is_featured=item.get("is_featured", False),
84
  is_active=True,
85
- specs=item.get("specs") # New: Save embedded variants and specs
86
  )
87
  db.add(prod)
88
  db.flush()
@@ -97,11 +115,17 @@ def seed_database(clear=False):
97
  ))
98
 
99
  inserted += 1
 
 
 
100
  if inserted % 100 == 0:
101
- print(f" -> Progress: {inserted}/1000")
102
 
103
  db.commit()
104
- print(f"\n[SUCCESS] Seeding complete! {inserted} items in database.")
 
 
 
105
 
106
  except Exception as e:
107
  db.rollback()
 
2
  import random
3
  import sys
4
  import os
5
+ from sqlalchemy import text
6
  from sqlalchemy.orm import Session
7
  from app.db.base import SessionLocal, engine, Base
8
  from app.models.product import Product, Category, ProductImage
 
61
  db.refresh(sub_cat)
62
  cat_cache[parent_name] = sub_cat.id
63
 
64
+ # Cache existing products for duplicate check
65
+ existing_names = set(n[0] for n in db.execute(text("SELECT name_en FROM products")).fetchall())
66
+ existing_names.update(n[0] for n in db.execute(text("SELECT name_ar FROM products")).fetchall())
67
+
68
  # 2. Insert Products
69
  print("Feeding database with Saudi trends...")
70
  inserted = 0
71
+ skipped = 0
72
  for item in products_data:
73
+ name_en = item.get("name_en")
74
+ name_ar = item.get("name_ar")
75
+
76
+ if name_en in existing_names or name_ar in existing_names:
77
+ skipped += 1
78
+ continue
79
+
80
  cat_id = cat_cache.get(item.get("category_name"))
81
  if not cat_id:
82
+ # Try to match subcategories
83
+ matched_id = None
84
+ for cat_name, cid in cat_cache.items():
85
+ if item.get("category_name") in cat_name:
86
+ matched_id = cid
87
+ break
88
+ cat_id = matched_id or list(cat_cache.values())[0]
89
 
90
  prod = Product(
91
+ name_ar=name_ar,
92
+ name_en=name_en,
93
  description_ar=item.get("description_ar"),
94
  description_en=item.get("description_en"),
95
  price=item.get("price"),
 
100
  category_id=cat_id,
101
  is_featured=item.get("is_featured", False),
102
  is_active=True,
103
+ specs=item.get("specs")
104
  )
105
  db.add(prod)
106
  db.flush()
 
115
  ))
116
 
117
  inserted += 1
118
+ existing_names.add(name_en)
119
+ existing_names.add(name_ar)
120
+
121
  if inserted % 100 == 0:
122
+ print(f" -> Progress: {inserted} added, {skipped} skipped")
123
 
124
  db.commit()
125
+ print(f"\n[SUCCESS] Seeding complete!")
126
+ print(f" -> Total New: {inserted}")
127
+ print(f" -> Skipped Duplicates: {skipped}")
128
+ print(f" -> Current DB Count: {db.query(Product).count()}")
129
 
130
  except Exception as e:
131
  db.rollback()