Aller au contenu

Documentation Backend Teadle

📋 Table des matières

  1. Vue d'ensemble
  2. Architecture
  3. Technologies
  4. Structure du projet
  5. API Documentation
  6. Sécurité
  7. Base de données
  8. Déploiement

🎯 Vue d'ensemble

Teadle est une plateforme de mise en relation entre formateurs et organisations. Le backend est développé en Symfony 7.3 avec une architecture Domain-Driven Design (DDD) et utilise API Platform pour exposer les APIs REST.

Fonctionnalités principales

  • 🔐 Authentification : JWT, OAuth (LinkedIn, Google, Microsoft), codes d'authentification
  • 👥 Gestion des utilisateurs : Formateurs et Organisations
  • 📋 Onboarding : Processus d'inscription et de configuration
  • 💼 Opportunités : Gestion des missions et candidatures
  • 📅 Événements : Planning et interventions
  • 🔗 Partage de planning : Liens publics avec description facultative (255 car., création et édition)
  • 💰 Facturation : Gestion des factures
  • 📧 Notifications : Emails via Brevo, centralisés par EmailService (méthodes métier par type d'email ; templates Brevo mappés dans email_templates.yaml). Inclut notamment l'email de synthèse organisation envoyé au formateur après validation de ses tarifs (sendTrainerSummaryOrganizationInfoEmail, template #20).
  • 📊 Bilan mensuel automatique : Email envoyé le 1er du mois à 08h via Symfony Scheduler aux formateurs ayant opté pour l'optin MONTHLY_SUMMARY. Calcul via MonthlyReportCalculator, envoi via sendMonthlyReportEmail (template Brevo #23). Protection anti-doublon par stateful($cache) + processOnlyLastMissedRun(true).
  • 📅 Récap semaine à venir : Email chaque vendredi 17:00 (Europe/Paris) via Symfony Scheduler (SendUpcomingWeekReports → transport async). Cible : formateurs onboarding terminé avec au moins une intervention (affectation active) dont le début tombe dans la semaine calendaire suivante (lundi–dimanche contenant today + 7 jours, fenêtre calculée en Europe/Paris). Paramètres Brevo via sendUpcomingWeekReportEmail (template recap_week_upcoming / #24). Commande de debug : app:debug:send-upcoming-week-reports.
  • 🔄 Resync calendrier (lien iCal ou OAuth) : Chaque nuit à 01:00 (Europe/Paris), le scheduler envoie ScheduledTrainerCalendarRemoteResync sur le transport async ; DispatchScheduledTrainerCalendarRemoteResyncCommand enchaîne les contrôles puis SyncProcessTrainerCalendarIcalCommand::process pour chaque configurateur ayant une URL iCal ou des jetons OAuth persistés (équivalent au POST /trainer/calendar/configurator/{uuid}/sync). Le traitement async passe par ProcessTrainerCalendarConfiguratorSync : TrainerCalendarConfiguratorSynchronizer appelle parseCalendar (OAuth) ou IcsParserInterface puis boucle sur les stratégies (supportsIcal / supportsIcalProdId) et parseIcalVo, puis PersistTrainerCalendarConnectorEventsCommand (Application/Command/Trainer/Calendar/). Pour la déduction d’année scolaire côté parseurs iCal, la date de l’événement (DTSTART) est désormais prioritaire ; l’analyse textuelle reste un fallback (champs summary / description / location selon le parseur), avec normalisation des tirets Unicode avant lecture des plages d’années. Côté OAuth Outlook (OutlookOAuthCalendarEventsParser), l’année scolaire est maintenant toujours renseignée dans cet ordre : date de l’événement, fallback texte (subject / bodyPreview), puis année courante ; garde-fou explicite pour éviter toute valeur 0-0.
  • 📅 Agenda personnel distant (formateur) : sur TrainerAvailabilityConfigurationOAuth (jetons + oauth_calendar_id + personal_calendar_provider_type), URL iCal (personal_calendar_ical_url, migration Version20260420140000), ou fichier .ics (personal_calendar_provider_type = ical_file, clé S3 …/personal-availability-{id}.ics via TrainerPersonalCalendarIcalFileStorageKeyResolver, aligné sur le configurateur orga). API agenda personnel distant : TrainerPersonalCalendarOAuthResource — OAuth (/trainer/calendar/personal/oauth/...) et import iCal sans compte : POST /trainer/calendar/personal/ical/add (corps JSON identique au configurateur orga : icalLink, providerType optionnel) et POST /trainer/calendar/personal/ical/upload (multipart icsfile, providerType optionnel), traités par CreateProcessTrainerPersonalCalendarIcalLinkCommand / CreateProcessTrainerPersonalCalendarIcalFileCommand puis la même synchro async ProcessTrainerPersonalCalendarSyncTrainerPersonalCalendarSynchronizerPersistTrainerPersonalCalendarEventsCommand. Resync planifiée : TrainerAvailabilityConfigurationRepository::findAllWithPersonalCalendarRemoteSyncSource(). GET /trainer/availability/configuration expose personalCalendarRemoteSyncConfigured et personalCalendarIcalUrl. Déconnexion DELETE …/personal/oauth efface OAuth et URL iCal. Le flux OAuth SPA réutilise TrainerCalendarOAuthCallbackView + sessionStorage.teadle_calendar_oauth_personal.

🏗️ Architecture

Architecture Hexagonale (DDD)

Le projet suit une architecture hexagonale avec séparation claire des couches :

┌─────────────────────────────────────────────────────────────┐
│                    Infrastructure Layer                     │
├─────────────────────────────────────────────────────────────┤
│                    Application Layer                        │
├─────────────────────────────────────────────────────────────┤
│                      Domain Layer                           │
└─────────────────────────────────────────────────────────────┘

Couches de l'architecture

1. Domain Layer (src/Domain/)

  • Entities : Modèles métier (User, Trainer, Organization, etc.)
  • Value Objects : Objets de valeur (PhoneNumber, etc.)
  • Repositories : Interfaces des repositories
  • Services : Services métier
  • Events : Événements du domaine
  • Exceptions : Exceptions métier

2. Application Layer (src/Application/)

  • Commands : Commandes CQRS
  • Handlers : Gestionnaires de commandes
  • Services : Services d'application (dont Service/Email/EmailService et EmailTemplateRegistry pour l'envoi d'emails centralisé)

3. Infrastructure Layer (src/Infrastructure/)

  • API : Resources et Processors API Platform
  • ORM : Implémentations Doctrine
  • Security : Authentification et autorisation
  • Notification : Services d'envoi d'emails
  • Symfony : Intégrations Symfony (dont EventSubscribers écoutant les Domain Events pour déclencher des effets de bord — emails, notifications)

🛠️ Technologies

Framework et composants principaux

  • Symfony 7.3 : Framework PHP (inclut Symfony Scheduler pour les tâches planifiées)
  • API Platform 3.2 : API REST automatique
  • Doctrine ORM 3.3 : Mapping objet-relationnel
  • Lexik JWT Bundle : Authentification JWT
  • Gesdinet JWT Refresh Token : Refresh tokens
  • OAuth2 Client Bundle : Authentification OAuth

Services externes

  • Brevo : Service d'envoi d'emails
  • Stockage fichiers (S3 / Clever Cloud Cellar) : bucket formateurs via Flysystem (config/packages/flysystem.yaml). Les ressources exposées par URL directe (logoUrl facturation, avatar, etc.) passent par FilesystemWriter::writePublic() (ACL public-read côté S3) ; sans cela, l’URL renvoie 403 Access Denied. Les fichiers déjà uploadés en privé : ré-uploader pour corriger.
  • LinkedIn OAuth : Authentification LinkedIn
  • Google OAuth : Authentification Google
  • Microsoft OAuth : Authentification Microsoft

Monitoring & Observabilité

  • Sentry (sentry/sentry-symfony 5.x) : capture automatique des exceptions, erreurs Messenger, performance tracing (traces_sample_rate: 0.2). Handler Monolog sentry (level error) en prod. DSN via SENTRY_DSN dans .env.prod.
  • New Relic (agent PHP natif, extension newrelic.so) : APM, slow queries Doctrine, distributed tracing. Installé dans le Dockerfile prod, configuré dynamiquement au démarrage via start.sh si NEW_RELIC_LICENSE_KEY est défini. Région EU (collector.eu01.nr-data.net), framework symfony4.

Outils de développement

  • PHPStan : Analyse statique
  • PHP CS Fixer : Standards de code
  • Alice Bundle : Fixtures de données
  • Doctrine Migrations : Gestion des migrations

📁 Structure du projet

backend/
├── src/
│   ├── Domain/                    # Couche domaine
│   │   ├── Entity/               # Entités métier
│   │   ├── Repository/           # Interfaces repositories
│   │   ├── Service/              # Services métier
│   │   ├── ValueObject/          # Objets de valeur
│   │   ├── Event/                # Événements
│   │   ├── Exception/            # Exceptions métier
│   │   ├── Enum/                 # Énumérations (ex. Onboarding/OnboardingStep ; Optin/OptinType)
│   │   ├── Security/             # Sécurité domaine
│   │   ├── Notification/         # Notifications
│   │   └── Messenger/            # Messages (ex. SendMonthlyReports)
│   ├── Application/              # Couche application
│   │   ├── Command/              # Commandes CQRS
│   │   ├── Handler/              # Gestionnaires
│   │   └── Service/              # Services application
│   └── Infrastructure/           # Couche infrastructure
│       ├── Api/                  # API Platform
│       │   ├── Resource/         # Resources API
│       │   └── State/            # Processors API
│       ├── Orm/                  # Doctrine ORM
│       ├── Security/             # Sécurité infrastructure
│       ├── Notification/         # Services notification
│       ├── Symfony/              # Intégrations Symfony
│       │   └── EventSubscriber/ # Subscribers de Domain Events (emails, side effects)
│       ├── AuthCode/             # Codes d'authentification
│       ├── PdfParser/            # Parsing PDF
│       ├── IcsParser/            # Parsing ICS
│       └── PhoneNumber/          # Gestion numéros
├── config/                       # Configuration
├── migrations/                   # Migrations Doctrine
├── fixtures/                     # Fixtures de données
└── tests/                        # Tests

🔌 API Documentation

Endpoints principaux

Authentification

  • POST /security/login - Connexion JWT
  • POST /security/password-reset/request - Demande reset mot de passe
  • PUT /security/password-reset/perform - Reset mot de passe
  • POST /security/register - Inscription
  • POST /security/auth-code/send - Envoi code d'authentification
  • POST /security/auth-code/verify - Vérification code
  • POST /security/oauth/login - Connexion OAuth

Utilisateurs

  • GET /me - Informations utilisateur connecté. Pour un formateur : inclut onboardingProgress (état DISC-016 : étapes, compteurs, completedAt, checklistDismissedAt, sampleData : active, expiresAt, dismissedAt, firstRealReservationReceivedAt ; fenêtre J+N depuis registered_at + teadle.sample_data_duration_days).
  • GET /trainer/onboarding/sample-data — Si le sample data est actif : payload illustratif (mêmes DTOs que demandes + dashboard : requests, interventions, dashboard, expiresAt) ; sinon { "active": false } (200). Formateur authentifié uniquement. Le contenu fictif est défini dans backend/config/packages/teadle_onboarding_sample_data.yaml (paramètre teadle_onboarding_sample_data, lu via ParameterBagProviderInterface) ; GetTrainerSampleDataHandler renvoie TrainerSampleDataResultDto (VO Domain pour les demandes / interventions, DTO Application pour le bloc dashboard figé YAML) ; TrainerSampleDataResponseMapper produit SampleDataResponse pour API Platform.
  • Prévu (sample organisations) : même principe que demandes / dashboard — ajouter côté backend une slice dédiée (organisations ou assignments alignée sur le listing /trainer/organization/dashboard ou équivalent consommé par OrganizationsView) lorsque le sample data est actif : données fictives dans le YAML + mapping dans TrainerSampleDataResultDto / SampleDataResponse (et pas de logique « sample » dans le listing frontend, comme pour les autres blocs). À traiter dans une issue API dédiée quand le périmètre métier (nombre de lignes, statuts, UUID fictifs) sera figé.
  • POST /trainer/onboarding/sample-data/dismiss — Masque les données d’exemple (sample_data_dismissed_at), réponse = TrainerOnboardingProgressResponse (snapshot onboardingProgress). Sans corps. Formateur authentifié uniquement.
  • POST /trainer/onboarding/dismiss — Masque la checklist « Démarrer » (checklist_dismissed_at), même réponse TrainerOnboardingProgressResponse. Idempotent. Formateur authentifié uniquement (FEAT-088).
  • POST /trainer/onboarding/restore — Réaffiche la checklist (checklist_dismissed_at = null), même réponse. Idempotent (FEAT-088).
  • POST /trainer/onboarding/step/{step}/complete — Marque une étape OnboardingStep comme complétée via MarkTrainerOnboardingStepCompletedCommand ; {step} invalide → 400. Réponse TrainerOnboardingProgressResponse. Idempotent (FEAT-088).
  • PUT /trainer/updatePreferences - Mise à jour préférences formateur
  • PUT /organization/updatePreferences - Mise à jour préférences organisation
  • PUT /organization/updateInfo - Mise à jour infos organisation

Dashboard formateur (/trainer/dashboard/*)

  • GET /trainer/dashboard/summary - Résumé (revenu mensuel, heures, taux d’utilisation, clients actifs, taux de réponse). Les agrégations « mois courant » et le trimestre clients actifs utilisent le fuseau du formateur (user.timezone, défaut Europe/Paris). La réponse inclut periodMonth (1–12), periodYear, timezone (IANA), plus les enrichissements FEAT-074 : weeklyLoad (interventionCount, totalHours, realizedHours, dailyBreakdown), monthlySessions (total, realized, previousMonthTotal, evolution), schoolBreakdowns (top 3 écoles), monthlyRevenue.realizedAmount/realizedHours, monthlyHours.previousMonthHours/evolution, et FEAT-076 : concentrationCA (percentage, topClient, level), effectiveHourlyRate (current, previousMonth, evolution), annualEvolution (currentYearRevenue, previousYearRevenue, evolutionPercent, comparisonStartMonth, comparisonEndMonth, comparisonYear). Les mois de comparaison sont renvoyés en numérique (1..12) pour traduction côté frontend.
  • GET /trainer/dashboard/revenue-evolution - Évolution du revenu (fenêtre calendaire selon le fuseau formateur). Période par défaut 12_months (plus 6_months et year). Retourne toujours 200 (plus de gating onboarding).
  • GET /trainer/dashboard/upcoming-interventions - Prochaines interventions. Paramètre scope supporté (today, week, défaut null = comportement historique upcoming). Chaque item expose désormais durationHours. Retourne toujours 200 (plus de gating onboarding).
  • GET /trainer/dashboard/recent-requests - Demandes récentes. Limite par défaut portée à 5 (surchargable via limit) ; chaque item expose module, slotsCount, conflictsCount (initialisé à 0 dans FEAT-074). Retourne toujours 200 (plus de gating onboarding).
  • GET /trainer/dashboard/revenue-by-client - Revenu par client. Paramètre period supporté : quarter (défaut), year, 12_months. Chaque item inclut trend (up/down/stable), nextSession (ISO ou null), healthStatus (green/orange/red), organizationStatus (ok/conflict/error/none, même règle que /trainer/organization/dashboard) et organizationId (UUID pour deeplink facturation). Retourne toujours 200 (plus de gating onboarding).
  • GET /trainer/dashboard/badges — Compteurs pour le polling (sidebar / barre d’état) : notifications (notifications in-app non lues, non expirées — même filtre que /notification/list), requests { pending, urgent, newUnseen } (newUnseen = firstViewAt null sur les lots NEW/PENDING non expirés), et unreadInbox { total, conversations, notifications, critical } : agrégat inbox unifiée (total = conversations + notifications ; critical = notifications non lues de niveau URGENT). Le front limite déjà la fréquence des appels (ex. 2 min). Pas de 409 onboarding.
  • GET /staff/dashboard/badges — Même principe que le formateur (notifications, requests, unreadInbox). pending compte les demandes (groupes ReservationBatchGroup avec statut agrégé « en attente », comme l’onglet Mes demandes), pas chaque lot isolément ; urgent / newUnseen restent par lot NEW/PENDING dans ces demandes.

Notifications in-app (NotificationResource)

  • GET /notification/list — Liste des notifications actives non lues pour l’utilisateur connecté (sous-types discriminés : reservation, invoice, normal, association, calendar ; champs communs incluent uuid, dismissedAt, title, body). 401 si non authentifié (plus de réponse vide anonyme).
  • GET /notifications/search — Recherche paginée (mêmes sous-types). Paramètres query (comme la liste conversations) : page, limit ou itemsPerPage, sort[…], filters[…] — ex. filters[uuid] pour cibler une notification par UUID. 401 si non authentifié.
  • POST /notifications/read-all — Marque toutes les notifications actives encore non lues comme lues ; réponse JSON { "updated": <nombre> }.
  • POST /notifications/{uuid}/read — Marque une notification comme lue (readAt) ; idempotent si déjà lue ; réponse = ressource notification (même forme que les autres endpoints) ; 403 / 404 comme ci-dessous.
  • POST /notifications/{uuid}/dismiss — Persiste dismissedAt pour la notification identifiée par uuid (jamais par id entier) ; 403 si la notification appartient à un autre utilisateur ; 404 si absente ou UUID invalide.

Demandes de réservation (formateur)

  • GET /trainer/reservation/requests — Liste des lots (lecture seule pour firstViewAt : la liste ne marque plus le lot comme vu ; seul PATCH …/view met à jour firstViewAt) ; chaque élément inclut notamment createdAt, firstViewAt, respondedAt, contactRole (rôle du membership organisation si présent). Chaque entrée de reservations[] expose weekEvents : pour la semaine lun–ven du créneau, fusion des événements du planning du formateur et des créneaux demandés du lot en cours (titres du type Demande — {module}), triés par heure de début. Chaque entrée de weekEvents inclut isMainEvent (créneau correspondant à la ligne reservations[] courante — « celui sur lequel on raisonne ») et isBatchEvent (autre créneau du même lot) ; les événements planning ont les deux à false. Vide sans contexte formateur.
  • PATCH /trainer/reservation/batch/{uuid}/view — Marque le lot comme vu (firstViewAt), idempotent ; 204 sans corps ; 404 si inconnu ; 401 si le lot n’appartient pas au formateur connecté.
  • POST /trainer/reservation/batches/mark-all-as-viewed — Marque en masse tous les lots du formateur comme vus (firstViewAt) avec une mise à jour idempotente (firstViewAt IS NULL uniquement), puis renvoie 200 avec le même payload que GET /trainer/reservation/requests (liste + compteurs).
  • accept / refuse : idempotents si le lot est déjà ACCEPTED ou REFUSED (réponse DTO sans ré-exécuter les effets de bord — pas de double création d’événements agenda ; un second refuse ignore les nouveaux motif/message). slots inchangé.

  • GET /trainer/organization/dashboard — Carte organisations : par assignation, métriques planifiées (totalHours, totalRevenue) et réalisées (totalRealizedHours, totalRealizedRevenue, realizedHoursPercent), plus calendarColor, organizationInitials, lastImportSuccessful, isSyncPossible (aligné sur CalendarConfigurator::canSyncFromRemote() : URL iCal, fichier .ics serveur ou jetons OAuth), providerType (id technique du connecteur FEAT-043 : google, edusign, hyperplanning, ical, etc. — null si pas de configurateur ; libellé FR dérivé côté front), countUnlinkedAssignments (nombre d’assignations iCal actives sans PMO / « à relier » ; remplace l’ancien booléen hasSynchonizedIcalError), lastImportErrorCode (enum string, présent seulement si lastImportSuccessful est false — libellés côté front), et status (enum métier ok/conflict/error/none calculé côté backend : none si aucun configurateur, error si dernier import KO, conflict si au moins une assignation active non reliée, sinon ok). La règle est mutualisée dans Domain/Service/Dashboard/OrganizationStatusResolver. Compteur global inchangé : countSynchonizedIcalError.

Dashboard (autres)

  • GET /dashboard/trainer/income - Revenus formateur
  • GET /dashboard/trainer/favorite-organizations - Organisations favorites
  • GET /dashboard/trainer/events - Événements formateur
  • GET /dashboard/trainer/opportunity-proposals - Propositions d'opportunités

Calendrier / connecteurs (formateur, FEAT-043)

  • GET /trainer/calendar/configurator/{uuid} — réponse configurateur inclut providerType (sélection hub), icalUrl et defaultPrice (tarif horaire par défaut du formateur : trainer_billing_settings.default_price si défini, sinon paramètre applicatif trainer.default_hourly_price, défaut 35). Les imports iCal / réservation utilisent la même résolution via TrainerDefaultHourlyPriceResolver.
  • POST .../configurator/{organizationUuid}/ical/add — corps JSON : icalLink, optionnel providerType ; enregistre l’URL et le type sur le configurateur (jetons OAuth effacés si présents), renvoie un process, puis import asynchrone via ProcessTrainerCalendarConfiguratorSync (comme le callback OAuth).
  • POST .../configurator/{organizationUuid}/ical/upload — multipart icsfile, optionnel providerType ; écrit le .ics sur S3, met à jour le configurateur (ical_file, URL iCal effacée, jetons OAuth effacés), renvoie un process, puis ProcessTrainerCalendarConfiguratorSync.
  • POST /trainer/calendar/configurator/{uuid}/sync — relance d’import.
  • POST /trainer/calendar/oauth/{provider}/authorizeAPI Platform (JWT formateur) : corps JSON {"organizationUuid":"<uuid>"} ; réponse JSON authorizationUrl. Fournisseurs : google, outlook, apple, edusign, hyperplanning, ypareo, ical (URL). Les stubs OAuth répondent 501 ; edusign / hyperplanning / ypareo exposent toutefois l’enrichissement iCal (parseurs PRODID via les stratégies connecteur (parseIcalVo / *IcalCalendarEventParser) sur le VO Event, champ supportIcal du hub inchangé côté API). Google / Outlook utilisent GOOGLE_CLIENT_* / MICROSOFT_CLIENT_* + TRAINER_CALENDAR_*_REDIRECT_URI : URL du SPA (ex. https://app…/trainer/calendar/oauth/google/callback) enregistrée chez le fournisseur ; identique à l’URL utilisée pour l’échange du code.
  • POST /trainer/calendar/oauth/{provider}/calendar/listAPI Platform (TrainerCalendarOAuthCalendarListProcessor) : JWT formateur ; corps { "code": "…", "state": "…" } (ou error). Échange le code contre des jetons sans les persister ; renvoie la liste des agendas (disabled si pour ce formateur l’identifiant d’agenda distant est déjà lié à une autre organisation ou déjà utilisé comme agenda personnel OAuth de ce formateurTrainerOAuthRemoteCalendarListConflictMarker, requêtes filtrées par formateur). Réponse JSON tokens + calendars.
  • POST /trainer/calendar/oauth/{provider}/callbackAPI Platform (TrainerCalendarOAuthCallbackProcessor) : JWT formateur ; corps JSON { "accessToken", "refreshToken?", "expiresAt?", "calendarId", "state" } ou { "error" }. Vérifie le state signé, enregistre jetons + oauth_calendar_id sur calendar_configurator, async ProcessTrainerCalendarConfiguratorSync. Réponse 200 ProcessResponse (comme iCal add / sync) : uuid, metadata (dont organization_uuid, calendar_oauth_provider). Erreur ou paramètres invalides : 400.

Company & Facturation (formateur)

  • GET /trainer/company - Entreprise du formateur (identité + infos bancaires uniquement)
  • PUT /trainer/company - Mise à jour entreprise (identité + bancaire)
  • GET /trainer/billing/settings - Paramètres de facturation (TVA, unité, délai, tarif par défaut, mentions, template email, logo)
  • PUT /trainer/billing/settings - Mise à jour des paramètres de facturation
  • GET /trainer/invoices/config - Config facturation (company + billing séparés, organizations, products ; utilisé pour préremplir la création de facture)
  • GET /trainer/invoices/{uuid} - Détail facture (inclut defaultVatRate, defaultUnit, paymentTerm appliqués, en lecture seule)
  • GET /trainer/invoices/{uuid}/pdf - PDF facture : en brouillon (DRAFT), généré à la volée sans stockage S3 ; dès que la facture n’est plus brouillon (FINALIZED, SENT, PAID, etc.), le backend sert d’abord l’objet S3 {trainerUuid}/billing/invoices/{invoiceUuid} (même clé que celle écrite à la finalisation via InvoiceFinalizedEvent), et ne régénère + réécrit que si l’objet est absent.
  • GET /trainer/activity-reports/{uuid}/pdf - PDF CRA : toujours généré à la volée à chaque requête, sans stockage S3.

Validation factures : l'entité Invoice impose dueDate >= issueDate lorsque les deux dates sont renseignées (Assert Symfony sur le Domain). La commande UpdateInvoiceCommand valide avant persistance ; les Processors Create/Update renvoient 400 en cas de violation.

Format des réponses

Toutes les réponses sont au format JSON avec la structure suivante :

{
  "data": {
    // Données de la réponse
  },
  "meta": {
    "timestamp": "2025-01-13T10:00:00Z",
    "version": "1.0.0"
  }
}

🔒 Sécurité

Authentification

JWT (JSON Web Tokens)

  • Algorithme : RS256
  • Durée de vie : 1 heure
  • Refresh token : 30 jours
  • Header : Authorization: Bearer <token>

OAuth 2.0

  • Providers supportés : LinkedIn, Google, Microsoft
  • Flow : Authorization Code Flow
  • Scopes : email, profile

Codes d'authentification

  • Format : 6 chiffres
  • Durée de vie : 10 minutes
  • Envoi : Email via Brevo

Autorisation

Rôles utilisateurs

  • ROLE_USER : Utilisateur de base
  • ROLE_TRAINER : Formateur
  • ROLE_ORGANIZATION : Organisation
  • ROLE_ADMIN : Administrateur

Contrôle d'accès

  • Routes publiques : /security/*, /onboarding/*, /health/*
  • Routes protégées : Toutes les autres routes
  • Validation : Via annotations Symfony Security

🗄️ Base de données

Schéma principal

erDiagram
    User {
        int id PK
        string firstname
        string lastname
        string email UK
        string password
        array roles
        string oauthProvider
        string oauthProviderId UK
        boolean acceptCgv
        boolean acceptOptIn
        boolean verifiedEmail
        boolean finishedOnBoarding
        string type
    }

    Trainer {
        string SIRET
        json onboardingCompletedSteps
    }

    Organization {
        string name
        string organizationType
        string service
        phone_number phoneNumber
    }

    Event {
        int id PK
        int trainer_id FK
        datetime date_start
        datetime date_end
        string type
    }

    InterventionEvent {
        int organization_id FK
        string name
        float amount
        boolean is_approve_by_organization
    }

    Opportunity {
        int id PK
        int organization_id FK
        string name
        datetime date_start
        datetime date_end
    }

    OpportunityProposal {
        int id PK
        int opportunity_id FK
        int trainer_id FK
        int candidacy_id FK
    }

    Candidacy {
        int id PK
        datetime date
    }

    Invoice {
        int id PK
        int trainer_id FK
        int organization_id FK
        int intervention_event_id FK
        string amount
    }

    User ||--|| Trainer : "inherits"
    User ||--|| Organization : "inherits"
    Trainer ||--o{ Event : "has"
    Trainer ||--o{ Candidacy : "has"
    Trainer ||--o{ Invoice : "has"
    Organization ||--o{ InterventionEvent : "has"
    Organization ||--o{ Opportunity : "has"
    Organization ||--o{ Invoice : "has"
    Opportunity ||--o{ OpportunityProposal : "has"
    Trainer ||--o{ OpportunityProposal : "proposes"
    Candidacy ||--o{ OpportunityProposal : "linked_to"

Onboarding formateur

Parcours unique DISC-016 :

  • Étapes backend : CREATE_ACCOUNT, VERIFY_AVAILABILITY, SHARE_LINK, IMPORT_CALENDAR, DEFINE_PRICING.
  • L’avancement est stocké sur le Trainer dans onboardingCompletedSteps (JSON : liste de valeurs d’enum des étapes validées). À l’inscription (CreateNewTrainerCommand, formulaire ou OAuth), CREATE_ACCOUNT est enregistrée dès la création du formateur.
  • L’exposition API canonique passe par onboardingProgress (GetTrainerOnboardingProgressHandler + TrainerOnboardingProgressResponse).

Migrations

Le projet utilise Doctrine Migrations pour la gestion du schéma :

# Créer une nouvelle migration
php bin/console doctrine:migrations:diff

# Exécuter les migrations
php bin/console doctrine:migrations:migrate

# Annuler la dernière migration
php bin/console doctrine:migrations:migrate prev

🚀 Déploiement

Prérequis

  • PHP : 8.2+
  • Composer : 2.0+
  • PostgreSQL : 13+
  • Redis : 6.0+ (optionnel, pour le cache)

Variables d'environnement

# Base de données
DATABASE_URL="postgresql://user:password@localhost:5432/teadle"

# Sécurité
APP_SECRET="your-secret-key"
JWT_SECRET_KEY="path/to/private.key"
JWT_PUBLIC_KEY="path/to/public.key"

# OAuth
LINKEDIN_CLIENT_ID="your-linkedin-client-id"
LINKEDIN_CLIENT_SECRET="your-linkedin-client-secret"
LINKEDIN_REDIRECT_URI="https://your-domain.com/oauth/linkedin/callback"

GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
GOOGLE_REDIRECT_URI="https://your-domain.com/oauth/google/callback"

MICROSOFT_CLIENT_ID="your-microsoft-client-id"
MICROSOFT_CLIENT_SECRET="your-microsoft-client-secret"
MICROSOFT_REDIRECT_URI="https://your-domain.com/oauth/microsoft/callback"

# OAuth calendrier formateur (callbacks dédiés ; identifiants = GOOGLE_CLIENT_* / MICROSOFT_CLIENT_* ci-dessus)
TRAINER_CALENDAR_GOOGLE_REDIRECT_URI="https://app.example.com/trainer/calendar/oauth/google/callback"
TRAINER_CALENDAR_MICROSOFT_REDIRECT_URI="https://app.example.com/trainer/calendar/oauth/outlook/callback"
TRAINER_CALENDAR_OAUTH_SUCCESS_REDIRECT_URL="https://app.example.com"

# Email (Brevo)
BREVO_SECRET="your-brevo-api-key"

# URLs
PASSWORD_RESET_LINK="https://your-domain.com/password-reset"

Installation

# Installer les dépendances
composer install --no-dev --optimize-autoloader

# Configurer la base de données
php bin/console doctrine:database:create
php bin/console doctrine:migrations:migrate

# Charger les fixtures (développement)
php bin/console hautelook:alice:doctrine:load

# Vider le cache
php bin/console cache:clear --env=prod

# Configurer les permissions
chmod -R 755 var/
chmod -R 755 public/

Commandes utiles

# Analyser le code
php bin/console phpstan:analyse

# Corriger le style de code
php bin/console php-cs-fixer:fix

# Générer les clés JWT
php bin/console lexik:jwt:generate-keypair

# Vider le cache
php bin/console cache:clear

# Vérifier la santé de l'application
curl http://localhost:8000/health

# Le bilan mensuel est déclenché automatiquement par le Symfony Scheduler
# (via le worker supervisor symfony-scheduler consommant scheduler_default)
# Pas de commande console à lancer manuellement

📚 Ressources supplémentaires