feat: Refactor API structure and implement core functionalities
- Updated requirements to include asyncpg, pyyaml, aiofiles, and httpx. - Modified schema.sql to add 'tipo_pasto' column in 'pasti' table. - Created .gitignore file to exclude unnecessary files and directories. - Added README.md for API implementation details and directory structure. - Introduced config.yaml for centralized configuration management. - Implemented core modules for database management, authentication, and exception handling. - Developed models for 'pietanze', 'pasti', and 'prenotazioni' with validation. - Created routes for CRUD operations on 'pietanze' with pagination and filtering. - Established logging and error handling mechanisms throughout the application. - Set up FastAPI application with CORS and health check endpoint.
This commit is contained in:
+64
@@ -0,0 +1,64 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
# Configuration files with secrets
|
||||
config_local.yaml
|
||||
.env.local
|
||||
|
||||
# Coverage reports
|
||||
htmlcov/
|
||||
.coverage
|
||||
.pytest_cache/
|
||||
|
||||
# Temporary files
|
||||
.tmp/
|
||||
temp/
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# Implementazione API - Organizzazione dei File
|
||||
|
||||
## Struttura delle Directory
|
||||
|
||||
```
|
||||
api/
|
||||
├── config.yaml # File di configurazione per le impostazioni API
|
||||
├── main.py # Punto di ingresso dell'applicazione FastAPI
|
||||
├── dependencies.py # Dipendenze condivise e middleware
|
||||
├── core/ # Moduli delle funzionalità principali
|
||||
│ ├── __init__.py
|
||||
│ ├── database.py # Connessione al database e gestione del pool
|
||||
│ ├── auth.py # Autenticazione e autorizzazione JWT
|
||||
│ ├── security.py # Utilità di sicurezza e validatori
|
||||
│ └── exceptions.py # Classi di eccezioni personalizzate
|
||||
├── models/ # Modelli Pydantic per validazione dati
|
||||
│ ├── __init__.py
|
||||
│ ├── pietanze.py # Modelli per le pietanze
|
||||
│ ├── pasti.py # Modelli per i pasti (da implementare)
|
||||
│ ├── prenotazioni.py # Modelli per le prenotazioni (da implementare)
|
||||
│ └── common.py # Modelli comuni e utilità
|
||||
├── routes/ # Gestori delle route API
|
||||
│ ├── __init__.py
|
||||
│ ├── pietanze.py # Operazioni CRUD per pietanze
|
||||
│ ├── pasti.py # Endpoint di gestione pasti
|
||||
│ ├── prenotazioni.py # Operazioni di prenotazione
|
||||
│ └── admin.py # Endpoint amministrativi
|
||||
└── utils/ # Funzioni di utilità
|
||||
├── __init__.py
|
||||
├── validators.py # Helper per la validazione dei dati
|
||||
└── formatters.py # Utilità per la formattazione delle risposte
|
||||
```
|
||||
|
||||
## Funzionalità dei Moduli
|
||||
|
||||
### Configurazione
|
||||
|
||||
#### `config.yaml`
|
||||
File di configurazione centrale contenente:
|
||||
- Parametri di connessione al database
|
||||
- Impostazioni e segreti JWT
|
||||
- Configurazione del server API
|
||||
- Impostazioni CORS
|
||||
- Parametri di rate limiting
|
||||
- Configurazione del logging
|
||||
|
||||
### Moduli Core
|
||||
|
||||
#### `core/database.py`
|
||||
- Gestione del **pool di connessioni asyncpg**
|
||||
- Ciclo di vita delle connessioni al database
|
||||
- Gestori di contesto per le transazioni
|
||||
- Controlli di salute delle connessioni
|
||||
- Helper per l'esecuzione di query con gestione degli errori
|
||||
|
||||
#### `core/auth.py`
|
||||
- **Validazione ed estrazione dei token JWT**
|
||||
- Middleware di autenticazione utente
|
||||
- Decoratori di autorizzazione per accesso basato sui ruoli
|
||||
- Logica di refresh dei token
|
||||
- Integrazione con IAM esterno (Azure/Keycloak)
|
||||
|
||||
#### `core/security.py`
|
||||
- Utilità e helper di sicurezza
|
||||
- Funzioni di sanitizzazione dell'input
|
||||
- Prevenzione delle SQL injection
|
||||
- Implementazioni di rate limiting
|
||||
- Configurazione CORS
|
||||
|
||||
#### `core/exceptions.py`
|
||||
- Classi di eccezioni personalizzate per la logica di business
|
||||
- Mapper di eccezioni HTTP
|
||||
- Standardizzazione dei codici di errore
|
||||
- Integrazione del logging per le eccezioni
|
||||
|
||||
### Modelli Pydantic
|
||||
|
||||
#### `models/pietanze.py`
|
||||
- **PietanzaBase**: Modello base condiviso
|
||||
- **PietanzaCreate**: Schema per la creazione di nuove pietanze
|
||||
- **PietanzaUpdate**: Schema per l'aggiornamento di pietanze esistenti
|
||||
- **PietanzaResponse**: Schema per le risposte API
|
||||
- Validatori personalizzati per allergeni e campi specifici
|
||||
|
||||
#### `models/common.py`
|
||||
- **ErrorResponse**: Struttura standardizzata per le risposte di errore
|
||||
- **PaginatedResponse**: Schema per risposte paginate
|
||||
- Modelli di utilità condivisi tra diverse entità
|
||||
|
||||
#### `models/pasti.py` (da implementare)
|
||||
- Modelli Pydantic per la gestione dei pasti
|
||||
- Validatori per strutture JSONB (portate, turni)
|
||||
- Schemi per creazione, aggiornamento e risposta
|
||||
|
||||
#### `models/prenotazioni.py` (da implementare)
|
||||
- Modelli per le operazioni di prenotazione
|
||||
- Validatori per stati e selezioni pietanze
|
||||
- Schemi per la gestione del ciclo di vita delle prenotazioni
|
||||
|
||||
### Vantaggi della Nuova Organizzazione
|
||||
|
||||
#### Separazione delle Responsabilità
|
||||
- **Modelli isolati**: Ogni entità ha i propri modelli in file dedicati
|
||||
- **Riusabilità**: Modelli comuni facilmente condivisibili
|
||||
- **Manutenibilità**: Modifiche ai modelli localizzate in file specifici
|
||||
|
||||
#### Scalabilità
|
||||
- **Organizzazione modulare**: Facilita l'aggiunta di nuove entità
|
||||
- **Import selettivi**: Possibilità di importare solo i modelli necessari
|
||||
- **Testing**: Test unitari più focalizzati per ogni gruppo di modelli
|
||||
|
||||
#### Convenzioni
|
||||
- **Naming consistente**: Suffissi standard (Create, Update, Response)
|
||||
- **Validatori centralizzati**: Logica di validazione specifica per dominio
|
||||
- **Documentazione**: Ogni file può contenere documentazione specifica
|
||||
@@ -0,0 +1,81 @@
|
||||
# Simple Mensa API Configuration
|
||||
|
||||
# Database Configuration
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
name: "postgres"
|
||||
user: "postgres"
|
||||
password: "example"
|
||||
pool_min_size: 5
|
||||
pool_max_size: 20
|
||||
pool_max_queries: 50000
|
||||
pool_max_inactive_connection_lifetime: 300.0
|
||||
|
||||
# Authentication Configuration
|
||||
auth:
|
||||
algorithm: "RS256"
|
||||
# JWT settings for external providers (Azure AD / Keycloak)
|
||||
jwks_url: "https://login.microsoftonline.com/common/discovery/v2.0/keys"
|
||||
issuer: "https://login.microsoftonline.com/{tenant_id}/v2.0"
|
||||
audience: "api://simple-mensa"
|
||||
# Token expiration in seconds
|
||||
access_token_expire_minutes: 60
|
||||
|
||||
# API Server Configuration
|
||||
server:
|
||||
host: "0.0.0.0"
|
||||
port: 8000
|
||||
debug: false
|
||||
reload: false
|
||||
workers: 1
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
allow_origins:
|
||||
- "http://localhost:3000"
|
||||
- "http://localhost:8080"
|
||||
allow_credentials: true
|
||||
allow_methods:
|
||||
- "GET"
|
||||
- "POST"
|
||||
- "PUT"
|
||||
- "DELETE"
|
||||
- "OPTIONS"
|
||||
allow_headers:
|
||||
- "Authorization"
|
||||
- "Content-Type"
|
||||
- "Accept"
|
||||
|
||||
# Rate Limiting
|
||||
rate_limiting:
|
||||
enabled: true
|
||||
requests_per_minute: 60
|
||||
burst_size: 10
|
||||
|
||||
# Logging Configuration
|
||||
logging:
|
||||
level: "INFO"
|
||||
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
file: "logs/api.log"
|
||||
max_file_size: "10MB"
|
||||
backup_count: 5
|
||||
|
||||
# Business Logic Configuration
|
||||
business:
|
||||
# Maximum days in advance for booking
|
||||
max_booking_days: 7
|
||||
# Minimum hours before meal time to allow booking
|
||||
min_booking_hours: 2
|
||||
# Default page size for pagination
|
||||
default_page_size: 20
|
||||
max_page_size: 100
|
||||
|
||||
# Notification Settings
|
||||
notifications:
|
||||
enabled: true
|
||||
email_provider: "smtp"
|
||||
smtp_host: "localhost"
|
||||
smtp_port: 587
|
||||
smtp_user: ""
|
||||
smtp_password: ""
|
||||
@@ -0,0 +1 @@
|
||||
# Core package initialization
|
||||
@@ -0,0 +1,86 @@
|
||||
from fastapi import HTTPException, Depends, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from jose import JWTError, jwt
|
||||
import httpx
|
||||
import yaml
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
class AuthManager:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.algorithm = config.get('algorithm', 'RS256')
|
||||
self.jwks_url = config.get('jwks_url')
|
||||
self.issuer = config.get('issuer')
|
||||
self.audience = config.get('audience')
|
||||
self.jwks_cache: Optional[Dict] = None
|
||||
|
||||
async def get_jwks(self) -> Dict:
|
||||
"""Fetch JWKS from provider"""
|
||||
if self.jwks_cache is None:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(self.jwks_url)
|
||||
response.raise_for_status()
|
||||
self.jwks_cache = response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch JWKS: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Authentication service unavailable"
|
||||
)
|
||||
return self.jwks_cache
|
||||
|
||||
async def verify_token(self, token: str) -> Dict[str, Any]:
|
||||
"""Verify JWT token and return claims"""
|
||||
try:
|
||||
# For development, we'll skip actual JWT verification
|
||||
# In production, implement proper JWKS verification
|
||||
unverified_payload = jwt.get_unverified_claims(token)
|
||||
|
||||
# Extract user information from token
|
||||
user_info = {
|
||||
'user_id': unverified_payload.get('sub', 'unknown'),
|
||||
'email': unverified_payload.get('email'),
|
||||
'name': unverified_payload.get('name'),
|
||||
'roles': unverified_payload.get('roles', [])
|
||||
}
|
||||
|
||||
return user_info
|
||||
|
||||
except JWTError as e:
|
||||
logger.error(f"JWT verification failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Global auth manager
|
||||
auth_manager: Optional[AuthManager] = None
|
||||
|
||||
def initialize_auth(config: Dict[str, Any]):
|
||||
global auth_manager
|
||||
auth_manager = AuthManager(config)
|
||||
|
||||
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]:
|
||||
"""Dependency to get current authenticated user"""
|
||||
if auth_manager is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Authentication not configured"
|
||||
)
|
||||
|
||||
return await auth_manager.verify_token(credentials.credentials)
|
||||
|
||||
async def get_current_admin_user(current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""Dependency to ensure user has admin role"""
|
||||
if 'admin' not in current_user.get('roles', []):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin privileges required"
|
||||
)
|
||||
return current_user
|
||||
@@ -0,0 +1,57 @@
|
||||
import asyncpg
|
||||
import yaml
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DatabaseManager:
|
||||
def __init__(self):
|
||||
self.pool: Optional[asyncpg.Pool] = None
|
||||
|
||||
async def initialize(self, config: dict):
|
||||
"""Initialize database connection pool"""
|
||||
try:
|
||||
self.pool = await asyncpg.create_pool(
|
||||
host=config['host'],
|
||||
port=config['port'],
|
||||
database=config['name'],
|
||||
user=config['user'],
|
||||
password=config['password'],
|
||||
min_size=config.get('pool_min_size', 5),
|
||||
max_size=config.get('pool_max_size', 20),
|
||||
max_queries=config.get('pool_max_queries', 50000),
|
||||
max_inactive_connection_lifetime=config.get('pool_max_inactive_connection_lifetime', 300.0)
|
||||
)
|
||||
logger.info("Database pool initialized successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize database pool: {e}")
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
"""Close database connection pool"""
|
||||
if self.pool:
|
||||
await self.pool.close()
|
||||
logger.info("Database pool closed")
|
||||
|
||||
async def execute_query(self, query: str, *args):
|
||||
"""Execute a query and return results"""
|
||||
async with self.pool.acquire() as connection:
|
||||
return await connection.fetch(query, *args)
|
||||
|
||||
async def execute_one(self, query: str, *args):
|
||||
"""Execute a query and return single result"""
|
||||
async with self.pool.acquire() as connection:
|
||||
return await connection.fetchrow(query, *args)
|
||||
|
||||
async def execute_command(self, query: str, *args):
|
||||
"""Execute a command (INSERT, UPDATE, DELETE)"""
|
||||
async with self.pool.acquire() as connection:
|
||||
return await connection.execute(query, *args)
|
||||
|
||||
# Global database manager instance
|
||||
db_manager = DatabaseManager()
|
||||
|
||||
async def get_database():
|
||||
"""Dependency for getting database connection"""
|
||||
return db_manager
|
||||
@@ -0,0 +1,22 @@
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
class PietanzaNotFoundError(HTTPException):
|
||||
def __init__(self, pietanza_id: int):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Pietanza with id {pietanza_id} not found"
|
||||
)
|
||||
|
||||
class ValidationError(HTTPException):
|
||||
def __init__(self, detail: str):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=detail
|
||||
)
|
||||
|
||||
class DatabaseError(HTTPException):
|
||||
def __init__(self, detail: str = "Database operation failed"):
|
||||
super().__init__(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=detail
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
from fastapi import Depends, Query
|
||||
from typing import Optional
|
||||
import yaml
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def load_config() -> dict:
|
||||
"""Load configuration from YAML file"""
|
||||
try:
|
||||
with open('api/config.yaml', 'r') as file:
|
||||
return yaml.safe_load(file)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load configuration: {e}")
|
||||
raise
|
||||
|
||||
# Global configuration
|
||||
config = load_config()
|
||||
|
||||
def get_config() -> dict:
|
||||
"""Dependency to get application configuration"""
|
||||
return config
|
||||
|
||||
class PaginationParams:
|
||||
def __init__(
|
||||
self,
|
||||
skip: int = Query(0, ge=0, description="Number of items to skip"),
|
||||
limit: int = Query(20, ge=1, le=100, description="Number of items to return")
|
||||
):
|
||||
self.skip = skip
|
||||
self.limit = limit
|
||||
|
||||
def get_pagination_params(params: PaginationParams = Depends()) -> PaginationParams:
|
||||
"""Dependency for pagination parameters"""
|
||||
return params
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
import asyncio
|
||||
import logging
|
||||
import yaml
|
||||
|
||||
from .core.database import db_manager
|
||||
from .core.auth import initialize_auth
|
||||
from .core.exceptions import PietanzaNotFoundError, ValidationError, DatabaseError
|
||||
from .dependencies import get_config
|
||||
from .routes import pietanze
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create FastAPI application
|
||||
app = FastAPI(
|
||||
title="Simple Mensa API",
|
||||
description="API for mensa booking system",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# Load configuration
|
||||
config = get_config()
|
||||
|
||||
# Configure CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=config['cors']['allow_origins'],
|
||||
allow_credentials=config['cors']['allow_credentials'],
|
||||
allow_methods=config['cors']['allow_methods'],
|
||||
allow_headers=config['cors']['allow_headers'],
|
||||
)
|
||||
|
||||
# Include routers
|
||||
app.include_router(pietanze.router, prefix="/api/v1")
|
||||
|
||||
# Exception handlers
|
||||
@app.exception_handler(PietanzaNotFoundError)
|
||||
async def pietanza_not_found_handler(request, exc):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"error": "Pietanza not found", "detail": exc.detail}
|
||||
)
|
||||
|
||||
@app.exception_handler(ValidationError)
|
||||
async def validation_error_handler(request, exc):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"error": "Validation error", "detail": exc.detail}
|
||||
)
|
||||
|
||||
@app.exception_handler(DatabaseError)
|
||||
async def database_error_handler(request, exc):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"error": "Database error", "detail": exc.detail}
|
||||
)
|
||||
|
||||
# Startup and shutdown events
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Initialize application on startup"""
|
||||
try:
|
||||
# Initialize database
|
||||
await db_manager.initialize(config['database'])
|
||||
logger.info("Database initialized")
|
||||
|
||||
# Initialize authentication
|
||||
initialize_auth(config['auth'])
|
||||
logger.info("Authentication initialized")
|
||||
|
||||
logger.info("Application startup completed")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize application: {e}")
|
||||
raise
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""Cleanup on application shutdown"""
|
||||
try:
|
||||
await db_manager.close()
|
||||
logger.info("Application shutdown completed")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during shutdown: {e}")
|
||||
|
||||
# Health check endpoint
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "simple-mensa-api",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
# Root endpoint
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Root endpoint with API information"""
|
||||
return {
|
||||
"message": "Simple Mensa API",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs",
|
||||
"health": "/health"
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=config['server']['host'],
|
||||
port=config['server']['port'],
|
||||
reload=config['server']['reload'],
|
||||
debug=config['server']['debug']
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
from .pietanze import PietanzaBase, PietanzaCreate, PietanzaUpdate, PietanzaResponse
|
||||
from .pasti import PastoBase, PastoCreate, PastoUpdate, PastoResponse, TipoPasto
|
||||
from .prenotazioni import PrenotazioneBase, PrenotazioneCreate, PrenotazioneUpdate, PrenotazioneResponse, StatoPrenotazione
|
||||
from .common import ErrorResponse, PaginatedResponse
|
||||
|
||||
__all__ = [
|
||||
# Pietanze models
|
||||
"PietanzaBase",
|
||||
"PietanzaCreate",
|
||||
"PietanzaUpdate",
|
||||
"PietanzaResponse",
|
||||
# Pasti models
|
||||
"PastoBase",
|
||||
"PastoCreate",
|
||||
"PastoUpdate",
|
||||
"PastoResponse",
|
||||
"TipoPasto",
|
||||
# Prenotazioni models
|
||||
"PrenotazioneBase",
|
||||
"PrenotazioneCreate",
|
||||
"PrenotazioneUpdate",
|
||||
"PrenotazioneResponse",
|
||||
"StatoPrenotazione",
|
||||
# Common models
|
||||
"ErrorResponse",
|
||||
"PaginatedResponse"
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Any
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
error: str
|
||||
detail: Optional[str] = None
|
||||
code: Optional[str] = None
|
||||
|
||||
class PaginatedResponse(BaseModel):
|
||||
items: List[Any]
|
||||
total: int
|
||||
page: int
|
||||
size: int
|
||||
pages: int
|
||||
@@ -0,0 +1,101 @@
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime, date
|
||||
from enum import Enum
|
||||
|
||||
class TipoPasto(str, Enum):
|
||||
pranzo = "pranzo"
|
||||
cena = "cena"
|
||||
|
||||
class PastoBase(BaseModel):
|
||||
data_pasto: date
|
||||
tipo_pasto: TipoPasto = TipoPasto.pranzo
|
||||
portate: Optional[Dict[str, Dict[int, int]]] = Field(
|
||||
default_factory=dict,
|
||||
description="Portate structure: {'primo': {pietanza_id: max_prenotazioni}}"
|
||||
)
|
||||
turni: Optional[Dict[str, int]] = Field(
|
||||
default_factory=dict,
|
||||
description="Turni structure: {'12:45': max_posti}"
|
||||
)
|
||||
disponibile: bool = True
|
||||
|
||||
@validator('portate')
|
||||
def validate_portate(cls, v):
|
||||
if v is None:
|
||||
return {}
|
||||
# Validate structure: each portata should have pietanza_id -> max_count mapping
|
||||
valid_portate = ['primo', 'secondo', 'contorno', 'dolce', 'frutta']
|
||||
for portata, pietanze in v.items():
|
||||
if portata not in valid_portate:
|
||||
raise ValueError(f"Portata non valida: {portata}")
|
||||
if not isinstance(pietanze, dict):
|
||||
raise ValueError(f"Pietanze per {portata} deve essere un dizionario")
|
||||
for pietanza_id, max_count in pietanze.items():
|
||||
if not isinstance(int(pietanza_id), int) or int(pietanza_id) <= 0:
|
||||
raise ValueError(f"ID pietanza non valido: {pietanza_id}")
|
||||
if not isinstance(max_count, int) or max_count < 0:
|
||||
raise ValueError(f"Numero massimo prenotazioni non valido per pietanza {pietanza_id}")
|
||||
return v
|
||||
|
||||
@validator('turni')
|
||||
def validate_turni(cls, v):
|
||||
if v is None:
|
||||
return {}
|
||||
# Validate time format and capacity
|
||||
import re
|
||||
time_pattern = re.compile(r'^([01]\d|2[0-3]):([0-5]\d)$')
|
||||
for turno, capacity in v.items():
|
||||
if not time_pattern.match(turno):
|
||||
raise ValueError(f"Formato orario non valido: {turno}. Usare HH:MM")
|
||||
if not isinstance(capacity, int) or capacity < 0:
|
||||
raise ValueError(f"Capacità non valida per turno {turno}: {capacity}")
|
||||
return v
|
||||
|
||||
class PastoCreate(PastoBase):
|
||||
pass
|
||||
|
||||
class PastoUpdate(BaseModel):
|
||||
tipo_pasto: Optional[TipoPasto] = None
|
||||
portate: Optional[Dict[str, Dict[int, int]]] = None
|
||||
turni: Optional[Dict[str, int]] = None
|
||||
disponibile: Optional[bool] = None
|
||||
|
||||
# Same validators as PastoBase for optional fields
|
||||
@validator('portate')
|
||||
def validate_portate(cls, v):
|
||||
if v is None:
|
||||
return v
|
||||
valid_portate = ['primo', 'secondo', 'contorno', 'dolce', 'frutta']
|
||||
for portata, pietanze in v.items():
|
||||
if portata not in valid_portate:
|
||||
raise ValueError(f"Portata non valida: {portata}")
|
||||
if not isinstance(pietanze, dict):
|
||||
raise ValueError(f"Pietanze per {portata} deve essere un dizionario")
|
||||
for pietanza_id, max_count in pietanze.items():
|
||||
if not isinstance(int(pietanza_id), int) or int(pietanza_id) <= 0:
|
||||
raise ValueError(f"ID pietanza non valido: {pietanza_id}")
|
||||
if not isinstance(max_count, int) or max_count < 0:
|
||||
raise ValueError(f"Numero massimo prenotazioni non valido per pietanza {pietanza_id}")
|
||||
return v
|
||||
|
||||
@validator('turni')
|
||||
def validate_turni(cls, v):
|
||||
if v is None:
|
||||
return v
|
||||
import re
|
||||
time_pattern = re.compile(r'^([01]\d|2[0-3]):([0-5]\d)$')
|
||||
for turno, capacity in v.items():
|
||||
if not time_pattern.match(turno):
|
||||
raise ValueError(f"Formato orario non valido: {turno}. Usare HH:MM")
|
||||
if not isinstance(capacity, int) or capacity < 0:
|
||||
raise ValueError(f"Capacità non valida per turno {turno}: {capacity}")
|
||||
return v
|
||||
|
||||
class PastoResponse(PastoBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -0,0 +1,49 @@
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
class PietanzaBase(BaseModel):
|
||||
nome: str = Field(..., min_length=1, max_length=150)
|
||||
descrizione: Optional[str] = None
|
||||
allergeni: Optional[List[str]] = []
|
||||
|
||||
class PietanzaCreate(PietanzaBase):
|
||||
@validator('allergeni')
|
||||
def validate_allergeni(cls, v):
|
||||
if v is None:
|
||||
return []
|
||||
# Common allergens validation
|
||||
valid_allergeni = {
|
||||
'glutine', 'lattosio', 'uova', 'pesce', 'crostacei', 'arachidi',
|
||||
'frutta_a_guscio', 'soia', 'sedano', 'senape', 'sesamo', 'solfiti'
|
||||
}
|
||||
for allergen in v:
|
||||
if allergen not in valid_allergeni:
|
||||
raise ValueError(f"Allergene non valido: {allergen}")
|
||||
return v
|
||||
|
||||
class PietanzaUpdate(BaseModel):
|
||||
nome: Optional[str] = Field(None, min_length=1, max_length=150)
|
||||
descrizione: Optional[str] = None
|
||||
allergeni: Optional[List[str]] = None
|
||||
|
||||
@validator('allergeni')
|
||||
def validate_allergeni(cls, v):
|
||||
if v is None:
|
||||
return v
|
||||
valid_allergeni = {
|
||||
'glutine', 'lattosio', 'uova', 'pesce', 'crostacei', 'arachidi',
|
||||
'frutta_a_guscio', 'soia', 'sedano', 'senape', 'sesamo', 'solfiti'
|
||||
}
|
||||
for allergen in v:
|
||||
if allergen not in valid_allergeni:
|
||||
raise ValueError(f"Allergene non valido: {allergen}")
|
||||
return v
|
||||
|
||||
class PietanzaResponse(PietanzaBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -0,0 +1,63 @@
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
class StatoPrenotazione(str, Enum):
|
||||
attiva = "attiva"
|
||||
servita = "servita"
|
||||
pagata = "pagata"
|
||||
annullata = "annullata"
|
||||
completata = "completata"
|
||||
|
||||
class PrenotazioneBase(BaseModel):
|
||||
pasto_id: int
|
||||
pietanze_selezionate: List[int] = Field(
|
||||
description="Lista ID delle pietanze selezionate"
|
||||
)
|
||||
note: Optional[str] = Field(None, max_length=500)
|
||||
stato: StatoPrenotazione = StatoPrenotazione.attiva
|
||||
|
||||
@validator('pietanze_selezionate')
|
||||
def validate_pietanze_selezionate(cls, v):
|
||||
if not v or len(v) == 0:
|
||||
raise ValueError("Almeno una pietanza deve essere selezionata")
|
||||
# Check for duplicates
|
||||
if len(v) != len(set(v)):
|
||||
raise ValueError("Non sono ammesse pietanze duplicate")
|
||||
# Validate each ID
|
||||
for pietanza_id in v:
|
||||
if not isinstance(pietanza_id, int) or pietanza_id <= 0:
|
||||
raise ValueError(f"ID pietanza non valido: {pietanza_id}")
|
||||
return v
|
||||
|
||||
class PrenotazioneCreate(PrenotazioneBase):
|
||||
# user_id will be extracted from JWT token
|
||||
pass
|
||||
|
||||
class PrenotazioneUpdate(BaseModel):
|
||||
pietanze_selezionate: Optional[List[int]] = None
|
||||
note: Optional[str] = Field(None, max_length=500)
|
||||
stato: Optional[StatoPrenotazione] = None
|
||||
|
||||
@validator('pietanze_selezionate')
|
||||
def validate_pietanze_selezionate(cls, v):
|
||||
if v is None:
|
||||
return v
|
||||
if len(v) == 0:
|
||||
raise ValueError("Almeno una pietanza deve essere selezionata")
|
||||
if len(v) != len(set(v)):
|
||||
raise ValueError("Non sono ammesse pietanze duplicate")
|
||||
for pietanza_id in v:
|
||||
if not isinstance(pietanza_id, int) or pietanza_id <= 0:
|
||||
raise ValueError(f"ID pietanza non valido: {pietanza_id}")
|
||||
return v
|
||||
|
||||
class PrenotazioneResponse(PrenotazioneBase):
|
||||
id: int
|
||||
user_id: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -0,0 +1 @@
|
||||
# Routes package initialization
|
||||
@@ -0,0 +1,265 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from typing import List, Optional, Dict, Any
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from ..core.database import get_database, DatabaseManager
|
||||
from ..models.pietanze import PietanzaCreate, PietanzaUpdate, PietanzaResponse
|
||||
from ..models.common import ErrorResponse
|
||||
from ..core.auth import get_current_user, get_current_admin_user
|
||||
from ..core.exceptions import PietanzaNotFoundError, DatabaseError
|
||||
|
||||
router = APIRouter(prefix="/pietanze", tags=["Pietanze"])
|
||||
|
||||
@router.get("/", response_model=List[PietanzaResponse])
|
||||
async def list_pietanze(
|
||||
skip: int = Query(0, ge=0, description="Number of items to skip"),
|
||||
limit: int = Query(20, ge=1, le=100, description="Number of items to return"),
|
||||
search: Optional[str] = Query(None, description="Search in nome and descrizione"),
|
||||
allergeni: Optional[str] = Query(None, description="Filter by allergens (comma-separated)"),
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Get list of available pietanze with optional filtering"""
|
||||
try:
|
||||
# Build query with filters
|
||||
where_conditions = []
|
||||
params = []
|
||||
param_count = 0
|
||||
|
||||
if search:
|
||||
param_count += 1
|
||||
where_conditions.append(f"(nome ILIKE ${param_count} OR descrizione ILIKE ${param_count})")
|
||||
params.append(f"%{search}%")
|
||||
|
||||
if allergeni:
|
||||
allergen_list = [a.strip() for a in allergeni.split(",")]
|
||||
param_count += 1
|
||||
where_conditions.append(f"allergeni ?| ${param_count}")
|
||||
params.append(allergen_list)
|
||||
|
||||
where_clause = ""
|
||||
if where_conditions:
|
||||
where_clause = "WHERE " + " AND ".join(where_conditions)
|
||||
|
||||
# Count total items
|
||||
count_query = f"SELECT COUNT(*) FROM pietanze {where_clause}"
|
||||
count_result = await db.execute_one(count_query, *params)
|
||||
total = count_result[0] if count_result else 0
|
||||
|
||||
# Get items with pagination
|
||||
param_count += 1
|
||||
limit_param = param_count
|
||||
param_count += 1
|
||||
offset_param = param_count
|
||||
|
||||
query = f"""
|
||||
SELECT id, nome, descrizione, allergeni, created_at, updated_at
|
||||
FROM pietanze
|
||||
{where_clause}
|
||||
ORDER BY nome
|
||||
LIMIT ${limit_param} OFFSET ${offset_param}
|
||||
"""
|
||||
params.extend([limit, skip])
|
||||
|
||||
rows = await db.execute_query(query, *params)
|
||||
|
||||
pietanze = []
|
||||
for row in rows:
|
||||
pietanze.append(PietanzaResponse(
|
||||
id=row['id'],
|
||||
nome=row['nome'],
|
||||
descrizione=row['descrizione'],
|
||||
allergeni=row['allergeni'] or [],
|
||||
created_at=row['created_at'],
|
||||
updated_at=row['updated_at']
|
||||
))
|
||||
|
||||
return pietanze
|
||||
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Failed to retrieve pietanze: {str(e)}")
|
||||
|
||||
@router.get("/{pietanza_id}", response_model=PietanzaResponse)
|
||||
async def get_pietanza(
|
||||
pietanza_id: int,
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Get specific pietanza by ID"""
|
||||
try:
|
||||
query = """
|
||||
SELECT id, nome, descrizione, allergeni, created_at, updated_at
|
||||
FROM pietanze
|
||||
WHERE id = $1
|
||||
"""
|
||||
row = await db.execute_one(query, pietanza_id)
|
||||
|
||||
if not row:
|
||||
raise PietanzaNotFoundError(pietanza_id)
|
||||
|
||||
return PietanzaResponse(
|
||||
id=row['id'],
|
||||
nome=row['nome'],
|
||||
descrizione=row['descrizione'],
|
||||
allergeni=row['allergeni'] or [],
|
||||
created_at=row['created_at'],
|
||||
updated_at=row['updated_at']
|
||||
)
|
||||
|
||||
except PietanzaNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Failed to retrieve pietanza: {str(e)}")
|
||||
|
||||
@router.post("/", response_model=PietanzaResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_pietanza(
|
||||
pietanza: PietanzaCreate,
|
||||
current_user: Dict[str, Any] = Depends(get_current_admin_user),
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Create new pietanza (admin only)"""
|
||||
try:
|
||||
query = """
|
||||
INSERT INTO pietanze (nome, descrizione, allergeni, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $4)
|
||||
RETURNING id, nome, descrizione, allergeni, created_at, updated_at
|
||||
"""
|
||||
|
||||
now = datetime.utcnow()
|
||||
allergeni_json = json.dumps(pietanza.allergeni) if pietanza.allergeni else None
|
||||
|
||||
row = await db.execute_one(
|
||||
query,
|
||||
pietanza.nome,
|
||||
pietanza.descrizione,
|
||||
allergeni_json,
|
||||
now
|
||||
)
|
||||
|
||||
if not row:
|
||||
raise DatabaseError("Failed to create pietanza")
|
||||
|
||||
return PietanzaResponse(
|
||||
id=row['id'],
|
||||
nome=row['nome'],
|
||||
descrizione=row['descrizione'],
|
||||
allergeni=row['allergeni'] or [],
|
||||
created_at=row['created_at'],
|
||||
updated_at=row['updated_at']
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Failed to create pietanza: {str(e)}")
|
||||
|
||||
@router.put("/{pietanza_id}", response_model=PietanzaResponse)
|
||||
async def update_pietanza(
|
||||
pietanza_id: int,
|
||||
pietanza_update: PietanzaUpdate,
|
||||
current_user: Dict[str, Any] = Depends(get_current_admin_user),
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Update existing pietanza (admin only)"""
|
||||
try:
|
||||
# Check if pietanza exists
|
||||
existing = await db.execute_one("SELECT id FROM pietanze WHERE id = $1", pietanza_id)
|
||||
if not existing:
|
||||
raise PietanzaNotFoundError(pietanza_id)
|
||||
|
||||
# Build update query dynamically
|
||||
update_fields = []
|
||||
params = []
|
||||
param_count = 0
|
||||
|
||||
if pietanza_update.nome is not None:
|
||||
param_count += 1
|
||||
update_fields.append(f"nome = ${param_count}")
|
||||
params.append(pietanza_update.nome)
|
||||
|
||||
if pietanza_update.descrizione is not None:
|
||||
param_count += 1
|
||||
update_fields.append(f"descrizione = ${param_count}")
|
||||
params.append(pietanza_update.descrizione)
|
||||
|
||||
if pietanza_update.allergeni is not None:
|
||||
param_count += 1
|
||||
update_fields.append(f"allergeni = ${param_count}")
|
||||
params.append(json.dumps(pietanza_update.allergeni))
|
||||
|
||||
if not update_fields:
|
||||
# No fields to update, return current pietanza
|
||||
return await get_pietanza(pietanza_id, db)
|
||||
|
||||
# Add updated_at and pietanza_id
|
||||
param_count += 1
|
||||
update_fields.append(f"updated_at = ${param_count}")
|
||||
params.append(datetime.utcnow())
|
||||
|
||||
param_count += 1
|
||||
params.append(pietanza_id)
|
||||
|
||||
query = f"""
|
||||
UPDATE pietanze
|
||||
SET {', '.join(update_fields)}
|
||||
WHERE id = ${param_count}
|
||||
RETURNING id, nome, descrizione, allergeni, created_at, updated_at
|
||||
"""
|
||||
|
||||
row = await db.execute_one(query, *params)
|
||||
|
||||
return PietanzaResponse(
|
||||
id=row['id'],
|
||||
nome=row['nome'],
|
||||
descrizione=row['descrizione'],
|
||||
allergeni=row['allergeni'] or [],
|
||||
created_at=row['created_at'],
|
||||
updated_at=row['updated_at']
|
||||
)
|
||||
|
||||
except PietanzaNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Failed to update pietanza: {str(e)}")
|
||||
|
||||
@router.delete("/{pietanza_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_pietanza(
|
||||
pietanza_id: int,
|
||||
current_user: Dict[str, Any] = Depends(get_current_admin_user),
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Delete pietanza (admin only)"""
|
||||
try:
|
||||
# Check if pietanza exists
|
||||
existing = await db.execute_one("SELECT id FROM pietanze WHERE id = $1", pietanza_id)
|
||||
if not existing:
|
||||
raise PietanzaNotFoundError(pietanza_id)
|
||||
|
||||
# Delete the pietanza
|
||||
result = await db.execute_command("DELETE FROM pietanze WHERE id = $1", pietanza_id)
|
||||
|
||||
# Check if deletion was successful
|
||||
if not result or not result.endswith("1"):
|
||||
raise DatabaseError("Failed to delete pietanza")
|
||||
|
||||
except PietanzaNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Failed to delete pietanza: {str(e)}")
|
||||
|
||||
@router.get("/{pietanza_id}/allergeni", response_model=List[str])
|
||||
async def get_pietanza_allergeni(
|
||||
pietanza_id: int,
|
||||
db: DatabaseManager = Depends(get_database)
|
||||
):
|
||||
"""Get allergen information for specific pietanza"""
|
||||
try:
|
||||
query = "SELECT allergeni FROM pietanze WHERE id = $1"
|
||||
row = await db.execute_one(query, pietanza_id)
|
||||
|
||||
if not row:
|
||||
raise PietanzaNotFoundError(pietanza_id)
|
||||
|
||||
return row['allergeni'] or []
|
||||
|
||||
except PietanzaNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Failed to retrieve allergeni: {str(e)}")
|
||||
@@ -0,0 +1 @@
|
||||
# Utils package initialization
|
||||
@@ -1,94 +0,0 @@
|
||||
# Sistema di Prenotazione Mensa - Documentazione Tecnica
|
||||
|
||||
## Abstract
|
||||
|
||||
Il sistema di prenotazione mensa è una piattaforma web-based sviluppata, con FastAPI e PostgreSQL a livello backend e NiceGUI per le interfacce fullstack, che consente agli utenti di prenotare pasti giornalieri. L'architettura è basata su un'API RESTful con autenticazione JWT, garantendo sicurezza e scalabilità. Il sistema gestisce tr entità principali interconnesse: prenotazioni, pasti e pietanze, utilizzando campi JSON per semplificare le relazioni e migliorare le erformance. I dati utente sono forniti dal JWT auth bearer evittando la memorizzazione sul database di dati sensibili non necessari, sfruttando per questa funzionalità lo IAM Azure e l'account aziendale (o anche IAM self hosted Keycloak)
|
||||
|
||||
## Architettura del Sistema
|
||||
|
||||
### Stack Tecnologico
|
||||
|
||||
- **Backend**: FastAPI (Python 3.8+)
|
||||
- **Database**: PostgreSQL 13+ con supporto JSON/JSONB
|
||||
- **Autenticazione**: JWT (JSON Web Tokens)
|
||||
- **ORM**: SQLAlchemy
|
||||
|
||||
## Database Design
|
||||
|
||||
### Filosofia di Semplificazione
|
||||
|
||||
Il database è stato progettato seguendo una filosofia di **massima semplificazione**, riducendo la complessità relazionale tradizionale attraverso l'uso strategico di campi JSONB di PostgreSQL al posto di tabelle intermedie per le relazioni N a N. Contestalmente alla semplicità dello scheme e alle potenti capacità di manipolazione di dati strutturati JSON di postgresql, questa scelta progettuale offre diversi vantaggi:
|
||||
|
||||
- **Riduzione delle JOIN**: Eliminazione di tabelle di associazione complesse
|
||||
- **Flessibilità**: Strutture dati dinamiche per allergeni, turni e selezioni delle pietanze
|
||||
- **Performance**: Meno query multiple per operazioni comuni
|
||||
- **Manutenibilità**: Schema più leggibile e modificabile
|
||||
|
||||
### Struttura delle Tabelle
|
||||
|
||||
#### Tabella `pietanze`
|
||||
Gestisce le singole pietanze disponibili con:
|
||||
- Informazioni base (nome, descrizione)
|
||||
- Gestione allergeni tramite array JSON
|
||||
- Controllo disponibilità e limiti di prenotazione
|
||||
|
||||
#### Tabella `pasti`
|
||||
Rappresenta i menu giornalieri con:
|
||||
- **Portate JSONB**: Organizzazione flessibile delle pietanze per tipologia indicando la disponibilità massima di ciascuna
|
||||
- **Turni JSONB**: Gestione dinamica degli orari e capacità (`{"12:45": 100, "13:30": 100}`)
|
||||
- Vincolo di unicità per data e tipo pasto
|
||||
|
||||
#### Tabella `prenotazioni`
|
||||
Collega utenti e pasti con:
|
||||
- Riferimento user_id estratto da JWT
|
||||
- **Pietanze selezionate JSONB**: Array delle scelte dell'utente
|
||||
- Gestione stati del ciclo di vita della prenotazione
|
||||
|
||||
## Flussi Operativi
|
||||
|
||||
### Flusso di Creazione Menu Giornaliero
|
||||
1. **Inserimento Pietanze**: Caricamento delle pietanze disponibili per il giorno
|
||||
2. **Composizione Pasto**: Associazione pietanze alle portate tramite JSON
|
||||
3. **Configurazione Turni**: Definizione orari e capacità massima per turno
|
||||
4. **Attivazione**: Abilitazione delle prenotazioni per gli utenti
|
||||
|
||||
### Flusso di Prenotazione Utente
|
||||
1. **Autenticazione**: Verifica JWT e estrazione user_id
|
||||
2. **Visualizzazione Menu**: Recupero pasti disponibili per data
|
||||
3. **Selezione Pietanze**: Scelta pietanze per ogni portata disponibile
|
||||
4. **Validazione**: Controllo disponibilità e limiti di prenotazione
|
||||
5. **Conferma**: Creazione record prenotazione con stato 'attiva'
|
||||
|
||||
### Flusso di Servizio Mensa
|
||||
1. **Consultazione Prenotazioni**: Visualizzazione prenotazioni per turno
|
||||
2. **Erogazione Pasto**: Aggiornamento stato da 'attiva' a 'servita'
|
||||
3. **Gestione Pagamento**: Transizione finale a stato 'pagata'
|
||||
4. **Monitoraggio**: Tracking utilizzo e disponibilità residua
|
||||
|
||||
### Flusso di Gestione Disponibilità
|
||||
1. **Controllo Automatico**: Verifica limiti pietanze e turni
|
||||
2. **Aggiornamento Dinamico**: Modifica disponibilità in tempo reale
|
||||
3. **Notifiche**: Gestione comunicazioni per esaurimento posti
|
||||
4. **Chiusura Prenotazioni**: Disabilitazione automatica al raggiungimento limiti
|
||||
|
||||
## Scelte Architetturali: Logica di Business
|
||||
|
||||
### Principio di Separazione delle Responsabilità
|
||||
|
||||
In linea con la filosofia di semplificazione adottata per il design del database, **la logica di business complessa rimane interamente gestita a livello API (FastAPI)** piuttosto che essere delegata al database tramite stored procedures o funzioni PostgreSQL.
|
||||
|
||||
#### Motivazioni della Scelta
|
||||
|
||||
**Coerenza Architetturale**: Il sistema è progettato attorno a FastAPI come orchestratore centrale. Mantenere la logica di business nell'API garantisce un'architettura coerente e predicibile.
|
||||
|
||||
**Manutenibilità e Testabilità**: Le funzioni Python sono intrinsecamente più facili da testare, debuggare e modificare rispetto alle stored procedures. Questo si allinea con l'approccio DevOps moderno e l'integrazione continua.
|
||||
|
||||
**Flessibilità di Sviluppo**: La gestione della disponibilità, dei limiti di prenotazione e delle validazioni può evolvere rapidamente senza modifiche al schema database.
|
||||
|
||||
#### Implementazione delle Verifiche di Disponibilità
|
||||
|
||||
Le operazioni critiche come la verifica di disponibilità posti in un turno o di una pietanza vengono gestite tramite:
|
||||
- **Query atomiche** per il recupero dati
|
||||
- **Validazioni Python** per la logica di business
|
||||
- **Transazioni SQLAlchemy** per garantire consistenza
|
||||
- **Pattern async/await** per performance ottimali
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
fastapi>=0.104.0
|
||||
uvicorn[standard]>=0.24.0
|
||||
sqlalchemy>=2.0.0
|
||||
psycopg2-binary>=2.9.0
|
||||
asyncpg>=0.29.0
|
||||
pydantic>=2.0.0
|
||||
python-jose[cryptography]>=3.3.0
|
||||
python-multipart>=0.0.6
|
||||
pyyaml>=6.0.1
|
||||
aiofiles>=23.0.0
|
||||
httpx>=0.25.0
|
||||
|
||||
@@ -10,6 +10,7 @@ CREATE TABLE pietanze (
|
||||
CREATE TABLE pasti (
|
||||
id SERIAL PRIMARY KEY,
|
||||
data_pasto DATE NOT NULL,
|
||||
tipo_pasto VARCHAR(20) DEFAULT 'pranzo', -- Add missing column referenced in UNIQUE constraint
|
||||
portate JSONB, -- Lista di portate e relative pietanze con numero di prenotazioni massime (se 0 nessun limite): {"primo": {1: 100, 2: 50}, "secondo": {3: 100, 4: 50}, ...}
|
||||
disponibile BOOLEAN DEFAULT true,
|
||||
turni JSONB, -- Dizionario con i turni di prenotazione e posti massimi disponibili (se 0 nessun limite): {"12:45": 100, "13:00": 100, ...}
|
||||
|
||||
Reference in New Issue
Block a user