API REST de collecte des retours clients construite avec FastAPI + SQLAlchemy (async) + SQLite/PostgreSQL, en appliquant strictement les cinq principes SOLID.
Les tests couvrent chaque principe SOLID indépendamment: value objects, repositories mockés, services d'analyse, calcul NPS, interfaces ségrégées et substitution de Liskov.
feedback_api_solid/
├── domain/ ← Couche Domaine (aucune dépendance externe)
│ ├── value_objects/__init__.py ← NPSScore, NPSResult, Sentiment, Tag, Enums
│ ├── entities/__init__.py ← Entités métier pures (Survey, Response, User…)
│ ├── repositories/__init__.py ← Interfaces abstraites (ISurveyRepository…)
│ └── services/__init__.py ← Interfaces abstraites (IAnalysisService…)
│
├── application/ ← Couche Application (dépend uniquement du Domain)
│ ├── dtos/__init__.py ← DTOs Pydantic (entrées/sorties des use cases)
│ └── use_cases/
│ ├── auth/ ← RegisterUserUseCase, AuthenticateUserUseCase
│ ├── survey/ ← CreateSurveyUseCase, GetSurveyUseCase…
│ ├── response/ ← SubmitResponseUseCase, ListResponsesUseCase…
│ ├── analytics/ ← GetDashboardUseCase, GetSurveyAnalyticsUseCase
│ ├── distribution/ ← DistributeSurveyUseCase, GenerateWebTokenUseCase
│ └── export/ ← ExportSurveyUseCase (CSV, JSON, PDF)
│
├── infrastructure/ ← Couche Infrastructure (implémentations concrètes)
│ ├── database/
│ │ ├── models/__init__.py ← Modèles SQLAlchemy (ORM)
│ │ └── repositories/__init__.py← Implémentations SQLAlchemy des interfaces
│ └── services/
│ ├── analysis/__init__.py ← LLMAnalysisService (OpenAI gpt-4.1-nano)
│ ├── distribution/__init__.py← EmailDistributionService, SMSDistributionService
│ └── export/__init__.py ← CSVExportService, JSONExportService, PDFExportService
│
├── presentation/ ← Couche Présentation (FastAPI)
│ ├── dependencies/__init__.py ← Conteneur DI (providers FastAPI)
│ └── routers/
│ ├── auth.py ← POST /auth/register, /auth/login, GET /auth/me
│ ├── surveys.py ← CRUD /surveys + /public/surveys/{slug}
│ ├── responses.py ← POST /responses, GET /responses/survey/{id}
│ ├── analytics.py ← GET /analytics/dashboard, /analytics/surveys/{id}
│ ├── distributions.py ← POST /surveys/{id}/distributions
│ └── export.py ← GET /surveys/{id}/export/{csv|json|pdf}
│
├── core/
│ └── config.py ← Settings (pydantic-settings, .env)
│
├── main.py ← Point d'entrée FastAPI + lifespan
└── tests/
└── test_solid_architecture.py ← 44 tests unitaires (mocks, value objects)
Chaque classe a une seule raison de changer.
| Classe | Responsabilité unique |
|---|---|
NPSScore |
Valider et classifier un score NPS (0-10) |
NPSResult |
Calculer les métriques agrégées NPS |
Sentiment |
Encapsuler un résultat d'analyse de sentiment |
CreateSurveyUseCase |
Créer une enquête (et uniquement cela) |
SubmitResponseUseCase |
Soumettre une réponse et déclencher l'analyse |
CSVExportService |
Exporter en CSV (pas JSON, pas PDF) |
SQLAlchemySurveyRepository |
Persister les enquêtes en base de données |
Le système est ouvert à l'extension, fermé à la modification.
Pour ajouter un nouveau canal de distribution (ex. WhatsApp), il suffit de :
- Créer
WhatsAppDistributionService(IDistributionService)dansinfrastructure/services/distribution/ - L'enregistrer dans le conteneur de dépendances
Aucune modification du use case DistributeSurveyUseCase ni du router n'est nécessaire.
De même pour un nouveau format d'export (ex. Excel) : créer ExcelExportService(IExportService) et l'injecter.
Toute implémentation concrète est substituable à son interface abstraite.
# Ces deux lignes sont interchangeables sans changer le use case :
analysis_service: IAnalysisService = LLMAnalysisService() # Production
analysis_service: IAnalysisService = MockAnalysisService() # TestsLes 44 tests utilisent des mocks qui respectent exactement les contrats définis dans domain/services/.
Les interfaces sont granulaires et ne forcent pas les implémentations à dépendre de méthodes inutiles.
# Interfaces séparées (domain/services/__init__.py)
class IAnalysisService(ABC):
async def analyze_sentiment(self, text: str) -> Sentiment: ...
async def extract_tags(self, text: str) -> List[Tag]: ...
class IPasswordHasher(ABC):
def hash(self, password: str) -> str: ...
def verify(self, plain: str, hashed: str) -> bool: ...
class ITokenService(ABC):
def create_token(self, user_id: int) -> str: ...
def decode_token(self, token: str) -> dict: ...
class IExportService(ABC):
async def export(self, survey, responses) -> tuple[bytes, str, str]: ...Chaque service n'implémente que ce dont il a besoin.
Les modules de haut niveau (use cases, routers) dépendent d'abstractions, jamais d'implémentations concrètes.
# Correct — le router dépend d'une abstraction injectée
@router.post("/responses")
async def submit_response(
dto: SubmitResponseDTO,
use_case: SubmitResponseUseCase = Depends(get_submit_response_use_case),
):
return await use_case.execute(dto)
# Le use case lui-même reçoit des interfaces, pas des classes concrètes
class SubmitResponseUseCase:
def __init__(
self,
response_repo: IResponseRepository, # interface
survey_repo: ISurveyRepository, # interface
analysis_service: IAnalysisService, # interface
): ...Le conteneur de dépendances (presentation/dependencies/__init__.py) est le seul
endroit où les implémentations concrètes sont instanciées.
| Méthode | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/auth/register |
Créer un compte | Non |
POST |
/auth/login |
Obtenir un token JWT | Non |
GET |
/auth/me |
Profil utilisateur | Oui |
POST |
/surveys |
Créer une enquête | Oui |
GET |
/surveys |
Lister mes enquêtes | Oui |
GET |
/surveys/{id} |
Détail d'une enquête | Oui |
PUT |
/surveys/{id} |
Modifier une enquête | Oui |
DELETE |
/surveys/{id} |
Supprimer une enquête | Oui |
POST |
/surveys/{id}/questions |
Ajouter une question | Oui |
GET |
/public/surveys/{slug} |
Accès public à l'enquête | Non |
POST |
/responses |
Soumettre une réponse | Non |
GET |
/responses/survey/{id} |
Réponses d'une enquête | Oui |
GET |
/analytics/dashboard |
Tableau de bord global | Oui |
GET |
/analytics/surveys/{id} |
Analytics d'une enquête | Oui |
POST |
/surveys/{id}/distributions |
Distribuer l'enquête | Oui |
GET |
/surveys/{id}/export/csv |
Export CSV | Oui |
GET |
/surveys/{id}/export/json |
Export JSON | Oui |
GET |
/surveys/{id}/export/pdf |
Export PDF | Oui |
pip install fastapi uvicorn sqlalchemy aiosqlite pydantic pydantic-settings \
python-jose python-multipart openai reportlab email-validatorcp .env.example .env
# Éditer .env avec vos valeurs# .env.example
ASYNC_DATABASE_URL=sqlite+aiosqlite:///./feedback.db
# Pour PostgreSQL :
# ASYNC_DATABASE_URL=postgresql+asyncpg://user:pass@localhost/feedback_db
SECRET_KEY=votre-cle-secrete-longue-et-aleatoire
OPENAI_API_KEY=sk-... # Optionnel — active l'analyse LLM réelleuvicorn main:app --host 0.0.0.0 --port 8000 --reloadDocumentation interactive disponible sur : http://localhost:8000/docs
pytest tests/test_solid_architecture.py -v
# 44 passed in 0.08sClient HTTP
│
▼ POST /responses
Router (presentation)
│ Depends(get_submit_response_use_case)
▼
SubmitResponseUseCase (application)
│ IResponseRepository.create()
│ IAnalysisService.analyze_sentiment()
│ IAnalysisService.extract_tags()
▼
SQLAlchemyResponseRepository (infrastructure) ← persiste en DB
LLMAnalysisService (infrastructure) ← appelle OpenAI
│
▼
ResponseResponseDTO (retourné au client)
Lorsqu'une réponse textuelle est soumise, le service LLMAnalysisService appelle le modèle gpt-4.1-nano via l'API
OpenAI. Il retourne:
- Sentiment :
positive/negative/neutralavec un score de confiance entre -1.0 et +1.0 - Tags : jusqu'à 5 étiquettes thématiques (ex.
support,prix,délai) avec catégorie et score de confiance
Sans clé OpenAI configurée, le service retourne un sentiment neutre par défaut, garantissant que l'API reste fonctionnelle en mode dégradé.
Le NPS est calculé via le Value Object NPSResult.from_scores() :
NPS = ((Promoteurs / Total) - (Détracteurs / Total)) × 100
| Segment | Score | Interprétation |
|---|---|---|
| Promoteurs | 9 – 10 | Clients fidèles et ambassadeurs |
| Passifs | 7 – 8 | Clients satisfaits mais non engagés |
| Détracteurs | 0 – 6 | Clients insatisfaits, risque de churn |
| Canal | Implémentation | Comportement sans config |
|---|---|---|
email |
EmailDistributionService |
Log simulé (pas d'envoi réel) |
sms |
SMSDistributionService |
Log simulé |
web |
WebDistributionService |
Génère un token URL partageable |
Chaque envoi crée un token unique par destinataire, permettant le suivi des réponses par canal.
| Format | Contenu | Headers HTTP |
|---|---|---|
| CSV | Toutes les réponses avec colonnes par question | text/csv |
| JSON | Structure complète avec métriques NPS et sentiments | application/json |
| Rapport analytique avec tableau NPS et verbatims | application/pdf |