orders.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import db
  2. import schemas
  3. import auth_utils
  4. import notifications
  5. import config
  6. from typing import List, Optional
  7. import json
  8. import os
  9. import uuid
  10. import shutil
  11. import hashlib
  12. import preview_utils
  13. import slicer_utils
  14. from fastapi import APIRouter, Request, Form, Depends, HTTPException, BackgroundTasks, UploadFile, File
  15. from services import pricing, order_processing, event_hooks
  16. router = APIRouter(prefix="/orders", tags=["orders"])
  17. @router.post("")
  18. async def create_order(
  19. request: Request,
  20. background_tasks: BackgroundTasks,
  21. first_name: str = Form(...),
  22. last_name: str = Form(...),
  23. phone: str = Form(...),
  24. email: str = Form(...),
  25. shipping_address: str = Form(...),
  26. model_link: Optional[str] = Form(None),
  27. allow_portfolio: bool = Form(False),
  28. notes: Optional[str] = Form(None),
  29. material_id: int = Form(...),
  30. file_ids: str = Form("[]"),
  31. file_quantities: str = Form("[]"),
  32. quantity: int = Form(1),
  33. token: str = Depends(auth_utils.oauth2_scheme_optional)
  34. ):
  35. user_id = None
  36. if token:
  37. payload = auth_utils.decode_token(token)
  38. if payload:
  39. user_id = payload.get("id")
  40. parsed_ids = []
  41. parsed_quantities = []
  42. if file_ids:
  43. try:
  44. parsed_ids = json.loads(file_ids)
  45. parsed_quantities = json.loads(file_quantities)
  46. except:
  47. pass
  48. lang = request.query_params.get("lang", "en")
  49. name_col = f"name_{lang}" if lang in ["en", "ru", "me"] else "name_en"
  50. mat_info = db.execute_query(f"SELECT {name_col}, price_per_cm3 FROM materials WHERE id = %s", (material_id,))
  51. mat_name = mat_info[0][name_col] if mat_info else "Unknown"
  52. mat_price = mat_info[0]['price_per_cm3'] if mat_info else 0.0
  53. file_sizes = []
  54. if parsed_ids:
  55. format_strings = ','.join(['%s'] * len(parsed_ids))
  56. file_rows = db.execute_query(f"SELECT file_size FROM order_files WHERE id IN ({format_strings})", tuple(parsed_ids))
  57. file_sizes = [r['file_size'] for r in file_rows]
  58. estimated_price = pricing.calculate_estimated_price(material_id, file_sizes, parsed_quantities if parsed_quantities else None)
  59. order_query = """
  60. INSERT INTO orders (user_id, first_name, last_name, phone, email, shipping_address, model_link, status, allow_portfolio, estimated_price, material_name, material_price, quantity, notes)
  61. VALUES (%s, %s, %s, %s, %s, %s, %s, 'pending', %s, %s, %s, %s, %s, %s)
  62. """
  63. order_params = (user_id, first_name, last_name, phone, email, shipping_address, model_link, allow_portfolio, estimated_price, mat_name, mat_price, quantity, notes)
  64. try:
  65. order_insert_id = db.execute_commit(order_query, order_params)
  66. if parsed_ids:
  67. for idx, f_id in enumerate(parsed_ids):
  68. qty = parsed_quantities[idx] if idx < len(parsed_quantities) else 1
  69. db.execute_commit("UPDATE order_files SET order_id = %s, quantity = %s WHERE id = %s", (order_insert_id, qty, f_id))
  70. background_tasks.add_task(order_processing.process_order_slicing, order_insert_id)
  71. background_tasks.add_task(event_hooks.on_order_created, order_insert_id)
  72. return {"status": "success", "order_id": order_insert_id, "message": "Order submitted successfully"}
  73. except Exception as e:
  74. print(f"Error creating order: {e}")
  75. raise HTTPException(status_code=500, detail="Internal server error occurred while processing order")
  76. @router.get("/my")
  77. async def get_my_orders(token: str = Depends(auth_utils.oauth2_scheme)):
  78. payload = auth_utils.decode_token(token)
  79. if not payload:
  80. raise HTTPException(status_code=401, detail="Invalid token")
  81. user_id = payload.get("id")
  82. query = """
  83. SELECT o.*,
  84. GROUP_CONCAT(JSON_OBJECT('filename', f.filename, 'file_path', f.file_path, 'quantity', f.quantity, 'preview_path', f.preview_path, 'print_time', f.print_time, 'filament_g', f.filament_g)) as files
  85. FROM orders o
  86. LEFT JOIN order_files f ON o.id = f.order_id
  87. WHERE o.user_id = %s
  88. GROUP BY o.id
  89. ORDER BY o.created_at DESC
  90. """
  91. results = db.execute_query(query, (user_id,))
  92. for row in results:
  93. if row['files']:
  94. try: row['files'] = json.loads(f"[{row['files']}]")
  95. except: row['files'] = []
  96. else: row['files'] = []
  97. return results
  98. @router.post("/estimate")
  99. async def get_price_estimate(data: schemas.EstimateRequest):
  100. material = db.execute_query("SELECT price_per_cm3 FROM materials WHERE id = %s", (data.material_id,))
  101. price_per_cm3 = float(material[0]['price_per_cm3']) if material else 0.0
  102. file_prices = []
  103. base_fee = 5.0
  104. for size in data.file_sizes:
  105. total_size_mb = size / (1024 * 1024)
  106. estimated_volume = total_size_mb * 8.0
  107. file_cost = base_fee + (estimated_volume * price_per_cm3)
  108. file_prices.append(round(file_cost, 2))
  109. qts = data.file_quantities if data.file_quantities else [1]*len(data.file_sizes)
  110. return {"file_prices": file_prices, "total_estimate": round(sum(p * q for p, q in zip(file_prices, qts)), 2)}
  111. # --- ADMIN ORDER ENDPOINTS ---
  112. @router.get("/admin/list") # Using /admin/list to avoid conflict with /my
  113. async def get_admin_orders(token: str = Depends(auth_utils.oauth2_scheme)):
  114. payload = auth_utils.decode_token(token)
  115. if not payload or payload.get("role") != 'admin':
  116. raise HTTPException(status_code=403, detail="Admin role required")
  117. query = """
  118. SELECT o.*,
  119. GROUP_CONCAT(JSON_OBJECT('filename', f.filename, 'file_path', f.file_path, 'file_size', f.file_size, 'quantity', f.quantity, 'preview_path', f.preview_path, 'print_time', f.print_time, 'filament_g', f.filament_g)) as files
  120. FROM orders o
  121. LEFT JOIN order_files f ON o.id = f.order_id
  122. GROUP BY o.id
  123. ORDER BY o.created_at DESC
  124. """
  125. results = db.execute_query(query)
  126. import session_utils
  127. for row in results:
  128. row['is_online'] = session_utils.is_user_online(row['user_id']) if row.get('user_id') else False
  129. if row['files']:
  130. try: row['files'] = json.loads(f"[{row['files']}]")
  131. except: row['files'] = []
  132. else: row['files'] = []
  133. photos = db.execute_query("SELECT id, file_path, is_public FROM order_photos WHERE order_id = %s", (row['id'],))
  134. row['photos'] = photos
  135. return results
  136. @router.patch("/{order_id}/admin")
  137. async def update_order_admin(
  138. order_id: int,
  139. data: schemas.AdminOrderUpdate,
  140. background_tasks: BackgroundTasks,
  141. token: str = Depends(auth_utils.oauth2_scheme)
  142. ):
  143. payload = auth_utils.decode_token(token)
  144. if not payload or payload.get("role") != 'admin':
  145. raise HTTPException(status_code=403, detail="Admin role required")
  146. update_fields = []
  147. params = []
  148. if data.status:
  149. order_info = db.execute_query("SELECT * FROM orders WHERE id = %s", (order_id,))
  150. if order_info:
  151. background_tasks.add_task(
  152. event_hooks.on_order_status_changed,
  153. order_id,
  154. data.status,
  155. order_info[0],
  156. data.send_notification
  157. )
  158. update_fields.append("status = %s")
  159. params.append(data.status)
  160. if data.total_price is not None:
  161. update_fields.append("total_price = %s")
  162. params.append(data.total_price)
  163. if update_fields:
  164. query = f"UPDATE orders SET {', '.join(update_fields)} WHERE id = %s"
  165. params.append(order_id)
  166. db.execute_commit(query, tuple(params))
  167. return {"id": order_id, "status": "updated"}
  168. @router.post("/{order_id}/attach-file")
  169. async def admin_attach_file(
  170. order_id: int,
  171. file: UploadFile = File(...),
  172. token: str = Depends(auth_utils.oauth2_scheme)
  173. ):
  174. payload = auth_utils.decode_token(token)
  175. if not payload or payload.get("role") != 'admin':
  176. raise HTTPException(status_code=403, detail="Admin role required")
  177. unique_filename = f"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}"
  178. file_path = os.path.join(config.UPLOAD_DIR, unique_filename).replace("\\", "/")
  179. sha256_hash = hashlib.sha256()
  180. with open(file_path, "wb") as buffer:
  181. while chunk := file.file.read(8192):
  182. sha256_hash.update(chunk)
  183. buffer.write(chunk)
  184. preview_path = None
  185. if file_path.lower().endswith(".stl"):
  186. preview_filename = f"{uuid.uuid4()}.png"
  187. preview_path = os.path.join(config.PREVIEW_DIR, preview_filename).replace("\\", "/")
  188. preview_utils.generate_stl_preview(file_path, preview_path)
  189. filament_g = None
  190. print_time = None
  191. if file_path.lower().endswith(".stl"):
  192. result = slicer_utils.slice_model(file_path)
  193. if result and result.get('success'):
  194. filament_g = result.get('filament_g')
  195. print_time = result.get('print_time_str')
  196. query = "INSERT INTO order_files (order_id, filename, file_path, file_size, quantity, file_hash, print_time, filament_g, preview_path) VALUES (%s, %s, %s, %s, 1, %s, %s, %s, %s)"
  197. f_id = db.execute_commit(query, (order_id, file.filename, file_path, file.size, sha256_hash.hexdigest(), print_time, filament_g, preview_path))
  198. return {"id": f_id, "filename": file.filename, "preview_path": preview_path, "filament_g": filament_g, "print_time": print_time}