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
Calcule les offres de maintenance personnalisées en fonction d'une demande client. Ce service gère trois composants de prix :
Voir la documentation détaillée du calcul des tarifs pour comprendre la logique complète, les coefficients et les tests unitaires.
/**
* 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
// 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
]
Gère la génération et la signature électronique des contrats de maintenance.
/**
* 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
Gère la validation et la recherche des codes postaux.
/**
* Trouve une ville par son code postal
*/
public function getCityByCode(string $code): ?string
/**
* Valide un code postal
*/
public function isValid(string $code): bool
Intègre l'API YouSign pour la signature électronique des documents.
/**
* 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.
Gère l'anonymisation des données personnelles pour la conformité RGPD.
/**
* 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
Gère les sessions de paiement Stripe selon le type de règlement choisi par le client.
/** 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
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 :
unit_amount envoyé à Stripe est le prix mensuel de base (getTtcBase()), sans réduction.duration: repeating et duration_in_months: 12.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
| Type de coupon | Qui peut l'utiliser | Durée | Stripe |
|---|---|---|---|
numeric | Particuliers uniquement | Première année seulement | Coupon Stripe repeating 12 mois |
percent | Particuliers et professionnels | Toute la durée du contrat | Prix 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).Tous les services sont injectés via le constructeur :
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly GotenbergPdfInterface $gotenbergPdf,
private readonly Filesystem $filesystem,
// ...
) {}
Utilisation d'exceptions personnalisées :
if (!$this->isValid($code)) {
throw new InvalidPostalCodeException($code);
}
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/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']);
}
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'])
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
Déclenché lorsqu'un contrat est généré.
Propriétés :
Contract $contract\DateTimeImmutable $createdAtDéclenché après le calcul des offres.
Propriétés :
Request $requestarray $offers\DateTimeImmutable $createdAtToutes 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...
}
| Version | Date | Description |
|---|---|---|
| 1.0.0 | 2023-01-15 | Version initiale |
| 1.1.0 | 2023-03-22 | Ajout des coefficients dynamiques |
| 1.2.0 | 2026-02-16 | Fix : Calcul de la main d'œuvre par catégorie d'équipement (au lieu du total) |
coefficient_rate table)