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 %} +
{{ 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 +
+
+ {{ contrat.reference }}
+Choisissez votre mode de paiement pour activer vos services.
+ + {# Resume #} +Montant mensuel
+{{ totalHt|number_format(2, ',', ' ') }} € HT / mois
+Carte bancaire
+Payez votre premier mois immediatement par carte bancaire via Stripe.
+ {% if cbCheckoutUrl %} + + Payer {{ totalHt|number_format(2, ',', ' ') }} € par CB + + {% endif %} +Prelevement SEPA
+ Recommande +Renseignez votre IBAN une seule fois. Les prelevements seront automatiques chaque mois.
+Pour toute question : client@e-cosplay.fr
+
+ 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. + + +
+ Vous avez le choix entre : + + +
+
+
+ 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 + + |
+