This commit is contained in:
Iliyan Angelov
2025-11-17 23:50:14 +02:00
parent 0c59fe1173
commit a1bd576540
43 changed files with 2598 additions and 359 deletions

View File

@@ -0,0 +1,419 @@
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:
"""Generate unique service booking number"""
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)
):
"""Create a new service booking"""
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")
# Validate services and calculate total
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")
# Check if service exists and is active
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
})
# Verify calculated total matches provided total (with small tolerance for floating point)
if abs(calculated_total - total_amount) > 0.01:
raise HTTPException(
status_code=400,
detail=f"Total amount mismatch. Calculated: {calculated_total}, Provided: {total_amount}"
)
# Generate booking number
booking_number = generate_service_booking_number()
# Create service booking
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() # Flush to get the ID
# Create service booking items
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)
# Load relationships
service_booking = db.query(ServiceBooking).options(
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
).filter(ServiceBooking.id == service_booking.id).first()
# Format response
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)
):
"""Get all service bookings for current user"""
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)
):
"""Get service booking by ID"""
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")
# Check access
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)
):
"""Create Stripe payment intent for service booking"""
try:
# Check if Stripe is configured
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")
# Verify service booking exists and user has access
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")
# Verify amount matches booking total
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}"
)
# Create payment intent
intent = StripeService.create_payment_intent(
amount=amount,
currency=currency,
description=f"Service Booking #{booking.booking_number}",
db=db
)
# Get publishable key
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)
):
"""Confirm Stripe payment for service booking"""
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")
# Verify service booking exists and user has access
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")
# Retrieve and verify payment intent
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'"
)
# Verify amount matches
amount_paid = intent_data["amount"] / 100 # Convert from cents
if abs(float(booking.total_amount) - amount_paid) > 0.01:
raise HTTPException(
status_code=400,
detail="Payment amount does not match booking total"
)
# Create payment record
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)
# Update booking status
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))