Files
booking-service/app/routers/public.py

114 lines
4.2 KiB
Python

"""Endpoint pubblici — no auth. Usati dal widget di prenotazione."""
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload
from app.database import get_db
from app.models import Service, Booking, ProviderService
from app.schemas import ServiceOut, BookingCreate, BookingOut, TimeSlot
from app.services.availability import get_available_slots
from app.services.notifications import notify_booking_confirmed, notify_operator
router = APIRouter(prefix="/api", tags=["public"])
TZ = ZoneInfo("Europe/Rome")
@router.get("/services", response_model=list[ServiceOut])
def list_services(db: Session = Depends(get_db)):
"""Lista servizi attivi."""
return (
db.query(Service)
.filter(Service.active == True)
.order_by(Service.sort_order, Service.name)
.all()
)
@router.get("/services/{service_id}/slots")
def get_slots(service_id: int, date: date, db: Session = Depends(get_db)):
"""Slot disponibili per un servizio in una data."""
service = db.query(Service).filter(Service.id == service_id).first()
if not service:
raise HTTPException(404, "Servizio non trovato")
slots = get_available_slots(db, service_id, date, service.duration_min)
return {"service": service.name, "date": str(date), "duration_min": service.duration_min, "slots": slots}
@router.post("/bookings", response_model=BookingOut)
async def create_booking(data: BookingCreate, db: Session = Depends(get_db)):
"""Crea una prenotazione. No login richiesto."""
# Verifica servizio
service = db.query(Service).filter(Service.id == data.service_id).first()
if not service:
raise HTTPException(404, "Servizio non trovato")
# Verifica che lo slot sia ancora libero
slots = get_available_slots(db, data.service_id, data.start_at.date(), service.duration_min)
slot_time = data.start_at.astimezone(TZ).strftime("%H:%M")
available = [s for s in slots if s.start == slot_time and s.provider_id == data.provider_id]
if not available:
raise HTTPException(409, "Lo slot non è più disponibile")
# Crea prenotazione
end_at = data.start_at + timedelta(minutes=service.duration_min)
booking = Booking(
service_id=data.service_id,
provider_id=data.provider_id,
customer_name=data.customer_name,
customer_phone=data.customer_phone,
customer_email=data.customer_email,
start_at=data.start_at,
end_at=end_at,
notes=data.notes,
)
db.add(booking)
db.commit()
db.refresh(booking)
# Carica relazioni per la risposta
booking = db.query(Booking).options(
joinedload(Booking.service), joinedload(Booking.provider)
).filter(Booking.id == booking.id).first()
# Notifiche async
await notify_booking_confirmed(booking, service.name, booking.provider.name)
if booking.provider.email:
await notify_operator(booking, service.name, booking.provider.email)
return booking
@router.get("/bookings/my")
def my_bookings(phone: str, db: Session = Depends(get_db)):
"""Le prenotazioni di un numero di telefono (per check 'ho già prenotato?')."""
clean = phone.replace("+", "").replace(" ", "").replace("-", "")
bookings = (
db.query(Booking)
.options(joinedload(Booking.service), joinedload(Booking.provider))
.filter(
Booking.customer_phone.contains(clean),
Booking.status == "confirmed",
Booking.start_at >= datetime.now(TZ),
)
.order_by(Booking.start_at)
.all()
)
return bookings
@router.delete("/bookings/{booking_id}")
def cancel_booking(booking_id: int, phone: str, db: Session = Depends(get_db)):
"""Cancella una prenotazione (verifica phone per sicurezza)."""
booking = db.query(Booking).filter(Booking.id == booking_id).first()
if not booking:
raise HTTPException(404, "Prenotazione non trovata")
clean = phone.replace("+", "").replace(" ", "").replace("-", "")
if clean not in booking.customer_phone:
raise HTTPException(403, "Numero non corrispondente")
booking.status = "cancelled"
db.commit()
return {"ok": True, "message": "Prenotazione cancellata"}