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:
286
backend/app/api/vehicle_share.py
Normal file
286
backend/app/api/vehicle_share.py
Normal file
@@ -0,0 +1,286 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from ..database import get_db
|
||||
from ..models import User, VehicleShare, ShareReward, RequestVehicle
|
||||
from ..models.vehicle_share import generate_share_code
|
||||
from ..schemas import (
|
||||
VehicleShareCreate, VehicleShareResponse,
|
||||
ShareRewardResponse, ShareRewardSummary,
|
||||
)
|
||||
from .auth import get_current_user, get_current_user_optional
|
||||
from .notification import notify_share_purchased
|
||||
|
||||
router = APIRouter(prefix="/share", tags=["vehicle-share"])
|
||||
|
||||
# Tax rate for rewards (3.3% withholding tax in Korea)
|
||||
TAX_RATE = 0.033
|
||||
# Reward percentage (90% of markup goes to sharer)
|
||||
REWARD_RATE = 0.90
|
||||
|
||||
|
||||
@router.post("/create", response_model=VehicleShareResponse)
|
||||
def create_share(
|
||||
share_data: VehicleShareCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a shareable link for a vehicle with optional price markup"""
|
||||
# Get the request vehicle
|
||||
request_vehicle = db.query(RequestVehicle).filter(
|
||||
RequestVehicle.id == share_data.request_vehicle_id
|
||||
).first()
|
||||
|
||||
if not request_vehicle:
|
||||
raise HTTPException(status_code=404, detail="Vehicle not found")
|
||||
|
||||
# Check if user owns this request (through VehicleRequest)
|
||||
if request_vehicle.vehicle_request.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="You can only share vehicles from your own requests")
|
||||
|
||||
# Check if vehicle is approved
|
||||
if not request_vehicle.is_approved:
|
||||
raise HTTPException(status_code=400, detail="Only approved vehicles can be shared")
|
||||
|
||||
# Generate unique share code
|
||||
share_code = generate_share_code()
|
||||
while db.query(VehicleShare).filter(VehicleShare.share_code == share_code).first():
|
||||
share_code = generate_share_code()
|
||||
|
||||
# Calculate prices
|
||||
original_price = request_vehicle.price_krw or 0
|
||||
markup = share_data.markup_amount_krw if share_data.markup_amount_krw > 0 else 0
|
||||
shared_price = original_price + markup
|
||||
|
||||
# Create share
|
||||
vehicle_share = VehicleShare(
|
||||
user_id=current_user.id,
|
||||
request_vehicle_id=share_data.request_vehicle_id,
|
||||
share_code=share_code,
|
||||
original_price_krw=original_price,
|
||||
markup_amount_krw=markup,
|
||||
shared_price_krw=shared_price,
|
||||
)
|
||||
|
||||
db.add(vehicle_share)
|
||||
db.commit()
|
||||
db.refresh(vehicle_share)
|
||||
|
||||
return vehicle_share
|
||||
|
||||
|
||||
@router.get("/my-shares", response_model=List[VehicleShareResponse])
|
||||
def get_my_shares(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all vehicle shares created by current user"""
|
||||
shares = db.query(VehicleShare).filter(
|
||||
VehicleShare.user_id == current_user.id
|
||||
).order_by(VehicleShare.created_at.desc()).all()
|
||||
|
||||
return shares
|
||||
|
||||
|
||||
@router.get("/my-rewards", response_model=List[ShareRewardResponse])
|
||||
def get_my_rewards(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get all rewards earned from vehicle shares"""
|
||||
rewards = db.query(ShareReward).filter(
|
||||
ShareReward.user_id == current_user.id
|
||||
).order_by(ShareReward.created_at.desc()).all()
|
||||
|
||||
return rewards
|
||||
|
||||
|
||||
@router.get("/my-rewards/summary", response_model=ShareRewardSummary)
|
||||
def get_rewards_summary(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get summary of share rewards"""
|
||||
rewards = db.query(ShareReward).filter(
|
||||
ShareReward.user_id == current_user.id
|
||||
).all()
|
||||
|
||||
total_rewards = sum(r.net_amount for r in rewards)
|
||||
total_withdrawn = sum(r.net_amount for r in rewards if r.status == "withdrawn")
|
||||
pending = sum(r.net_amount for r in rewards if r.status == "pending")
|
||||
approved = sum(r.net_amount for r in rewards if r.status == "approved")
|
||||
|
||||
return ShareRewardSummary(
|
||||
total_rewards=total_rewards,
|
||||
total_withdrawn=total_withdrawn,
|
||||
pending_amount=pending,
|
||||
available_for_withdrawal=approved,
|
||||
reward_count=len(rewards)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{share_code}")
|
||||
def get_shared_vehicle(
|
||||
share_code: str,
|
||||
current_user: User = Depends(get_current_user_optional),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get shared vehicle details (public endpoint)"""
|
||||
share = db.query(VehicleShare).filter(
|
||||
VehicleShare.share_code == share_code
|
||||
).first()
|
||||
|
||||
if not share:
|
||||
raise HTTPException(status_code=404, detail="Shared vehicle not found")
|
||||
|
||||
# Increment view count
|
||||
share.view_count += 1
|
||||
db.commit()
|
||||
|
||||
# Get vehicle details
|
||||
vehicle = share.request_vehicle
|
||||
|
||||
return {
|
||||
"share": {
|
||||
"id": share.id,
|
||||
"share_code": share.share_code,
|
||||
"shared_price_krw": share.shared_price_krw,
|
||||
"original_price_krw": share.original_price_krw,
|
||||
"markup_amount_krw": share.markup_amount_krw,
|
||||
"view_count": share.view_count,
|
||||
"is_purchased": share.is_purchased,
|
||||
"created_at": share.created_at,
|
||||
},
|
||||
"vehicle": {
|
||||
"id": vehicle.id,
|
||||
"car_id": vehicle.car_id,
|
||||
"maker": vehicle.maker,
|
||||
"model": vehicle.model,
|
||||
"year": vehicle.year,
|
||||
"mileage": vehicle.mileage,
|
||||
"fuel_type": vehicle.fuel_type,
|
||||
"color": vehicle.color,
|
||||
"grade": vehicle.grade,
|
||||
"image_url": vehicle.image_url,
|
||||
"performance_check_url": vehicle.performance_check_url,
|
||||
"dealer_name": vehicle.dealer_name,
|
||||
"dealer_phone": vehicle.dealer_phone,
|
||||
},
|
||||
"sharer": {
|
||||
"name": share.user.name or "Anonymous",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{share_code}/purchase")
|
||||
def purchase_shared_vehicle(
|
||||
share_code: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Purchase a vehicle through a shared link"""
|
||||
share = db.query(VehicleShare).filter(
|
||||
VehicleShare.share_code == share_code
|
||||
).first()
|
||||
|
||||
if not share:
|
||||
raise HTTPException(status_code=404, detail="Shared vehicle not found")
|
||||
|
||||
if share.is_purchased:
|
||||
raise HTTPException(status_code=400, detail="This vehicle has already been purchased")
|
||||
|
||||
if share.user_id == current_user.id:
|
||||
raise HTTPException(status_code=400, detail="You cannot purchase your own shared vehicle")
|
||||
|
||||
# Mark as purchased
|
||||
share.is_purchased = True
|
||||
share.purchased_by_user_id = current_user.id
|
||||
share.purchased_at = datetime.utcnow()
|
||||
|
||||
# Create reward for the sharer (if there's markup)
|
||||
reward_net = 0
|
||||
if share.markup_amount_krw > 0:
|
||||
reward_amount = share.markup_amount_krw * REWARD_RATE # 90%
|
||||
tax_amount = reward_amount * TAX_RATE # 3.3% tax
|
||||
net_amount = reward_amount - tax_amount
|
||||
reward_net = net_amount
|
||||
|
||||
reward = ShareReward(
|
||||
user_id=share.user_id,
|
||||
vehicle_share_id=share.id,
|
||||
markup_amount=share.markup_amount_krw,
|
||||
reward_amount=reward_amount,
|
||||
tax_amount=tax_amount,
|
||||
net_amount=net_amount,
|
||||
status="pending" # Needs admin approval
|
||||
)
|
||||
db.add(reward)
|
||||
|
||||
db.commit()
|
||||
|
||||
# Send notification to sharer about the sale
|
||||
vehicle = share.request_vehicle
|
||||
car_name = f"{vehicle.maker} {vehicle.model}" if vehicle else "차량"
|
||||
notify_share_purchased(db, share.user_id, share.id, reward_net, car_name)
|
||||
|
||||
return {
|
||||
"message": "Vehicle purchase initiated",
|
||||
"share_code": share_code,
|
||||
"price": share.shared_price_krw
|
||||
}
|
||||
|
||||
|
||||
# Admin endpoints
|
||||
@router.get("/admin/all", response_model=List[VehicleShareResponse])
|
||||
def get_all_shares(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""[Admin] Get all vehicle shares"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
shares = db.query(VehicleShare).order_by(VehicleShare.created_at.desc()).all()
|
||||
return shares
|
||||
|
||||
|
||||
@router.get("/admin/rewards", response_model=List[ShareRewardResponse])
|
||||
def get_all_rewards(
|
||||
status_filter: str = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""[Admin] Get all share rewards"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
query = db.query(ShareReward)
|
||||
if status_filter:
|
||||
query = query.filter(ShareReward.status == status_filter)
|
||||
|
||||
rewards = query.order_by(ShareReward.created_at.desc()).all()
|
||||
return rewards
|
||||
|
||||
|
||||
@router.put("/admin/rewards/{reward_id}/approve")
|
||||
def approve_reward(
|
||||
reward_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""[Admin] Approve a share reward for withdrawal"""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
reward = db.query(ShareReward).filter(ShareReward.id == reward_id).first()
|
||||
if not reward:
|
||||
raise HTTPException(status_code=404, detail="Reward not found")
|
||||
|
||||
if reward.status != "pending":
|
||||
raise HTTPException(status_code=400, detail="Reward is not pending")
|
||||
|
||||
reward.status = "approved"
|
||||
db.commit()
|
||||
|
||||
return {"message": "Reward approved", "reward_id": reward_id}
|
||||
Reference in New Issue
Block a user