Automated Action b9798f0eaf Implement comprehensive crypto P2P trading platform
- Complete FastAPI application with JWT authentication
- SQLite database with SQLAlchemy ORM and Alembic migrations
- User registration/login with secure password hashing
- Multi-cryptocurrency wallet system with balance tracking
- Advertisement system for buy/sell listings with fund locking
- Order management with automatic payment integration
- Payment provider API integration with mock fallback
- Automatic crypto release after payment confirmation
- Health monitoring endpoint and CORS configuration
- Comprehensive API documentation with OpenAPI/Swagger
- Database models for users, wallets, ads, orders, and payments
- Complete CRUD operations for all entities
- Security features including fund locking and order expiration
- Detailed README with setup and usage instructions

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-26 14:48:18 +00:00

310 lines
11 KiB
Python

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"}