import db import schemas import auth_utils import notifications import config from typing import List, Optional import json import os import uuid import shutil import hashlib import preview_utils import slicer_utils from fastapi import APIRouter, Request, Form, Depends, HTTPException, BackgroundTasks, UploadFile, File from services import pricing, order_processing, event_hooks router = APIRouter(prefix="/orders", tags=["orders"]) @router.post("") async def create_order( request: Request, background_tasks: BackgroundTasks, first_name: str = Form(...), last_name: str = Form(...), phone: str = Form(...), email: str = Form(...), shipping_address: str = Form(...), model_link: Optional[str] = Form(None), allow_portfolio: bool = Form(False), notes: Optional[str] = Form(None), material_id: int = Form(...), file_ids: str = Form("[]"), file_quantities: str = Form("[]"), quantity: int = Form(1), token: str = Depends(auth_utils.oauth2_scheme_optional) ): user_id = None if token: payload = auth_utils.decode_token(token) if payload: user_id = payload.get("id") parsed_ids = [] parsed_quantities = [] if file_ids: try: parsed_ids = json.loads(file_ids) parsed_quantities = json.loads(file_quantities) except: pass lang = request.query_params.get("lang", "en") name_col = f"name_{lang}" if lang in ["en", "ru", "me"] else "name_en" mat_info = db.execute_query(f"SELECT {name_col}, price_per_cm3 FROM materials WHERE id = %s", (material_id,)) mat_name = mat_info[0][name_col] if mat_info else "Unknown" mat_price = mat_info[0]['price_per_cm3'] if mat_info else 0.0 file_sizes = [] if parsed_ids: format_strings = ','.join(['%s'] * len(parsed_ids)) file_rows = db.execute_query(f"SELECT file_size FROM order_files WHERE id IN ({format_strings})", tuple(parsed_ids)) file_sizes = [r['file_size'] for r in file_rows] estimated_price = pricing.calculate_estimated_price(material_id, file_sizes, parsed_quantities if parsed_quantities else None) order_query = """ 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) VALUES (%s, %s, %s, %s, %s, %s, %s, 'pending', %s, %s, %s, %s, %s, %s) """ order_params = (user_id, first_name, last_name, phone, email, shipping_address, model_link, allow_portfolio, estimated_price, mat_name, mat_price, quantity, notes) try: order_insert_id = db.execute_commit(order_query, order_params) if parsed_ids: for idx, f_id in enumerate(parsed_ids): qty = parsed_quantities[idx] if idx < len(parsed_quantities) else 1 db.execute_commit("UPDATE order_files SET order_id = %s, quantity = %s WHERE id = %s", (order_insert_id, qty, f_id)) background_tasks.add_task(order_processing.process_order_slicing, order_insert_id) background_tasks.add_task(event_hooks.on_order_created, order_insert_id) return {"status": "success", "order_id": order_insert_id, "message": "Order submitted successfully"} except Exception as e: print(f"Error creating order: {e}") raise HTTPException(status_code=500, detail="Internal server error occurred while processing order") @router.get("/my") async def get_my_orders(token: str = Depends(auth_utils.oauth2_scheme)): payload = auth_utils.decode_token(token) if not payload: raise HTTPException(status_code=401, detail="Invalid token") user_id = payload.get("id") query = """ SELECT o.*, 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 FROM orders o LEFT JOIN order_files f ON o.id = f.order_id WHERE o.user_id = %s GROUP BY o.id ORDER BY o.created_at DESC """ results = db.execute_query(query, (user_id,)) for row in results: if row['files']: try: row['files'] = json.loads(f"[{row['files']}]") except: row['files'] = [] else: row['files'] = [] return results @router.post("/estimate") async def get_price_estimate(data: schemas.EstimateRequest): material = db.execute_query("SELECT price_per_cm3 FROM materials WHERE id = %s", (data.material_id,)) price_per_cm3 = float(material[0]['price_per_cm3']) if material else 0.0 file_prices = [] base_fee = 5.0 for size in data.file_sizes: total_size_mb = size / (1024 * 1024) estimated_volume = total_size_mb * 8.0 file_cost = base_fee + (estimated_volume * price_per_cm3) file_prices.append(round(file_cost, 2)) qts = data.file_quantities if data.file_quantities else [1]*len(data.file_sizes) return {"file_prices": file_prices, "total_estimate": round(sum(p * q for p, q in zip(file_prices, qts)), 2)} # --- ADMIN ORDER ENDPOINTS --- @router.get("/admin/list") # Using /admin/list to avoid conflict with /my async def get_admin_orders(token: str = Depends(auth_utils.oauth2_scheme)): payload = auth_utils.decode_token(token) if not payload or payload.get("role") != 'admin': raise HTTPException(status_code=403, detail="Admin role required") query = """ SELECT o.*, 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 FROM orders o LEFT JOIN order_files f ON o.id = f.order_id GROUP BY o.id ORDER BY o.created_at DESC """ results = db.execute_query(query) import session_utils for row in results: row['is_online'] = session_utils.is_user_online(row['user_id']) if row.get('user_id') else False if row['files']: try: row['files'] = json.loads(f"[{row['files']}]") except: row['files'] = [] else: row['files'] = [] photos = db.execute_query("SELECT id, file_path, is_public FROM order_photos WHERE order_id = %s", (row['id'],)) row['photos'] = photos return results @router.patch("/{order_id}/admin") async def update_order_admin( order_id: int, data: schemas.AdminOrderUpdate, background_tasks: BackgroundTasks, token: str = Depends(auth_utils.oauth2_scheme) ): payload = auth_utils.decode_token(token) if not payload or payload.get("role") != 'admin': raise HTTPException(status_code=403, detail="Admin role required") update_fields = [] params = [] if data.status: order_info = db.execute_query("SELECT * FROM orders WHERE id = %s", (order_id,)) if order_info: background_tasks.add_task( event_hooks.on_order_status_changed, order_id, data.status, order_info[0], data.send_notification ) update_fields.append("status = %s") params.append(data.status) if data.total_price is not None: update_fields.append("total_price = %s") params.append(data.total_price) if update_fields: query = f"UPDATE orders SET {', '.join(update_fields)} WHERE id = %s" params.append(order_id) db.execute_commit(query, tuple(params)) return {"id": order_id, "status": "updated"} @router.post("/{order_id}/attach-file") async def admin_attach_file( order_id: int, file: UploadFile = File(...), token: str = Depends(auth_utils.oauth2_scheme) ): payload = auth_utils.decode_token(token) if not payload or payload.get("role") != 'admin': raise HTTPException(status_code=403, detail="Admin role required") unique_filename = f"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}" file_path = os.path.join(config.UPLOAD_DIR, unique_filename).replace("\\", "/") sha256_hash = hashlib.sha256() with open(file_path, "wb") as buffer: while chunk := file.file.read(8192): sha256_hash.update(chunk) buffer.write(chunk) preview_path = None if file_path.lower().endswith(".stl"): preview_filename = f"{uuid.uuid4()}.png" preview_path = os.path.join(config.PREVIEW_DIR, preview_filename).replace("\\", "/") preview_utils.generate_stl_preview(file_path, preview_path) filament_g = None print_time = None if file_path.lower().endswith(".stl"): result = slicer_utils.slice_model(file_path) if result and result.get('success'): filament_g = result.get('filament_g') print_time = result.get('print_time_str') 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)" f_id = db.execute_commit(query, (order_id, file.filename, file_path, file.size, sha256_hash.hexdigest(), print_time, filament_g, preview_path)) return {"id": f_id, "filename": file.filename, "preview_path": preview_path, "filament_g": filament_g, "print_time": print_time}