"""Endpoint pubblici — no auth. Usati dal widget di prenotazione.""" import secrets from datetime import date, datetime, timedelta from zoneinfo import ZoneInfo from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import text 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 from fastapi.responses import Response from app.services.receipt import render_receipt_pdf 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, ) booking.receipt_token = secrets.token_urlsafe(24) 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"} @router.get("/receipts/{token}") def get_receipt(token: str, db: Session = Depends(get_db)): """Restituisce il PDF della ricevuta. Token random anti-enumeration.""" booking = ( db.query(Booking) .options(joinedload(Booking.service), joinedload(Booking.provider)) .filter(Booking.receipt_token == token) .first() ) if not booking: raise HTTPException(404, "Ricevuta non trovata") if booking.status not in ("confirmed", "completed"): raise HTTPException(410, "Ricevuta non disponibile per questa prenotazione") # Settings → dict settings_rows = db.execute(text("SELECT key, value FROM settings")).fetchall() settings_map = {k: v for k, v in settings_rows} pdf_bytes = render_receipt_pdf(booking, booking.service, booking.provider, settings_map) fname = f"ricevuta-{booking.receipt_number or booking.id}.pdf" return Response( content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": f'inline; filename="{fname}"'}, )