from typing import Any, List from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from app.core.deps import get_current_active_user, get_db from app.models.user import User from app.models.order import Order, OrderStatus from app.models.advertisement import Advertisement, AdType from app.models.wallet import Wallet from app.models.payment import Payment, PaymentStatus from app.schemas.order import Order as OrderSchema, OrderCreate, OrderUpdate from app.services.payment_service import PaymentService router = APIRouter() @router.get("/", response_model=List[OrderSchema]) def read_orders( skip: int = 0, limit: int = 100, db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user), ) -> Any: orders = db.query(Order).filter( (Order.buyer_id == current_user.id) | (Order.seller_id == current_user.id) ).offset(skip).limit(limit).all() return orders @router.post("/", response_model=OrderSchema) def create_order( *, db: Session = Depends(get_db), order_in: OrderCreate, current_user: User = Depends(get_current_active_user), ) -> Any: # Get the advertisement advertisement = db.query(Advertisement).filter( Advertisement.id == order_in.advertisement_id ).first() if not advertisement: raise HTTPException(status_code=404, detail="Advertisement not found") # Check if user is trying to trade with themselves if advertisement.user_id == current_user.id: raise HTTPException(status_code=400, detail="Cannot trade with yourself") # Validate order amount if order_in.crypto_amount < advertisement.min_order_amount: raise HTTPException(status_code=400, detail="Order amount below minimum") if order_in.crypto_amount > advertisement.max_order_amount: raise HTTPException(status_code=400, detail="Order amount above maximum") if order_in.crypto_amount > advertisement.available_amount: raise HTTPException(status_code=400, detail="Insufficient advertisement balance") # Calculate price based on advertisement price = advertisement.price calculated_fiat = order_in.crypto_amount * price # Allow small variance in fiat amount (1% tolerance) if abs(order_in.fiat_amount - calculated_fiat) > calculated_fiat * 0.01: raise HTTPException(status_code=400, detail="Fiat amount doesn't match calculated price") # Determine buyer and seller if advertisement.ad_type == AdType.SELL: buyer_id = current_user.id seller_id = advertisement.user_id else: # AdType.BUY buyer_id = advertisement.user_id seller_id = current_user.id # For sell advertisements, the seller's crypto is already locked # For buy advertisements, need to lock the seller's crypto if advertisement.ad_type == AdType.BUY: seller_wallet = db.query(Wallet).filter( Wallet.user_id == seller_id, Wallet.cryptocurrency_id == advertisement.cryptocurrency_id ).first() if not seller_wallet or seller_wallet.available_balance < order_in.crypto_amount: raise HTTPException(status_code=400, detail="Seller has insufficient balance") # Lock seller's crypto seller_wallet.available_balance -= order_in.crypto_amount seller_wallet.locked_balance += order_in.crypto_amount db.add(seller_wallet) # Create the order order = Order( advertisement_id=order_in.advertisement_id, buyer_id=buyer_id, seller_id=seller_id, cryptocurrency_id=advertisement.cryptocurrency_id, crypto_amount=order_in.crypto_amount, fiat_amount=order_in.fiat_amount, price=price, status=OrderStatus.PENDING, expires_at=datetime.utcnow() + timedelta(minutes=30), # 30 minutes to complete notes=order_in.notes ) db.add(order) db.commit() db.refresh(order) # Generate payment account details payment_service = PaymentService() try: payment_details = payment_service.generate_payment_account(order.fiat_amount) # Create payment record payment = Payment( order_id=order.id, account_number=payment_details["account_number"], account_name=payment_details["account_name"], bank_name=payment_details["bank_name"], amount=order.fiat_amount, reference=payment_details["reference"], ) db.add(payment) order.payment_account_number = payment_details["account_number"] order.payment_reference = payment_details["reference"] order.status = OrderStatus.PAYMENT_PENDING db.add(order) db.commit() db.refresh(order) except Exception as e: # If payment generation fails, clean up the order db.delete(order) db.commit() raise HTTPException(status_code=500, detail=f"Failed to generate payment details: {str(e)}") # Update advertisement available amount advertisement.available_amount -= order_in.crypto_amount db.add(advertisement) db.commit() return order @router.get("/{order_id}", response_model=OrderSchema) def read_order( order_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user), ) -> Any: order = db.query(Order).filter( Order.id == order_id, (Order.buyer_id == current_user.id) | (Order.seller_id == current_user.id) ).first() if not order: raise HTTPException(status_code=404, detail="Order not found") return order @router.put("/{order_id}", response_model=OrderSchema) def update_order( *, db: Session = Depends(get_db), order_id: int, order_in: OrderUpdate, current_user: User = Depends(get_current_active_user), ) -> Any: order = db.query(Order).filter( Order.id == order_id, (Order.buyer_id == current_user.id) | (Order.seller_id == current_user.id) ).first() if not order: raise HTTPException(status_code=404, detail="Order not found") update_data = order_in.dict(exclude_unset=True) for field, value in update_data.items(): setattr(order, field, value) db.add(order) db.commit() db.refresh(order) return order @router.post("/{order_id}/confirm-payment") def confirm_payment( order_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user), ) -> Any: order = db.query(Order).filter( Order.id == order_id, Order.buyer_id == current_user.id ).first() if not order: raise HTTPException(status_code=404, detail="Order not found or not authorized") if order.status != OrderStatus.PAYMENT_PENDING: raise HTTPException(status_code=400, detail="Order is not in payment pending status") # Mark payment as confirmed (in real implementation, this would verify with payment provider) payment = db.query(Payment).filter(Payment.order_id == order_id).first() if payment: payment.status = PaymentStatus.CONFIRMED payment.confirmed_at = datetime.utcnow() db.add(payment) order.status = OrderStatus.PAYMENT_CONFIRMED db.add(order) db.commit() # Auto-release crypto to buyer return release_crypto(order_id, db, current_user) @router.post("/{order_id}/release") def release_crypto( order_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user), ) -> Any: order = db.query(Order).filter( Order.id == order_id, Order.seller_id == current_user.id ).first() if not order: raise HTTPException(status_code=404, detail="Order not found or not authorized") if order.status not in [OrderStatus.PAYMENT_CONFIRMED, OrderStatus.PAYMENT_PENDING]: raise HTTPException(status_code=400, detail="Order is not ready for crypto release") # Get seller's wallet seller_wallet = db.query(Wallet).filter( Wallet.user_id == order.seller_id, Wallet.cryptocurrency_id == order.cryptocurrency_id ).first() # Get or create buyer's wallet buyer_wallet = db.query(Wallet).filter( Wallet.user_id == order.buyer_id, Wallet.cryptocurrency_id == order.cryptocurrency_id ).first() if not buyer_wallet: buyer_wallet = Wallet( user_id=order.buyer_id, cryptocurrency_id=order.cryptocurrency_id, available_balance=0.0, locked_balance=0.0 ) db.add(buyer_wallet) # Transfer crypto from seller to buyer if seller_wallet.locked_balance >= order.crypto_amount: seller_wallet.locked_balance -= order.crypto_amount buyer_wallet.available_balance += order.crypto_amount db.add(seller_wallet) db.add(buyer_wallet) order.status = OrderStatus.COMPLETED db.add(order) db.commit() return {"message": "Crypto released successfully", "amount": order.crypto_amount} else: raise HTTPException(status_code=400, detail="Insufficient locked funds") @router.post("/{order_id}/cancel") def cancel_order( order_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_active_user), ) -> Any: order = db.query(Order).filter( Order.id == order_id, (Order.buyer_id == current_user.id) | (Order.seller_id == current_user.id) ).first() if not order: raise HTTPException(status_code=404, detail="Order not found") if order.status in [OrderStatus.COMPLETED, OrderStatus.CANCELLED]: raise HTTPException(status_code=400, detail="Order cannot be cancelled") # Unlock seller's crypto seller_wallet = db.query(Wallet).filter( Wallet.user_id == order.seller_id, Wallet.cryptocurrency_id == order.cryptocurrency_id ).first() if seller_wallet and seller_wallet.locked_balance >= order.crypto_amount: seller_wallet.locked_balance -= order.crypto_amount seller_wallet.available_balance += order.crypto_amount db.add(seller_wallet) # Restore advertisement available amount advertisement = db.query(Advertisement).filter( Advertisement.id == order.advertisement_id ).first() if advertisement: advertisement.available_amount += order.crypto_amount db.add(advertisement) order.status = OrderStatus.CANCELLED db.add(order) db.commit() return {"message": "Order cancelled successfully"}