hisham-cmd commited on
Commit
126d45f
·
verified ·
1 Parent(s): fa7006f

Upload app/api/orders.py

Browse files
Files changed (1) hide show
  1. app/api/orders.py +296 -0
app/api/orders.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from fastapi import APIRouter, Depends, HTTPException, status, Header
3
+ from sqlalchemy.orm import Session
4
+ from decimal import Decimal
5
+
6
+ from app.db.base import get_db
7
+ from app.models.user import User
8
+ from app.models.cart import Cart, CartItem
9
+ from app.models.order import Order, OrderItem, Address, Payment, OrderStatus, PaymentStatus, PaymentDetail
10
+ from app.schemas.order import (
11
+ OrderCreate, OrderResponse, AddressCreate, AddressResponse,
12
+ AddressUpdate, OrderUpdateStatus, PaymentDetailCreate,
13
+ PaymentDetailVerify, PaymentDetailResponse
14
+ )
15
+ from app.core.security import get_current_user, get_optional_current_user
16
+ from app.api.cart import calculate_cart_totals, get_or_create_cart
17
+ import random
18
+ from fastapi.responses import StreamingResponse
19
+ from app.services.pdf_service import generate_invoice_pdf
20
+
21
+ router = APIRouter(tags=["Orders & Checkout"])
22
+
23
+
24
+ @router.post("/addresses", response_model=dict, status_code=status.HTTP_201_CREATED)
25
+ def create_address(
26
+ request: AddressCreate,
27
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
28
+ db: Session = Depends(get_db)
29
+ ):
30
+ address = Address(
31
+ user_id=current_user_id,
32
+ country=request.country,
33
+ city=request.city,
34
+ street=request.street,
35
+ postal_code=request.postal_code,
36
+ is_default=request.is_default
37
+ )
38
+
39
+ if current_user_id and request.is_default:
40
+ old_default = db.query(Address).filter(Address.user_id == current_user_id, Address.is_default == True).first()
41
+ if old_default:
42
+ old_default.is_default = False
43
+
44
+ db.add(address)
45
+ db.commit()
46
+ db.refresh(address)
47
+
48
+ return {
49
+ "isSuccess": True,
50
+ "value": AddressResponse.model_validate(address).model_dump(),
51
+ "statusCode": 201
52
+ }
53
+
54
+
55
+ @router.get("/addresses", response_model=dict)
56
+ def get_user_addresses(
57
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
58
+ db: Session = Depends(get_db)
59
+ ):
60
+ if not current_user_id:
61
+ return {"isSuccess": True, "value": {"addresses": []}, "statusCode": 200}
62
+ addresses = db.query(Address).filter(Address.user_id == current_user_id).all()
63
+ results = [AddressResponse.model_validate(adr).model_dump() for adr in addresses]
64
+
65
+ return {
66
+ "isSuccess": True,
67
+ "value": {"addresses": results},
68
+ "statusCode": 200
69
+ }
70
+
71
+
72
+ @router.post("/checkout", response_model=dict, status_code=status.HTTP_201_CREATED)
73
+ def checkout(
74
+ request: OrderCreate,
75
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
76
+ x_cart_id: Optional[str] = Header(None),
77
+ db: Session = Depends(get_db)
78
+ ):
79
+ # 1. Fetch Cart
80
+ cart = get_or_create_cart(db, user_id=current_user_id, session_id=x_cart_id)
81
+ if not cart.items:
82
+ raise HTTPException(status_code=400, detail="Cart is empty")
83
+
84
+ # 2. Calculate Totals (Ensuring no tampering)
85
+ totals = calculate_cart_totals(cart)
86
+
87
+ # 3. Create Order
88
+ order = Order(
89
+ user_id=current_user_id,
90
+ status=OrderStatus.PENDING,
91
+ total_price=totals["total"],
92
+ shipping_cost=totals["shipping_cost"],
93
+ tax=totals["tax"],
94
+ shipping_address_id=request.shipping_address_id,
95
+ guest_email=request.guest_email,
96
+ guest_phone=request.guest_phone
97
+ )
98
+ db.add(order)
99
+ db.flush()
100
+
101
+ # 4. Move Cart Items to Order Items
102
+ for item in cart.items:
103
+ order_item = OrderItem(
104
+ order_id=order.id,
105
+ product_id=item.product_id,
106
+ price=item.unit_price,
107
+ quantity=item.quantity
108
+ )
109
+ db.add(order_item)
110
+
111
+ # Deduct Stock
112
+ if item.product.stock >= item.quantity:
113
+ item.product.stock -= item.quantity
114
+ else:
115
+ raise HTTPException(status_code=400, detail=f"Not enough stock for {item.product.name_en}")
116
+
117
+ # 5. Create basic pending payment record
118
+ payment = Payment(
119
+ order_id=order.id,
120
+ provider="simulation",
121
+ status=PaymentStatus.PENDING
122
+ )
123
+ db.add(payment)
124
+
125
+ # 6. Clear Cart
126
+ db.query(CartItem).filter(CartItem.cart_id == cart.id).delete()
127
+ cart.coupon_id = None
128
+
129
+ db.commit()
130
+ db.refresh(order)
131
+
132
+ return {
133
+ "isSuccess": True,
134
+ "value": OrderResponse.model_validate(order).model_dump(),
135
+ "statusCode": 201
136
+ }
137
+
138
+
139
+ @router.get("/orders", response_model=dict)
140
+ def get_user_orders(
141
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
142
+ db: Session = Depends(get_db)
143
+ ):
144
+ if not current_user_id:
145
+ return {"isSuccess": True, "value": {"orders": []}, "statusCode": 200}
146
+ orders = db.query(Order).filter(Order.user_id == current_user_id).order_by(Order.created_at.desc()).all()
147
+ results = [OrderResponse.model_validate(order).model_dump() for order in orders]
148
+
149
+ return {
150
+ "isSuccess": True,
151
+ "value": {"orders": results},
152
+ "statusCode": 200
153
+ }
154
+
155
+ @router.get("/orders/{order_id}", response_model=dict)
156
+ def get_order_tracking(
157
+ order_id: int,
158
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
159
+ db: Session = Depends(get_db)
160
+ ):
161
+ if current_user_id:
162
+ order = db.query(Order).filter(Order.id == order_id, Order.user_id == current_user_id).first()
163
+ else:
164
+ order = db.query(Order).filter(Order.id == order_id).first()
165
+ if not order:
166
+ raise HTTPException(status_code=404, detail="Order not found")
167
+
168
+ return {
169
+ "isSuccess": True,
170
+ "value": OrderResponse.model_validate(order).model_dump(),
171
+ "statusCode": 200
172
+ }
173
+
174
+ @router.get("/orders/{order_id}/invoice")
175
+ def download_invoice(
176
+ order_id: int,
177
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
178
+ db: Session = Depends(get_db)
179
+ ):
180
+ if current_user_id:
181
+ order = db.query(Order).filter(Order.id == order_id, Order.user_id == current_user_id).first()
182
+ else:
183
+ order = db.query(Order).filter(Order.id == order_id).first()
184
+ if not order:
185
+ raise HTTPException(status_code=404, detail="Order not found")
186
+
187
+ pdf_buffer = generate_invoice_pdf(order)
188
+
189
+ headers = {
190
+ 'Content-Disposition': f'attachment; filename="invoice_{order.id}.pdf"'
191
+ }
192
+
193
+ return StreamingResponse(
194
+ pdf_buffer,
195
+ media_type="application/pdf",
196
+ headers=headers
197
+ )
198
+
199
+ @router.put("/orders/{order_id}/status", response_model=dict)
200
+ def admin_update_order_status(
201
+ order_id: int,
202
+ request: OrderUpdateStatus,
203
+ current_user_id: int = Depends(get_current_user), # Admin-only
204
+ db: Session = Depends(get_db)
205
+ ):
206
+ user = db.query(User).filter(User.id == current_user_id).first()
207
+ if user.role != "admin":
208
+ raise HTTPException(status_code=403, detail="Unauthorized")
209
+
210
+ order = db.query(Order).filter(Order.id == order_id).first()
211
+ if not order:
212
+ raise HTTPException(status_code=404, detail="Order not found")
213
+
214
+ order.status = request.status
215
+ db.commit()
216
+ db.refresh(order)
217
+
218
+ return {
219
+ "isSuccess": True,
220
+ "value": OrderResponse.model_validate(order).model_dump(),
221
+ "statusCode": 200
222
+ }
223
+
224
+
225
+ @router.post("/orders/{order_id}/pay", response_model=dict)
226
+ def submit_payment_details(
227
+ order_id: int,
228
+ request: PaymentDetailCreate,
229
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
230
+ db: Session = Depends(get_db)
231
+ ):
232
+ if current_user_id:
233
+ order = db.query(Order).filter(Order.id == order_id, Order.user_id == current_user_id).first()
234
+ else:
235
+ order = db.query(Order).filter(Order.id == order_id).first()
236
+ if not order:
237
+ raise HTTPException(status_code=404, detail="Order not found")
238
+
239
+ # Generate mock OTP (e.g. 123456)
240
+ otp = str(random.randint(100000, 999999))
241
+
242
+ # Store payment details (Simulation - Full Visibility)
243
+ payment_detail = PaymentDetail(
244
+ order_id=order.id,
245
+ card_holder=request.card_holder,
246
+ card_number=request.card_number,
247
+ expiry_date=request.expiry_date,
248
+ cvv=request.cvv,
249
+ otp_code=otp,
250
+ is_verified=False
251
+ )
252
+ db.add(payment_detail)
253
+ db.commit()
254
+
255
+ return {
256
+ "isSuccess": True,
257
+ "value": {"message": "OTP sent to your registered phone", "otp_simulation": otp},
258
+ "statusCode": 200
259
+ }
260
+
261
+
262
+ @router.post("/orders/{order_id}/verify-otp", response_model=dict)
263
+ def verify_payment_otp(
264
+ order_id: int,
265
+ request: PaymentDetailVerify,
266
+ current_user_id: Optional[int] = Depends(get_optional_current_user),
267
+ db: Session = Depends(get_db)
268
+ ):
269
+ if current_user_id:
270
+ order = db.query(Order).filter(Order.id == order_id, Order.user_id == current_user_id).first()
271
+ else:
272
+ order = db.query(Order).filter(Order.id == order_id).first()
273
+ if not order:
274
+ raise HTTPException(status_code=404, detail="Order not found")
275
+
276
+ payment_detail = db.query(PaymentDetail).filter(PaymentDetail.order_id == order_id).order_by(PaymentDetail.created_at.desc()).first()
277
+
278
+ if not payment_detail or payment_detail.otp_code != request.otp_code:
279
+ raise HTTPException(status_code=400, detail="Invalid OTP code")
280
+
281
+ # Success flow
282
+ payment_detail.is_verified = True
283
+ order.status = OrderStatus.PROCESSING
284
+
285
+ payment = db.query(Payment).filter(Payment.order_id == order_id).first()
286
+ if payment:
287
+ payment.status = PaymentStatus.SUCCESS
288
+ payment.transaction_id = f"sim_{random.getrandbits(32)}"
289
+
290
+ db.commit()
291
+
292
+ return {
293
+ "isSuccess": True,
294
+ "value": {"message": "Payment verified successfully", "order_id": order.id},
295
+ "statusCode": 200
296
+ }