Files
AutonetSellCar/backend/app/api/withdrawal.py
AutonetSellCar Deploy 1f0dcb1ddb 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>
2025-12-30 13:24:39 +09:00

218 lines
7.0 KiB
Python

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