"""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"}