Initial commit: AutonetSellCar platform with deployment system
- Frontend: Next.js 14 with TypeScript - Backend: FastAPI with SQLAlchemy - Agent: Carmodoo sync agent - Deployment: Docker Compose based staging/production setup - Scripts: Automated deployment with rollback support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
217
backend/app/api/withdrawal.py
Normal file
217
backend/app/api/withdrawal.py
Normal file
@@ -0,0 +1,217 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func as sql_func
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from ..database import get_db
|
||||
from ..models import User, WithdrawalRequest, DealerInfo, ShareReward, ReferralReward
|
||||
from ..schemas import (
|
||||
WithdrawalRequestCreate, WithdrawalRequestResponse,
|
||||
WithdrawalProcess, WithdrawalBalance,
|
||||
)
|
||||
from .auth import get_current_user
|
||||
from .notification import notify_withdrawal_processed
|
||||
|
||||
router = APIRouter(prefix="/withdrawal", tags=["withdrawal"])
|
||||
|
||||
# Tax rate (3.3% withholding)
|
||||
TAX_RATE = 0.033
|
||||
|
||||
|
||||
def calculate_user_balance(user: User, db: Session) -> WithdrawalBalance:
|
||||
"""Calculate user's withdrawal balance from all sources"""
|
||||
total_earned = 0.0
|
||||
total_withdrawn = 0.0
|
||||
pending_withdrawal = 0.0
|
||||
|
||||
# Get dealer earnings if user is a dealer
|
||||
if user.is_dealer:
|
||||
dealer_info = db.query(DealerInfo).filter(DealerInfo.user_id == user.id).first()
|
||||
if dealer_info:
|
||||
total_earned += dealer_info.total_commission_earned
|
||||
total_withdrawn += dealer_info.total_withdrawn
|
||||
|
||||
# Get share rewards
|
||||
share_rewards = db.query(ShareReward).filter(
|
||||
ShareReward.user_id == user.id,
|
||||
ShareReward.status.in_(["approved", "withdrawn"])
|
||||
).all()
|
||||
|
||||
for reward in share_rewards:
|
||||
total_earned += reward.net_amount
|
||||
if reward.status == "withdrawn":
|
||||
total_withdrawn += reward.net_amount
|
||||
|
||||
# Get referral rewards
|
||||
referral_rewards = db.query(ReferralReward).filter(
|
||||
ReferralReward.referrer_id == user.id,
|
||||
ReferralReward.status.in_(["credited", "withdrawn"])
|
||||
).all()
|
||||
|
||||
for reward in referral_rewards:
|
||||
total_earned += reward.reward_amount
|
||||
if reward.status == "withdrawn":
|
||||
total_withdrawn += reward.reward_amount
|
||||
|
||||
# Get pending withdrawals
|
||||
pending_requests = db.query(WithdrawalRequest).filter(
|
||||
WithdrawalRequest.user_id == user.id,
|
||||
WithdrawalRequest.status.in_(["pending", "approved"])
|
||||
).all()
|
||||
|
||||
for req in pending_requests:
|
||||
pending_withdrawal += req.net_amount
|
||||
|
||||
available_balance = total_earned - total_withdrawn - pending_withdrawal
|
||||
|
||||
return WithdrawalBalance(
|
||||
total_earned=total_earned,
|
||||
total_withdrawn=total_withdrawn,
|
||||
pending_withdrawal=pending_withdrawal,
|
||||
available_balance=max(0, available_balance)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/balance", response_model=WithdrawalBalance)
|
||||
def get_balance(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get current user's withdrawal balance"""
|
||||
return calculate_user_balance(current_user, db)
|
||||
|
||||
|
||||
@router.post("/request", response_model=WithdrawalRequestResponse)
|
||||
def create_withdrawal_request(
|
||||
request_data: WithdrawalRequestCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new withdrawal request"""
|
||||
# Check balance
|
||||
balance = calculate_user_balance(current_user, db)
|
||||
|
||||
if request_data.amount <= 0:
|
||||
raise HTTPException(status_code=400, detail="Amount must be positive")
|
||||
|
||||
if request_data.amount > balance.available_balance:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Insufficient balance. Available: {balance.available_balance}"
|
||||
)
|
||||
|
||||
# Minimum withdrawal amount
|
||||
MIN_WITHDRAWAL = 10 # 10 USD minimum
|
||||
if request_data.amount < MIN_WITHDRAWAL:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Minimum withdrawal amount is ${MIN_WITHDRAWAL} USD"
|
||||
)
|
||||
|
||||
# Calculate tax and net amount
|
||||
tax_amount = request_data.amount * TAX_RATE
|
||||
net_amount = request_data.amount - tax_amount
|
||||
|
||||
# Create withdrawal request
|
||||
withdrawal = WithdrawalRequest(
|
||||
user_id=current_user.id,
|
||||
amount=request_data.amount,
|
||||
tax_withheld=tax_amount,
|
||||
net_amount=net_amount,
|
||||
bank_name=request_data.bank_name,
|
||||
bank_account=request_data.bank_account,
|
||||
account_holder=request_data.account_holder,
|
||||
status="pending"
|
||||
)
|
||||
|
||||
db.add(withdrawal)
|
||||
db.commit()
|
||||
db.refresh(withdrawal)
|
||||
|
||||
return withdrawal
|
||||
|
||||
|
||||
@router.get("/my-requests", response_model=List[WithdrawalRequestResponse])
|
||||
def get_my_requests(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get current user's withdrawal requests"""
|
||||
requests = db.query(WithdrawalRequest).filter(
|
||||
WithdrawalRequest.user_id == current_user.id
|
||||
).order_by(WithdrawalRequest.requested_at.desc()).all()
|
||||
|
||||
return requests
|
||||
|
||||
|
||||
# Admin endpoints
|
||||
@router.get("/admin/list", response_model=List[WithdrawalRequestResponse])
|
||||
def get_all_requests(
|
||||
status_filter: str = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""[Admin] Get all withdrawal requests"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
query = db.query(WithdrawalRequest)
|
||||
if status_filter:
|
||||
query = query.filter(WithdrawalRequest.status == status_filter)
|
||||
|
||||
requests = query.order_by(WithdrawalRequest.requested_at.desc()).all()
|
||||
return requests
|
||||
|
||||
|
||||
@router.put("/admin/{request_id}/process", response_model=WithdrawalRequestResponse)
|
||||
def process_withdrawal(
|
||||
request_id: int,
|
||||
process_data: WithdrawalProcess,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""[Admin] Process a withdrawal request"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
withdrawal = db.query(WithdrawalRequest).filter(
|
||||
WithdrawalRequest.id == request_id
|
||||
).first()
|
||||
|
||||
if not withdrawal:
|
||||
raise HTTPException(status_code=404, detail="Request not found")
|
||||
|
||||
valid_statuses = ["approved", "completed", "rejected"]
|
||||
if process_data.status not in valid_statuses:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid status. Must be one of: {valid_statuses}"
|
||||
)
|
||||
|
||||
# Update status
|
||||
withdrawal.status = process_data.status
|
||||
withdrawal.admin_note = process_data.admin_note
|
||||
withdrawal.processed_at = datetime.utcnow()
|
||||
|
||||
# If completed, update user's withdrawal totals
|
||||
if process_data.status == "completed":
|
||||
user = db.query(User).filter(User.id == withdrawal.user_id).first()
|
||||
|
||||
# Update dealer info if applicable
|
||||
if user.is_dealer:
|
||||
dealer_info = db.query(DealerInfo).filter(
|
||||
DealerInfo.user_id == user.id
|
||||
).first()
|
||||
if dealer_info:
|
||||
dealer_info.total_withdrawn += withdrawal.net_amount
|
||||
|
||||
# Mark related share rewards as withdrawn
|
||||
# (This is a simplified version - in production you'd track which specific rewards were withdrawn)
|
||||
|
||||
db.commit()
|
||||
db.refresh(withdrawal)
|
||||
|
||||
# Send notification to user about withdrawal status
|
||||
notify_withdrawal_processed(db, withdrawal.user_id, withdrawal.id, process_data.status, withdrawal.net_amount)
|
||||
|
||||
return withdrawal
|
||||
Reference in New Issue
Block a user