- 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>
287 lines
9.2 KiB
Python
287 lines
9.2 KiB
Python
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}
|