diff --git a/src/Controller/ContratProcessController.php b/src/Controller/ContratProcessController.php index ce01897..c143f68 100644 --- a/src/Controller/ContratProcessController.php +++ b/src/Controller/ContratProcessController.php @@ -8,9 +8,11 @@ use App\Service\MailerService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Twig\Environment; class ContratProcessController extends AbstractController @@ -118,6 +120,160 @@ class ContratProcessController extends AbstractController throw $this->createNotFoundException('Lien de signature introuvable.'); } + /** + * Page de configuration du paiement (CB ou SEPA). + */ + #[Route('/process/contrat/{id}/setup-payment', name: 'app_contrat_setup_payment', requirements: ['id' => '\d+'])] + public function setupPayment( + int $id, + Request $request, + #[Autowire(env: 'STRIPE_SK')] string $stripeSk = '', + #[Autowire(env: 'STRIPE_PK')] string $stripePk = '', + ): Response { + $contrat = $this->em->getRepository(Contrat::class)->find($id); + if (null === $contrat || Contrat::STATE_SIGNED !== $contrat->getState()) { + throw $this->createNotFoundException('Contrat introuvable.'); + } + + $session = $request->getSession(); + if (!$session->get('contrat_verified_'.$contrat->getId(), false)) { + return $this->redirectToRoute('app_contrat_verify', ['id' => $id]); + } + + $customer = $contrat->getCustomer(); + if (null === $customer) { + throw $this->createNotFoundException('Client introuvable.'); + } + + // @codeCoverageIgnoreStart + \Stripe\Stripe::setApiKey($stripeSk); + + $stripeCustomerId = $customer->getStripeCustomerId(); + if (null === $stripeCustomerId) { + $stripeCustomer = \Stripe\Customer::create([ + 'email' => $customer->getEmail() ?? $contrat->getEmail(), + 'name' => $contrat->getRaisonSociale(), + ]); + $stripeCustomerId = $stripeCustomer->id; + $customer->setStripeCustomerId($stripeCustomerId); + $this->em->flush(); + } + + // Creer un SetupIntent pour SEPA + $setupIntent = \Stripe\SetupIntent::create([ + 'customer' => $stripeCustomerId, + 'payment_method_types' => ['sepa_debit'], + 'metadata' => ['contrat_id' => (string) $contrat->getId()], + ]); + + // Creer aussi un Checkout Session pour CB (premier paiement) + $totalCents = (int) round($contrat->getTotalHt() * 100); + + $cbCheckoutUrl = null; + if ($totalCents > 0) { + $checkoutSession = \Stripe\Checkout\Session::create([ + 'mode' => 'payment', + 'payment_method_types' => ['card'], + 'customer' => $stripeCustomerId, + 'line_items' => [[ + 'price_data' => [ + 'currency' => 'eur', + 'unit_amount' => $totalCents, + 'product_data' => [ + 'name' => 'Premier paiement - '.$contrat->getReference(), + ], + ], + 'quantity' => 1, + ]], + 'payment_intent_data' => [ + 'setup_future_usage' => 'off_session', + 'metadata' => [ + 'contrat_id' => (string) $contrat->getId(), + 'first_payment' => '1', + ], + ], + 'success_url' => $this->generateUrl('app_contrat_payment_success', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL), + 'cancel_url' => $this->generateUrl('app_contrat_setup_payment', ['id' => $id], UrlGeneratorInterface::ABSOLUTE_URL), + ]); + $cbCheckoutUrl = $checkoutSession->url; + } + // @codeCoverageIgnoreEnd + + return $this->render('contrat/setup_payment.html.twig', [ + 'contrat' => $contrat, + 'customer' => $customer, + 'clientSecret' => $setupIntent->client_secret, + 'stripePk' => $stripePk, + 'cbCheckoutUrl' => $cbCheckoutUrl, + 'totalHt' => $contrat->getTotalHt(), + ]); + } + + /** + * Confirmation SEPA pour contrat. + */ + #[Route('/process/contrat/{id}/setup-payment/confirm', name: 'app_contrat_setup_payment_confirm', requirements: ['id' => '\d+'], methods: ['POST'])] + public function setupPaymentConfirm( + int $id, + Request $request, + #[Autowire(env: 'STRIPE_SK')] string $stripeSk = '', + ): Response { + $contrat = $this->em->getRepository(Contrat::class)->find($id); + if (null === $contrat) { + return new JsonResponse(['error' => 'Contrat introuvable'], Response::HTTP_NOT_FOUND); + } + + $session = $request->getSession(); + if (!$session->get('contrat_verified_'.$contrat->getId(), false)) { + return new JsonResponse(['error' => 'Non autorise'], Response::HTTP_FORBIDDEN); + } + + $data = json_decode($request->getContent(), true); + $paymentMethodId = $data['payment_method'] ?? null; + + if (null === $paymentMethodId) { + return new JsonResponse(['error' => 'payment_method manquant'], Response::HTTP_BAD_REQUEST); + } + + $customer = $contrat->getCustomer(); + if (null === $customer) { + return new JsonResponse(['error' => 'Client introuvable'], Response::HTTP_NOT_FOUND); + } + + // @codeCoverageIgnoreStart + try { + \Stripe\Stripe::setApiKey($stripeSk); + + $stripeCustomerId = $customer->getStripeCustomerId(); + $pm = \Stripe\PaymentMethod::retrieve($paymentMethodId); + $pm->attach(['customer' => $stripeCustomerId]); + \Stripe\Customer::update($stripeCustomerId, [ + 'invoice_settings' => ['default_payment_method' => $paymentMethodId], + ]); + } catch (\Throwable $e) { + return new JsonResponse(['error' => $e->getMessage()], Response::HTTP_INTERNAL_SERVER_ERROR); + } + // @codeCoverageIgnoreEnd + + return new JsonResponse(['status' => 'ok']); + } + + /** + * Page succes apres premier paiement CB. + */ + #[Route('/process/contrat/{id}/payment-success', name: 'app_contrat_payment_success', requirements: ['id' => '\d+'])] + public function paymentSuccess(int $id): Response + { + $contrat = $this->em->getRepository(Contrat::class)->find($id); + if (null === $contrat) { + throw $this->createNotFoundException('Contrat introuvable.'); + } + + return $this->render('contrat/payment_success.html.twig', [ + 'contrat' => $contrat, + ]); + } + private function sendVerificationCode(Contrat $contrat, object $session, int $id): void { $code = str_pad((string) random_int(0, 999999), 6, '0', \STR_PAD_LEFT); diff --git a/src/Controller/WebhookDocuSealController.php b/src/Controller/WebhookDocuSealController.php index 1efb075..a2ff401 100644 --- a/src/Controller/WebhookDocuSealController.php +++ b/src/Controller/WebhookDocuSealController.php @@ -311,6 +311,30 @@ class WebhookDocuSealController extends AbstractController // silencieux } + // Mail client : configuration du paiement + if (null !== $customer) { + try { + $paymentUrl = $this->generateUrl('app_contrat_setup_payment', [ + 'id' => $contrat->getId(), + ], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL); + + $mailer->sendEmail( + $contrat->getEmail(), + 'Configurez votre paiement - '.$contrat->getReference(), + $twig->render('emails/contrat_setup_payment.html.twig', [ + 'contrat' => $contrat, + 'customer' => $customer, + 'paymentUrl' => $paymentUrl, + ]), + null, + null, + false, + ); + } catch (\Throwable) { + // silencieux + } + } + return new JsonResponse(['status' => 'ok', 'event' => 'contrat_signed', 'reference' => $contrat->getReference(), 'customer_created' => null !== $customer]); } diff --git a/templates/contrat/payment_success.html.twig b/templates/contrat/payment_success.html.twig new file mode 100644 index 0000000..21a0b16 --- /dev/null +++ b/templates/contrat/payment_success.html.twig @@ -0,0 +1,36 @@ +{% extends 'base.html.twig' %} + +{% block title %}Paiement configure - {{ contrat.reference }} - Association E-Cosplay{% endblock %} + +{% block body %} +
+
+
+
+ + + +
+

