Save France
Backend

Intégration YouSign

Gestion de la signature électronique des contrats avec YouSign

Introduction

L'intégration YouSign permet la signature électronique des contrats de maintenance. Le système gère automatiquement la création des demandes de signature, le suivi via webhook, et propose une vérification manuelle en cas de défaillance du webhook.

Architecture

flowchart TD
    A[Demande de signature] --> B[YouSignService]
    B --> C[Création signature YouSign]
    C --> D[Envoi email au client]
    D --> E{Client signe ?}
    E -->|Oui| F[Webhook YouSign]
    E -->|Oui| G[Vérification manuelle]
    F --> H[ContractSignatureController]
    G --> I[CheckSignatureActionController]
    H --> J[Mise à jour statut + emails]
    I --> J
    J --> K[Dispatch CreateStripePaymentMessage]
    K --> L[Worker Messenger]
    L --> M[Création paiement Stripe]
    L -->|Échec| N[Retry automatique x3]
    
    style F fill:#4CAF50
    style G fill:#FFA726
    style H fill:#42B883
    style I fill:#FF6B6B
    style L fill:#2196F3
    style N fill:#FF9800

YouSignService

Vue d'ensemble

Service principal pour l'intégration avec l'API YouSign v3.

Emplacement : api/sources/src/Service/Yousign/YouSignService.php

Configuration

Variables d'environnement requises :

# API YouSign
YOUSIGN_API_KEY=your_api_key_here
YOUSIGN_API_URL=https://api.yousign.app/v3
YOUSIGN_WEBHOOK_SECRET=your_webhook_secret

# URL de l'application
HOST=save-france.fr

Méthodes principales

createSignature()

Crée une demande de signature complète pour un contrat.

/**
 * Crée une demande de signature YouSign
 * 
 * @param Contract $contract Le contrat à faire signer
 * @return array ['signId' => string, 'signUrl' => string]
 * @throws \Exception Si la création échoue
 */
public function createSignature(Contract $contract): array

Workflow :

  1. Vérifie l'existence du PDF du contrat
  2. Crée une signature request sur YouSign
  3. Upload le document PDF
  4. Ajoute le signataire avec ses informations
  5. Active la signature request
  6. Retourne l'ID de signature et l'URL de signature

Exemple d'utilisation :

$result = $youSignService->createSignature($contract);
// ['signId' => '123e4567-e89b...', 'signUrl' => 'https://yousign.app/procedure/...']

$contract->setSignId($result['signId']);
$contract->setSignUrl($result['signUrl']);

downloadDocument()

Télécharge le document signé depuis YouSign.

/**
 * Télécharge le document signé
 * 
 * @param string $signRequestId ID de la signature request
 * @return string Contenu binaire du PDF
 * @throws \Exception Si le téléchargement échoue
 */
public function downloadDocument(string $signRequestId): string

Exemple d'utilisation :

$pdfContent = $yousignService->downloadDocument($contract->getSignId());
file_put_contents(Contract::PDF_DIRECTORY . '/' . $contract->getId() . '.pdf', $pdfContent);

getSignatureStatus()

Vérifie l'état actuel d'une signature sur YouSign.

/**
 * Récupère le statut d'une signature depuis l'API YouSign
 * 
 * @param string $signRequestId ID de la signature request
 * @return array ['status' => string, 'signed_at' => ?string, 'signers' => array]
 * @throws \Exception Si la récupération échoue
 */
public function getSignatureStatus(string $signRequestId): array

Statuts possibles :

  • ongoing : Signature en cours
  • done : Signature complétée
  • expired : Demande expirée
  • declined : Signature refusée
  • canceled : Demande annulée

Exemple d'utilisation :

try {
    $status = $yousignService->getSignatureStatus($contract->getSignId());
    
    if ($status['status'] === 'done') {
        // Traiter la signature complétée
        echo "Signé le : " . $status['signed_at'];
    }
} catch (\Exception $e) {
    // Signature introuvable ou erreur API
    $logger->error('Erreur vérification signature', ['error' => $e->getMessage()]);
}

Webhooks YouSign

ContractSignatureController

