Complete invitation landing: offers, commissions, how it works, unsubscribe handling

- Landing page: features grid, 3-step how it works, 3 offers with highlight,
  commissions breakdown (E-Ticket + Stripe) with example calculation
- Unsubscribe: auto-refuse pending invitations, notify contact@e-cosplay.fr
- Email: enable List-Unsubscribe header for invitation emails
- Accept/refuse now via POST forms (CSRF safe)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-22 19:16:22 +01:00
parent 0bb6f43339
commit d7a498292f
3 changed files with 102 additions and 7 deletions

View File

@@ -489,8 +489,6 @@ class AdminController extends AbstractController
'Invitation organisateur - E-Ticket',
$html,
'E-Ticket <contact@e-cosplay.fr>',
null,
false,
);
$this->addFlash('success', 'Invitation envoyee a '.$email.'.');
@@ -541,8 +539,6 @@ class AdminController extends AbstractController
'Invitation organisateur - E-Ticket',
$html,
'E-Ticket <contact@e-cosplay.fr>',
null,
false,
);
$invitation->setStatus(OrganizerInvitation::STATUS_SENT);

View File

@@ -2,7 +2,10 @@
namespace App\Controller;
use App\Entity\OrganizerInvitation;
use App\Service\MailerService;
use App\Service\UnsubscribeManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -11,7 +14,7 @@ use Symfony\Component\Routing\Attribute\Route;
class UnsubscribeController extends AbstractController
{
#[Route('/unsubscribe/{token}', name: 'app_unsubscribe', methods: ['GET', 'POST'])]
public function __invoke(Request $request, string $token, UnsubscribeManager $unsubscribeManager): Response
public function __invoke(Request $request, string $token, UnsubscribeManager $unsubscribeManager, EntityManagerInterface $em, MailerService $mailerService): Response
{
$email = base64_decode($token, true);
@@ -22,6 +25,24 @@ class UnsubscribeController extends AbstractController
if ($request->isMethod('POST')) {
$unsubscribeManager->unsubscribe($email);
$invitations = $em->getRepository(OrganizerInvitation::class)->findBy(['email' => $email]);
foreach ($invitations as $invitation) {
$invitation->setStatus(OrganizerInvitation::STATUS_REFUSED);
$invitation->setRespondedAt(new \DateTimeImmutable());
}
$em->flush();
if (\count($invitations) > 0) {
$mailerService->sendEmail(
'contact@e-cosplay.fr',
'Desinscription invitation organisateur - '.$email,
'<p>'.$email.' s\'est desinscrit et avait '.\count($invitations).' invitation(s) en cours. Elles ont ete automatiquement refusees.</p>',
'E-Ticket <contact@e-cosplay.fr>',
null,
false,
);
}
return $this->render('unsubscribe/confirmed.html.twig', [
'email' => $email,
'breadcrumbs' => [

View File

@@ -46,7 +46,7 @@
<div class="max-w-3xl mx-auto">
<h2 class="text-3xl font-black uppercase tracking-tighter text-center mb-8">Decouvrir E-Ticket by E-Cosplay</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<div class="text-3xl mb-3">&#127915;</div>
<h3 class="font-black uppercase text-sm tracking-widest mb-2">Evenements</h3>
@@ -55,7 +55,7 @@
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<div class="text-3xl mb-3">&#128179;</div>
<h3 class="font-black uppercase text-sm tracking-widest mb-2">Paiement securise</h3>
<p class="text-sm font-bold text-gray-600">Paiement en ligne via Stripe avec encaissement direct sur votre compte.</p>
<p class="text-sm font-bold text-gray-600">Paiement en ligne via Stripe avec encaissement direct sur votre compte connect.</p>
</div>
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<div class="text-3xl mb-3">&#128200;</div>
@@ -68,6 +68,84 @@
<p class="text-sm font-bold text-gray-600">Generez des billets PDF personnalises avec QR code, envoyes automatiquement par email.</p>
</div>
</div>
<h2 class="text-2xl font-black uppercase tracking-tighter text-center mb-6">Comment ca fonctionne ?</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<div class="text-center">
<div class="w-12 h-12 border-4 border-gray-900 bg-[#fabf04] flex items-center justify-center mx-auto mb-3 font-black text-xl shadow-[4px_4px_0px_rgba(0,0,0,1)]">1</div>
<h3 class="font-black uppercase text-xs tracking-widest mb-2">Creez votre compte</h3>
<p class="text-xs font-bold text-gray-500">Configurez votre profil organisateur et connectez votre compte Stripe.</p>
</div>
<div class="text-center">
<div class="w-12 h-12 border-4 border-gray-900 bg-[#fabf04] flex items-center justify-center mx-auto mb-3 font-black text-xl shadow-[4px_4px_0px_rgba(0,0,0,1)]">2</div>
<h3 class="font-black uppercase text-xs tracking-widest mb-2">Publiez vos evenements</h3>
<p class="text-xs font-bold text-gray-500">Ajoutez vos evenements, categories et billets. Mettez en ligne en un clic.</p>
</div>
<div class="text-center">
<div class="w-12 h-12 border-4 border-gray-900 bg-[#fabf04] flex items-center justify-center mx-auto mb-3 font-black text-xl shadow-[4px_4px_0px_rgba(0,0,0,1)]">3</div>
<h3 class="font-black uppercase text-xs tracking-widest mb-2">Vendez et encaissez</h3>
<p class="text-xs font-bold text-gray-500">Les ventes arrivent directement sur votre compte Stripe. Billets generes automatiquement.</p>
</div>
</div>
<h2 class="text-2xl font-black uppercase tracking-tighter text-center mb-6">Les offres</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] {{ invitation.offer == 'free' ? 'ring-4 ring-[#fabf04]' : '' }}">
<h3 class="font-black uppercase text-sm tracking-widest mb-1">Gratuit</h3>
{% if invitation.offer == 'free' %}<span class="inline-block px-2 py-0.5 bg-[#fabf04] border-2 border-gray-900 text-[10px] font-black uppercase mb-3">Votre offre</span>{% endif %}
<ul class="space-y-2 text-xs font-bold text-gray-600 mt-3">
<li class="text-green-600">&#10003; 1 evenement</li>
<li class="text-green-600">&#10003; Billets standards</li>
<li class="text-green-600">&#10003; QR code</li>
<li class="text-gray-300">&#10005; Image par billet</li>
<li class="text-gray-300">&#10005; Design personnalise</li>
<li class="text-gray-300">&#10005; Generation PDF</li>
</ul>
</div>
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] {{ invitation.offer == 'basic' ? 'ring-4 ring-[#fabf04]' : '' }}">
<h3 class="font-black uppercase text-sm tracking-widest mb-1">Basic</h3>
{% if invitation.offer == 'basic' %}<span class="inline-block px-2 py-0.5 bg-[#fabf04] border-2 border-gray-900 text-[10px] font-black uppercase mb-3">Votre offre</span>{% endif %}
<ul class="space-y-2 text-xs font-bold text-gray-600 mt-3">
<li class="text-green-600">&#10003; Evenements illimites</li>
<li class="text-green-600">&#10003; Billets standards</li>
<li class="text-green-600">&#10003; QR code</li>
<li class="text-green-600">&#10003; Generation PDF</li>
<li class="text-gray-300">&#10005; Image par billet</li>
<li class="text-gray-300">&#10005; Design personnalise</li>
</ul>
</div>
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)] {{ invitation.offer == 'custom' ? 'ring-4 ring-[#fabf04]' : '' }}">
<h3 class="font-black uppercase text-sm tracking-widest mb-1">Sur-mesure</h3>
{% if invitation.offer == 'custom' %}<span class="inline-block px-2 py-0.5 bg-[#fabf04] border-2 border-gray-900 text-[10px] font-black uppercase mb-3">Votre offre</span>{% endif %}
<ul class="space-y-2 text-xs font-bold text-gray-600 mt-3">
<li class="text-green-600">&#10003; Evenements illimites</li>
<li class="text-green-600">&#10003; Design personnalise</li>
<li class="text-green-600">&#10003; Image par billet</li>
<li class="text-green-600">&#10003; QR code</li>
<li class="text-green-600">&#10003; Generation PDF</li>
<li class="text-green-600">&#10003; Categories illimitees</li>
</ul>
</div>
</div>
<h2 class="text-2xl font-black uppercase tracking-tighter text-center mb-6">Commissions</h2>
<div class="border-4 border-gray-900 p-6 bg-white shadow-[6px_6px_0px_rgba(0,0,0,1)]">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="text-center">
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mb-1">Commission E-Ticket</p>
<p class="text-4xl font-black text-indigo-600">{{ invitation.commissionRate ?? 3 }}%</p>
<p class="text-xs font-bold text-gray-500 mt-1">Par transaction sur le montant HT</p>
</div>
<div class="text-center">
<p class="text-xs font-black uppercase tracking-widest text-gray-400 mb-1">Commission Stripe</p>
<p class="text-4xl font-black text-gray-900">1.5% + 0.25 &euro;</p>
<p class="text-xs font-bold text-gray-500 mt-1">Frais standard cartes europeennes</p>
</div>
</div>
<div class="mt-6 pt-4 border-t-2 border-gray-200 text-center">
<p class="text-xs font-bold text-gray-400">Exemple : pour un billet a 20 &euro; HT, vous recevez <strong class="text-green-600">{{ (20 - (20 * (invitation.commissionRate ?? 3) / 100) - (20 * 0.015 + 0.25))|number_format(2, ',', ' ') }} &euro;</strong> (commission E-Ticket {{ ((20 * (invitation.commissionRate ?? 3) / 100))|number_format(2, ',', ' ') }} &euro; + Stripe {{ (20 * 0.015 + 0.25)|number_format(2, ',', ' ') }} &euro;)</p>
</div>
</div>
</div>
</section>