Paiement configure

+

{{ contrat.reference }}

+
+
+
+
+

+ Votre mode de paiement a ete configure avec succes. Vos services seront actifs prochainement. +

+
+

Contrat : {{ contrat.reference }}

+

Montant : {{ contrat.totalHt|number_format(2, ',', ' ') }} € HT / mois

+
+

+ Vous recevrez un email de confirmation a chaque prelevement. Votre espace client est accessible sur client.e-cosplay.fr +

+

+ Pour toute question : client@e-cosplay.fr +

+
+
+
+{% endblock %} diff --git a/templates/contrat/setup_payment.html.twig b/templates/contrat/setup_payment.html.twig new file mode 100644 index 0000000..340e6a1 --- /dev/null +++ b/templates/contrat/setup_payment.html.twig @@ -0,0 +1,140 @@ +{% extends 'base.html.twig' %} + +{% block title %}Configuration paiement - {{ contrat.reference }} - Association E-Cosplay{% endblock %} + +{% block body %} +
+
+
+
+ E-Cosplay +
+

Configuration du paiement

+

{{ contrat.reference }}

+
+
+
+ +
+

Choisissez votre mode de paiement pour activer vos services.

+ + {# Resume #} +
+

Montant mensuel

+

{{ totalHt|number_format(2, ',', ' ') }} € HT / mois

+
+ + {# Choix methode #} +

Comment souhaitez-vous payer ?

+
+ {# Carte bancaire #} +
+ + + +

Carte bancaire

+

Payez votre premier mois immediatement par carte bancaire via Stripe.

+ {% if cbCheckoutUrl %} + + Payer {{ totalHt|number_format(2, ',', ' ') }} € par CB + + {% endif %} +
+ + {# SEPA #} +
+ + + +

Prelevement SEPA

+ Recommande +

Renseignez votre IBAN une seule fois. Les prelevements seront automatiques chaque mois.

+
+
+ + {# Formulaire SEPA #} +
+

Configurer le prelevement SEPA

+
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+

Mandat SEPA

+

En fournissant vos informations de paiement, vous autorisez Association E-Cosplay et Stripe a debiter votre compte conformement aux instructions.

+
+ + + + +
+
+ +

Pour toute question : client@e-cosplay.fr

+
+
+
+ + + +{% endblock %} diff --git a/templates/emails/contrat_setup_payment.html.twig b/templates/emails/contrat_setup_payment.html.twig new file mode 100644 index 0000000..8e69ff9 --- /dev/null +++ b/templates/emails/contrat_setup_payment.html.twig @@ -0,0 +1,54 @@ +{% extends 'email/base.html.twig' %} + +{% block content %} + + + + +
+

Chez {{ contrat.raisonSociale }},

+ +

+ Votre contrat {{ contrat.reference }} a ete signe avec succes. Pour activer vos services, veuillez configurer votre mode de paiement et effectuer le premier reglement. +

+ + + + + + + + + + +
Reference{{ contrat.reference }}
Total HT / mois{{ contrat.totalHt|number_format(2, ',', ' ') }} €
+ +

+ Vous avez le choix entre : +

+ +
    +
  • Payer par carte bancaire : reglez votre premier mois immediatement.
  • +
  • Configurer le prelevement SEPA : renseignez votre IBAN une seule fois, les prelevements seront automatiques chaque mois.
  • +
+ + {% if paymentUrl is defined and paymentUrl %} + + + + +
+ Configurer mon paiement +
+ {% endif %} + +
+

Important

+

Le premier paiement doit etre effectue par carte bancaire ou prelevement SEPA. Les virements ne sont pas acceptes pour le premier paiement.

+
+ +

+ Pour toute question : client@e-cosplay.fr +

+
+{% endblock %}