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:
886
backend/app/api/cc.py
Normal file
886
backend/app/api/cc.py
Normal file
@@ -0,0 +1,886 @@
|
||||
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
|
||||
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"""
|
||||
existing_view = db.query(CarView).filter(
|
||||
CarView.user_id == current_user.id,
|
||||
CarView.car_id == car_id
|
||||
).first()
|
||||
|
||||
return {
|
||||
"has_access": existing_view is not None,
|
||||
"cc_balance": current_user.cc_balance or 0
|
||||
}
|
||||
|
||||
|
||||
PERFORMANCE_CHECK_COST = 0.1 # 0.1 CC for performance check view
|
||||
|
||||
|
||||
@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 0.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 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"
|
||||
}
|
||||
Reference in New Issue
Block a user