Save France
Backend

Services Métiers

Services métiers et logique de l'application Symfony

Vue d'ensemble

Les services métiers constituent le cœur de la logique de l'application. Ils encapsulent les règles métier et les opérations complexes, assurant une séparation claire des responsabilités.

classDiagram
    class CalculateOfferFromRequestService {
        +calculate(Request $request): array
        -calculateOffers(): void
        -applyCoefficients(): void
    }
    
    class ContractService {
        +generateContract(Request $request): Contract
        +signContract(Contract $contract): void
        -generatePdf(Contract $contract): string
    }
    
    class PostalCodeService {
        +findByCode(string $code): ?PostalCode
        +getCityByCode(string $code): ?string
    }
    
    class YouSignService {
        +createSignatureRequest(Contract $contract): string
        +handleWebhook(array $data): void
    }
    
    CalculateOfferFromRequestService --> PostalCodeService
    ContractService --> YouSignService
    ContractService --> GotenbergPdf

Services Principaux

1. CalculateOfferFromRequestService

Objectif

Calcule les offres de maintenance personnalisées en fonction d'une demande client. Ce service gère trois composants de prix :

  • Déplacement (shift) : Basé sur le code postal et le type d'offre
  • Main d'œuvre (workforce) : Basé sur les équipements et coefficients par catégorie
  • Fournitures (supplies) : Pièces et consommables

Voir la documentation détaillée du calcul des tarifs pour comprendre la logique complète, les coefficients et les tests unitaires.

Méthodes clés

/**
 * Calcule toutes les offres possibles pour une demande donnée
 * 
 * @param Request $request La demande de maintenance
 * @return array Tableau des offres calculées avec détails des coûts
 */
public function calculate(Request $request): array

/**
 * Calcule le coût de la main d'œuvre
 * Utilise les coefficients par catégorie d'équipement
 */
private function calculateWorkforce(MaintenanceOffer $maintenanceOffer): PriceWorkforce

/**
 * Calcule les frais de déplacement
 */
private function calculateShift(MaintenanceOffer $maintenanceOffer): ?PriceShift

/**
 * Calcule le coût des fournitures
 */
private function calculateSupplies(MaintenanceOffer $maintenanceOffer): PriceSupplies

Utilisation

// Dans un contrôleur ou un gestionnaire de commande
$offers = $calculateOfferService->calculate($request);

// Exemple de sortie
[
    'basic' => [
        'id' => 'uuid',
        'type' => 'basic',
        'label' => 'Forfait Basique',
        'price' => 299.99,
        'description' => 'Description du forfait...',
        'includes' => ['Service 1', 'Service 2']
    ],
    // ... autres forfaits
]

2. ContractService

Objectif

Gère la génération et la signature électronique des contrats de maintenance.

Méthodes clés

/**
 * Génère un nouveau contrat à partir d'une demande
 * 
 * @param Request $request La demande de maintenance
 * @return Contract Le contrat généré
 * @throws \RuntimeException Si la génération échoue
 */
public function generateContract(Request $request): Contract

/**
 * Lance le processus de signature électronique
 */
public function signContract(Contract $contract): void

Workflow de signature

  1. Génération du PDF du contrat
  2. Envoi à YouSign pour signature
  3. Suivi du statut de signature
  4. Notification des parties prenantes

3. PostalCodeService

Objectif

Gère la validation et la recherche des codes postaux.

Méthodes clés

/**
 * Trouve une ville par son code postal
 */
public function getCityByCode(string $code): ?string

/**
 * Valide un code postal
 */
public function isValid(string $code): bool

4. YouSignService

Objectif

Intègre l'API YouSign pour la signature électronique des documents.

Méthodes clés

/**
 * Crée une demande de signature pour un contrat
 */
public function createSignature(Contract $contract): array

/**
 * Télécharge le document signé
 */
public function downloadDocument(string $signRequestId): string

/**
 * Vérifie l'état d'une signature sur l'API YouSign
 */
public function getSignatureStatus(string $signRequestId): array

Voir la documentation complète YouSign pour plus de détails sur l'intégration, les webhooks et la vérification manuelle.

Services de Données

1. AnonymizationService

Objectif

Gère l'anonymisation des données personnelles pour la conformité RGPD.

Méthodes clés

/**
 * Anonymise une demande et ses données associées
 */
public function anonymizeRequest(Request $request): void

/**
 * Vérifie si une demande peut être anonymisée
 */
public function canBeAnonymized(Request $request): bool

2. StripeService

Objectif

Gère les sessions de paiement Stripe selon le type de règlement choisi par le client.

Méthodes clés

/** Paiement unique (carte) : montant total du contrat en une seule fois. */
public function createOneShotPayment(Request $request): Session

/** Abonnement mensuel (SEPA) : prélèvement mensuel récurrent. */
public function createSubscriptionPayment(Request $request): Session

Gestion des coupons numéraires (abonnement SEPA)

Pour les abonnements (SUBSCRIPTION), un coupon numéraire doit s'appliquer uniquement la première année. Cela est géré nativement via l'API Stripe :

  1. Le unit_amount envoyé à Stripe est le prix mensuel de base (getTtcBase()), sans réduction.
  2. Un Stripe Coupon est créé dynamiquement avec duration: repeating et duration_in_months: 12.
  3. Stripe retire automatiquement la réduction après 12 mois, sans aucune tâche planifiée côté Symfony.

