hisham-cmd commited on
Commit
fa7006f
·
verified ·
1 Parent(s): 5c90fa4

Upload app/api/cart.py

Browse files
Files changed (1) hide show
  1. app/api/cart.py +236 -0
app/api/cart.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Optional
2
+ from fastapi import APIRouter, Depends, HTTPException, status, Header
3
+ from sqlalchemy.orm import Session
4
+ from datetime import datetime, timezone
5
+
6
+ from app.db.base import get_db
7
+ from app.models.user import User
8
+ from app.models.product import Product
9
+ from app.models.cart import Cart, CartItem, Coupon
10
+ from app.schemas.cart import (
11
+ CartResponse, CartItemCreate, CartItemUpdate,
12
+ CartItemResponse, CouponApply, CouponResponse
13
+ )
14
+ from app.core.security import get_optional_current_user
15
+
16
+ router = APIRouter(tags=["Cart"])
17
+
18
+ TAX_RATE = 0.15 # 15% VAT
19
+ SHIPPING_FLAT_RATE = 20.0 # Flat shipping rate
20
+ FREE_SHIPPING_THRESHOLD = 500.0 # Free shipping over 500
21
+
22
+
23
+ def calculate_cart_totals(cart: Cart) -> dict:
24
+ subtotal = sum(item.quantity * item.unit_price for item in cart.items)
25
+
26
+ # Calculate discount
27
+ discount_amount = 0.0
28
+ applied_coupon = None
29
+ if cart.coupon and cart.coupon.is_active:
30
+ if cart.coupon.expires_at is None or cart.coupon.expires_at > datetime.now(timezone.utc):
31
+ discount_amount = subtotal * (cart.coupon.discount_percent / 100.0)
32
+ applied_coupon = CouponResponse.model_validate(cart.coupon)
33
+
34
+ subtotal_after_discount = subtotal - discount_amount
35
+
36
+ # Calculate tax
37
+ tax = subtotal_after_discount * TAX_RATE
38
+
39
+ # Calculate shipping
40
+ shipping_cost = 0.0 if subtotal_after_discount >= FREE_SHIPPING_THRESHOLD or subtotal_after_discount == 0 else SHIPPING_FLAT_RATE
41
+
42
+ total = subtotal_after_discount + tax + shipping_cost
43
+
44
+ return {
45
+ "subtotal": round(subtotal, 2),
46
+ "tax": round(tax, 2),
47
+ "shipping_cost": round(shipping_cost, 2),
48
+ "discount_amount": round(discount_amount, 2),
49
+ "total": round(total, 2),
50
+ "applied_coupon": applied_coupon
51
+ }
52
+
53
+ def get_or_create_cart(db: Session, user_id: Optional[int] = None, session_id: Optional[str] = None) -> Cart:
54
+ """Get or create a cart for either an authenticated user or a guest session."""
55
+ if user_id:
56
+ cart = db.query(Cart).filter(Cart.user_id == user_id).first()
57
+ if not cart:
58
+ cart = Cart(user_id=user_id)
59
+ db.add(cart)
60
+ db.commit()
61
+ db.refresh(cart)
62
+ return cart
63
+ elif session_id:
64
+ cart = db.query(Cart).filter(Cart.session_id == session_id).first()
65
+ if not cart:
66
+ cart = Cart(session_id=session_id)
67
+ db.add(cart)
68
+ db.commit()
69
+ db.refresh(cart)
70
+ return cart
71
+ else:
72
+ raise HTTPException(status_code=400, detail="Either login or provide X-Cart-ID header")
73
+
74
+ @router.get("/cart", response_model=dict)
75
+ def get_cart(
76
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
77
+ x_cart_id: Optional[str] = Header(None),
78
+ db: Session = Depends(get_db)
79
+ ) -> Any:
80
+ """Get the current user's (or guest session's) shopping cart."""
81
+ cart = get_or_create_cart(db, user_id=current_user_id, session_id=x_cart_id)
82
+ totals = calculate_cart_totals(cart)
83
+
84
+ items_response = []
85
+ for item in cart.items:
86
+ item_data = CartItemResponse(
87
+ id=item.id,
88
+ cart_id=item.cart_id,
89
+ product_id=item.product_id,
90
+ quantity=item.quantity,
91
+ unit_price=item.unit_price,
92
+ total_price=item.quantity * item.unit_price,
93
+ product=item.product
94
+ )
95
+ items_response.append(item_data)
96
+
97
+ return {
98
+ "isSuccess": True,
99
+ "value": CartResponse(
100
+ id=cart.id,
101
+ user_id=cart.user_id,
102
+ items=items_response,
103
+ **totals
104
+ ),
105
+ "statusCode": 200
106
+ }
107
+
108
+ @router.post("/cart/items", response_model=dict)
109
+ def add_item_to_cart(
110
+ item_in: CartItemCreate,
111
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
112
+ x_cart_id: Optional[str] = Header(None),
113
+ db: Session = Depends(get_db)
114
+ ) -> Any:
115
+ """Add a product to the cart."""
116
+ cart = get_or_create_cart(db, user_id=current_user_id, session_id=x_cart_id)
117
+
118
+ # Check product availability
119
+ product = db.query(Product).filter(Product.id == item_in.product_id).first()
120
+ if not product:
121
+ raise HTTPException(status_code=404, detail="Product not found")
122
+ if not product.is_active:
123
+ raise HTTPException(status_code=400, detail="Product is not currently available")
124
+
125
+ # Check if item already in cart
126
+ cart_item = db.query(CartItem).filter(
127
+ CartItem.cart_id == cart.id,
128
+ CartItem.product_id == item_in.product_id
129
+ ).first()
130
+
131
+ if cart_item:
132
+ if cart_item.quantity + item_in.quantity > product.stock:
133
+ raise HTTPException(status_code=400, detail=f"Not enough stock. Only {product.stock} available.")
134
+ cart_item.quantity += item_in.quantity
135
+ # refresh price just to be safe
136
+ cart_item.unit_price = product.price
137
+ else:
138
+ if item_in.quantity > product.stock:
139
+ raise HTTPException(status_code=400, detail=f"Not enough stock. Only {product.stock} available.")
140
+ cart_item = CartItem(
141
+ cart_id=cart.id,
142
+ product_id=item_in.product_id,
143
+ quantity=item_in.quantity,
144
+ unit_price=product.price
145
+ )
146
+ db.add(cart_item)
147
+
148
+ db.commit()
149
+ return {"isSuccess": True, "value": {"message": "Item added to cart"}, "statusCode": 200}
150
+
151
+ @router.put("/cart/items/{item_id}", response_model=dict)
152
+ def update_cart_item(
153
+ item_id: int,
154
+ item_in: CartItemUpdate,
155
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
156
+ x_cart_id: Optional[str] = Header(None),
157
+ db: Session = Depends(get_db)
158
+ ) -> Any:
159
+ """Update quantity of an item in the cart."""
160
+ cart = get_or_create_cart(db, user_id=current_user_id, session_id=x_cart_id)
161
+
162
+ cart_item = db.query(CartItem).filter(
163
+ CartItem.id == item_id,
164
+ CartItem.cart_id == cart.id
165
+ ).first()
166
+
167
+ if not cart_item:
168
+ raise HTTPException(status_code=404, detail="Item not found in cart")
169
+
170
+ if item_in.quantity > cart_item.product.stock:
171
+ raise HTTPException(status_code=400, detail=f"Not enough stock. Only {cart_item.product.stock} available.")
172
+
173
+ cart_item.quantity = item_in.quantity
174
+ db.commit()
175
+
176
+ return {"isSuccess": True, "value": {"message": "Cart updated"}, "statusCode": 200}
177
+
178
+ @router.delete("/cart/items/{item_id}", response_model=dict)
179
+ def remove_cart_item(
180
+ item_id: int,
181
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
182
+ x_cart_id: Optional[str] = Header(None),
183
+ db: Session = Depends(get_db)
184
+ ) -> Any:
185
+ """Remove an item from the cart."""
186
+ cart = get_or_create_cart(db, user_id=current_user_id, session_id=x_cart_id)
187
+
188
+ cart_item = db.query(CartItem).filter(
189
+ CartItem.id == item_id,
190
+ CartItem.cart_id == cart.id
191
+ ).first()
192
+
193
+ if not cart_item:
194
+ raise HTTPException(status_code=404, detail="Item not found in cart")
195
+
196
+ db.delete(cart_item)
197
+ db.commit()
198
+
199
+ return {"isSuccess": True, "value": {"message": "Item removed from cart"}, "statusCode": 200}
200
+
201
+ @router.post("/cart/coupon", response_model=dict)
202
+ def apply_coupon(
203
+ coupon_in: CouponApply,
204
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
205
+ x_cart_id: Optional[str] = Header(None),
206
+ db: Session = Depends(get_db)
207
+ ) -> Any:
208
+ """Apply a discount coupon to the cart."""
209
+ cart = get_or_create_cart(db, user_id=current_user_id, session_id=x_cart_id)
210
+
211
+ coupon = db.query(Coupon).filter(Coupon.code == coupon_in.code.upper()).first()
212
+ if not coupon or not coupon.is_active:
213
+ raise HTTPException(status_code=400, detail="Invalid coupon code")
214
+
215
+ if coupon.expires_at and coupon.expires_at < datetime.now(timezone.utc):
216
+ raise HTTPException(status_code=400, detail="Coupon has expired")
217
+
218
+ cart.coupon_id = coupon.id
219
+ db.commit()
220
+
221
+ return {"isSuccess": True, "value": {"message": f"Coupon {coupon.code} applied successfully"}, "statusCode": 200}
222
+
223
+ @router.delete("/cart/clear", response_model=dict)
224
+ def clear_cart(
225
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
226
+ x_cart_id: Optional[str] = Header(None),
227
+ db: Session = Depends(get_db)
228
+ ) -> Any:
229
+ """Remove all items from the cart and detach coupons."""
230
+ cart = get_or_create_cart(db, user_id=current_user_id, session_id=x_cart_id)
231
+
232
+ db.query(CartItem).filter(CartItem.cart_id == cart.id).delete()
233
+ cart.coupon_id = None
234
+ db.commit()
235
+
236
+ return {"isSuccess": True, "value": {"message": "Cart cleared"}, "statusCode": 200}