v1.4.0 — 16 servizi reali Ianni, sala unica, ricevuta PDF
- Migration: services.price_cents, services.availability_text
- Migration: bookings.receipt_number (trigger annuale IANNI-YYYY-NNNN) + receipt_token
- Constraint EXCLUDE bookings_no_overlap (sala unica, status confirmed/completed)
- availability.py: calcolo slot globale (non più per-provider)
- 16 servizi reali da Servizi.xls inseriti, 9 demo archiviati con FK preservata
- provider_services: 3 profili orari (lun-sab 9-19, lun-mar 9-13, lun-gio 9-13)
- Endpoint GET /api/receipts/{token} → PDF WeasyPrint
- Template HTML ricevuta con palette Ianni
- Dockerfile: deps sistema weasyprint (pango/cairo/fonts)
- requirements: +weasyprint>=63.0
- Frontend index.html: prezzo + fascia oraria nelle card servizio, link Scarica ricevuta nella conferma
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
"""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")
|
||||
@@ -62,6 +66,7 @@ async def create_booking(data: BookingCreate, db: Session = Depends(get_db)):
|
||||
end_at=end_at,
|
||||
notes=data.notes,
|
||||
)
|
||||
booking.receipt_token = secrets.token_urlsafe(24)
|
||||
db.add(booking)
|
||||
db.commit()
|
||||
db.refresh(booking)
|
||||
@@ -111,3 +116,30 @@ def cancel_booking(booking_id: int, phone: str, db: Session = Depends(get_db)):
|
||||
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}"'},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user