Reçoit et traite les notifications de YouSign lorsqu'un contrat est signé.

Route : POST /webhook/contract-signed

Emplacement : api/sources/src/Controller/Webhook/ContractSignatureController.php

Sécurité du webhook

Le webhook vérifie la signature HMAC-SHA256 pour s'assurer de l'authenticité :

private function verifySignature(string $payload, ?string $signature, string $secret): bool
{
    if (empty($signature)) {
        return false;
    }

    $digest = hash_hmac('sha256', $payload, $secret);
    $computedSignature = 'sha256=' . $digest;

    return hash_equals($signature, $computedSignature);
}

Header requis : X-Yousign-Signature-256

Traitement du webhook

sequenceDiagram
    participant YS as YouSign
    participant W as Webhook Controller
    participant DB as Base de données
    participant M as MailerService
    participant Bus as Messenger Bus
    participant Worker as Worker (async)
    participant S as StripeService

    YS->>W: POST /webhook/contract-signed
    W->>W: Vérification signature HMAC
    W->>DB: Récupération contrat via signId
    W->>W: Vérifications (statut, expiration)
    W->>YS: Téléchargement document signé
    W->>DB: Contrat → SIGNED, Request → WAITING_PAYMENT
    W->>M: Envoi email client + admin
    W->>Bus: Dispatch CreateStripePaymentMessage
    W-->>YS: HTTP 200 OK (immédiat)
    Bus-->>Worker: Consommation async
    Worker->>S: Création session paiement Stripe
    S-->>Worker: Session (url + id)
    Worker->>DB: Mise à jour paymentUrl / paymentId
    note over Worker: En cas d'échec Stripe :<br/>retry x3 (1s, 2s, 4s)<br/>puis transport "failed"

Résilience Stripe via Messenger

Depuis la mise en place de Symfony Messenger, la création de la session de paiement Stripe est découplée du webhook YouSign :

  • YouSign reçoit toujours un HTTP 200 dès que les actions de signature sont traitées (PDF téléchargé, contrat signé, emails envoyés).
  • La création Stripe s'exécute en arrière-plan via un worker dédié (messenger:consume async).
  • En cas d'échec Stripe, Messenger effectue 3 retries automatiques (délais exponentiels : 1s, 2s, 4s).
  • Si les 3 retries échouent, le message va dans le transport failed pour traitement manuel.
# Voir les messages en échec
docker compose exec api php bin/console messenger:failed:show

# Rejouer un message en échec
docker compose exec api php bin/console messenger:failed:retry

Voir la documentation Messenger pour les détails sur la configuration et le monitoring du worker.

Cas de non-réception du webhook

Problèmes possibles :

  • Erreur réseau temporaire
  • Configuration webhook incorrecte sur YouSign
  • Firewall bloquant les requêtes
  • Service temporairement indisponible

Solution : Vérification manuelle → Voir section suivante

Vérification Manuelle de Signature

CheckSignatureActionController

Action EasyAdmin permettant de vérifier manuellement l'état d'une signature et de synchroniser si nécessaire.

Route : GET /check_signature/request/{id}

Emplacement : api/sources/src/Controller/Admin/Request/CheckSignatureActionController.php

Fonctionnalités

  1. Vérifications préalables
    • Demande en statut WAITING_SIGNATURE
    • Contrat existant avec sign_id valide
  2. Interrogation de l'API YouSign
    • Récupère le statut actuel de la signature
    • Compare avec l'état local
  3. Synchronisation automatique Si signature complétée (done) :
    • Crée la session de paiement Stripe
    • Télécharge le document signé
    • Met à jour le statut vers WAITING_PAYMENT
    • Envoie les emails de confirmation
  4. Gestion des erreurs Si erreur API (signature introuvable) :
    • Invalide automatiquement le contrat (EXPIRED)
    • Affiche un message d'erreur explicite

Workflow de vérification manuelle

