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.
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
Service principal pour l'intégration avec l'API YouSign v3.
Emplacement : api/sources/src/Service/Yousign/YouSignService.php
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
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 :
Exemple d'utilisation :
$result = $youSignService->createSignature($contract);
// ['signId' => '123e4567-e89b...', 'signUrl' => 'https://yousign.app/procedure/...']
$contract->setSignId($result['signId']);
$contract->setSignUrl($result['signUrl']);
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);
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 coursdone : Signature complétéeexpired : Demande expiréedeclined : Signature refuséecanceled : Demande annuléeExemple 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()]);
}
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
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
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"
Depuis la mise en place de Symfony Messenger, la création de la session de paiement Stripe est découplée du webhook YouSign :
messenger:consume async).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.
Problèmes possibles :
Solution : Vérification manuelle → Voir section suivante
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
WAITING_SIGNATUREsign_id validedone) :WAITING_PAYMENTEXPIRED)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
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%"
Le bouton "Vérifier l'état de la signature" apparaît :
WAITING_SIGNATUREbtn-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')
;
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
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
}
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"
}
}
}'
| Code | Signification | Action |
|---|---|---|
| 401 | API Key invalide | Vérifier YOUSIGN_API_KEY |
| 404 | Signature request introuvable | Contrat invalidé automatiquement |
| 422 | Données invalides | Vérifier les données du contrat |
| 500 | Erreur serveur YouSign | Réessayer plus tard |
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
declinedScénario 3 : Erreur technique
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
}
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);
}
Configurer un timeout approprié pour les appels API :
$client = HttpClient::create([
'timeout' => 30,
'max_duration' => 60,
'headers' => [
'Authorization' => 'Bearer ' . $this->apiKey,
]
]);
// 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
SELECT id, created_at, sign_id
FROM request
WHERE status = 'WAITING_SIGNATURE'
AND created_at < NOW() - INTERVAL '2 hours';
// Command personnalisée
php bin/console app:signature:check-pending
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']);
getSignatureStatus() permet de synchroniser manuellement l'état