- Add new /my-vehicles API endpoint returning both recommended and direct purchases - Add DirectPurchasedCarResponse and MyVehiclesResponse schemas - Update frontend to display directly purchased cars (from banners with 1CC) - Show separate collapsible section for direct purchases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
581 lines
20 KiB
Python
581 lines
20 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
from typing import List
|
|
from datetime import datetime, timedelta
|
|
|
|
from ..database import get_db
|
|
from ..models import VehicleRequest, RequestVehicle, PurchasedVehicle, User, DealerInfo, SystemSettings, Car, CarView
|
|
from ..schemas import (
|
|
VehicleRequestCreate, VehicleRequestResponse,
|
|
RequestVehicleCreate, RequestVehicleResponse, RequestVehicleApprove,
|
|
PurchasedVehicleCreate, PurchasedVehicleResponse, PurchasedVehicleUpdateStatus,
|
|
VehicleRequestWithVehicles, DirectPurchasedCarResponse, MyVehiclesResponse,
|
|
)
|
|
from .auth import get_current_user
|
|
from .notification import notify_vehicle_recommended, notify_shipping_update
|
|
|
|
|
|
def get_system_settings(db: Session) -> SystemSettings:
|
|
"""Get or create system settings"""
|
|
settings = db.query(SystemSettings).first()
|
|
if not settings:
|
|
settings = SystemSettings()
|
|
db.add(settings)
|
|
db.commit()
|
|
db.refresh(settings)
|
|
return settings
|
|
|
|
|
|
def calculate_dealer_commission(vehicle_price_krw: int, db: Session) -> tuple:
|
|
"""Calculate dealer and platform commission based on Mongolia margin"""
|
|
settings = get_system_settings(db)
|
|
|
|
# Calculate Mongolia margin (vehicle price * margin percent)
|
|
mongolia_margin = vehicle_price_krw * (settings.mongolia_margin_percent / 100)
|
|
|
|
# 50/50 split between dealer and platform
|
|
dealer_commission = int(mongolia_margin * 0.5)
|
|
platform_commission = int(mongolia_margin * 0.5)
|
|
|
|
return dealer_commission, platform_commission
|
|
|
|
router = APIRouter(prefix="/vehicle-requests", tags=["vehicle-requests"])
|
|
|
|
# Development mode - skip 24 hour wait
|
|
DEV_MODE = True
|
|
|
|
|
|
# =====================
|
|
# User Endpoints
|
|
# =====================
|
|
|
|
QUOTE_REQUEST_COST = 1.0 # 1 CC for quote request submission
|
|
|
|
|
|
@router.post("/", response_model=VehicleRequestResponse)
|
|
def create_request(
|
|
request_data: VehicleRequestCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Create a new vehicle search request (costs 1 CC)"""
|
|
# Check if user has enough CC
|
|
if (current_user.cc_balance or 0) < QUOTE_REQUEST_COST:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Insufficient CC balance. You need {QUOTE_REQUEST_COST} CC to submit a vehicle request. Current balance: {current_user.cc_balance or 0}"
|
|
)
|
|
|
|
# Deduct CC from user's balance
|
|
current_user.cc_balance = (current_user.cc_balance or 0) - QUOTE_REQUEST_COST
|
|
|
|
# Create the request
|
|
request = VehicleRequest(
|
|
user_id=current_user.id,
|
|
cc_paid=QUOTE_REQUEST_COST,
|
|
**request_data.model_dump()
|
|
)
|
|
db.add(request)
|
|
db.commit()
|
|
db.refresh(request)
|
|
return request
|
|
|
|
|
|
@router.get("/my-requests", response_model=List[VehicleRequestWithVehicles])
|
|
def get_my_requests(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get current user's vehicle requests with approved vehicles"""
|
|
requests = db.query(VehicleRequest).filter(
|
|
VehicleRequest.user_id == current_user.id
|
|
).order_by(VehicleRequest.created_at.desc()).all()
|
|
|
|
result = []
|
|
for req in requests:
|
|
# In dev mode, show all approved vehicles immediately
|
|
# In production, only show after 24 hours
|
|
if DEV_MODE or (req.created_at and datetime.utcnow() - req.created_at > timedelta(hours=24)):
|
|
approved_vehicles = [v for v in req.recommended_vehicles if v.is_approved]
|
|
else:
|
|
approved_vehicles = []
|
|
|
|
# Enrich approved vehicles with latest soldout status from cars table
|
|
enriched_vehicles = []
|
|
for v in approved_vehicles:
|
|
vehicle_response = RequestVehicleResponse.model_validate(v)
|
|
# Get latest soldout status from cars table
|
|
if v.car_id:
|
|
car = db.query(Car).filter(Car.id == v.car_id).first()
|
|
if car:
|
|
# Add soldout status to car_data
|
|
vehicle_response.car_data = {**vehicle_response.car_data, "soldout": car.soldout}
|
|
enriched_vehicles.append(vehicle_response)
|
|
|
|
result.append(VehicleRequestWithVehicles(
|
|
request=VehicleRequestResponse.model_validate(req),
|
|
approved_vehicles=enriched_vehicles
|
|
))
|
|
|
|
return result
|
|
|
|
|
|
@router.get("/my-vehicles", response_model=MyVehiclesResponse)
|
|
def get_my_vehicles(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get all vehicles accessible to user: recommended + directly purchased from banners"""
|
|
|
|
# 1. Get vehicle requests with recommended vehicles (existing logic)
|
|
requests = db.query(VehicleRequest).filter(
|
|
VehicleRequest.user_id == current_user.id
|
|
).order_by(VehicleRequest.created_at.desc()).all()
|
|
|
|
vehicle_requests = []
|
|
for req in requests:
|
|
if DEV_MODE or (req.created_at and datetime.utcnow() - req.created_at > timedelta(hours=24)):
|
|
approved_vehicles = [v for v in req.recommended_vehicles if v.is_approved]
|
|
else:
|
|
approved_vehicles = []
|
|
|
|
enriched_vehicles = []
|
|
for v in approved_vehicles:
|
|
vehicle_response = RequestVehicleResponse.model_validate(v)
|
|
if v.car_id:
|
|
car = db.query(Car).filter(Car.id == v.car_id).first()
|
|
if car:
|
|
vehicle_response.car_data = {**vehicle_response.car_data, "soldout": car.soldout}
|
|
enriched_vehicles.append(vehicle_response)
|
|
|
|
vehicle_requests.append(VehicleRequestWithVehicles(
|
|
request=VehicleRequestResponse.model_validate(req),
|
|
approved_vehicles=enriched_vehicles
|
|
))
|
|
|
|
# 2. Get directly purchased cars (from CarView - paid 1CC on banner)
|
|
car_views = db.query(CarView).filter(
|
|
CarView.user_id == current_user.id
|
|
).order_by(CarView.created_at.desc()).all()
|
|
|
|
# Get the car_ids that are already in recommended vehicles (to avoid duplicates)
|
|
recommended_car_ids = set()
|
|
for vr in vehicle_requests:
|
|
for v in vr.approved_vehicles:
|
|
if v.car_id:
|
|
recommended_car_ids.add(v.car_id)
|
|
|
|
direct_purchases = []
|
|
for cv in car_views:
|
|
# Skip if already in recommended (user got recommendation first, then it's not a "direct" purchase)
|
|
if cv.car_id in recommended_car_ids:
|
|
continue
|
|
|
|
car = db.query(Car).filter(Car.id == cv.car_id).first()
|
|
if car:
|
|
# Build car_data similar to recommended vehicles
|
|
main_image = None
|
|
if car.images:
|
|
main_img = next((img for img in car.images if img.is_main), None)
|
|
if main_img:
|
|
main_image = main_img.url
|
|
elif car.images:
|
|
main_image = car.images[0].url
|
|
|
|
car_data = {
|
|
"id": str(car.source_id) if car.source_id else str(car.id),
|
|
"car_name": car.car_name,
|
|
"maker_name": car.maker.name if car.maker else None,
|
|
"year": car.year,
|
|
"mileage": car.mileage,
|
|
"final_price": car.final_price_krw,
|
|
"fuel": car.fuel,
|
|
"transmission": car.transmission,
|
|
"color": car.color,
|
|
"main_image": main_image,
|
|
"soldout": car.soldout,
|
|
"local_car_id": car.id,
|
|
}
|
|
|
|
direct_purchases.append(DirectPurchasedCarResponse(
|
|
id=cv.id,
|
|
car_id=cv.car_id,
|
|
car_data=car_data,
|
|
cc_paid=cv.cc_paid,
|
|
purchased_at=cv.created_at
|
|
))
|
|
|
|
return MyVehiclesResponse(
|
|
vehicle_requests=vehicle_requests,
|
|
direct_purchases=direct_purchases
|
|
)
|
|
|
|
|
|
# =====================
|
|
# Purchased Vehicles (Find My Car)
|
|
# =====================
|
|
|
|
@router.get("/purchased", response_model=List[PurchasedVehicleResponse])
|
|
def get_purchased_vehicles(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get user's purchased vehicles with shipping status"""
|
|
vehicles = db.query(PurchasedVehicle).filter(
|
|
PurchasedVehicle.user_id == current_user.id
|
|
).order_by(PurchasedVehicle.purchased_at.desc()).all()
|
|
return vehicles
|
|
|
|
|
|
# =====================
|
|
# Admin Endpoints
|
|
# =====================
|
|
|
|
@router.get("/admin/list", response_model=List[VehicleRequestResponse])
|
|
def admin_get_all_requests(
|
|
status: str = None,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Admin: Get all vehicle requests"""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
|
|
query = db.query(VehicleRequest)
|
|
if status:
|
|
query = query.filter(VehicleRequest.status == status)
|
|
|
|
requests = query.order_by(VehicleRequest.created_at.desc()).all()
|
|
|
|
# User 정보 추가
|
|
result = []
|
|
for req in requests:
|
|
user = db.query(User).filter(User.id == req.user_id).first()
|
|
req_dict = {
|
|
"id": req.id,
|
|
"user_id": req.user_id,
|
|
"user_email": user.email if user else None,
|
|
"user_name": user.name if user else None,
|
|
"maker_code": req.maker_code,
|
|
"maker_name": req.maker_name,
|
|
"model_code": req.model_code,
|
|
"model_name": req.model_name,
|
|
"grade_code": req.grade_code,
|
|
"grade_name": req.grade_name,
|
|
"year_from": req.year_from,
|
|
"year_to": req.year_to,
|
|
"mileage_min": req.mileage_min,
|
|
"mileage_max": req.mileage_max,
|
|
"fuel": req.fuel,
|
|
"displacement_min": req.displacement_min,
|
|
"displacement_max": req.displacement_max,
|
|
"status": req.status,
|
|
"admin_reviewed_at": req.admin_reviewed_at,
|
|
"created_at": req.created_at,
|
|
}
|
|
result.append(req_dict)
|
|
|
|
return result
|
|
|
|
|
|
@router.get("/admin/{request_id}", response_model=VehicleRequestWithVehicles)
|
|
def admin_get_request_detail(
|
|
request_id: int,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Admin: Get request detail with all recommended vehicles"""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
|
|
request = db.query(VehicleRequest).filter(VehicleRequest.id == request_id).first()
|
|
if not request:
|
|
raise HTTPException(status_code=404, detail="Request not found")
|
|
|
|
# Import CarPerformanceCheck for PDF status
|
|
from ..models import CarPerformanceCheck
|
|
|
|
# Enrich with PDF status and soldout
|
|
enriched_vehicles = []
|
|
for v in request.recommended_vehicles:
|
|
vehicle_response = RequestVehicleResponse.model_validate(v)
|
|
|
|
# Get PDF status and soldout from car
|
|
if v.car_id:
|
|
car = db.query(Car).filter(Car.id == v.car_id).first()
|
|
perf_check = db.query(CarPerformanceCheck).filter(CarPerformanceCheck.car_id == v.car_id).first()
|
|
|
|
vehicle_response.car_data = {
|
|
**vehicle_response.car_data,
|
|
"soldout": car.soldout if car else False,
|
|
"has_pdf": bool(perf_check and perf_check.pdf_path),
|
|
"check_num": perf_check.check_number if perf_check else None,
|
|
}
|
|
enriched_vehicles.append(vehicle_response)
|
|
|
|
return VehicleRequestWithVehicles(
|
|
request=VehicleRequestResponse.model_validate(request),
|
|
approved_vehicles=enriched_vehicles
|
|
)
|
|
|
|
|
|
@router.post("/admin/{request_id}/vehicles", response_model=RequestVehicleResponse)
|
|
def admin_add_vehicle(
|
|
request_id: int,
|
|
vehicle_data: RequestVehicleCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Admin: Add a vehicle to a request (also imports to cars table)"""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
|
|
request = db.query(VehicleRequest).filter(VehicleRequest.id == request_id).first()
|
|
if not request:
|
|
raise HTTPException(status_code=404, detail="Request not found")
|
|
|
|
# Extract car data
|
|
car_data = vehicle_data.car_data
|
|
source_id = str(car_data.get("id", ""))
|
|
|
|
# Check if car already exists in cars table
|
|
existing_car = None
|
|
if source_id:
|
|
existing_car = db.query(Car).filter(
|
|
Car.source == "carmodoo",
|
|
Car.source_id == source_id
|
|
).first()
|
|
|
|
car_id = None
|
|
if existing_car:
|
|
car_id = existing_car.id
|
|
elif source_id:
|
|
# Create new car record from car_data
|
|
new_car = Car(
|
|
source="carmodoo",
|
|
source_id=source_id,
|
|
car_name=car_data.get("car_name", ""),
|
|
year=car_data.get("year"),
|
|
mileage=car_data.get("mileage"),
|
|
price_krw=car_data.get("original_price"),
|
|
fuel=car_data.get("fuel"),
|
|
transmission=car_data.get("transmission"),
|
|
color=car_data.get("color"),
|
|
displacement=car_data.get("displacement"),
|
|
margin_krw=car_data.get("korea_margin"),
|
|
margin_mn=car_data.get("mongolia_margin"),
|
|
check_num=car_data.get("check_num"),
|
|
is_displayed=True, # Displayed so user can view recommended car
|
|
status="active"
|
|
)
|
|
db.add(new_car)
|
|
db.flush()
|
|
car_id = new_car.id
|
|
|
|
# Update car_data with local car_id for frontend
|
|
car_data["local_car_id"] = car_id
|
|
|
|
vehicle = RequestVehicle(
|
|
request_id=request_id,
|
|
car_id=car_id,
|
|
car_data=car_data,
|
|
is_approved=vehicle_data.is_approved,
|
|
approved_at=datetime.utcnow() if vehicle_data.is_approved else None
|
|
)
|
|
db.add(vehicle)
|
|
|
|
# Update request status
|
|
request.status = "reviewed"
|
|
request.admin_reviewed_at = datetime.utcnow()
|
|
|
|
db.commit()
|
|
db.refresh(vehicle)
|
|
return vehicle
|
|
|
|
|
|
@router.post("/admin/{request_id}/approve-vehicles")
|
|
def admin_approve_vehicles(
|
|
request_id: int,
|
|
approval: RequestVehicleApprove,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Admin: Approve multiple vehicles for a request"""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
|
|
vehicles = db.query(RequestVehicle).filter(
|
|
RequestVehicle.request_id == request_id,
|
|
RequestVehicle.id.in_(approval.vehicle_ids)
|
|
).all()
|
|
|
|
for vehicle in vehicles:
|
|
vehicle.is_approved = True
|
|
vehicle.approved_at = datetime.utcnow()
|
|
|
|
# Update request status
|
|
request = db.query(VehicleRequest).filter(VehicleRequest.id == request_id).first()
|
|
if request:
|
|
request.status = "completed"
|
|
request.admin_reviewed_at = datetime.utcnow()
|
|
|
|
db.commit()
|
|
|
|
# Send notification to user
|
|
if request and len(vehicles) > 0:
|
|
notify_vehicle_recommended(db, request.user_id, request_id, len(vehicles))
|
|
|
|
return {"message": f"Approved {len(vehicles)} vehicles"}
|
|
|
|
|
|
@router.put("/admin/{request_id}/status")
|
|
def admin_update_request_status(
|
|
request_id: int,
|
|
new_status: str,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Admin: Update request status"""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
|
|
request = db.query(VehicleRequest).filter(VehicleRequest.id == request_id).first()
|
|
if not request:
|
|
raise HTTPException(status_code=404, detail="Request not found")
|
|
|
|
request.status = new_status
|
|
request.admin_reviewed_at = datetime.utcnow()
|
|
db.commit()
|
|
|
|
return {"message": "Status updated"}
|
|
|
|
|
|
@router.delete("/admin/{request_id}/vehicles/{vehicle_id}")
|
|
def admin_delete_vehicle(
|
|
request_id: int,
|
|
vehicle_id: int,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Admin: Delete a recommended vehicle from a request"""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
|
|
vehicle = db.query(RequestVehicle).filter(
|
|
RequestVehicle.id == vehicle_id,
|
|
RequestVehicle.request_id == request_id
|
|
).first()
|
|
|
|
if not vehicle:
|
|
raise HTTPException(status_code=404, detail="Vehicle not found")
|
|
|
|
db.delete(vehicle)
|
|
db.commit()
|
|
|
|
return {"message": "Vehicle deleted successfully"}
|
|
|
|
|
|
# =====================
|
|
# Admin: Purchased Vehicles Management
|
|
# =====================
|
|
|
|
@router.post("/admin/purchased", response_model=PurchasedVehicleResponse)
|
|
def admin_create_purchased(
|
|
vehicle_data: PurchasedVehicleCreate,
|
|
user_id: int,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Admin: Create a purchased vehicle record"""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
|
|
# Calculate dealer commission if dealer is selected
|
|
dealer_commission_krw = 0
|
|
platform_commission_krw = 0
|
|
|
|
if vehicle_data.selected_dealer_id:
|
|
# Verify dealer exists and is active
|
|
dealer_info = db.query(DealerInfo).filter(
|
|
DealerInfo.id == vehicle_data.selected_dealer_id,
|
|
DealerInfo.is_active == True
|
|
).first()
|
|
|
|
if not dealer_info:
|
|
raise HTTPException(status_code=400, detail="Selected dealer not found or inactive")
|
|
|
|
# Calculate commissions
|
|
dealer_commission_krw, platform_commission_krw = calculate_dealer_commission(
|
|
vehicle_data.vehicle_price_krw, db
|
|
)
|
|
|
|
# Credit commission to dealer's account
|
|
dealer_info.total_commission_earned += dealer_commission_krw
|
|
|
|
vehicle = PurchasedVehicle(
|
|
user_id=user_id,
|
|
car_name=vehicle_data.car_name,
|
|
car_data=vehicle_data.car_data,
|
|
car_image=vehicle_data.car_image,
|
|
vehicle_price_krw=vehicle_data.vehicle_price_krw,
|
|
domestic_cost_krw=vehicle_data.domestic_cost_krw,
|
|
shipping_cost_usd=vehicle_data.shipping_cost_usd,
|
|
total_cost_krw=vehicle_data.total_cost_krw,
|
|
car_type=vehicle_data.car_type,
|
|
selected_dealer_id=vehicle_data.selected_dealer_id,
|
|
dealer_commission_krw=dealer_commission_krw,
|
|
platform_commission_krw=platform_commission_krw,
|
|
)
|
|
db.add(vehicle)
|
|
db.commit()
|
|
db.refresh(vehicle)
|
|
return vehicle
|
|
|
|
|
|
@router.get("/admin/purchased/all", response_model=List[PurchasedVehicleResponse])
|
|
def admin_get_all_purchased(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Admin: Get all purchased vehicles"""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
|
|
vehicles = db.query(PurchasedVehicle).order_by(PurchasedVehicle.purchased_at.desc()).all()
|
|
return vehicles
|
|
|
|
|
|
@router.put("/admin/purchased/{vehicle_id}/status", response_model=PurchasedVehicleResponse)
|
|
def admin_update_shipping_status(
|
|
vehicle_id: int,
|
|
status_update: PurchasedVehicleUpdateStatus,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Admin: Update shipping status of a purchased vehicle"""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
|
|
vehicle = db.query(PurchasedVehicle).filter(PurchasedVehicle.id == vehicle_id).first()
|
|
if not vehicle:
|
|
raise HTTPException(status_code=404, detail="Vehicle not found")
|
|
|
|
vehicle.shipping_status = status_update.shipping_status
|
|
vehicle.status_updated_at = datetime.utcnow()
|
|
|
|
if status_update.current_location:
|
|
vehicle.current_location = status_update.current_location
|
|
if status_update.estimated_arrival:
|
|
vehicle.estimated_arrival = status_update.estimated_arrival
|
|
|
|
if status_update.shipping_status == 7: # Delivered (배송완료)
|
|
vehicle.delivered_at = datetime.utcnow()
|
|
|
|
db.commit()
|
|
db.refresh(vehicle)
|
|
|
|
# Send notification to user about shipping update
|
|
notify_shipping_update(db, vehicle.user_id, vehicle.id, status_update.shipping_status, vehicle.car_name)
|
|
|
|
return vehicle
|