408 lines
14 KiB
Python
408 lines
14 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session, joinedload
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
import random
|
|
|
|
from ..config.database import get_db
|
|
from ..middleware.auth import get_current_user
|
|
from ..models.user import User
|
|
from ..models.service import Service
|
|
from ..models.service_booking import (
|
|
ServiceBooking,
|
|
ServiceBookingItem,
|
|
ServicePayment,
|
|
ServiceBookingStatus,
|
|
ServicePaymentStatus,
|
|
ServicePaymentMethod
|
|
)
|
|
from ..services.stripe_service import StripeService, get_stripe_secret_key, get_stripe_publishable_key
|
|
from ..config.settings import settings
|
|
|
|
router = APIRouter(prefix="/service-bookings", tags=["service-bookings"])
|
|
|
|
def generate_service_booking_number() -> str:
|
|
prefix = "SB"
|
|
timestamp = datetime.utcnow().strftime("%Y%m%d")
|
|
random_suffix = random.randint(1000, 9999)
|
|
return f"{prefix}{timestamp}{random_suffix}"
|
|
|
|
@router.post("/")
|
|
async def create_service_booking(
|
|
booking_data: dict,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
try:
|
|
services = booking_data.get("services", [])
|
|
total_amount = float(booking_data.get("total_amount", 0))
|
|
notes = booking_data.get("notes")
|
|
|
|
if not services or len(services) == 0:
|
|
raise HTTPException(status_code=400, detail="At least one service is required")
|
|
|
|
if total_amount <= 0:
|
|
raise HTTPException(status_code=400, detail="Total amount must be greater than 0")
|
|
|
|
|
|
calculated_total = 0
|
|
service_items_data = []
|
|
|
|
for service_item in services:
|
|
service_id = service_item.get("service_id")
|
|
quantity = service_item.get("quantity", 1)
|
|
|
|
if not service_id:
|
|
raise HTTPException(status_code=400, detail="Service ID is required for each item")
|
|
|
|
|
|
service = db.query(Service).filter(Service.id == service_id).first()
|
|
if not service:
|
|
raise HTTPException(status_code=404, detail=f"Service with ID {service_id} not found")
|
|
|
|
if not service.is_active:
|
|
raise HTTPException(status_code=400, detail=f"Service {service.name} is not active")
|
|
|
|
unit_price = float(service.price)
|
|
item_total = unit_price * quantity
|
|
calculated_total += item_total
|
|
|
|
service_items_data.append({
|
|
"service": service,
|
|
"quantity": quantity,
|
|
"unit_price": unit_price,
|
|
"total_price": item_total
|
|
})
|
|
|
|
|
|
if abs(calculated_total - total_amount) > 0.01:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Total amount mismatch. Calculated: {calculated_total}, Provided: {total_amount}"
|
|
)
|
|
|
|
|
|
booking_number = generate_service_booking_number()
|
|
|
|
|
|
service_booking = ServiceBooking(
|
|
booking_number=booking_number,
|
|
user_id=current_user.id,
|
|
total_amount=total_amount,
|
|
status=ServiceBookingStatus.pending,
|
|
notes=notes
|
|
)
|
|
|
|
db.add(service_booking)
|
|
db.flush()
|
|
|
|
|
|
for item_data in service_items_data:
|
|
booking_item = ServiceBookingItem(
|
|
service_booking_id=service_booking.id,
|
|
service_id=item_data["service"].id,
|
|
quantity=item_data["quantity"],
|
|
unit_price=item_data["unit_price"],
|
|
total_price=item_data["total_price"]
|
|
)
|
|
db.add(booking_item)
|
|
|
|
db.commit()
|
|
db.refresh(service_booking)
|
|
|
|
|
|
service_booking = db.query(ServiceBooking).options(
|
|
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
|
).filter(ServiceBooking.id == service_booking.id).first()
|
|
|
|
|
|
booking_dict = {
|
|
"id": service_booking.id,
|
|
"booking_number": service_booking.booking_number,
|
|
"user_id": service_booking.user_id,
|
|
"total_amount": float(service_booking.total_amount),
|
|
"status": service_booking.status.value,
|
|
"notes": service_booking.notes,
|
|
"created_at": service_booking.created_at.isoformat() if service_booking.created_at else None,
|
|
"service_items": [
|
|
{
|
|
"id": item.id,
|
|
"service_id": item.service_id,
|
|
"quantity": item.quantity,
|
|
"unit_price": float(item.unit_price),
|
|
"total_price": float(item.total_price),
|
|
"service": {
|
|
"id": item.service.id,
|
|
"name": item.service.name,
|
|
"description": item.service.description,
|
|
"price": float(item.service.price),
|
|
}
|
|
}
|
|
for item in service_booking.service_items
|
|
]
|
|
}
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": "Service booking created successfully",
|
|
"data": {"service_booking": booking_dict}
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.get("/me")
|
|
async def get_my_service_bookings(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
try:
|
|
bookings = db.query(ServiceBooking).options(
|
|
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
|
).filter(ServiceBooking.user_id == current_user.id).order_by(ServiceBooking.created_at.desc()).all()
|
|
|
|
result = []
|
|
for booking in bookings:
|
|
booking_dict = {
|
|
"id": booking.id,
|
|
"booking_number": booking.booking_number,
|
|
"total_amount": float(booking.total_amount),
|
|
"status": booking.status.value,
|
|
"notes": booking.notes,
|
|
"created_at": booking.created_at.isoformat() if booking.created_at else None,
|
|
"service_items": [
|
|
{
|
|
"id": item.id,
|
|
"service_id": item.service_id,
|
|
"quantity": item.quantity,
|
|
"unit_price": float(item.unit_price),
|
|
"total_price": float(item.total_price),
|
|
"service": {
|
|
"id": item.service.id,
|
|
"name": item.service.name,
|
|
"description": item.service.description,
|
|
"price": float(item.service.price),
|
|
}
|
|
}
|
|
for item in booking.service_items
|
|
]
|
|
}
|
|
result.append(booking_dict)
|
|
|
|
return {
|
|
"status": "success",
|
|
"data": {"service_bookings": result}
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.get("/{id}")
|
|
async def get_service_booking_by_id(
|
|
id: int,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
try:
|
|
booking = db.query(ServiceBooking).options(
|
|
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
|
).filter(ServiceBooking.id == id).first()
|
|
|
|
if not booking:
|
|
raise HTTPException(status_code=404, detail="Service booking not found")
|
|
|
|
|
|
if booking.user_id != current_user.id and current_user.role_id != 1:
|
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
|
|
booking_dict = {
|
|
"id": booking.id,
|
|
"booking_number": booking.booking_number,
|
|
"user_id": booking.user_id,
|
|
"total_amount": float(booking.total_amount),
|
|
"status": booking.status.value,
|
|
"notes": booking.notes,
|
|
"created_at": booking.created_at.isoformat() if booking.created_at else None,
|
|
"service_items": [
|
|
{
|
|
"id": item.id,
|
|
"service_id": item.service_id,
|
|
"quantity": item.quantity,
|
|
"unit_price": float(item.unit_price),
|
|
"total_price": float(item.total_price),
|
|
"service": {
|
|
"id": item.service.id,
|
|
"name": item.service.name,
|
|
"description": item.service.description,
|
|
"price": float(item.service.price),
|
|
}
|
|
}
|
|
for item in booking.service_items
|
|
]
|
|
}
|
|
|
|
return {
|
|
"status": "success",
|
|
"data": {"service_booking": booking_dict}
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post("/{id}/payment/stripe/create-intent")
|
|
async def create_service_stripe_payment_intent(
|
|
id: int,
|
|
intent_data: dict,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
try:
|
|
|
|
secret_key = get_stripe_secret_key(db)
|
|
if not secret_key:
|
|
secret_key = settings.STRIPE_SECRET_KEY
|
|
|
|
if not secret_key:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Stripe is not configured. Please configure Stripe settings in Admin Panel."
|
|
)
|
|
|
|
amount = float(intent_data.get("amount", 0))
|
|
currency = intent_data.get("currency", "usd")
|
|
|
|
if amount <= 0:
|
|
raise HTTPException(status_code=400, detail="Amount must be greater than 0")
|
|
|
|
|
|
booking = db.query(ServiceBooking).filter(ServiceBooking.id == id).first()
|
|
if not booking:
|
|
raise HTTPException(status_code=404, detail="Service booking not found")
|
|
|
|
if booking.user_id != current_user.id and current_user.role_id != 1:
|
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
|
|
|
|
if abs(float(booking.total_amount) - amount) > 0.01:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Amount mismatch. Booking total: {booking.total_amount}, Provided: {amount}"
|
|
)
|
|
|
|
|
|
intent = StripeService.create_payment_intent(
|
|
amount=amount,
|
|
currency=currency,
|
|
description=f"Service Booking #{booking.id}",
|
|
db=db
|
|
)
|
|
|
|
|
|
publishable_key = get_stripe_publishable_key(db)
|
|
if not publishable_key:
|
|
publishable_key = settings.STRIPE_PUBLISHABLE_KEY
|
|
|
|
if not publishable_key:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Stripe publishable key is not configured."
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"data": {
|
|
"client_secret": intent["client_secret"],
|
|
"payment_intent_id": intent["id"],
|
|
"publishable_key": publishable_key
|
|
}
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
@router.post("/{id}/payment/stripe/confirm")
|
|
async def confirm_service_stripe_payment(
|
|
id: int,
|
|
payment_data: dict,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
try:
|
|
payment_intent_id = payment_data.get("payment_intent_id")
|
|
|
|
if not payment_intent_id:
|
|
raise HTTPException(status_code=400, detail="payment_intent_id is required")
|
|
|
|
|
|
booking = db.query(ServiceBooking).filter(ServiceBooking.id == id).first()
|
|
if not booking:
|
|
raise HTTPException(status_code=404, detail="Service booking not found")
|
|
|
|
if booking.user_id != current_user.id and current_user.role_id != 1:
|
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
|
|
|
|
intent_data = StripeService.retrieve_payment_intent(payment_intent_id, db)
|
|
|
|
if intent_data["status"] != "succeeded":
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Payment intent status is {intent_data['status']}, expected 'succeeded'"
|
|
)
|
|
|
|
|
|
amount_paid = intent_data["amount"] / 100
|
|
if abs(float(booking.total_amount) - amount_paid) > 0.01:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Payment amount does not match booking total"
|
|
)
|
|
|
|
|
|
payment = ServicePayment(
|
|
service_booking_id=booking.id,
|
|
amount=booking.total_amount,
|
|
payment_method=ServicePaymentMethod.stripe,
|
|
payment_status=ServicePaymentStatus.completed,
|
|
transaction_id=payment_intent_id,
|
|
payment_date=datetime.utcnow(),
|
|
notes=f"Stripe payment - Intent: {payment_intent_id}"
|
|
)
|
|
|
|
db.add(payment)
|
|
|
|
|
|
booking.status = ServiceBookingStatus.confirmed
|
|
|
|
db.commit()
|
|
db.refresh(payment)
|
|
db.refresh(booking)
|
|
|
|
return {
|
|
"status": "success",
|
|
"message": "Payment confirmed successfully",
|
|
"data": {
|
|
"payment": {
|
|
"id": payment.id,
|
|
"amount": float(payment.amount),
|
|
"payment_method": payment.payment_method.value,
|
|
"payment_status": payment.payment_status.value,
|
|
"transaction_id": payment.transaction_id,
|
|
},
|
|
"service_booking": {
|
|
"id": booking.id,
|
|
"booking_number": booking.booking_number,
|
|
"status": booking.status.value,
|
|
}
|
|
}
|
|
}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|