Pour les coupons pourcentage, le prix mensuel réduit est envoyé directement à Stripe (réduction permanente, conforme à la spec).

Admin EasyAdmin         Backend (webhook YouSign)        Stripe
──────────────          ────────────────────────         ──────
Crée Discount      →    createSubscriptionPayment()  →   Coupon Stripe (12 mois)
  type: numeric          → stripe->coupons->create()      automatiquement retiré
  amount: 50€            → checkout->sessions->create()   après 1 an

Règles métier coupons (résumé)

Type de couponQui peut l'utiliserDuréeStripe
numericParticuliers uniquementPremière année seulementCoupon Stripe repeating 12 mois
percentParticuliers et professionnelsToute la durée du contratPrix réduit directement dans unit_amount

Ces règles sont enforced à plusieurs niveaux :

  • RequestDiscountPostProcessor : rejette immédiatement un coupon numeric si le contact est Pro.
  • DiscountConstraintValidator : vérifie au PATCH que le coupon numeric n'est pas utilisé par un Pro, et qu'il n'a pas déjà été utilisé par le même email (contrôle anti-réutilisation au renouvellement).
  • Stripe (abonnement) : retire automatiquement la réduction après 12 mois.

Bonnes Pratiques

1. Injection de Dépendances

Tous les services sont injectés via le constructeur :

public function __construct(
    private readonly EntityManagerInterface $entityManager,
    private readonly GotenbergPdfInterface $gotenbergPdf,
    private readonly Filesystem $filesystem,
    // ...
) {}

2. Gestion des Erreurs

Utilisation d'exceptions personnalisées :

if (!$this->isValid($code)) {
    throw new InvalidPostalCodeException($code);
}

3. Journalisation

Utilisation du logger de Symfony :

use Psr\Log\LoggerInterface;

public function __construct(
    private readonly LoggerInterface $logger
) {}

public function someMethod() {
    $this->logger->info('Opération effectuée', ['id' => $id]);
}

Tests des Services

Exemple de test unitaire

// tests/Service/CalculateOfferFromRequestServiceTest.php
public function testCalculateBasicOffer(): void
{
    $service = new CalculateOfferFromRequestService(
        $this->createMock(MaintenanceOfferRepository::class),
        $this->createMock(PostalCodeService::class),
        $this->createMock(ParameterRepository::class),
        $this->createMock(CoefficientRateRepository::class)
    );
    
    $request = new Request();
    // Configuration de la requête...
    
    $result = $service->calculate($request);
    
    $this->assertArrayHasKey('basic', $result);
    $this->assertGreaterThan(0, $result['basic']['price']);
}

Workflows Métiers

1. Création d'une Offre

sequenceDiagram
    participant C as Contrôleur
    participant S as CalculateOfferFromRequestService
    participant R as Request
    participant O as Offer
    
    C->>S: calculate(Request $request)
    S->>R: getEquipment()
    S->>S: calculateBasePrice()
    S->>S: applyCoefficients()
    S->>S: formatResults()
    S-->>C: array $offers
    C->>O: createFromArray($offers['basic'])

2. Signature d'un Contrat

sequenceDiagram
    participant C as Contrôleur
    participant CS as ContractService
    participant YS as YouSignService
    participant DB as Base de données
    
    C->>CS: generateContract($request)
    CS->>CS: generatePdf($contract)
    CS->>YS: createSignatureRequest($contract)
    YS-->>CS: signatureRequestId
    CS->>DB: persist($contract)
    CS-->>C: $contract

Événements Métiers

1. ContractGeneratedEvent

Déclenché lorsqu'un contrat est généré.

Propriétés :

  • Contract $contract
  • \DateTimeImmutable $createdAt

2. OfferCalculatedEvent

Déclenché après le calcul des offres.

Propriétés :

  • Request $request
  • array $offers
  • \DateTimeImmutable $createdAt

Sécurité

Vérifications d'Accès

Toutes les méthodes sensibles doivent vérifier les autorisations :

public function signContract(Contract $contract, User $user): void
{
    if (!$this->authorizationChecker->isGranted('SIGN', $contract)) {
        throw new AccessDeniedException('Accès refusé');
    }
    
    // Suite du traitement...
}

Maintenance et Évolutions

Journal des Modifications

VersionDateDescription
1.0.02023-01-15Version initiale
1.1.02023-03-22Ajout des coefficients dynamiques
1.2.02026-02-16Fix : Calcul de la main d'œuvre par catégorie d'équipement (au lieu du total)

Améliorations Futures

  • Implémentation d'un système de cache pour les calculs fréquents
  • Ajout de méthodes de paiement supplémentaires
  • Intégration avec d'autres services de signature électronique

Dépannage

Problèmes Courants

  1. Calculs incorrects
    • Vérifier les coefficients en base de données (coefficient_rate table)
    • Vérifier les paramètres de la demande
    • S'assurer que les coefficients sont définis pour chaque catégorie d'équipement
    • Voir la documentation du calcul des tarifs pour les détails
  2. Échec de génération de PDF
    • Vérifier le service Gotenberg
    • Vérifier les droits d'écriture dans le répertoire de sortie
  3. Problèmes de signature
    • Vérifier les identifiants YouSign
    • Vérifier la configuration du webhook

Ressources