Skip to content

flavien-hugs/ms-feedbacks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Customer Feedback API — Architecture SOLID

API REST de collecte des retours clients construite avec FastAPI + SQLAlchemy (async) + SQLite/PostgreSQL, en appliquant strictement les cinq principes SOLID.


Résultats des tests

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.


Architecture en couches

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)

Les cinq principes SOLID appliqués

S — Single Responsibility Principle

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

O — Open/Closed Principle

Le système est ouvert à l'extension, fermé à la modification.

Pour ajouter un nouveau canal de distribution (ex. WhatsApp), il suffit de :

  1. Créer WhatsAppDistributionService(IDistributionService) dans infrastructure/services/distribution/
  2. 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.

L — Liskov Substitution Principle

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()  # Tests

Les 44 tests utilisent des mocks qui respectent exactement les contrats définis dans domain/services/.

I — Interface Segregation Principle

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.

D — Dependency Inversion Principle

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.


Endpoints de l'API

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

Démarrage rapide

Prérequis

pip install fastapi uvicorn sqlalchemy aiosqlite pydantic pydantic-settings \
            python-jose python-multipart openai reportlab email-validator

Configuration

cp .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éelle

Lancement

uvicorn main:app --host 0.0.0.0 --port 8000 --reload

Documentation interactive disponible sur : http://localhost:8000/docs

Tests

pytest tests/test_solid_architecture.py -v
# 44 passed in 0.08s

Flux de soumission d'une réponse

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

Analyse des sentiments et étiquetage

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 / neutral avec 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é.


Calcul du Score NPS

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

Distribution multi-canal

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.


Formats d'export

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
PDF Rapport analytique avec tableau NPS et verbatims application/pdf

About

Example API Customer Feedback in Architecture SOLID

Resources

Stars

Watchers

Forks

Contributors

Languages