diff --git a/src/Controller/Admin/DevisController.php b/src/Controller/Admin/DevisController.php index 5ded53a..44282e2 100644 --- a/src/Controller/Admin/DevisController.php +++ b/src/Controller/Admin/DevisController.php @@ -435,7 +435,6 @@ class DevisController extends AbstractController ]); } - #[Route('/{id}/create-advert', name: 'create_advert', requirements: ['id' => '\d+'], methods: ['POST'])] /** @codeCoverageIgnore */ private function sendDevisSignEmail(Devis $devis, \App\Entity\Customer $customer, MailerService $mailer, Environment $twig, UrlGeneratorInterface $urlGenerator, string $subject): void { @@ -455,6 +454,7 @@ class DevisController extends AbstractController ); } + #[Route('/{id}/create-advert', name: 'create_advert', requirements: ['id' => '\d+'], methods: ['POST'])] public function createAdvert(int $id, AdvertService $advertService): Response { $devis = $this->em->getRepository(Devis::class)->find($id); diff --git a/src/Controller/WebhookStripeController.php b/src/Controller/WebhookStripeController.php index 3c7e00f..e969c3a 100644 --- a/src/Controller/WebhookStripeController.php +++ b/src/Controller/WebhookStripeController.php @@ -4,6 +4,8 @@ namespace App\Controller; use App\Entity\Advert; use App\Entity\AdvertPayment; +use App\Entity\Echeancier; +use App\Entity\EcheancierLine; use App\Entity\Facture; use App\Entity\StripeWebhookSecret; use App\Repository\StripeWebhookSecretRepository; @@ -91,6 +93,8 @@ class WebhookStripeController extends AbstractController return match ($event->type) { 'payment_intent.succeeded' => $this->handlePaymentSucceeded($event, $channel), 'payment_intent.payment_failed' => $this->handlePaymentFailed($event, $channel), + 'invoice.paid' => $this->handleInvoicePaid($event, $channel), + 'invoice.payment_failed' => $this->handleInvoiceFailed($event, $channel), default => new JsonResponse(['status' => 'ok', 'channel' => $channel, 'event' => $event->type]), }; } @@ -317,6 +321,172 @@ class WebhookStripeController extends AbstractController return new JsonResponse(['status' => 'ok', 'action' => 'payment_failed', 'advert' => $numOrder, 'method' => $method]); } + /** + * Traite une invoice Stripe payee (echeancier). + * + * @codeCoverageIgnore + */ + private function handleInvoicePaid(\Stripe\Event $event, string $channel): JsonResponse + { + $invoice = $event->data->object; + $subscriptionId = $invoice->subscription ?? null; + + if (null === $subscriptionId) { + return new JsonResponse(['status' => 'ok', 'action' => 'no_subscription']); + } + + $echeancier = $this->em->getRepository(Echeancier::class)->findOneBy(['stripeSubscriptionId' => $subscriptionId]); + if (null === $echeancier) { + $this->logger->info('Stripe invoice.paid ['.$channel.']: echeancier non trouve pour subscription '.$subscriptionId); + + return new JsonResponse(['status' => 'ok', 'action' => 'echeancier_not_found']); + } + + // Trouver la prochaine ligne en attente + $nextLine = null; + foreach ($echeancier->getLines() as $line) { + if (EcheancierLine::STATE_PREPARED === $line->getState()) { + $nextLine = $line; + break; + } + } + + if (null === $nextLine) { + $this->logger->warning('Stripe invoice.paid ['.$channel.']: aucune ligne en attente pour echeancier '.$echeancier->getId()); + + return new JsonResponse(['status' => 'ok', 'action' => 'no_pending_line']); + } + + $nextLine->setState(EcheancierLine::STATE_OK); + $nextLine->setPaidAt(new \DateTimeImmutable()); + $nextLine->setStripeInvoiceId($invoice->id); + $this->em->flush(); + + // Verifier si toutes les echeances sont payees + if ($echeancier->getNbPaid() >= $echeancier->getNbLines()) { + $echeancier->setState(Echeancier::STATE_COMPLETED); + $this->em->flush(); + $this->logger->info('Stripe invoice.paid ['.$channel.']: echeancier '.$echeancier->getId().' termine'); + } + + // Notification client + $customer = $echeancier->getCustomer(); + if (null !== $customer->getEmail()) { + try { + $amount = number_format((float) $nextLine->getAmount(), 2, ',', ' '); + $this->mailer->sendEmail( + $customer->getEmail(), + 'Echeance '.$nextLine->getPosition().'/'.$echeancier->getNbLines().' payee - '.$amount.' EUR', + $this->twig->render('emails/echeancier_echeance_payee.html.twig', [ + 'customer' => $customer, + 'echeancier' => $echeancier, + 'line' => $nextLine, + ]), + null, + null, + false, + ); + } catch (\Throwable $e) { + $this->logger->error('Stripe invoice.paid: erreur envoi mail: '.$e->getMessage()); + } + } + + $this->logger->info('Stripe invoice.paid ['.$channel.']: echeance '.$nextLine->getPosition().' payee pour echeancier '.$echeancier->getId()); + + return new JsonResponse(['status' => 'ok', 'action' => 'echeance_paid', 'position' => $nextLine->getPosition()]); + } + + /** + * Traite un echec de paiement d'invoice Stripe (echeancier). + * + * @codeCoverageIgnore + */ + private function handleInvoiceFailed(\Stripe\Event $event, string $channel): JsonResponse + { + $invoice = $event->data->object; + $subscriptionId = $invoice->subscription ?? null; + + if (null === $subscriptionId) { + return new JsonResponse(['status' => 'ok', 'action' => 'no_subscription']); + } + + $echeancier = $this->em->getRepository(Echeancier::class)->findOneBy(['stripeSubscriptionId' => $subscriptionId]); + if (null === $echeancier) { + return new JsonResponse(['status' => 'ok', 'action' => 'echeancier_not_found']); + } + + // Trouver la prochaine ligne en attente + $nextLine = null; + foreach ($echeancier->getLines() as $line) { + if (EcheancierLine::STATE_PREPARED === $line->getState()) { + $nextLine = $line; + break; + } + } + + if (null === $nextLine) { + return new JsonResponse(['status' => 'ok', 'action' => 'no_pending_line']); + } + + $errorMessage = $invoice->last_finalization_error->message ?? ($invoice->status_transitions->finalized_at ? 'Paiement refuse' : 'Echec prelevement'); + + $nextLine->setState(EcheancierLine::STATE_KO); + $nextLine->setFailureReason($errorMessage); + $nextLine->setStripeInvoiceId($invoice->id); + + // Si trop d'echecs, passer en defaut + if ($echeancier->getNbFailed() >= 2) { + $echeancier->setState(Echeancier::STATE_DEFAULT); + } + + $this->em->flush(); + + // Notification client + admin + $customer = $echeancier->getCustomer(); + if (null !== $customer->getEmail()) { + try { + $this->mailer->sendEmail( + $customer->getEmail(), + 'Echec prelevement echeance '.$nextLine->getPosition().'/'.$echeancier->getNbLines(), + $this->twig->render('emails/echeancier_echeance_echec.html.twig', [ + 'customer' => $customer, + 'echeancier' => $echeancier, + 'line' => $nextLine, + 'errorMessage' => $errorMessage, + ]), + null, + null, + false, + ); + } catch (\Throwable $e) { + $this->logger->error('Stripe invoice.payment_failed: erreur envoi mail: '.$e->getMessage()); + } + } + + // Notification admin + try { + $this->mailer->sendEmail( + self::NOTIFICATION_EMAIL, + 'Echec echeance '.$nextLine->getPosition().' - '.$customer->getFullName(), + $this->twig->render('emails/echeancier_echeance_echec.html.twig', [ + 'customer' => $customer, + 'echeancier' => $echeancier, + 'line' => $nextLine, + 'errorMessage' => $errorMessage, + ]), + null, + null, + false, + ); + } catch (\Throwable $e) { + $this->logger->error('Stripe invoice.payment_failed: erreur envoi mail admin: '.$e->getMessage()); + } + + $this->logger->warning('Stripe invoice.payment_failed ['.$channel.']: echeance '.$nextLine->getPosition().' echouee pour echeancier '.$echeancier->getId().' - '.$errorMessage); + + return new JsonResponse(['status' => 'ok', 'action' => 'echeance_failed', 'position' => $nextLine->getPosition()]); + } + /** * Genere le PDF de la facture, le sauvegarde via Vich, et l'envoie au client par mail. * diff --git a/templates/emails/echeancier_echeance_echec.html.twig b/templates/emails/echeancier_echeance_echec.html.twig new file mode 100644 index 0000000..be3db52 --- /dev/null +++ b/templates/emails/echeancier_echeance_echec.html.twig @@ -0,0 +1,44 @@ +{% extends 'email/base.html.twig' %} + +{% block content %} +
+ {% set greeting = customer.raisonSociale ? 'Chez ' ~ customer.raisonSociale : 'Bonjour ' ~ customer.firstName %}
+ {{ greeting }},+ ++ Le prelevement de votre echeance {{ line.position }}/{{ echeancier.nbLines }} a echoue. + + +
+ Veuillez verifier votre moyen de paiement et contacter notre service si le probleme persiste. + Une nouvelle tentative de prelevement sera effectuee automatiquement par Stripe. + + ++ Pour toute question : contact@e-cosplay.fr + + |
+
+ {% set greeting = customer.raisonSociale ? 'Chez ' ~ customer.raisonSociale : 'Bonjour ' ~ customer.firstName %}
+ {{ greeting }},+ ++ Votre echeance {{ line.position }}/{{ echeancier.nbLines }} a ete prelevee avec succes. + + +
+ Pour toute question : contact@e-cosplay.fr + + |
+