feat: E-Flex - annulation auto apres 2 rejets + blocage creation

Annulation automatique:
- Apres 2 echecs de prelevement, E-Flex passe en STATE_CANCELLED
- Email d'annulation envoye au client (detail: total, paye, restant,
  rejets) + notification admin
- Template eflex_cancelled.html.twig

Blocage creation:
- Controller: refuse la creation si un E-Flex est en cours (active,
  pending_setup, draft) ou en defaut (cancelled avec nbFailed > 0)
- Template: bouton "Creer" remplace par "Creation bloquee (defaut)"
  ou "E-Flex en cours" selon le cas

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-09 08:00:01 +02:00
parent 5812c740e2
commit 6411db64c2
4 changed files with 130 additions and 1 deletions

View File

@@ -39,6 +39,21 @@ class EFlexController extends AbstractController
throw $this->createNotFoundException('Client introuvable');
}
// Bloquer si un E-Flex est en cours ou annule (defaut)
$existingEflex = $this->em->getRepository(EFlex::class)->findBy(['customer' => $customer]);
foreach ($existingEflex as $existing) {
if (\in_array($existing->getState(), [EFlex::STATE_ACTIVE, EFlex::STATE_PENDING_SETUP, EFlex::STATE_DRAFT], true)) {
$this->addFlash('error', 'Un E-Flex est deja en cours ('.$existing->getReference().'). Impossible d\'en creer un nouveau.');
return $this->redirectToRoute('app_admin_clients_show', ['id' => $customerId, 'tab' => 'esyflex']);
}
if (EFlex::STATE_CANCELLED === $existing->getState() && $existing->getNbFailed() > 0) {
$this->addFlash('error', 'Un E-Flex est en defaut ('.$existing->getReference().'). Le client doit regulariser avant de creer un nouveau E-Flex.');
return $this->redirectToRoute('app_admin_clients_show', ['id' => $customerId, 'tab' => 'esyflex']);
}
}
$description = trim($request->request->getString('description'));
$totalAmount = $request->request->getString('totalAmount');
$nbEcheances = $request->request->getInt('nbEcheances');

View File

