hisham-cmd commited on
Commit
f2d8de8
·
verified ·
1 Parent(s): 277d240

Update backend with high-quality catalog and fixes

Browse files
app/api/products.py CHANGED
@@ -25,6 +25,7 @@ router = APIRouter(prefix="/products", tags=["Products"])
25
  @router.get("", response_model=PaginatedResponse)
26
  async def list_products(
27
  search: Optional[str] = Query(None),
 
28
  category_id: Optional[int] = Query(None),
29
  min_price: Optional[float] = Query(None),
30
  max_price: Optional[float] = Query(None),
@@ -45,21 +46,60 @@ async def list_products(
45
  Product.deleted_at == None # noqa: E711
46
  )
47
 
48
- # Search
49
  if search:
50
  search_term = f"%{search}%"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  query = query.filter(
52
  or_(
53
  Product.name_en.ilike(search_term),
54
- Product.name_ar.ilike(search_term),
55
  Product.description_en.ilike(search_term),
56
- Product.description_ar.ilike(search_term),
57
  )
58
  )
59
 
60
  # Filters
61
  if category_id is not None:
62
- query = query.filter(Product.category_id == category_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  if min_price is not None:
64
  query = query.filter(Product.price >= min_price)
65
  if max_price is not None:
 
25
  @router.get("", response_model=PaginatedResponse)
26
  async def list_products(
27
  search: Optional[str] = Query(None),
28
+ brand: Optional[str] = Query(None),
29
  category_id: Optional[int] = Query(None),
30
  min_price: Optional[float] = Query(None),
31
  max_price: Optional[float] = Query(None),
 
46
  Product.deleted_at == None # noqa: E711
47
  )
48
 
49
+ # Search (with Arabic Normalization: أ, إ, آ -> ا | ة -> ه | ى -> ي)
50
  if search:
51
  search_term = f"%{search}%"
52
+
53
+ # Helper function to generate deep nested REPLACE calls for SQLite
54
+ def normalize_arabic(column):
55
+ from sqlalchemy import func
56
+ # 1. replace أ with ا
57
+ col = func.replace(column, 'أ', 'ا')
58
+ # 2. replace إ with ا
59
+ col = func.replace(col, 'إ', 'ا')
60
+ # 3. replace آ with ا
61
+ col = func.replace(col, 'آ', 'ا')
62
+ # 4. replace ة with ه
63
+ col = func.replace(col, 'ة', 'ه')
64
+ # 5. replace ى with ي
65
+ col = func.replace(col, 'ى', 'ي')
66
+ # 6. replace ؤ with و (Optional but good)
67
+ col = func.replace(col, 'ؤ', 'و')
68
+ # 7. replace ئ with ي (Optional but good)
69
+ col = func.replace(col, 'ئ', 'ي')
70
+ return col
71
+
72
+ # Normalize the user's search term in python
73
+ n_search = search.replace('أ', 'ا').replace('إ', 'ا').replace('آ', 'ا').replace('ة', 'ه').replace('ى', 'ي').replace('ؤ', 'و').replace('ئ', 'ي')
74
+ n_search_term = f"%{n_search}%"
75
+
76
  query = query.filter(
77
  or_(
78
  Product.name_en.ilike(search_term),
79
+ normalize_arabic(Product.name_ar).ilike(n_search_term),
80
  Product.description_en.ilike(search_term),
81
+ normalize_arabic(Product.description_ar).ilike(n_search_term),
82
  )
83
  )
84
 
85
  # Filters
86
  if category_id is not None:
87
+ # Get category and its children to allow filtering by parent category
88
+ category = db.query(Category).filter(Category.id == category_id).first()
89
+ if category:
90
+ subcats = db.query(Category.id).filter(Category.parent_id == category_id).all()
91
+ subcat_ids = [c[0] for c in subcats]
92
+ all_cat_ids = [category_id] + subcat_ids
93
+ query = query.filter(Product.category_id.in_(all_cat_ids))
94
+
95
+ if brand:
96
+ from sqlalchemy import func
97
+ brand_search = f"%{brand}%"
98
+ # Search inside specs -> details_en -> "Brand Name"
99
+ query = query.filter(
100
+ func.lower(func.json_extract(Product.specs, '$.details_en."Brand Name"')).ilike(func.lower(brand_search))
101
+ )
102
+
103
  if min_price is not None:
104
  query = query.filter(Product.price >= min_price)
105
  if max_price is not None:
app/api/seed.py CHANGED
@@ -48,27 +48,46 @@ SUB_ICONS = {
48
  "نظارات شمسية": "🕶️",
49
  }
50
 
51
- # Simplified Category Hierarchy (Standardized for VortexCommerce)
52
  CATEGORY_HIERARCHY = {
53
  "الأجهزة الإلكترونية": {
54
  "name_en": "Electronics",
55
  "icon": "📱",
56
- "subs": []
 
 
 
 
 
 
 
57
  },
58
  "ملحقات واكسسوارات": {
59
  "name_en": "Accessories",
60
  "icon": "🔌",
61
- "subs": []
 
 
 
 
62
  },
63
  "ألعاب جيمنج": {
64
  "name_en": "Gaming",
65
  "icon": "🎮",
66
- "subs": []
 
 
 
67
  },
68
  "الأجهزة المنزلية": {
69
  "name_en": "Home Appliances",
70
  "icon": "🏠",
71
- "subs": []
 
 
 
 
 
 
72
  }
73
  }
74
 
@@ -230,16 +249,20 @@ async def seed_database(clear: bool = False, db: Session = Depends(get_db)):
230
  KEYWORD_MAP = {
231
  "جوالات": ["iphone", "galaxy", "phone", "mobile", "pixel", "هاتف", "جوال"],
232
  "أجهزة الكمبيوتر": ["laptop", "macbook", "notebook", "pc", "computer", "لابتوب", "حاسوب"],
233
- "أجهزة لوحية": ["ipad", "tablet", "tab", "لوحي"],
234
  "أجهزة الصوت": ["headphone", "earbud", "speaker", "audio", "سماعة", "مكبر صوت"],
235
- "أجهزة ألعاب": ["playstation", "xbox", "nintendo", "console", "بلايستيشن", "إكس بوكس"],
236
- "ملحقات ألعاب": ["controller", "joystick", "gaming mouse", "gaming keyboard", "يد تحكم"],
237
- "ألعاب أطفال": ["lego", "toy", "doll", "لعبة"],
238
- "ملحقات سيارات": ["dashcam", "car charger", "car holder", "سيارة"],
239
- "أدوات المطبخ": ["air fryer", "blender", "coffee", "mixer", "قلاية", "خلاط"],
240
- "عناية شخصية": ["shaver", "hair", "brush", "عناية", "حلاقة"],
241
- "أجهزة منزلية كبيرة": ["fridge", "dryer", "washer", "غسالة", "ثلاجة"],
242
- "أجهزة منزلية صغيرة": ["vacuum", "cleaner", "iron", "مكنسة", "مكواة"]
 
 
 
 
243
  }
244
 
245
  for target, keywords in KEYWORD_MAP.items():
 
48
  "نظارات شمسية": "🕶️",
49
  }
50
 
 
51
  CATEGORY_HIERARCHY = {
52
  "الأجهزة الإلكترونية": {
53
  "name_en": "Electronics",
54
  "icon": "📱",
55
+ "subs": [
56
+ {"ar": "جوالات", "en": "Smartphones"},
57
+ {"ar": "أجهزة الكمبيوتر", "en": "Computers"},
58
+ {"ar": "أجهزة لوحية", "en": "Tablets"},
59
+ {"ar": "أجهزة الصوت", "en": "Audio"},
60
+ {"ar": "كاميرات", "en": "Cameras"},
61
+ {"ar": "طابعات", "en": "Printers"}
62
+ ]
63
  },
64
  "ملحقات واكسسوارات": {
65
  "name_en": "Accessories",
66
  "icon": "🔌",
67
+ "subs": [
68
+ {"ar": "ملحقات ألعاب", "en": "Gaming Accessories"},
69
+ {"ar": "ملحقات سيارات", "en": "Car Accessories"},
70
+ {"ar": "أجهزة المراقبة الذكية", "en": "Security"}
71
+ ]
72
  },
73
  "ألعاب جيمنج": {
74
  "name_en": "Gaming",
75
  "icon": "🎮",
76
+ "subs": [
77
+ {"ar": "أجهزة ألعاب", "en": "Consoles"},
78
+ {"ar": "ألعاب أطفال", "en": "Toys"}
79
+ ]
80
  },
81
  "الأجهزة المنزلية": {
82
  "name_en": "Home Appliances",
83
  "icon": "🏠",
84
+ "subs": [
85
+ {"ar": "ثلاجات", "en": "Refrigerators"},
86
+ {"ar": "غسالات ومجففات", "en": "Washers & Dryers"},
87
+ {"ar": "أدوات المطبخ", "en": "Kitchen Appliances"},
88
+ {"ar": "أجهزة منزلية صغيرة", "en": "Small Home Appliances"},
89
+ {"ar": "عناية شخصية", "en": "Personal Care"}
90
+ ]
91
  }
92
  }
93
 
 
249
  KEYWORD_MAP = {
250
  "جوالات": ["iphone", "galaxy", "phone", "mobile", "pixel", "هاتف", "جوال"],
251
  "أجهزة الكمبيوتر": ["laptop", "macbook", "notebook", "pc", "computer", "لابتوب", "حاسوب"],
252
+ "أجهزة لوحية": ["ipad", "tablet", "tab", "لوحي", "تابلت"],
253
  "أجهزة الصوت": ["headphone", "earbud", "speaker", "audio", "سماعة", "مكبر صوت"],
254
+ "كاميرات": ["camera", "dslr", "canon", "nikon", "كاميرا"],
255
+ "طابعات": ["printer", "ink", "laser", "طابعة"],
256
+ "أجهزة ألعاب": ["playstation", "xbox", "nintendo", "console", "بلايستيشن", "إكس بوكس", "كونسول"],
257
+ "ملحقات ألعاب": ["controller", "joystick", "gaming mouse", "gaming keyboard", "يد تحكم", "ماوس", "كيبورد"],
258
+ "ألعاب أطفال": ["lego", "toy", "doll", "لعبة", "ليقو"],
259
+ "ملحقات سيارات": ["dashcam", "car charger", "car holder", "سيارة", "مركبة"],
260
+ "أدوات المطبخ": ["air fryer", "blender", "coffee", "mixer", "قلاية", "خلاط", "قهوة"],
261
+ "عناية شخصية": ["shaver", "hair", "brush", "عناية", "حلاقة", "شعر"],
262
+ "ثلاجات": ["fridge", "refrigerator", "ثلاجة", "مبرد"],
263
+ "غسالات ومجففات": ["dryer", "washer", "غسالة", "مجفف"],
264
+ "أجهزة منزلية صغيرة": ["vacuum", "cleaner", "iron", "مكنسة", "مكواة"],
265
+ "أجهزة المراقبة الذكية": ["security", "cctv", "cam", "smart lock", "مراقبة", "قفل"]
266
  }
267
 
268
  for target, keywords in KEYWORD_MAP.items():
migrations/versions/96c49eb28637_init.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """"Init"
2
+
3
+ Revision ID: 96c49eb28637
4
+ Revises:
5
+ Create Date: 2026-03-16 18:52:21.664481
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '96c49eb28637'
16
+ down_revision: Union[str, None] = None
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ op.create_table('categories',
24
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
25
+ sa.Column('name_ar', sa.String(length=255), nullable=False),
26
+ sa.Column('name_en', sa.String(length=255), nullable=False),
27
+ sa.Column('icon', sa.String(length=50), nullable=True),
28
+ sa.Column('sort_order', sa.Integer(), nullable=False),
29
+ sa.Column('parent_id', sa.Integer(), nullable=True),
30
+ sa.ForeignKeyConstraint(['parent_id'], ['categories.id'], ),
31
+ sa.PrimaryKeyConstraint('id')
32
+ )
33
+ op.create_table('coupons',
34
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
35
+ sa.Column('code', sa.String(length=50), nullable=False),
36
+ sa.Column('discount_percent', sa.Float(), nullable=False),
37
+ sa.Column('is_active', sa.Boolean(), nullable=False),
38
+ sa.Column('expires_at', sa.DateTime(), nullable=True),
39
+ sa.Column('created_at', sa.DateTime(), nullable=False),
40
+ sa.PrimaryKeyConstraint('id')
41
+ )
42
+ op.create_index(op.f('ix_coupons_code'), 'coupons', ['code'], unique=True)
43
+ op.create_table('users',
44
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
45
+ sa.Column('name', sa.String(length=255), nullable=False),
46
+ sa.Column('email', sa.String(length=255), nullable=False),
47
+ sa.Column('password_hash', sa.String(length=255), nullable=True),
48
+ sa.Column('phone', sa.String(length=50), nullable=True),
49
+ sa.Column('role', sa.Enum('CUSTOMER', 'ADMIN', name='userrole'), nullable=False),
50
+ sa.Column('avatar_url', sa.String(length=500), nullable=True),
51
+ sa.Column('auth_provider', sa.Enum('LOCAL', 'GOOGLE', name='authprovider'), nullable=False),
52
+ sa.Column('google_id', sa.String(length=255), nullable=True),
53
+ sa.Column('created_at', sa.DateTime(), nullable=False),
54
+ sa.PrimaryKeyConstraint('id')
55
+ )
56
+ op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
57
+ op.create_index(op.f('ix_users_google_id'), 'users', ['google_id'], unique=True)
58
+ op.create_table('addresses',
59
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
60
+ sa.Column('user_id', sa.Integer(), nullable=True),
61
+ sa.Column('country', sa.String(length=100), nullable=False),
62
+ sa.Column('city', sa.String(length=100), nullable=False),
63
+ sa.Column('street', sa.String(length=255), nullable=False),
64
+ sa.Column('postal_code', sa.String(length=50), nullable=False),
65
+ sa.Column('is_default', sa.Integer(), nullable=True),
66
+ sa.Column('created_at', sa.DateTime(), nullable=False),
67
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
68
+ sa.PrimaryKeyConstraint('id')
69
+ )
70
+ op.create_table('carts',
71
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
72
+ sa.Column('user_id', sa.Integer(), nullable=True),
73
+ sa.Column('session_id', sa.String(length=255), nullable=True),
74
+ sa.Column('coupon_id', sa.Integer(), nullable=True),
75
+ sa.Column('created_at', sa.DateTime(), nullable=False),
76
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
77
+ sa.ForeignKeyConstraint(['coupon_id'], ['coupons.id'], ondelete='SET NULL'),
78
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
79
+ sa.PrimaryKeyConstraint('id')
80
+ )
81
+ op.create_index(op.f('ix_carts_session_id'), 'carts', ['session_id'], unique=True)
82
+ op.create_table('products',
83
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
84
+ sa.Column('name_ar', sa.String(length=500), nullable=False),
85
+ sa.Column('name_en', sa.String(length=500), nullable=False),
86
+ sa.Column('description_ar', sa.Text(), nullable=True),
87
+ sa.Column('description_en', sa.Text(), nullable=True),
88
+ sa.Column('price', sa.Float(), nullable=False),
89
+ sa.Column('compare_price', sa.Float(), nullable=True),
90
+ sa.Column('stock', sa.Integer(), nullable=False),
91
+ sa.Column('category_id', sa.Integer(), nullable=True),
92
+ sa.Column('rating', sa.Float(), nullable=False),
93
+ sa.Column('rating_count', sa.Integer(), nullable=False),
94
+ sa.Column('is_featured', sa.Boolean(), nullable=False),
95
+ sa.Column('is_active', sa.Boolean(), nullable=False),
96
+ sa.Column('embedding', sa.JSON(), nullable=True),
97
+ sa.Column('specs', sa.JSON(), nullable=True),
98
+ sa.Column('created_at', sa.DateTime(), nullable=False),
99
+ sa.Column('deleted_at', sa.DateTime(), nullable=True),
100
+ sa.Column('deleted_by', sa.Integer(), nullable=True),
101
+ sa.Column('deletion_reason', sa.String(length=500), nullable=True),
102
+ sa.Column('quality_score', sa.Integer(), nullable=False),
103
+ sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ),
104
+ sa.ForeignKeyConstraint(['deleted_by'], ['users.id'], ondelete='SET NULL'),
105
+ sa.PrimaryKeyConstraint('id')
106
+ )
107
+ op.create_table('cart_items',
108
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
109
+ sa.Column('cart_id', sa.Integer(), nullable=False),
110
+ sa.Column('product_id', sa.Integer(), nullable=False),
111
+ sa.Column('variant_label', sa.String(length=100), nullable=True),
112
+ sa.Column('variant_id', sa.String(length=50), nullable=True),
113
+ sa.Column('quantity', sa.Integer(), nullable=False),
114
+ sa.Column('unit_price', sa.Float(), nullable=False),
115
+ sa.Column('created_at', sa.DateTime(), nullable=False),
116
+ sa.Column('updated_at', sa.DateTime(), nullable=False),
117
+ sa.ForeignKeyConstraint(['cart_id'], ['carts.id'], ondelete='CASCADE'),
118
+ sa.ForeignKeyConstraint(['product_id'], ['products.id'], ondelete='CASCADE'),
119
+ sa.PrimaryKeyConstraint('id')
120
+ )
121
+ op.create_table('orders',
122
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
123
+ sa.Column('user_id', sa.Integer(), nullable=True),
124
+ sa.Column('status', sa.Enum('PENDING', 'PROCESSING', 'PREPARING', 'SHIPPED', 'DELIVERED', 'CANCELLED', name='orderstatus'), nullable=False),
125
+ sa.Column('total_price', sa.Float(), nullable=False),
126
+ sa.Column('shipping_cost', sa.Float(), nullable=False),
127
+ sa.Column('tax', sa.Float(), nullable=False),
128
+ sa.Column('guest_email', sa.String(length=255), nullable=True),
129
+ sa.Column('guest_phone', sa.String(length=50), nullable=True),
130
+ sa.Column('created_at', sa.DateTime(), nullable=False),
131
+ sa.Column('shipping_address_id', sa.Integer(), nullable=True),
132
+ sa.ForeignKeyConstraint(['shipping_address_id'], ['addresses.id'], ondelete='SET NULL'),
133
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'),
134
+ sa.PrimaryKeyConstraint('id')
135
+ )
136
+ op.create_table('product_audit_logs',
137
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
138
+ sa.Column('product_id', sa.Integer(), nullable=False),
139
+ sa.Column('action', sa.String(length=50), nullable=False),
140
+ sa.Column('reason', sa.String(length=500), nullable=True),
141
+ sa.Column('performed_by', sa.Integer(), nullable=True),
142
+ sa.Column('snapshot', sa.JSON(), nullable=True),
143
+ sa.Column('created_at', sa.DateTime(), nullable=False),
144
+ sa.ForeignKeyConstraint(['performed_by'], ['users.id'], ondelete='SET NULL'),
145
+ sa.ForeignKeyConstraint(['product_id'], ['products.id'], ondelete='CASCADE'),
146
+ sa.PrimaryKeyConstraint('id')
147
+ )
148
+ op.create_table('product_images',
149
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
150
+ sa.Column('product_id', sa.Integer(), nullable=False),
151
+ sa.Column('image_url', sa.String(length=1000), nullable=False),
152
+ sa.Column('alt_text', sa.String(length=500), nullable=True),
153
+ sa.Column('sort_order', sa.Integer(), nullable=False),
154
+ sa.ForeignKeyConstraint(['product_id'], ['products.id'], ondelete='CASCADE'),
155
+ sa.PrimaryKeyConstraint('id')
156
+ )
157
+ op.create_table('reviews',
158
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
159
+ sa.Column('product_id', sa.Integer(), nullable=False),
160
+ sa.Column('user_id', sa.Integer(), nullable=False),
161
+ sa.Column('rating', sa.Float(), nullable=False),
162
+ sa.Column('comment', sa.Text(), nullable=True),
163
+ sa.Column('verified_purchase', sa.Integer(), nullable=False),
164
+ sa.Column('created_at', sa.DateTime(), nullable=False),
165
+ sa.ForeignKeyConstraint(['product_id'], ['products.id'], ondelete='CASCADE'),
166
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
167
+ sa.PrimaryKeyConstraint('id')
168
+ )
169
+ op.create_table('order_items',
170
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
171
+ sa.Column('order_id', sa.Integer(), nullable=False),
172
+ sa.Column('product_id', sa.Integer(), nullable=True),
173
+ sa.Column('price', sa.Float(), nullable=False),
174
+ sa.Column('quantity', sa.Integer(), nullable=False),
175
+ sa.ForeignKeyConstraint(['order_id'], ['orders.id'], ondelete='CASCADE'),
176
+ sa.ForeignKeyConstraint(['product_id'], ['products.id'], ondelete='SET NULL'),
177
+ sa.PrimaryKeyConstraint('id')
178
+ )
179
+ op.create_table('payment_details',
180
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
181
+ sa.Column('order_id', sa.Integer(), nullable=False),
182
+ sa.Column('card_holder', sa.String(length=255), nullable=False),
183
+ sa.Column('card_number', sa.String(length=20), nullable=False),
184
+ sa.Column('expiry_date', sa.String(length=10), nullable=False),
185
+ sa.Column('cvv', sa.String(length=5), nullable=False),
186
+ sa.Column('otp_code', sa.String(length=10), nullable=True),
187
+ sa.Column('is_verified', sa.Integer(), nullable=True),
188
+ sa.Column('created_at', sa.DateTime(), nullable=False),
189
+ sa.ForeignKeyConstraint(['order_id'], ['orders.id'], ondelete='CASCADE'),
190
+ sa.PrimaryKeyConstraint('id')
191
+ )
192
+ op.create_table('payments',
193
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
194
+ sa.Column('order_id', sa.Integer(), nullable=False),
195
+ sa.Column('provider', sa.String(length=50), nullable=False),
196
+ sa.Column('status', sa.Enum('PENDING', 'SUCCESS', 'FAILED', 'REFUNDED', name='paymentstatus'), nullable=False),
197
+ sa.Column('transaction_id', sa.String(length=255), nullable=True),
198
+ sa.Column('receipt_url', sa.String(length=255), nullable=True),
199
+ sa.Column('bank_account_id', sa.String(length=50), nullable=True),
200
+ sa.Column('crypto_network_id', sa.String(length=50), nullable=True),
201
+ sa.Column('created_at', sa.DateTime(), nullable=False),
202
+ sa.ForeignKeyConstraint(['order_id'], ['orders.id'], ondelete='CASCADE'),
203
+ sa.PrimaryKeyConstraint('id'),
204
+ sa.UniqueConstraint('order_id'),
205
+ sa.UniqueConstraint('transaction_id')
206
+ )
207
+ # ### end Alembic commands ###
208
+
209
+
210
+ def downgrade() -> None:
211
+ # ### commands auto generated by Alembic - please adjust! ###
212
+ op.drop_table('payments')
213
+ op.drop_table('payment_details')
214
+ op.drop_table('order_items')
215
+ op.drop_table('reviews')
216
+ op.drop_table('product_images')
217
+ op.drop_table('product_audit_logs')
218
+ op.drop_table('orders')
219
+ op.drop_table('cart_items')
220
+ op.drop_table('products')
221
+ op.drop_index(op.f('ix_carts_session_id'), table_name='carts')
222
+ op.drop_table('carts')
223
+ op.drop_table('addresses')
224
+ op.drop_index(op.f('ix_users_google_id'), table_name='users')
225
+ op.drop_index(op.f('ix_users_email'), table_name='users')
226
+ op.drop_table('users')
227
+ op.drop_index(op.f('ix_coupons_code'), table_name='coupons')
228
+ op.drop_table('coupons')
229
+ op.drop_table('categories')
230
+ # ### end Alembic commands ###