versione 1.0

This commit is contained in:
2026-05-23 22:50:13 +02:00
parent f8e6daa084
commit 57367b75c8
48 changed files with 1073 additions and 433 deletions

View File

@@ -3,3 +3,11 @@ APP_ENV=development
APP_HOST=127.0.0.1
APP_PORT=8000
DATABASE_URL=sqlite:///C:/devel/clinica_veterinaria_formiginese/data/clinica.db
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_USE_TLS=true
BOOKING_EMAIL_FROM=
BOOKING_EMAIL_TO=
BOOKING_EMAIL_SUBJECT_PREFIX=[Clinica Veterinaria Formiginese]

View 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.",
)

View File

@@ -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()

View File

@@ -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)

View 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

View 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)

View File

@@ -4,3 +4,4 @@ sqlalchemy
pydantic
python-dotenv
aiosqlite
pydantic[email]