Add billing system: subscription, webhooks, and access control

- Add billing fields to User (isBilling, billingAmount, billingState,
  billingStripeSubscriptionId) and OrganizerInvitation (billingAmount)
- Registration: organizer gets billingState="poor" (pending review)
- Admin approval: sets isBilling=true, billingAmount from form, state="good"
- Invitation: billingAmount from invitation, if 0 then isBilling=false
- ROLE_ROOT accounts: billing free (amount=0, state="good")
- Block Stripe Connect creation and all organizer features if state is
  "poor" or "suspendu"
- Hide Stripe configuration section if billing not settled
- Add billing checkout via Stripe subscription with success route
- Webhooks: checkout.session.completed activates billing,
  invoice.payment_failed and customer.subscription.deleted suspend
  account and disable online events
- Show billing alert on /mon-compte with amount and subscribe button
- Display billing info in invitation email and landing page
- Add email templates for billing activated/failed/cancelled

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-24 14:30:21 +01:00
parent b14a15f0a4
commit e4c701456b
17 changed files with 398 additions and 1 deletions

View File

@@ -30,7 +30,21 @@
{% else %}
{% if isOrganizer %}
{% if isOrganizer and app.user.billing and app.user.billingState != 'good' %}
<div class="card-brutal-error p-8 text-center mb-8">
<div class="text-4xl mb-4">&#9888;</div>
{% if app.user.billingState == 'suspendu' %}
<h2 class="text-xl font-black uppercase tracking-tighter italic mb-2">Abonnement suspendu</h2>
<p class="font-bold text-gray-700 text-sm mb-4">Votre abonnement a ete suspendu suite a un echec de paiement. Vos evenements ne sont plus accessibles. Regularisez votre situation pour reactiver votre compte.</p>
{% else %}
<h2 class="text-xl font-black uppercase tracking-tighter italic mb-2">Abonnement requis</h2>
<p class="font-bold text-gray-700 text-sm mb-4">Vous devez regler les frais de votre abonnement pour utiliser notre plateforme. Votre abonnement mensuel est de <strong>{{ (app.user.billingAmount / 100)|number_format(2, ',', ' ') }} &euro;</strong>.</p>
{% endif %}
<a href="{{ path('app_account_billing_subscribe') }}" class="btn-brutal font-black uppercase text-xs tracking-widest hover:bg-indigo-600 hover:text-black transition-all">Regler mon abonnement</a>
</div>
{% endif %}
{% if isOrganizer and (not app.user.billing or app.user.billingState == 'good') %}
{% if not app.user.stripeAccountId %}
<div class="card-brutal-warn mb-8">
<h2 class="text-sm font-black uppercase tracking-widest mb-2">Configuration Stripe requise

View File

@@ -0,0 +1,12 @@
{% extends 'email/base.html.twig' %}
{% block title %}Abonnement active{% endblock %}
{% block content %}
<h2>Bonjour {{ firstName }},</h2>
<p>Votre abonnement E-Ticket de <strong>{{ amount }} &euro;/mois</strong> a ete active avec succes.</p>
<p>Vous pouvez desormais utiliser toutes les fonctionnalites de la plateforme.</p>
<p style="text-align:center;margin:32px 0;">
<a href="{{ url('app_account') }}" class="btn">Acceder a mon compte</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends 'email/base.html.twig' %}
{% block title %}Abonnement annule{% endblock %}
{% block content %}
<h2>Bonjour {{ firstName }},</h2>
<p>Votre abonnement E-Ticket a ete <strong>annule</strong>.</p>
<p>Votre compte organisateur est suspendu. Pour reactiver vos services, souscrivez a un nouvel abonnement depuis votre espace.</p>
<p style="text-align:center;margin:32px 0;">
<a href="{{ url('app_account') }}" class="btn">Acceder a mon compte</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends 'email/base.html.twig' %}
{% block title %}Echec de paiement{% endblock %}
{% block content %}
<h2>Bonjour {{ firstName }},</h2>
<p>Le paiement de votre abonnement E-Ticket a <strong>echoue</strong>.</p>
<p>Votre compte organisateur est suspendu jusqu'a la regularisation du paiement. Veuillez mettre a jour votre moyen de paiement.</p>
<p>Si vous pensez qu'il s'agit d'une erreur, contactez <a href="mailto:contact@e-cosplay.fr">contact@e-cosplay.fr</a>.</p>
<p style="text-align:center;margin:32px 0;">
<a href="{{ url('app_account') }}" class="btn">Acceder a mon compte</a>
</p>
{% endblock %}

View File

@@ -16,6 +16,15 @@
avec un taux de commission de {{ invitation.commissionRate }}%
{% endif %}
</p>
{% if invitation.billingAmount is not null %}
<p style="margin: 8px 0 0; font-size: 13px; font-weight: 700; color: #111827;">
{% if invitation.billingAmount == 0 %}
Aucun abonnement mensuel — utilisation gratuite de la plateforme.
{% else %}
Abonnement mensuel : <strong>{{ (invitation.billingAmount / 100)|number_format(2, ',', ' ') }} &euro;/mois</strong>
{% endif %}
</p>
{% endif %}
<p style="margin: 6px 0 0; font-size: 12px; font-weight: 700; color: #374151;">(hors frais de commission Stripe)</p>
</div>
{% endif %}

View File

@@ -27,6 +27,15 @@
{% if invitation.commissionRate is not null %}
<p class="text-sm font-bold text-gray-400 mt-1">Taux de commission E-Ticket : {{ invitation.commissionRate }}% <span class="text-gray-500">(hors frais Stripe)</span></p>
{% endif %}
{% if invitation.billingAmount is not null %}
<p class="text-sm font-bold mt-3">
{% if invitation.billingAmount == 0 %}
<span class="text-green-400">Aucun abonnement mensuel — utilisation gratuite</span>
{% else %}
<span class="text-[#fabf04]">Abonnement mensuel : {{ (invitation.billingAmount / 100)|number_format(2, ',', ' ') }} &euro;/mois</span>
{% endif %}
</p>
{% endif %}
</div>
</section>
{% endif %}