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:
34
migrations/Version20260324131819.php
Normal file
34
migrations/Version20260324131819.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260324131819 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add billing fields to user and organizer_invitation tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE organizer_invitation ADD billing_amount INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE "user" ADD is_billing BOOLEAN DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE "user" ADD billing_amount INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE "user" ADD billing_state VARCHAR(20) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE "user" ADD billing_stripe_subscription_id VARCHAR(255) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE organizer_invitation DROP billing_amount');
|
||||
$this->addSql('ALTER TABLE "user" DROP is_billing');
|
||||
$this->addSql('ALTER TABLE "user" DROP billing_amount');
|
||||
$this->addSql('ALTER TABLE "user" DROP billing_state');
|
||||
$this->addSql('ALTER TABLE "user" DROP billing_stripe_subscription_id');
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,36 @@ class AccountController extends AbstractController
|
||||
return $this->redirectToRoute('app_account', ['tab' => 'settings']);
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore Requires live Stripe API */
|
||||
#[Route('/mon-compte/abonnement', name: 'app_account_billing_subscribe')]
|
||||
public function billingSubscribe(StripeService $stripeService, EntityManagerInterface $em): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
if (!$this->isGranted('ROLE_ORGANIZER') || !$user->isBilling() || 'poor' !== $user->getBillingState()) {
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
try {
|
||||
$url = $stripeService->createBillingCheckoutSession($user);
|
||||
|
||||
return $this->redirect($url);
|
||||
} catch (\Throwable $e) {
|
||||
$this->addFlash('error', 'Erreur lors de la creation de l\'abonnement : '.$e->getMessage());
|
||||
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/mon-compte/abonnement/succes', name: 'app_account_billing_success')]
|
||||
public function billingSuccess(): Response
|
||||
{
|
||||
$this->addFlash('success', 'Votre abonnement a ete active avec succes.');
|
||||
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore Requires live Stripe API */
|
||||
#[Route('/mon-compte/stripe-connect', name: 'app_account_stripe_connect')]
|
||||
public function stripeConnect(StripeService $stripeService, EntityManagerInterface $em): Response
|
||||
@@ -165,6 +195,12 @@ class AccountController extends AbstractController
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
if ($user->isBilling() && 'good' !== $user->getBillingState()) {
|
||||
$this->addFlash('error', 'Vous devez regler votre abonnement avant de configurer Stripe.');
|
||||
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$user->getStripeAccountId()) {
|
||||
$accountId = $stripeService->createAccountConnect($user);
|
||||
@@ -1185,6 +1221,10 @@ class AccountController extends AbstractController
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
if ($user->isBilling() && 'good' !== $user->getBillingState()) {
|
||||
return $this->redirectToRoute('app_account');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -345,10 +345,14 @@ class AdminController extends AbstractController
|
||||
{
|
||||
$offer = $request->request->getString('offer', 'free');
|
||||
$commissionRate = (float) $request->request->getString('commission_rate', '3');
|
||||
$billingAmount = (int) $request->request->getString('billing_amount', '1000');
|
||||
|
||||
$user->setIsApproved(true);
|
||||
$user->setOffer($offer);
|
||||
$user->setCommissionRate($commissionRate);
|
||||
$user->setIsBilling(true);
|
||||
$user->setBillingAmount($billingAmount);
|
||||
$user->setBillingState('good');
|
||||
$em->flush();
|
||||
|
||||
$meilisearch->createIndexIfNotExists('organizers');
|
||||
@@ -609,6 +613,7 @@ class AdminController extends AbstractController
|
||||
$message = trim($request->request->getString('message')) ?: null;
|
||||
$offer = $request->request->getString('offer', 'free');
|
||||
$commissionRate = (float) $request->request->getString('commission_rate', '3');
|
||||
$billingAmount = (int) $request->request->getString('billing_amount', '1000');
|
||||
|
||||
if ('' === $companyName || '' === $firstName || '' === $lastName || '' === $email) {
|
||||
$this->addFlash('error', 'Tous les champs obligatoires doivent etre remplis.');
|
||||
@@ -624,6 +629,7 @@ class AdminController extends AbstractController
|
||||
$invitation->setMessage($message);
|
||||
$invitation->setOffer($offer);
|
||||
$invitation->setCommissionRate($commissionRate);
|
||||
$invitation->setBillingAmount($billingAmount);
|
||||
|
||||
$em->persist($invitation);
|
||||
$em->flush();
|
||||
|
||||
@@ -424,6 +424,10 @@ class HomeController extends AbstractController
|
||||
$user->setCommissionRate($invitation->getCommissionRate());
|
||||
$user->setIsApproved(true);
|
||||
$user->setIsVerified(true);
|
||||
$billingAmount = $invitation->getBillingAmount() ?? 1000;
|
||||
$user->setBillingAmount($billingAmount);
|
||||
$user->setIsBilling(0 !== $billingAmount);
|
||||
$user->setBillingState('good');
|
||||
$user->setSiret(trim($request->request->getString('siret')) ?: null);
|
||||
$user->setAddress(trim($request->request->getString('address')) ?: null);
|
||||
$user->setPostalCode(trim($request->request->getString('postal_code')) ?: null);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\BilletBuyer;
|
||||
use App\Entity\BilletOrder;
|
||||
use App\Entity\Event;
|
||||
use App\Entity\Payout;
|
||||
use App\Entity\User;
|
||||
use App\Service\AuditService;
|
||||
@@ -51,6 +52,9 @@ class StripeWebhookController extends AbstractController
|
||||
'payment_intent.succeeded' => $this->handlePaymentIntentSucceeded($event, $em, $billetOrderService),
|
||||
'payment_intent.payment_failed' => $this->handlePaymentIntentFailed($event, $em, $mailerService, $audit),
|
||||
'charge.refunded' => $this->handleChargeRefunded($event, $em, $mailerService, $audit, $billetOrderService),
|
||||
'checkout.session.completed' => $this->handleCheckoutSessionCompleted($event, $em, $mailerService),
|
||||
'invoice.payment_failed' => $this->handleInvoicePaymentFailed($event, $em, $mailerService),
|
||||
'customer.subscription.deleted' => $this->handleSubscriptionDeleted($event, $em, $mailerService),
|
||||
default => null,
|
||||
};
|
||||
|
||||
@@ -315,6 +319,100 @@ class StripeWebhookController extends AbstractController
|
||||
);
|
||||
}
|
||||
|
||||
private function handleCheckoutSessionCompleted(\Stripe\Event $event, EntityManagerInterface $em, MailerService $mailerService): void
|
||||
{
|
||||
$session = $event->data->object;
|
||||
|
||||
if ('subscription' !== ($session->mode ?? null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = $session->metadata->user_id ?? null;
|
||||
if (!$userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $em->getRepository(User::class)->find((int) $userId);
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->setBillingState('good');
|
||||
$user->setBillingStripeSubscriptionId($session->subscription ?? null);
|
||||
$em->flush();
|
||||
|
||||
$mailerService->sendEmail(
|
||||
$user->getEmail(),
|
||||
'Abonnement active - E-Ticket',
|
||||
$this->renderView('email/billing_activated.html.twig', [
|
||||
'firstName' => $user->getFirstName(),
|
||||
'amount' => number_format(($user->getBillingAmount() ?? 0) / 100, 2, ',', ' '),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
private function handleInvoicePaymentFailed(\Stripe\Event $event, EntityManagerInterface $em, MailerService $mailerService): void
|
||||
{
|
||||
$invoice = $event->data->object;
|
||||
$subscriptionId = $invoice->subscription ?? null;
|
||||
|
||||
if (!$subscriptionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $em->getRepository(User::class)->findOneBy(['billingStripeSubscriptionId' => $subscriptionId]);
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->setBillingState('suspendu');
|
||||
$this->disableUserEvents($user, $em);
|
||||
$em->flush();
|
||||
|
||||
$mailerService->sendEmail(
|
||||
$user->getEmail(),
|
||||
'Echec de paiement de votre abonnement - E-Ticket',
|
||||
$this->renderView('email/billing_failed.html.twig', [
|
||||
'firstName' => $user->getFirstName(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
private function handleSubscriptionDeleted(\Stripe\Event $event, EntityManagerInterface $em, MailerService $mailerService): void
|
||||
{
|
||||
$subscription = $event->data->object;
|
||||
$subscriptionId = $subscription->id ?? null;
|
||||
|
||||
if (!$subscriptionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $em->getRepository(User::class)->findOneBy(['billingStripeSubscriptionId' => $subscriptionId]);
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->setBillingState('suspendu');
|
||||
$this->disableUserEvents($user, $em);
|
||||
$em->flush();
|
||||
|
||||
$mailerService->sendEmail(
|
||||
$user->getEmail(),
|
||||
'Abonnement annule - E-Ticket',
|
||||
$this->renderView('email/billing_cancelled.html.twig', [
|
||||
'firstName' => $user->getFirstName(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
private function disableUserEvents(User $user, EntityManagerInterface $em): void
|
||||
{
|
||||
$events = $em->getRepository(Event::class)->findBy(['account' => $user, 'isOnline' => true]);
|
||||
foreach ($events as $event) {
|
||||
$event->setIsOnline(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
|
||||
@@ -42,6 +42,9 @@ class OrganizerInvitation
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?float $commissionRate = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $billingAmount = null;
|
||||
|
||||
#[ORM\Column(length: 64, unique: true)]
|
||||
private string $token;
|
||||
|
||||
@@ -158,6 +161,18 @@ class OrganizerInvitation
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBillingAmount(): ?int
|
||||
{
|
||||
return $this->billingAmount;
|
||||
}
|
||||
|
||||
public function setBillingAmount(?int $billingAmount): static
|
||||
{
|
||||
$this->billingAmount = $billingAmount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getToken(): string
|
||||
{
|
||||
return $this->token;
|
||||
|
||||
@@ -118,6 +118,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column]
|
||||
private bool $stripePayoutsEnabled = false;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $isBilling = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $billingAmount = null;
|
||||
|
||||
#[ORM\Column(length: 20, nullable: true)]
|
||||
private ?string $billingState = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $billingStripeSubscriptionId = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: self::class)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?self $parentOrganizer = null;
|
||||
@@ -445,6 +457,54 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isBilling(): ?bool
|
||||
{
|
||||
return $this->isBilling;
|
||||
}
|
||||
|
||||
public function setIsBilling(?bool $isBilling): static
|
||||
{
|
||||
$this->isBilling = $isBilling;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBillingAmount(): ?int
|
||||
{
|
||||
return $this->billingAmount;
|
||||
}
|
||||
|
||||
public function setBillingAmount(?int $billingAmount): static
|
||||
{
|
||||
$this->billingAmount = $billingAmount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBillingState(): ?string
|
||||
{
|
||||
return $this->billingState;
|
||||
}
|
||||
|
||||
public function setBillingState(?string $billingState): static
|
||||
{
|
||||
$this->billingState = $billingState;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBillingStripeSubscriptionId(): ?string
|
||||
{
|
||||
return $this->billingStripeSubscriptionId;
|
||||
}
|
||||
|
||||
public function setBillingStripeSubscriptionId(?string $billingStripeSubscriptionId): static
|
||||
{
|
||||
$this->billingStripeSubscriptionId = $billingStripeSubscriptionId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParentOrganizer(): ?self
|
||||
{
|
||||
return $this->parentOrganizer;
|
||||
|
||||
@@ -84,6 +84,9 @@ class KeycloakAuthenticator extends OAuth2Authenticator
|
||||
$newUser->setIsApproved(true);
|
||||
$newUser->setOffer('custom');
|
||||
$newUser->setEmailVerifiedAt(new \DateTimeImmutable());
|
||||
$newUser->setIsBilling(false);
|
||||
$newUser->setBillingAmount(0);
|
||||
$newUser->setBillingState('good');
|
||||
|
||||
$this->em->persist($newUser);
|
||||
$this->em->flush();
|
||||
|
||||
@@ -135,6 +135,35 @@ class StripeService
|
||||
], ['stripe_account' => $connectedAccountId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore Requires live Stripe API
|
||||
*/
|
||||
public function createBillingCheckoutSession(User $user): string
|
||||
{
|
||||
$session = $this->stripe->checkout->sessions->create([
|
||||
'mode' => 'subscription',
|
||||
'customer_email' => $user->getEmail(),
|
||||
'line_items' => [[
|
||||
'price_data' => [
|
||||
'currency' => 'eur',
|
||||
'unit_amount' => $user->getBillingAmount(),
|
||||
'recurring' => ['interval' => 'month'],
|
||||
'product_data' => [
|
||||
'name' => 'Abonnement E-Ticket',
|
||||
],
|
||||
],
|
||||
'quantity' => 1,
|
||||
]],
|
||||
'metadata' => [
|
||||
'user_id' => (string) $user->getId(),
|
||||
],
|
||||
'success_url' => $this->outsideUrl.'/mon-compte/abonnement/succes',
|
||||
'cancel_url' => $this->outsideUrl.'/mon-compte',
|
||||
]);
|
||||
|
||||
return $session->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore Simple getter
|
||||
*/
|
||||
|
||||
@@ -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">⚠</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, ',', ' ') }} €</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
|
||||
|
||||
12
templates/email/billing_activated.html.twig
Normal file
12
templates/email/billing_activated.html.twig
Normal 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 }} €/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 %}
|
||||
12
templates/email/billing_cancelled.html.twig
Normal file
12
templates/email/billing_cancelled.html.twig
Normal 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 %}
|
||||
13
templates/email/billing_failed.html.twig
Normal file
13
templates/email/billing_failed.html.twig
Normal 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 %}
|
||||
@@ -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, ',', ' ') }} €/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 %}
|
||||
|
||||
@@ -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, ',', ' ') }} €/mois</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -76,6 +76,21 @@ class OrganizerInvitationTest extends TestCase
|
||||
self::assertSame($inv, $result);
|
||||
}
|
||||
|
||||
public function testSetAndGetBillingAmount(): void
|
||||
{
|
||||
$inv = new OrganizerInvitation();
|
||||
|
||||
self::assertNull($inv->getBillingAmount());
|
||||
|
||||
$result = $inv->setBillingAmount(1000);
|
||||
|
||||
self::assertSame(1000, $inv->getBillingAmount());
|
||||
self::assertSame($inv, $result);
|
||||
|
||||
$inv->setBillingAmount(0);
|
||||
self::assertSame(0, $inv->getBillingAmount());
|
||||
}
|
||||
|
||||
public function testSetAndGetStatus(): void
|
||||
{
|
||||
$inv = new OrganizerInvitation();
|
||||
|
||||
@@ -187,6 +187,30 @@ class UserTest extends TestCase
|
||||
self::assertTrue($user->isStripePayoutsEnabled());
|
||||
}
|
||||
|
||||
public function testBillingFields(): void
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
self::assertNull($user->isBilling());
|
||||
self::assertNull($user->getBillingAmount());
|
||||
self::assertNull($user->getBillingState());
|
||||
self::assertNull($user->getBillingStripeSubscriptionId());
|
||||
|
||||
$result = $user->setIsBilling(true)
|
||||
->setBillingAmount(2990)
|
||||
->setBillingState('good')
|
||||
->setBillingStripeSubscriptionId('sub_123456');
|
||||
|
||||
self::assertSame($user, $result);
|
||||
self::assertTrue($user->isBilling());
|
||||
self::assertSame(2990, $user->getBillingAmount());
|
||||
self::assertSame('good', $user->getBillingState());
|
||||
self::assertSame('sub_123456', $user->getBillingStripeSubscriptionId());
|
||||
|
||||
$user->setBillingState('suspendu');
|
||||
self::assertSame('suspendu', $user->getBillingState());
|
||||
}
|
||||
|
||||
public function testEmailVerificationFields(): void
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
Reference in New Issue
Block a user