flowchart TD
    A[Admin clique sur 'Vérifier signature'] --> B{Demande en WAITING_SIGNATURE ?}
    B -->|Non| C[Message erreur]
    B -->|Oui| D{Contrat trouvé ?}
    D -->|Non| E[Message erreur]
    D -->|Oui| F{Sign ID existe ?}
    F -->|Non| G[Message erreur]
    F -->|Oui| H[Appel API YouSign]
    H -->|Erreur| I[Invalide contrat]
    H -->|Succès| J{Statut signature}
    J -->|done| K[Synchronisation complète]
    J -->|ongoing| L[Message info]
    J -->|expired| M[Invalide contrat]
    K --> N[Paiement Stripe créé]
    N --> O[Emails envoyés]
    O --> P[Message succès]
    
    style I fill:#FF6B6B
    style K fill:#4CAF50
    style P fill:#4CAF50

Messages utilisateur

Tous les messages sont traduits dans translations/messages.fr.yaml :

request:
  check_signature: "Vérifier l'état de la signature"
  check_signature_success: "La signature a été vérifiée et synchronisée..."
  check_signature_error:
    not_waiting_signature: "La demande n'est pas en attente de signature."
    contract_not_found: "Aucun contrat trouvé pour cette demande."
    no_sign_id: "Aucun ID de signature trouvé pour ce contrat."
    contract_expired: "Le contrat a expiré."
    payment_creation_failed: "Erreur lors de la création du paiement Stripe."
    signature_not_found: "Signature introuvable sur YouSign. Le contrat a été invalidé."
  check_signature_info:
    ongoing: "La signature est toujours en cours..."
    expired: "La demande de signature a expiré."
    status: "État de la signature: %status%"

Interface EasyAdmin

Le bouton "Vérifier l'état de la signature" apparaît :

  • ✅ Dans la page de détail d'une demande
  • ✅ Uniquement si statut = WAITING_SIGNATURE
  • ✅ Style : bouton jaune (btn-warning)

Configuration dans RequestCrudController.php :

$checkSignature = Action::new(self::ACTION_CHECK_SIGNATURE, 'request.check_signature')
    ->linkToRoute('app_check_signature_request', function (Request $request): array {
        return ['id' => $request->getId()];
    })
    ->displayIf(function (Request $request): bool {
        return $request->getStatus() === RequestStatusEnum::WAITING_SIGNATURE;
    })
    ->addCssClass('btn btn-warning')
;

Debugging et Logs

Vérifier si le webhook a été appelé

Logs Caddy :

# Rechercher les appels webhook dans les logs
docker-compose logs -f caddy | grep "webhook/contract-signed"

# Exemple de ligne de log attendue :
# {"level":"info","ts":1234567890,"msg":"POST /webhook/contract-signed","status":200}

Logs Symfony :

# Logs de production
docker-compose exec api tail -f var/log/prod.log

# Logs de développement
docker-compose exec api tail -f var/log/dev.log

Ajout de logs personnalisés

Pour ajouter des logs dans le webhook :

use Psr\Log\LoggerInterface;

public function __construct(
    private readonly LoggerInterface $logger,
    // ... autres dépendances
) {}

public function __invoke(Request $request, ...): Response
{
    $payload = $request->getContent();
    $signId = json_decode($payload, true)['data']['signature_request']['id'] ?? null;
    
    $this->logger->info('Webhook YouSign reçu', [
        'sign_id' => $signId,
        'ip' => $request->getClientIp(),
        'timestamp' => new \DateTime()
    ]);
    
    // ... traitement
}

Tester le webhook localement

Avec ngrok :

# Exposer le serveur local
ngrok http 80

# Configurer l'URL webhook sur YouSign Dashboard
# https://abc123.ngrok.io/webhook/contract-signed

Avec un payload de test :

curl -X POST https://your-domain.com/webhook/contract-signed \
  -H "Content-Type: application/json" \
  -H "X-Yousign-Signature-256: sha256=your_signature" \
  -d '{
    "event_name": "signature_request.done",
    "data": {
      "signature_request": {
        "id": "123e4567-e89b-12d3-a456-426614174000"
      }
    }
  }'

Gestion des Erreurs

Erreurs API YouSign

