feat(Product.php): Ajoute la relation avec ProductReserve.

 feat(DevisSubscriber.php): Crée un subscriber pour l'envoi de devis.

 feat(Devis.php): Ajoute la relation avec ProductReserve.

 feat: Crée le template de mail pour la notification de signature.

 feat(DevisSend.php): Crée l'événement DevisSend.

 feat(Customer.php): Ajoute la relation avec ProductReserve.

🐛 fix(SignatureController.php): Corrige la gestion de la signature complétée.

 feat(DevisController.php): Ajoute la relance de signature et pagination.

 feat: Crée le template de mail pour l'envoi du devis à signer.

 feat: Crée le template de mail pour la confirmation de signature.

 feat(Client.php): Gère la création et le suivi de la signature DocuSeal.

 feat(DevisPdfService.php): Intègre les champs Docuseal.

 feat(list.twig): Affiche la liste des devis avec actions et statuts.

 feat: Crée la page de succès de signature.

 feat(StripeExtension.php): Ajoute le filtre totalQuoto pour calculer le total HT.
```
This commit is contained in:
Serreau Jovann
2026-01-19 19:40:27 +01:00
parent 0afc9e3396
commit cd45a37d73
19 changed files with 879 additions and 55 deletions

View File

@@ -4,18 +4,25 @@ namespace App\Controller;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Entity\Devis;
use App\Entity\ProductReserve;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Logger\AppLogger;
use App\Repository\DevisRepository;
use App\Service\Mailer\Mailer;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
use App\Service\Signature\Client;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
@@ -26,9 +33,104 @@ class SignatureController extends AbstractController
{
#[Route('/signature/complete', name: 'app_sign_complete')]
public function appSignComplete()
{
public function appSignComplete(
Client $client,
DevisRepository $devisRepository,
EntityManagerInterface $entityManager,
Request $request,
Mailer $mailer,
): Response {
if ($request->get('type') === "devis") {
$devis = $devisRepository->find($request->get('id'));
if (!$devis) {
throw $this->createNotFoundException("Devis introuvable.");
}
// On évite de retraiter un devis déjà marqué comme signé
if ($devis->getState() === 'signed') {
return $this->render('sign/sign_success.twig', ['devis' => $devis]);
}
$submiter = $client->getSubmiter($devis->getSignatureId());
$submission = $client->getSubmition($submiter['submission_id']);
if ($submission['status'] === "completed") {
$devis->setState("signed");
$auditUrl = $submission['audit_log_url'];
$signedDocUrl = $submission['documents'][0]['url'];
try {
// 1. Gestion du PDF SIGNÉ
$tmpSigned = sys_get_temp_dir() . '/sign_' . uniqid() . '.pdf';
$signedContent = file_get_contents($signedDocUrl);
file_put_contents($tmpSigned, $signedContent);
// On utilise UploadedFile pour simuler un upload propre pour VichUploader
$devis->setDevisSignFile(new UploadedFile($tmpSigned, "sign-" . $devis->getNum() . ".pdf", "application/pdf", null, true));
// 2. Gestion de l'AUDIT LOG
$tmpAudit = sys_get_temp_dir() . '/audit_' . uniqid() . '.pdf';
$auditContent = file_get_contents($auditUrl);
file_put_contents($tmpAudit, $auditContent);
$devis->setDevisAuditFile(new UploadedFile($tmpAudit, "audit-" . $devis->getNum() . ".pdf", "application/pdf", null, true));
// 3. Préparation des pièces jointes pour le mail (Le PDF signé est le plus important)
$attachments = [
new DataPart($signedContent, "Devis-" . $devis->getNum() . "-Signe.pdf", "application/pdf"),
new DataPart($auditContent, "Certificat-Signature-" . $devis->getNum() . ".pdf", "application/pdf"),
];
// 4. Sauvegarde en base de données
$devis->setUpdateAt(new \DateTimeImmutable());
foreach ($devis->getDevisLines() as $line) {
$product = $line->getProduct();
$productReserve = new ProductReserve();
$productReserve->setProduct($product);
$productReserve->setCustomer($devis->getCustomer());
$productReserve->setStartAt($line->getStartAt());
$productReserve->setEndAt($line->getEndAt());
$productReserve->setDevis($devis);
$entityManager->persist($productReserve);
}
$entityManager->persist($devis);
$entityManager->flush();
// 5. Envoi du mail de confirmation avec le récapitulatif
$mailer->send(
$devis->getCustomer()->getEmail(),
$devis->getCustomer()->getName() . " " . $devis->getCustomer()->getSurname(),
"[Ludikevent] Confirmation de signature - Devis " . $devis->getNum(),
"mails/sign/signed.twig",
[
'devis' => $devis // Correction ici : passage de l'objet, pas d'un string
],
$attachments
);
$mailer->send(
"contact@ludikevent.fr",
"Ludikevent",
"[Intranet Ludikevent] Confirmation signature d'un client pour - Devis " . $devis->getNum(),
"mails/sign/signed_notification.twig",
[
'devis' => $devis // Correction ici : passage de l'objet, pas d'un string
],
$attachments
);
} catch (\Exception $e) {
return new Response("Erreur lors de la récupération ou de l'envoi des documents : " . $e->getMessage(), 500);
}
}
}
return $this->render('sign/sign_success.twig', [
'devis' => $devis ?? null
]);
}
}