@@ -761,12 +761,52 @@ class WebhookStripeController extends AbstractController
$line->setStripePaymentIntentId($paymentIntent->id);
$this->em->flush();
$cancelled = false;
if ($eflex->getNbFailed() >= 2) {
$eflex->setState(\App\Entity\EFlex::STATE_CANCELLED);
$this->em->flush();
$cancelled = true;
}
$customer = $eflex->getCustomer();
// Si annule : envoyer mail d'annulation au client + admin
if ($cancelled) {
if (null !== $customer->getEmail()) {
try {
$this->mailer->sendEmail(
$customer->getEmail(),
'E-Flex '.$eflex->getReference().' annule - Rejets de prelevement',
$this->twig->render('emails/eflex_cancelled.html.twig', [
'customer' => $customer,
'eflex' => $eflex,
]),
null,
null,
false,
);
} catch (\Throwable) {
// silencieux
}
}
try {
$this->mailer->sendEmail(
self::NOTIFICATION_EMAIL,
'E-Flex '.$eflex->getReference().' annule (2 rejets) - '.$customer->getFullName(),
$this->twig->render('emails/eflex_cancelled.html.twig', [
'customer' => $customer,
'eflex' => $eflex,
]),
null,
null,
false,
);
} catch (\Throwable) {
// silencieux
}
}
if (null !== $customer->getEmail()) {
try {
$payUrl = $this->urlGenerator->generate('app_eflex_pay', [

View File

@@ -1208,9 +1208,26 @@
{# Tab: E-Flex #}
{% elseif tab == 'esyflex' %}
{% set hasActiveEflex = false %}
{% set hasDefaultEflex = false %}
{% for e in eflexList %}
{% if e.state in ['active', 'pending_setup', 'draft'] %}
{% set hasActiveEflex = true %}
{% endif %}
{% if e.state == 'cancelled' and e.nbFailed > 0 %}
{% set hasDefaultEflex = true %}
{% endif %}
{% endfor %}
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-bold uppercase">E-Flex</h2>
{% if hasDefaultEflex %}
<span class="px-4 py-2 bg-red-500/20 text-red-700 font-bold uppercase text-[10px] tracking-wider cursor-not-allowed" title="E-Flex en defaut - regularisation necessaire">Creation bloquee (defaut)</span>
{% elseif hasActiveEflex %}
<span class="px-4 py-2 bg-yellow-500/20 text-yellow-700 font-bold uppercase text-[10px] tracking-wider cursor-not-allowed" title="Un E-Flex est deja en cours">E-Flex en cours</span>
{% else %}
<button type="button" data-modal-open="modal-eflex" class="btn-gold px-4 py-2 font-bold uppercase text-[10px] tracking-wider text-gray-900">Creer un E-Flex</button>
{% endif %}
</div>
{% if eflexList|length > 0 %}

View File

@@ -0,0 +1,57 @@
{% extends 'email/base.html.twig' %}
{% block content %}
<table width="600" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tr>
<td style="padding: 32px;">
{% set greeting = customer.raisonSociale ? 'Chez ' ~ customer.raisonSociale : 'Bonjour ' ~ customer.firstName %}
<h1 style="font-family: Arial, Helvetica, sans-serif; font-size: 22px; font-weight: 700; color: #111827; margin: 0 0 16px;">{{ greeting }},</h1>
<div style="background: #fef2f2; border-left: 4px solid #dc2626; padding: 16px; margin: 0 0 20px;">
<p style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; font-weight: 700; color: #dc2626; margin: 0;">
E-Flex {{ eflex.reference }} annule
</p>
</div>
<p style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #374151; line-height: 22px; margin: 0 0 16px;">
Votre contrat de financement E-Flex a ete automatiquement annule suite a des rejets repetes de prelevement. Le solde restant reste du.
</p>
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 20px 0; border: 1px solid #e5e7eb;">
<tr>
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb; width: 35%;">Reference</td>
<td style="padding: 10px 16px; font-family: monospace; font-size: 13px; font-weight: 700;">{{ eflex.reference }}</td>
</tr>
<tr>
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb;">Total contrat</td>
<td style="padding: 10px 16px; font-family: monospace; font-size: 14px; font-weight: 700;">{{ eflex.totalAmount|number_format(2, ',', ' ') }} &euro;</td>
</tr>
<tr>
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb;">Deja paye</td>
<td style="padding: 10px 16px; font-family: monospace; font-size: 14px; font-weight: 700; color: #16a34a;">{{ eflex.totalPaid|number_format(2, ',', ' ') }} &euro;</td>
</tr>
<tr>
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb;">Restant du</td>
<td style="padding: 10px 16px; font-family: monospace; font-size: 16px; font-weight: 700; color: #dc2626;">{{ (eflex.totalAmount|number_format(2, '.', '') - eflex.totalPaid)|number_format(2, ',', ' ') }} &euro;</td>
</tr>
<tr>
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb;">Echeances payees</td>
<td style="padding: 10px 16px; font-size: 13px;">{{ eflex.nbPaid }}/{{ eflex.nbLines }}</td>
</tr>
<tr>
<td style="padding: 10px 16px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; background: #f9fafb;">Rejets</td>
<td style="padding: 10px 16px; font-size: 13px; font-weight: 700; color: #dc2626;">{{ eflex.nbFailed }} echec(s)</td>
</tr>
</table>
<p style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; color: #374151; line-height: 22px; margin: 16px 0;">
Veuillez contacter notre service pour regulariser votre situation dans les plus brefs delais.
</p>
<p style="font-family: Arial, Helvetica, sans-serif; font-size: 12px; color: #9ca3af; margin: 16px 0 0;">
Pour toute question : <a href="mailto:contact@e-cosplay.fr" style="color: #fabf04;">contact@e-cosplay.fr</a>
</p>
</td>
</tr>
</table>
{% endblock %}