CodeSignificationAction
401API Key invalideVérifier YOUSIGN_API_KEY
404Signature request introuvableContrat invalidé automatiquement
422Données invalidesVérifier les données du contrat
500Erreur serveur YouSignRéessayer plus tard

Cas d'échec de signature

Scénario 1 : Client n'a pas signé avant expiration

// Commande exécutée quotidiennement
php bin/console app:contract:expire

// Marque les contrats expirés automatiquement
if ($contract->getExpiredAt() < new DateTime()) {
    $contract->setStatus(ContractStatusEnum::EXPIRED);
}

Scénario 2 : Signature refusée par le client

  • Statut YouSign : declined
  • Action : Invalider le contrat, notifier l'admin
  • L'utilisateur peut redemander un nouveau contrat

Scénario 3 : Erreur technique

  • Webhook non reçu → Vérification manuelle possible
  • API YouSign indisponible → Retry automatique
  • Document corrompu → Regénération nécessaire

Bonnes Pratiques

1. Gestion des statuts

Toujours vérifier le statut avant d'effectuer une action :

if ($request->getStatus() === RequestStatusEnum::WAITING_SIGNATURE) {
    // OK pour vérifier la signature
} else {
    // Ne pas traiter
}

2. Idempotence du webhook

Le webhook peut être appelé plusieurs fois pour la même signature :

// Vérification du statut actuel avant traitement
if ($request->getStatus() === RequestStatusEnum::WAITING_PAYMENT) {
    return new Response('', Response::HTTP_UNPROCESSABLE_ENTITY);
}

if ($request->getStatus() === RequestStatusEnum::COMPLETE) {
    return new Response('', Response::HTTP_UNPROCESSABLE_ENTITY);
}

3. Timeout API

Configurer un timeout approprié pour les appels API :

$client = HttpClient::create([
    'timeout' => 30,
    'max_duration' => 60,
    'headers' => [
        'Authorization' => 'Bearer ' . $this->apiKey,
    ]
]);

4. Stockage sécurisé des documents

// Définir les permissions correctes
$pdfPath = Contract::PDF_DIRECTORY . '/' . $contract->getId() . '.pdf';
file_put_contents($pdfPath, $pdfContent);
chmod($pdfPath, 0640); // Lecture seule pour le propriétaire et le groupe

Procédures d'Urgence

Webhook en panne

  1. Identifier les demandes bloquées
    SELECT id, created_at, sign_id 
    FROM request 
    WHERE status = 'WAITING_SIGNATURE' 
    AND created_at < NOW() - INTERVAL '2 hours';
    
  2. Vérification manuelle via EasyAdmin
    • Ouvrir chaque demande
    • Cliquer sur "Vérifier l'état de la signature"
    • Le système synchronisera automatiquement
  3. Vérification en masse (script)
    // Command personnalisée
    php bin/console app:signature:check-pending
    

Réinitialiser une signature

Si un contrat doit être resigné :

// 1. Invalider l'ancien contrat
$oldContract->setStatus(ContractStatusEnum::EXPIRED);

// 2. Générer un nouveau contrat
$newContract = $contractService->generateContract($request);

// 3. Lancer la nouvelle signature
$signature = $yousignService->createSignature($newContract);
$newContract->setSignId($signature['signId']);
$newContract->setSignUrl($signature['signUrl']);

Monitoring

Métriques à surveiller

  • Taux de signature (signés / envoyés)
  • Délai moyen de signature
  • Taux d'échec webhook
  • Nombre de vérifications manuelles

Alertes recommandées

  • ⚠️ Webhook non reçu depuis 1h
  • ⚠️ Plus de 5 vérifications manuelles en 1h
  • ⚠️ Taux d'échec API > 5%
  • ⚠️ Contrats en attente > 24h

Liens Utiles

Résumé

  • YouSign permet la signature électronique des contrats de maintenance
  • Le webhook automatique traite les signatures complétées en temps réel
  • Une vérification manuelle est disponible via EasyAdmin en cas de problème
  • L'API getSignatureStatus() permet de synchroniser manuellement l'état
  • Les contrats invalides ou expirés sont automatiquement marqués comme tels
  • Tous les messages sont traduits et les logs permettent le debugging