from fastapi import APIRouter, Depends, HTTPException, Request, Header from sqlalchemy.orm import Session from sqlalchemy import desc from typing import List, Optional from pydantic import BaseModel from datetime import datetime import stripe import logging from ..database import get_db from ..models import User, Car, CarView, PerformanceCheckView, ChargeHistory, CarPerformanceCheck, CCPackage, DEFAULT_CC_PACKAGES, VehicleRequest, RequestVehicle from ..models.settings import SystemSettings from ..models.user import PaymentSettings from ..schemas import UserResponse, CarViewResponse, PurchaseViewRequest from .auth import get_current_user, get_current_admin_user, get_current_user_optional from .referral import create_referral_reward from .carmodoo import CarmodooClient, capture_performance_check_pdf from .notification import notify_system from ..config import get_settings logger = logging.getLogger(__name__) settings = get_settings() # Configure Stripe stripe.api_key = settings.STRIPE_SECRET_KEY router = APIRouter(prefix="/cc", tags=["cc"]) class ChargeRequest(BaseModel): amount: int currency: str = "USD" payment_method: str = "card" transaction_id: Optional[str] = None # For crypto payments wallet_address: Optional[str] = None # User's wallet for refunds class USDCChargeRequest(BaseModel): amount_usdc: int transaction_hash: str wallet_address: str network: str = "Polygon" class ChargeHistoryResponse(BaseModel): id: int amount_usd: int cc_amount: int payment_method: str status: str created_at: str class Config: from_attributes = True @router.get("/balance") def get_cc_balance(current_user: User = Depends(get_current_user)): """Get current user's CC balance""" return {"cc_balance": current_user.cc_balance or 0} @router.get("/views", response_model=List[CarViewResponse]) def get_purchased_views( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Get list of cars the user has paid to view""" views = db.query(CarView).filter(CarView.user_id == current_user.id).all() return views @router.get("/views/car-ids") def get_purchased_car_ids( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Get list of car IDs the user has paid to view (for quick lookup)""" views = db.query(CarView.car_id).filter(CarView.user_id == current_user.id).all() return {"car_ids": [v[0] for v in views]} @router.post("/purchase-view") def purchase_car_view( request: PurchaseViewRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Purchase access to view full car details (costs 1 CC)""" car_id = request.car_id # Check if car exists car = db.query(Car).filter(Car.id == car_id).first() if not car: raise HTTPException(status_code=404, detail="Car not found") # Check if already purchased existing_view = db.query(CarView).filter( CarView.user_id == current_user.id, CarView.car_id == car_id ).first() if existing_view: return {"message": "Already purchased", "cc_balance": current_user.cc_balance} # Check if user has enough CC if (current_user.cc_balance or 0) < 1: raise HTTPException( status_code=400, detail="Insufficient CC balance. You need 1 CC to view full car details." ) # Deduct CC and create view record current_user.cc_balance = (current_user.cc_balance or 0) - 1 car_view = CarView( user_id=current_user.id, car_id=car_id, cc_paid=1 ) db.add(car_view) db.commit() return { "message": "Purchase successful", "cc_balance": current_user.cc_balance, "car_id": car_id } @router.get("/check-view/{car_id}") def check_car_view( car_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Check if user has purchased view access for a specific car""" # Check if user purchased view access existing_view = db.query(CarView).filter( CarView.user_id == current_user.id, CarView.car_id == car_id ).first() if existing_view: return { "has_access": True, "cc_balance": current_user.cc_balance or 0 } # Check if this car was recommended to the user (paid 1 CC for recommendation) recommended_vehicle = db.query(RequestVehicle).join(VehicleRequest).filter( VehicleRequest.user_id == current_user.id, RequestVehicle.car_id == car_id, RequestVehicle.is_approved == True ).first() return { "has_access": recommended_vehicle is not None, "cc_balance": current_user.cc_balance or 0 } def get_performance_check_cost(db: Session) -> float: """Get performance check cost from system settings""" system_settings = db.query(SystemSettings).first() if system_settings and system_settings.cc_per_banner_view is not None: return system_settings.cc_per_banner_view return 0.1 # Default fallback @router.post("/purchase-performance-check") async def purchase_performance_check_view( request: PurchaseViewRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Purchase access to view performance check (costs CC based on system settings)""" car_id = request.car_id PERFORMANCE_CHECK_COST = get_performance_check_cost(db) # Check if car exists car = db.query(Car).filter(Car.id == car_id).first() if not car: raise HTTPException(status_code=404, detail="Car not found") # Check if performance check record exists perf_check = db.query(CarPerformanceCheck).filter( CarPerformanceCheck.car_id == car_id ).first() # If no performance check record, try to fetch from Carmodoo if not perf_check: try: carmodoo_client = CarmodooClient() check_num = car.check_num or "" # Try to get check_num if not available if not check_num: check_num = await carmodoo_client.get_car_check_num(car.source_id, car.source_key or "") if check_num: # Fetch performance check data perf_result = await carmodoo_client.get_performance_check(car.source_id, car.source_key or "", check_num) if perf_result.get("found") and perf_result.get("data"): perf_data = perf_result["data"] # Create CarPerformanceCheck record perf_check = CarPerformanceCheck( car_id=car.id, check_number=perf_data.get("check_number") or check_num, check_date=perf_data.get("check_date"), valid_until=perf_data.get("valid_until"), first_registration=perf_data.get("first_registration"), mileage=perf_data.get("mileage"), mileage_status=perf_data.get("mileage_status"), seize_count=perf_data.get("seize_count", 0), collateral_count=perf_data.get("collateral_count", 0), is_flood_damaged=perf_data.get("is_flood_damaged", False), is_fire_damaged=perf_data.get("is_fire_damaged", False), is_total_loss=perf_data.get("is_total_loss", False), engine_status=perf_data.get("engine_status"), transmission_status=perf_data.get("transmission_status"), power_delivery_status=perf_data.get("power_delivery_status"), raw_data=perf_data, raw_html=perf_result.get("raw_html", "")[:50000], ) db.add(perf_check) db.flush() # Capture PDF try: pdf_path = await capture_performance_check_pdf(perf_check.check_number, car.id) if pdf_path: perf_check.pdf_path = pdf_path except Exception as pdf_error: logger.warning(f"PDF capture failed: {pdf_error}") db.commit() db.refresh(perf_check) except Exception as e: logger.error(f"Failed to fetch performance check: {e}") if not perf_check: raise HTTPException(status_code=404, detail="Performance check not available for this car") # Check if already purchased existing_view = db.query(PerformanceCheckView).filter( PerformanceCheckView.user_id == current_user.id, PerformanceCheckView.car_id == car_id ).first() if existing_view: return {"message": "Already purchased", "cc_balance": current_user.cc_balance} # Check if user has enough CC if (current_user.cc_balance or 0) < PERFORMANCE_CHECK_COST: raise HTTPException( status_code=400, detail=f"Insufficient CC balance. You need {PERFORMANCE_CHECK_COST} CC to view performance check." ) # Deduct CC and create view record current_user.cc_balance = (current_user.cc_balance or 0) - PERFORMANCE_CHECK_COST perf_view = PerformanceCheckView( user_id=current_user.id, car_id=car_id, cc_paid=PERFORMANCE_CHECK_COST ) db.add(perf_view) db.commit() return { "message": "Purchase successful", "cc_balance": current_user.cc_balance, "car_id": car_id } @router.get("/check-performance-check/{car_id}") def check_performance_check_view( car_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Check if user has purchased performance check view for a specific car""" # Check if performance check exists for this car perf_check = db.query(CarPerformanceCheck).filter( CarPerformanceCheck.car_id == car_id ).first() # Check 1: Purchased performance check (0.1 CC) existing_perf_view = db.query(PerformanceCheckView).filter( PerformanceCheckView.user_id == current_user.id, PerformanceCheckView.car_id == car_id ).first() # Check 2: Purchased full car view (1 CC) -> performance check included free existing_car_view = db.query(CarView).filter( CarView.user_id == current_user.id, CarView.car_id == car_id ).first() has_access = (existing_perf_view is not None) or (existing_car_view is not None) return { "has_access": has_access, "has_performance_check": perf_check is not None, "cc_balance": current_user.cc_balance or 0, "cost": PERFORMANCE_CHECK_COST, "included_in_car_view": existing_car_view is not None # True if already purchased car view } @router.get("/payment-info") def get_payment_info(): """Get payment information including USDC wallet address""" return { "usdc_wallet_address": PaymentSettings.USDC_WALLET_ADDRESS, "usdc_network": PaymentSettings.USDC_NETWORK, "min_charge_usd": PaymentSettings.MIN_CHARGE_USD, "max_charge_usd": PaymentSettings.MAX_CHARGE_USD, "supported_currencies": PaymentSettings.SUPPORTED_CURRENCIES, "supported_methods": PaymentSettings.SUPPORTED_METHODS, "rate": "1 USD = 1 CC", } @router.post("/charge") def charge_cc( request: ChargeRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Create a charge request (for card or bank transfer - requires admin verification)""" # Validate amount if request.amount < PaymentSettings.MIN_CHARGE_USD: raise HTTPException(status_code=400, detail=f"Minimum charge amount is ${PaymentSettings.MIN_CHARGE_USD}") if request.amount > PaymentSettings.MAX_CHARGE_USD: raise HTTPException(status_code=400, detail=f"Maximum charge amount is ${PaymentSettings.MAX_CHARGE_USD}") # Calculate CC amount (1 USD = 1 CC) cc_amount = request.amount # Determine status based on payment method # Card payments would go through a payment gateway (not implemented yet) # USDC and bank transfers require manual verification status = "pending" if request.payment_method in ["usdc", "bank_transfer"] else "pending" # Create charge history record charge_record = ChargeHistory( user_id=current_user.id, amount=request.amount, amount_usd=request.amount, # Backwards compatibility cc_amount=cc_amount, currency=request.currency, payment_method=request.payment_method, transaction_id=request.transaction_id, wallet_address=request.wallet_address, status=status ) db.add(charge_record) db.commit() db.refresh(charge_record) return { "message": "Charge request created" if status == "pending" else "Charge successful", "charge_id": charge_record.id, "amount": request.amount, "currency": request.currency, "cc_amount": cc_amount, "status": status, "payment_info": { "usdc_wallet": PaymentSettings.USDC_WALLET_ADDRESS if request.payment_method == "usdc" else None, "network": PaymentSettings.USDC_NETWORK if request.payment_method == "usdc" else None, } } @router.post("/charge/usdc") def charge_cc_usdc( request: USDCChargeRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Create USDC charge request with transaction hash""" # Validate amount if request.amount_usdc < PaymentSettings.MIN_CHARGE_USD: raise HTTPException(status_code=400, detail=f"Minimum charge amount is {PaymentSettings.MIN_CHARGE_USD} USDC") if request.amount_usdc > PaymentSettings.MAX_CHARGE_USD: raise HTTPException(status_code=400, detail=f"Maximum charge amount is {PaymentSettings.MAX_CHARGE_USD} USDC") # Check for duplicate transaction existing = db.query(ChargeHistory).filter( ChargeHistory.transaction_id == request.transaction_hash ).first() if existing: raise HTTPException(status_code=400, detail="This transaction has already been submitted") # Create pending charge record charge_record = ChargeHistory( user_id=current_user.id, amount=request.amount_usdc, amount_usd=request.amount_usdc, cc_amount=request.amount_usdc, currency="USDC", payment_method="usdc", transaction_id=request.transaction_hash, wallet_address=request.wallet_address, status="pending" ) db.add(charge_record) db.commit() db.refresh(charge_record) return { "message": "USDC payment submitted for verification", "charge_id": charge_record.id, "amount_usdc": request.amount_usdc, "cc_amount": request.amount_usdc, "status": "pending", "transaction_hash": request.transaction_hash } @router.get("/charge-history") def get_charge_history( current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Get user's charge history""" history = db.query(ChargeHistory).filter( ChargeHistory.user_id == current_user.id ).order_by(desc(ChargeHistory.created_at)).limit(50).all() return [ { "id": h.id, "amount": h.amount or h.amount_usd, "amount_usd": h.amount_usd, "currency": h.currency or "USD", "cc_amount": h.cc_amount, "payment_method": h.payment_method, "transaction_id": h.transaction_id, "status": h.status, "created_at": h.created_at.isoformat() if h.created_at else None } for h in history ] # Admin endpoints for payment verification @router.get("/admin/pending") def admin_get_pending_payments( db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user) ): """Get all pending payment requests (Admin only)""" pending = db.query(ChargeHistory).filter( ChargeHistory.status == "pending" ).order_by(desc(ChargeHistory.created_at)).all() return [ { "id": h.id, "user_id": h.user_id, "user_email": h.user.email if h.user else None, "user_name": h.user.name if h.user else None, "amount": h.amount or h.amount_usd, "currency": h.currency or "USD", "cc_amount": h.cc_amount, "payment_method": h.payment_method, "transaction_id": h.transaction_id, "wallet_address": h.wallet_address, "status": h.status, "created_at": h.created_at.isoformat() if h.created_at else None } for h in pending ] @router.get("/admin/all") def admin_get_all_payments( status: str = None, page: int = 1, page_size: int = 20, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user) ): """Get all payment records with optional status filter (Admin only)""" query = db.query(ChargeHistory) if status: query = query.filter(ChargeHistory.status == status) total = query.count() payments = query.order_by(desc(ChargeHistory.created_at)).offset((page - 1) * page_size).limit(page_size).all() return { "payments": [ { "id": h.id, "user_id": h.user_id, "user_email": h.user.email if h.user else None, "user_name": h.user.name if h.user else None, "amount": h.amount or h.amount_usd, "currency": h.currency or "USD", "cc_amount": h.cc_amount, "payment_method": h.payment_method, "transaction_id": h.transaction_id, "wallet_address": h.wallet_address, "admin_note": h.admin_note, "status": h.status, "verified_at": h.verified_at.isoformat() if h.verified_at else None, "created_at": h.created_at.isoformat() if h.created_at else None } for h in payments ], "total": total, "page": page, "page_size": page_size } @router.put("/admin/{charge_id}/verify") def admin_verify_payment( charge_id: int, approved: bool, admin_note: str = None, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_user) ): """Verify and approve/reject a pending payment (Admin only)""" charge = db.query(ChargeHistory).filter(ChargeHistory.id == charge_id).first() if not charge: raise HTTPException(status_code=404, detail="Charge record not found") if charge.status != "pending": raise HTTPException(status_code=400, detail=f"Charge is already {charge.status}") if approved: charge.status = "completed" charge.verified_at = datetime.utcnow() charge.verified_by = current_user.id charge.admin_note = admin_note # Credit CC to user user = db.query(User).filter(User.id == charge.user_id).first() if user: user.cc_balance = (user.cc_balance or 0) + charge.cc_amount # Trigger referral reward if applicable if user.referred_by: referrer = db.query(User).filter( User.referral_code == user.referred_by ).first() if referrer: create_referral_reward( referrer_id=referrer.id, referred_user_id=user.id, payment_amount=charge.amount_usd or charge.amount, db=db ) # Send notification to user notify_system( db, user.id, "Payment Confirmed", f"Your payment of {charge.amount} {charge.currency or 'USD'} has been confirmed. {charge.cc_amount} CC has been added to your balance.", "/profile" ) else: charge.status = "rejected" charge.verified_at = datetime.utcnow() charge.verified_by = current_user.id charge.admin_note = admin_note # Send notification to user user = db.query(User).filter(User.id == charge.user_id).first() if user: notify_system( db, user.id, "Payment Rejected", f"Your payment request for {charge.amount} {charge.currency or 'USD'} was rejected. Reason: {admin_note or 'No reason provided'}", "/profile" ) db.commit() return { "message": f"Payment {'approved' if approved else 'rejected'}", "charge_id": charge_id, "new_status": charge.status } # ============================================ # Stripe Payment Endpoints # ============================================ class CreateCheckoutRequest(BaseModel): package_id: int @router.get("/stripe-key") def get_stripe_publishable_key(): """Get Stripe publishable key for frontend""" return {"publishable_key": settings.STRIPE_PUBLISHABLE_KEY} @router.get("/packages") def get_cc_packages(db: Session = Depends(get_db)): """Get available CC packages""" # Get system settings for cars_per_cc system_settings = db.query(SystemSettings).first() cars_per_cc = system_settings.cars_per_cc if system_settings and system_settings.cars_per_cc else 3 # First try to get from database packages = db.query(CCPackage).filter(CCPackage.is_active == True).order_by(CCPackage.sort_order).all() # If no packages in DB, initialize with defaults if not packages: for pkg_data in DEFAULT_CC_PACKAGES: pkg = CCPackage(**pkg_data) db.add(pkg) db.commit() packages = db.query(CCPackage).filter(CCPackage.is_active == True).order_by(CCPackage.sort_order).all() return [ { "id": pkg.id, "name": pkg.name, "price_usd": pkg.price_usd, "cc_amount": pkg.cc_amount, "bonus_cc": pkg.bonus_cc, "total_cc": pkg.cc_amount + pkg.bonus_cc, "discount_percent": pkg.discount_percent, "recommendations": (pkg.cc_amount + pkg.bonus_cc) * cars_per_cc, "cars_per_cc": cars_per_cc, # 프론트엔드에서 표시용 } for pkg in packages ] @router.post("/create-checkout-session") def create_checkout_session( request: CreateCheckoutRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Create Stripe checkout session for CC purchase""" if not settings.STRIPE_SECRET_KEY: raise HTTPException(status_code=500, detail="Stripe is not configured") # Get package package = db.query(CCPackage).filter(CCPackage.id == request.package_id).first() if not package: raise HTTPException(status_code=404, detail="Package not found") if not package.is_active: raise HTTPException(status_code=400, detail="This package is no longer available") try: # Create Stripe Checkout Session checkout_session = stripe.checkout.Session.create( payment_method_types=["card"], line_items=[ { "price_data": { "currency": "usd", "unit_amount": package.price_usd * 100, # Stripe uses cents "product_data": { "name": f"AutonetSellCar CC - {package.name}", "description": f"{package.cc_amount + package.bonus_cc} CC ({package.cc_amount} + {package.bonus_cc} bonus)", }, }, "quantity": 1, } ], mode="payment", success_url=f"{settings.STRIPE_SUCCESS_URL}?session_id={{CHECKOUT_SESSION_ID}}", cancel_url=settings.STRIPE_CANCEL_URL, client_reference_id=str(current_user.id), metadata={ "user_id": str(current_user.id), "package_id": str(package.id), "cc_amount": str(package.cc_amount), "bonus_cc": str(package.bonus_cc), }, customer_email=current_user.email, ) # Create pending charge record charge_record = ChargeHistory( user_id=current_user.id, package_id=package.id, amount=package.price_usd, amount_usd=package.price_usd, cc_amount=package.cc_amount, bonus_cc=package.bonus_cc, currency="USD", payment_method="stripe", stripe_session_id=checkout_session.id, status="pending" ) db.add(charge_record) db.commit() return { "checkout_url": checkout_session.url, "session_id": checkout_session.id } except stripe.error.StripeError as e: logger.error(f"Stripe error: {e}") raise HTTPException(status_code=400, detail=str(e)) @router.post("/webhook") async def stripe_webhook( request: Request, db: Session = Depends(get_db) ): """Handle Stripe webhook events""" payload = await request.body() sig_header = request.headers.get("stripe-signature") if not settings.STRIPE_WEBHOOK_SECRET: logger.warning("Stripe webhook secret not configured") raise HTTPException(status_code=500, detail="Webhook not configured") try: event = stripe.Webhook.construct_event( payload, sig_header, settings.STRIPE_WEBHOOK_SECRET ) except ValueError as e: logger.error(f"Invalid payload: {e}") raise HTTPException(status_code=400, detail="Invalid payload") except stripe.error.SignatureVerificationError as e: logger.error(f"Invalid signature: {e}") raise HTTPException(status_code=400, detail="Invalid signature") # Handle the checkout.session.completed event if event["type"] == "checkout.session.completed": session = event["data"]["object"] # Get charge record by session ID charge = db.query(ChargeHistory).filter( ChargeHistory.stripe_session_id == session["id"] ).first() if charge and charge.status == "pending": # Update charge record charge.status = "completed" charge.stripe_payment_intent_id = session.get("payment_intent") charge.verified_at = datetime.utcnow() # Credit CC to user user = db.query(User).filter(User.id == charge.user_id).first() if user: total_cc = charge.cc_amount + (charge.bonus_cc or 0) user.cc_balance = (user.cc_balance or 0) + total_cc # Trigger referral reward if applicable if user.referred_by: referrer = db.query(User).filter( User.referral_code == user.referred_by ).first() if referrer: create_referral_reward( referrer_id=referrer.id, referred_user_id=user.id, payment_amount=charge.amount_usd or charge.amount, db=db ) # Send notification notify_system( db, user.id, "CC Purchase Successful", f"Your purchase of {total_cc} CC has been completed. Your new balance is {user.cc_balance} CC.", "/cc" ) logger.info(f"CC credited: user={user.id}, amount={total_cc}") db.commit() elif event["type"] == "checkout.session.expired": session = event["data"]["object"] # Update charge record to cancelled charge = db.query(ChargeHistory).filter( ChargeHistory.stripe_session_id == session["id"] ).first() if charge and charge.status == "pending": charge.status = "cancelled" db.commit() return {"status": "success"} @router.get("/checkout-success") def checkout_success( session_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Verify checkout session and return result""" # Find charge record charge = db.query(ChargeHistory).filter( ChargeHistory.stripe_session_id == session_id, ChargeHistory.user_id == current_user.id ).first() if not charge: raise HTTPException(status_code=404, detail="Payment record not found") # If still pending, try to verify with Stripe if charge.status == "pending": try: session = stripe.checkout.Session.retrieve(session_id) if session.payment_status == "paid": charge.status = "completed" charge.stripe_payment_intent_id = session.payment_intent charge.verified_at = datetime.utcnow() # Credit CC total_cc = charge.cc_amount + (charge.bonus_cc or 0) current_user.cc_balance = (current_user.cc_balance or 0) + total_cc db.commit() except stripe.error.StripeError as e: logger.error(f"Error verifying session: {e}") return { "status": charge.status, "cc_amount": charge.cc_amount, "bonus_cc": charge.bonus_cc or 0, "total_cc": charge.cc_amount + (charge.bonus_cc or 0), "cc_balance": current_user.cc_balance or 0 } # Manual CC charge request (for Russian users via Mongolian partner) class ManualChargeRequest(BaseModel): package_id: int payment_note: Optional[str] = None # e.g., "Paid via Mongolian partner bank" @router.post("/manual-request") def create_manual_charge_request( request: ManualChargeRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Create manual CC charge request (for Russian users)""" # Get package package = db.query(CCPackage).filter(CCPackage.id == request.package_id).first() if not package: raise HTTPException(status_code=404, detail="Package not found") # Create pending charge record charge_record = ChargeHistory( user_id=current_user.id, package_id=package.id, amount=package.price_usd, amount_usd=package.price_usd, cc_amount=package.cc_amount, bonus_cc=package.bonus_cc, currency="USD", payment_method="manual", admin_note=request.payment_note, status="pending" ) db.add(charge_record) db.commit() db.refresh(charge_record) # Notify admins admins = db.query(User).filter(User.is_admin == True).all() for admin in admins: notify_system( db, admin.id, "New Manual CC Request", f"User {current_user.email} requested {package.cc_amount} CC (${package.price_usd}). Payment method: manual.", "/admin/cc" ) return { "message": "Manual charge request created. An admin will verify your payment.", "charge_id": charge_record.id, "package": { "name": package.name, "price_usd": package.price_usd, "cc_amount": package.cc_amount + package.bonus_cc }, "status": "pending" }