From 4be001967b2a0d38b72d98c8895e88d4b9c83b05 Mon Sep 17 00:00:00 2001
From: Serreau Jovann
Date: Wed, 8 Apr 2026 20:18:11 +0200
Subject: [PATCH] feat: protection code email + boutons signer/refuser + page
refus
Protection :
- /echeancier/verify/{id} : verification par code email 6 chiffres
(meme pattern que OrderPayment verify, 15 min expiry)
- process redirige vers verify si non authentifie
Pages client :
- Boutons "Signer l'echeancier" (vert) et "Refuser" (rouge) dans process
- /echeancier/sign/{id} : redirige vers DocuSeal
- /echeancier/refuse/{id} : passe en cancelled, affiche page refused
- echeancier/verify.html.twig : saisie code
- echeancier/refused.html.twig : confirmation refus
Email :
- Lien pointe vers /echeancier/verify/{id} (protege)
- Texte Stripe ajoute dans page process
Co-Authored-By: Claude Opus 4.6 (1M context)
---
src/Controller/Admin/EcheancierController.php | 2 +-
.../EcheancierProcessController.php | 149 +++++++++++++++++-
templates/echeancier/process.html.twig | 19 +++
templates/echeancier/refused.html.twig | 25 +++
templates/echeancier/verify.html.twig | 34 ++++
5 files changed, 225 insertions(+), 4 deletions(-)
create mode 100644 templates/echeancier/refused.html.twig
create mode 100644 templates/echeancier/verify.html.twig
diff --git a/src/Controller/Admin/EcheancierController.php b/src/Controller/Admin/EcheancierController.php
index 6968aaf..136f1df 100644
--- a/src/Controller/Admin/EcheancierController.php
+++ b/src/Controller/Admin/EcheancierController.php
@@ -296,7 +296,7 @@ class EcheancierController extends AbstractController
$slug = $docuSeal->getSubmitterSlug($submitterId);
$signUrl = null !== $slug ? rtrim($docuSealUrl, '/').'/s/'.$slug : null;
- $processUrl = $urlGenerator->generate('app_echeancier_process', [
+ $processUrl = $urlGenerator->generate('app_echeancier_verify', [
'id' => $echeancier->getId(),
], UrlGeneratorInterface::ABSOLUTE_URL);
diff --git a/src/Controller/EcheancierProcessController.php b/src/Controller/EcheancierProcessController.php
index cf44d7f..a812049 100644
--- a/src/Controller/EcheancierProcessController.php
+++ b/src/Controller/EcheancierProcessController.php
@@ -3,29 +3,118 @@
namespace App\Controller;
use App\Entity\Echeancier;
+use App\Service\DocuSealService;
+use App\Service\MailerService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
+use Twig\Environment;
class EcheancierProcessController extends AbstractController
{
public function __construct(
private EntityManagerInterface $em,
+ private MailerService $mailer,
+ private Environment $twig,
) {
}
/**
- * Page de detail echeancier avant signature (publique).
+ * Verification par code email avant d'acceder a l'echeancier.
*/
- #[Route('/echeancier/process/{id}', name: 'app_echeancier_process', requirements: ['id' => '\d+'])]
- public function process(int $id): Response
+ #[Route('/echeancier/verify/{id}', name: 'app_echeancier_verify', requirements: ['id' => '\d+'])]
+ public function verify(int $id, Request $request): Response
{
$echeancier = $this->em->getRepository(Echeancier::class)->find($id);
if (null === $echeancier) {
throw $this->createNotFoundException('Echeancier introuvable.');
}
+ $customer = $echeancier->getCustomer();
+ $session = $request->getSession();
+ $sessionKey = 'echeancier_verified_'.$echeancier->getId();
+
+ if ($session->get($sessionKey, false)) {
+ return $this->redirectToRoute('app_echeancier_process', ['id' => $id]);
+ }
+
+ if (null === $customer->getEmail()) {
+ $session->set($sessionKey, true);
+
+ return $this->redirectToRoute('app_echeancier_process', ['id' => $id]);
+ }
+
+ $codeKey = 'echeancier_code_'.$id;
+ $codeExpiresKey = 'echeancier_code_expires_'.$id;
+ $error = null;
+
+ if ('POST' === $request->getMethod()) {
+ $code = trim($request->request->getString('code'));
+ $storedCode = $session->get($codeKey);
+ $expiresAt = $session->get($codeExpiresKey, 0);
+
+ if (time() > $expiresAt) {
+ $error = 'Code expire. Cliquez sur "Renvoyer" pour en recevoir un nouveau.';
+ $session->remove($codeKey);
+ $session->remove($codeExpiresKey);
+ } elseif ($code === $storedCode) {
+ $session->set($sessionKey, true);
+ $session->remove($codeKey);
+ $session->remove($codeExpiresKey);
+
+ return $this->redirectToRoute('app_echeancier_process', ['id' => $id]);
+ } else {
+ $error = 'Code incorrect. Veuillez reessayer.';
+ }
+ }
+
+ // Envoyer le code si pas encore envoye ou expire
+ if (null === $session->get($codeKey) || time() > $session->get($codeExpiresKey, 0)) {
+ $code = str_pad((string) random_int(0, 999999), 6, '0', \STR_PAD_LEFT);
+ $session->set($codeKey, $code);
+ $session->set($codeExpiresKey, time() + 900);
+
+ $this->mailer->sendEmail(
+ $customer->getEmail(),
+ 'Code de verification - Echeancier',
+ $this->twig->render('emails/order_verify_code.html.twig', [
+ 'customer' => $customer,
+ 'advert' => null,
+ 'code' => $code,
+ ]),
+ null,
+ null,
+ false,
+ );
+ }
+
+ return $this->render('echeancier/verify.html.twig', [
+ 'echeancier' => $echeancier,
+ 'customer' => $customer,
+ 'error' => $error,
+ ]);
+ }
+
+ /**
+ * Page de detail echeancier avant signature (protegee par code).
+ */
+ #[Route('/echeancier/process/{id}', name: 'app_echeancier_process', requirements: ['id' => '\d+'])]
+ public function process(int $id, Request $request): Response
+ {
+ $echeancier = $this->em->getRepository(Echeancier::class)->find($id);
+ if (null === $echeancier) {
+ throw $this->createNotFoundException('Echeancier introuvable.');
+ }
+
+ // Verifier si authentifie
+ $session = $request->getSession();
+ if (!$session->get('echeancier_verified_'.$echeancier->getId(), false)) {
+ return $this->redirectToRoute('app_echeancier_verify', ['id' => $id]);
+ }
+
if (\in_array($echeancier->getState(), [Echeancier::STATE_SIGNED, Echeancier::STATE_ACTIVE, Echeancier::STATE_COMPLETED], true)) {
return $this->render('echeancier/signed.html.twig', [
'echeancier' => $echeancier,
@@ -33,12 +122,66 @@ class EcheancierProcessController extends AbstractController
]);
}
+ if (Echeancier::STATE_CANCELLED === $echeancier->getState()) {
+ return $this->render('echeancier/refused.html.twig', [
+ 'echeancier' => $echeancier,
+ 'customer' => $echeancier->getCustomer(),
+ ]);
+ }
+
return $this->render('echeancier/process.html.twig', [
'echeancier' => $echeancier,
'customer' => $echeancier->getCustomer(),
]);
}
+ /**
+ * Redirige vers DocuSeal pour signer.
+ */
+ #[Route('/echeancier/sign/{id}', name: 'app_echeancier_sign', requirements: ['id' => '\d+'])]
+ public function sign(
+ int $id,
+ DocuSealService $docuSeal,
+ #[Autowire(env: 'DOCUSEAL_URL')] string $docuSealUrl = '',
+ ): Response {
+ $echeancier = $this->em->getRepository(Echeancier::class)->find($id);
+ if (null === $echeancier) {
+ throw $this->createNotFoundException('Echeancier introuvable.');
+ }
+
+ $submitterId = (int) ($echeancier->getSubmissionId() ?? '0');
+ if ($submitterId <= 0) {
+ throw $this->createNotFoundException('Echeancier non envoye pour signature.');
+ }
+
+ $slug = $docuSeal->getSubmitterSlug($submitterId);
+ if (null === $slug) {
+ throw $this->createNotFoundException('Lien de signature introuvable.');
+ }
+
+ return $this->redirect(rtrim($docuSealUrl, '/').'/s/'.$slug);
+ }
+
+ /**
+ * Refuse l'echeancier.
+ */
+ #[Route('/echeancier/refuse/{id}', name: 'app_echeancier_refuse', requirements: ['id' => '\d+'])]
+ public function refuse(int $id): Response
+ {
+ $echeancier = $this->em->getRepository(Echeancier::class)->find($id);
+ if (null === $echeancier) {
+ throw $this->createNotFoundException('Echeancier introuvable.');
+ }
+
+ $echeancier->setState(Echeancier::STATE_CANCELLED);
+ $this->em->flush();
+
+ return $this->render('echeancier/refused.html.twig', [
+ 'echeancier' => $echeancier,
+ 'customer' => $echeancier->getCustomer(),
+ ]);
+ }
+
/**
* Callback DocuSeal apres signature du client.
*/
diff --git a/templates/echeancier/process.html.twig b/templates/echeancier/process.html.twig
index d035f29..99838c9 100644
--- a/templates/echeancier/process.html.twig
+++ b/templates/echeancier/process.html.twig
@@ -83,6 +83,25 @@
Une fois le contrat signe, un email vous sera envoye pour effectuer la configuration des prelevements automatiques.
+
+ Les prelevements seront effectues via Stripe, plateforme de paiement securisee. Vous recevrez un email de confirmation a chaque echeance.
+
+
+ {# Boutons signer / refuser #}
+ {% if echeancier.submissionId %}
+
+ {% endif %}
+
Pour toute question : contact@e-cosplay.fr
diff --git a/templates/echeancier/refused.html.twig b/templates/echeancier/refused.html.twig
new file mode 100644
index 0000000..ecc11d8
--- /dev/null
+++ b/templates/echeancier/refused.html.twig
@@ -0,0 +1,25 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Echeancier refuse - Association E-Cosplay{% endblock %}
+
+{% block body %}
+
+
+
+
+
Echeancier refuse
+
+
+
+ L'echeancier de paiement a ete refuse.
+
+
+ Si vous souhaitez discuter d'autres modalites de paiement, contactez-nous a
+ contact@e-cosplay.fr
+
+
+
+
+{% endblock %}
diff --git a/templates/echeancier/verify.html.twig b/templates/echeancier/verify.html.twig
new file mode 100644
index 0000000..fcdbcd6
--- /dev/null
+++ b/templates/echeancier/verify.html.twig
@@ -0,0 +1,34 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Verification - Echeancier - Association E-Cosplay{% endblock %}
+
+{% block body %}
+
+
+
+
Verification
+
Un code a ete envoye a {{ customer.email }}
+
+
+ {% if error %}
+
{{ error }}
+ {% endif %}
+
+
+ Saisissez le code de verification a 6 chiffres recu par email pour acceder a votre echeancier.
+
+
+
+
+
Le code expire dans 15 minutes.
+
+
+
+{% endblock %}