versione 1.0
This commit is contained in:
27
backend/app/api/booking.py
Normal file
27
backend/app/api/booking.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from app.schemas.booking import BookingRequestCreate, BookingRequestResponse
|
||||
from app.services.mailer import MailConfigurationError, send_booking_request_email
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["booking"])
|
||||
|
||||
|
||||
@router.post("/booking-request", response_model=BookingRequestResponse)
|
||||
def create_booking_request(payload: BookingRequestCreate) -> BookingRequestResponse:
|
||||
try:
|
||||
send_booking_request_email(payload)
|
||||
except MailConfigurationError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Il servizio email non è ancora configurato.",
|
||||
) from exc
|
||||
except Exception as exc: # pragma: no cover - mail delivery path
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Si è verificato un problema durante l'invio della richiesta.",
|
||||
) from exc
|
||||
|
||||
return BookingRequestResponse(
|
||||
message="La richiesta è stata inviata correttamente e sarà presa in carico al più presto dal team.",
|
||||
)
|
||||
@@ -6,9 +6,11 @@ from dotenv import load_dotenv
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
PROJECT_ROOT = BASE_DIR.parent
|
||||
ENV_FILE = PROJECT_ROOT / ".env"
|
||||
ENV_FILE = BASE_DIR / ".env"
|
||||
ROOT_ENV_FILE = PROJECT_ROOT / ".env"
|
||||
|
||||
load_dotenv(ENV_FILE)
|
||||
load_dotenv(ROOT_ENV_FILE)
|
||||
load_dotenv(ENV_FILE, override=True)
|
||||
|
||||
|
||||
class Settings:
|
||||
@@ -19,6 +21,17 @@ class Settings:
|
||||
self.app_port = int(os.getenv("APP_PORT", "8000"))
|
||||
default_sqlite_path = (PROJECT_ROOT / "data" / "clinica.db").resolve()
|
||||
self.database_url = os.getenv("DATABASE_URL", f"sqlite:///{default_sqlite_path.as_posix()}")
|
||||
self.smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com")
|
||||
self.smtp_port = int(os.getenv("SMTP_PORT", "587"))
|
||||
self.smtp_username = os.getenv("SMTP_USERNAME", "")
|
||||
self.smtp_password = os.getenv("SMTP_PASSWORD", "")
|
||||
self.smtp_use_tls = os.getenv("SMTP_USE_TLS", "true").lower() in {"1", "true", "yes", "on"}
|
||||
self.booking_email_from = os.getenv("BOOKING_EMAIL_FROM", self.smtp_username)
|
||||
self.booking_email_to = os.getenv("BOOKING_EMAIL_TO", "")
|
||||
self.booking_email_subject_prefix = os.getenv(
|
||||
"BOOKING_EMAIL_SUBJECT_PREFIX",
|
||||
"[Clinica Veterinaria Formiginese]",
|
||||
)
|
||||
|
||||
@property
|
||||
def sqlite_file_path(self) -> str | None:
|
||||
@@ -27,5 +40,18 @@ class Settings:
|
||||
return self.database_url.removeprefix(sqlite_prefix)
|
||||
return None
|
||||
|
||||
@property
|
||||
def booking_mail_enabled(self) -> bool:
|
||||
return all(
|
||||
[
|
||||
self.smtp_host,
|
||||
self.smtp_port,
|
||||
self.smtp_username,
|
||||
self.smtp_password,
|
||||
self.booking_email_from,
|
||||
self.booking_email_to,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.api.booking import router as booking_router
|
||||
from app.api.health import router as health_router
|
||||
from app.config import settings
|
||||
from app.db.init_db import init_db
|
||||
@@ -9,3 +10,4 @@ init_db()
|
||||
|
||||
app = FastAPI(title=settings.app_name)
|
||||
app.include_router(health_router)
|
||||
app.include_router(booking_router)
|
||||
|
||||
17
backend/app/schemas/booking.py
Normal file
17
backend/app/schemas/booking.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class BookingRequestCreate(BaseModel):
|
||||
name: str = Field(min_length=2, max_length=120)
|
||||
phone: str = Field(min_length=5, max_length=40)
|
||||
pet_name: str = Field(default="", max_length=120)
|
||||
pet_type: str = Field(default="cane", max_length=40)
|
||||
service: str = Field(min_length=2, max_length=120)
|
||||
date: str = Field(min_length=8, max_length=20)
|
||||
time: str = Field(default="", max_length=20)
|
||||
notes: str = Field(default="", max_length=2000)
|
||||
|
||||
|
||||
class BookingRequestResponse(BaseModel):
|
||||
success: bool = True
|
||||
message: str
|
||||
78
backend/app/services/mailer.py
Normal file
78
backend/app/services/mailer.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from email.message import EmailMessage
|
||||
import smtplib
|
||||
|
||||
from app.config import settings
|
||||
from app.schemas.booking import BookingRequestCreate
|
||||
|
||||
|
||||
class MailConfigurationError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _build_booking_subject(payload: BookingRequestCreate) -> str:
|
||||
return f"{settings.booking_email_subject_prefix} Nuova richiesta visita - {payload.name}"
|
||||
|
||||
|
||||
def _build_booking_html(payload: BookingRequestCreate) -> str:
|
||||
submitted_at = datetime.now().strftime("%d/%m/%Y %H:%M")
|
||||
return f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.6;">
|
||||
<h2 style="color: #1B4F72;">Nuova richiesta di prenotazione visita</h2>
|
||||
<p>È stata inviata una nuova richiesta di prenotazione non vincolante dal sito web.</p>
|
||||
<table cellpadding="8" cellspacing="0" border="0" style="border-collapse: collapse;">
|
||||
<tr><td><strong>Nome e cognome</strong></td><td>{payload.name}</td></tr>
|
||||
<tr><td><strong>Telefono</strong></td><td>{payload.phone}</td></tr>
|
||||
<tr><td><strong>Nome animale</strong></td><td>{payload.pet_name or "-"}</td></tr>
|
||||
<tr><td><strong>Tipo animale</strong></td><td>{payload.pet_type}</td></tr>
|
||||
<tr><td><strong>Tipo di visita</strong></td><td>{payload.service}</td></tr>
|
||||
<tr><td><strong>Data preferita</strong></td><td>{payload.date}</td></tr>
|
||||
<tr><td><strong>Orario preferito</strong></td><td>{payload.time or "Qualsiasi orario"}</td></tr>
|
||||
<tr><td><strong>Note</strong></td><td>{payload.notes or "-"}</td></tr>
|
||||
<tr><td><strong>Inviata il</strong></td><td>{submitted_at}</td></tr>
|
||||
</table>
|
||||
<p style="margin-top: 20px;">
|
||||
Questa richiesta <strong>non costituisce conferma automatica dell'appuntamento</strong> e richiede
|
||||
presa in carico da parte dello staff.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def _build_booking_text(payload: BookingRequestCreate) -> str:
|
||||
submitted_at = datetime.now().strftime("%d/%m/%Y %H:%M")
|
||||
return (
|
||||
"Nuova richiesta di prenotazione visita\n\n"
|
||||
f"Nome e cognome: {payload.name}\n"
|
||||
f"Telefono: {payload.phone}\n"
|
||||
f"Nome animale: {payload.pet_name or '-'}\n"
|
||||
f"Tipo animale: {payload.pet_type}\n"
|
||||
f"Tipo di visita: {payload.service}\n"
|
||||
f"Data preferita: {payload.date}\n"
|
||||
f"Orario preferito: {payload.time or 'Qualsiasi orario'}\n"
|
||||
f"Note: {payload.notes or '-'}\n"
|
||||
f"Inviata il: {submitted_at}\n\n"
|
||||
"La richiesta non costituisce conferma automatica dell'appuntamento."
|
||||
)
|
||||
|
||||
|
||||
def send_booking_request_email(payload: BookingRequestCreate) -> None:
|
||||
if not settings.booking_mail_enabled:
|
||||
raise MailConfigurationError("Configurazione SMTP incompleta.")
|
||||
|
||||
message = EmailMessage()
|
||||
message["Subject"] = _build_booking_subject(payload)
|
||||
message["From"] = settings.booking_email_from
|
||||
message["To"] = settings.booking_email_to
|
||||
message.set_content(_build_booking_text(payload))
|
||||
message.add_alternative(_build_booking_html(payload), subtype="html")
|
||||
|
||||
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=30) as server:
|
||||
if settings.smtp_use_tls:
|
||||
server.starttls()
|
||||
server.login(settings.smtp_username, settings.smtp_password)
|
||||
server.send_message(message)
|
||||
Reference in New Issue
Block a user