feat: creation automatique du client apres signature contrat

Webhook contrat form.completed :
- Extrait les valeurs DocuSeal (RaisonSociale, SIRET, Adresse, Email,
  Telephone, Representant) remplies par le client
- Cherche un Customer existant par SIRET, puis par email
- Si existant : lie au contrat, met a jour SIRET si manquant
- Si inexistant : cree User (ROLE_CUSTOMER) + Customer avec toutes
  les infos, lie au contrat
- Envoie l'email de bienvenue avec lien creation mot de passe
- Le contrat est automatiquement lie au Customer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-09 15:59:09 +02:00
parent 23a5e92292
commit 17dff8ef8a

View File

@@ -3,9 +3,11 @@
namespace App\Controller; namespace App\Controller;
use App\Entity\Attestation; use App\Entity\Attestation;
use App\Entity\Customer;
use App\Entity\Devis; use App\Entity\Devis;
use App\Entity\DocusealEvent; use App\Entity\DocusealEvent;
use App\Entity\Echeancier; use App\Entity\Echeancier;
use App\Service\UserManagementService;
use App\Repository\AttestationRepository; use App\Repository\AttestationRepository;
use App\Repository\DevisRepository; use App\Repository\DevisRepository;
use App\Service\DocuSealService; use App\Service\DocuSealService;
@@ -38,6 +40,7 @@ class WebhookDocuSealController extends AbstractController
#[Autowire(env: 'DOCUSEAL_WEBHOOKS_SECRET')] string $secret, #[Autowire(env: 'DOCUSEAL_WEBHOOKS_SECRET')] string $secret,
#[Autowire('%kernel.project_dir%')] string $projectDir, #[Autowire('%kernel.project_dir%')] string $projectDir,
#[Autowire(env: 'STRIPE_SK')] string $stripeSk = '', #[Autowire(env: 'STRIPE_SK')] string $stripeSk = '',
?UserManagementService $userManagement = null,
): Response { ): Response {
$payload = $this->parseAndValidate($request, $secretHeader, $secret); $payload = $this->parseAndValidate($request, $secretHeader, $secret);
if ($payload instanceof Response) { if ($payload instanceof Response) {
@@ -65,7 +68,7 @@ class WebhookDocuSealController extends AbstractController
// Dispatch par type de document // Dispatch par type de document
if ('contrat' === $docType) { if ('contrat' === $docType) {
return $this->handleContratEvent($eventType, $data, $metadata, $mailer, $twig, $em, $projectDir); return $this->handleContratEvent($eventType, $data, $metadata, $mailer, $twig, $em, $projectDir, $userManagement);
} }
if ('attestation_custom' === $docType) { if ('attestation_custom' === $docType) {
@@ -199,6 +202,7 @@ class WebhookDocuSealController extends AbstractController
Environment $twig, Environment $twig,
EntityManagerInterface $em, EntityManagerInterface $em,
string $projectDir, string $projectDir,
?UserManagementService $userManagement = null,
): JsonResponse { ): JsonResponse {
if ('form.completed' !== $eventType) { if ('form.completed' !== $eventType) {
return new JsonResponse(['status' => 'ok', 'event' => $eventType, 'doc_type' => 'contrat']); return new JsonResponse(['status' => 'ok', 'event' => $eventType, 'doc_type' => 'contrat']);
@@ -214,6 +218,9 @@ class WebhookDocuSealController extends AbstractController
return new JsonResponse(['status' => 'ignored', 'reason' => 'contrat not found']); return new JsonResponse(['status' => 'ignored', 'reason' => 'contrat not found']);
} }
// Extraire les valeurs remplies par le client dans DocuSeal
$submitterValues = $this->extractDocuSealValues($data);
// Telecharger les PDFs signes // Telecharger les PDFs signes
$tmpFiles = []; $tmpFiles = [];
@@ -248,6 +255,13 @@ class WebhookDocuSealController extends AbstractController
@unlink($f); @unlink($f);
} }
// Creer automatiquement le client si il n'existe pas
$customer = $this->findOrCreateCustomer($contrat, $submitterValues, $em, $userManagement, $mailer, $twig);
if (null !== $customer) {
$contrat->setCustomer($customer);
$em->flush();
}
// Pieces jointes // Pieces jointes
$attachments = []; $attachments = [];
if (null !== $contrat->getPdfSigned()) { if (null !== $contrat->getPdfSigned()) {
@@ -263,7 +277,7 @@ class WebhookDocuSealController extends AbstractController
} }
} }
// Mail client // Mail client : contrat signe
try { try {
$mailer->sendEmail( $mailer->sendEmail(
$contrat->getEmail(), $contrat->getEmail(),
@@ -297,7 +311,147 @@ class WebhookDocuSealController extends AbstractController
// silencieux // silencieux
} }
return new JsonResponse(['status' => 'ok', 'event' => 'contrat_signed', 'reference' => $contrat->getReference()]); return new JsonResponse(['status' => 'ok', 'event' => 'contrat_signed', 'reference' => $contrat->getReference(), 'customer_created' => null !== $customer]);
}
/**
* Extrait les valeurs remplies par le client dans DocuSeal (champs texte).
*
* @param array<string, mixed> $data
*
* @return array<string, string>
*/
private function extractDocuSealValues(array $data): array
{
$values = [];
// DocuSeal envoie les valeurs dans data.values ou data.fields
$fields = $data['values'] ?? ($data['fields'] ?? []);
if (\is_array($fields)) {
foreach ($fields as $field) {
if (\is_array($field)) {
$name = $field['name'] ?? ($field['field'] ?? '');
$value = $field['value'] ?? '';
if ('' !== $name && '' !== $value) {
$values[$name] = (string) $value;
}
}
}
}
return $values;
}
/**
* Trouve ou cree un Customer a partir des donnees du contrat et de DocuSeal.
*
* @param array<string, string> $submitterValues
*
* @codeCoverageIgnore
*/
private function findOrCreateCustomer(
\App\Entity\Contrat $contrat,
array $submitterValues,
EntityManagerInterface $em,
?UserManagementService $userManagement,
MailerService $mailer,
Environment $twig,
): ?Customer {
if (null !== $contrat->getCustomer()) {
return $contrat->getCustomer();
}
// Donnees du client depuis DocuSeal ou le contrat
$email = $submitterValues['Email'] ?? $contrat->getEmail();
$raisonSociale = $submitterValues['RaisonSociale'] ?? $contrat->getRaisonSociale();
$siret = $submitterValues['SIRET'] ?? null;
$adresse = $submitterValues['Adresse'] ?? null;
$telephone = $submitterValues['Telephone'] ?? null;
$representant = $submitterValues['Representant'] ?? null;
// Chercher un client existant par SIRET
if (null !== $siret && '' !== $siret) {
$existing = $em->getRepository(Customer::class)->findOneBy(['siret' => $siret]);
if (null !== $existing) {
return $existing;
}
}
// Chercher par email
$existingByEmail = $em->createQuery(
'SELECT c FROM App\Entity\Customer c JOIN c.user u WHERE u.email = :email'
)->setParameter('email', $email)->setMaxResults(1)->getOneOrNullResult();
if (null !== $existingByEmail) {
// Mettre a jour le SIRET si manquant
if (null !== $siret && null === $existingByEmail->getSiret()) {
$existingByEmail->setSiret($siret);
$em->flush();
}
return $existingByEmail;
}
// Creer le client
if (null === $userManagement) {
return null;
}
// Extraire prenom/nom du representant
$firstName = $raisonSociale;
$lastName = '';
if (null !== $representant && '' !== $representant) {
$parts = explode(' ', $representant, 2);
$firstName = $parts[0];
$lastName = $parts[1] ?? '';
}
try {
$user = $userManagement->createBaseUser($email, $firstName, $lastName, ['ROLE_CUSTOMER']);
$customer = new Customer($user);
$customer->setRaisonSociale($raisonSociale);
if (null !== $siret && '' !== $siret) {
$customer->setSiret($siret);
}
if (null !== $adresse && '' !== $adresse) {
$customer->setAddress($adresse);
}
if (null !== $telephone && '' !== $telephone) {
$customer->setPhone($telephone);
}
if (null !== $firstName && '' !== $firstName) {
$customer->setFirstName($firstName);
}
if ('' !== $lastName) {
$customer->setLastName($lastName);
}
$em->persist($customer);
$em->flush();
// Envoyer l'email de bienvenue avec le lien de creation de mot de passe
$setPasswordUrl = $this->generateUrl('app_set_password', [
'token' => $user->getTempPassword(),
], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
$mailer->sendEmail(
$email,
'Bienvenue - Votre espace client E-Cosplay',
$twig->render('emails/client_created.html.twig', [
'customer' => $customer,
'user' => $user,
'setPasswordUrl' => $setPasswordUrl,
]),
null,
null,
false,
);
return $customer;
} catch (\Throwable) {
return null;
}
} }
/** /**