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:
AutonetSellCar Deploy
2025-12-30 13:24:39 +09:00
commit 1f0dcb1ddb
224 changed files with 55119 additions and 0 deletions

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