src/Controller/OrdersController.php line 51

  1. <?php
  2. namespace App\Controller;
  3. use App\Dto\MerchantPay\InitiateTransactionRequest;
  4. use App\Entity\Order;
  5. use App\Entity\Paiement;
  6. use App\Entity\Playlist;
  7. use App\Services\OrangeMoney;
  8. use App\Entity\PurchaceOrders;
  9. use App\Form\PaiementListType;
  10. use Symfony\Component\Uid\UuidV3;
  11. use App\Exception\MVolaApiException;
  12. use App\Repository\OrderRepository;
  13. use App\Repository\PaiementRepository;
  14. use App\Repository\PlaylistRepository;
  15. use App\Repository\UserRepository;
  16. use App\Services\MVolaApiService;
  17. use Doctrine\ORM\EntityManagerInterface;
  18. use Symfony\Component\HttpFoundation\Request;
  19. use Symfony\Component\HttpFoundation\Response;
  20. use Symfony\Component\Routing\Annotation\Route;
  21. use Symfony\Component\HttpFoundation\JsonResponse;
  22. use Symfony\Component\Security\Http\Attribute\IsGranted;
  23. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  24. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  25. use Symfony\Component\Serializer\SerializerInterface;
  26. use App\Services\StripePaymentService;
  27. use App\Services\AirtelMoney;
  28. use App\Exception\AirtelApiException;
  29. use Psr\Log\LoggerInterface;
  30. #[Route('/orders', name: 'app_orders')]
  31. class OrdersController extends AbstractController
  32. {
  33.    
  34.     
  35.     public function __construct(
  36.         private MVolaApiService $mvolaApiService,
  37.         private SerializerInterface $serializer, 
  38.         private StripePaymentService $stripePaymentService,
  39.         private AirtelMoney $airtelMoney,
  40.         private LoggerInterface $logger
  41.     ) {
  42.     }
  43.     #[Route('/', name: '_index')]
  44.     #[IsGranted('ROLE_USER')]
  45.     public function index(SessionInterface $sessionInterface,PlaylistRepository $playlistRepository): Response
  46.     {
  47.         $card = $sessionInterface->get('card', []);
  48.         $playlists = $playlistRepository->findBy(['id' => $card]);
  49.         return $this->render('orders/index.html.twig', [
  50.             'playlists' => $playlists,
  51.         ]);
  52.     }
  53.     
  54.     #[Route('/mycommande', name: '_mycommande')]
  55.     #[IsGranted('ROLE_USER')]
  56.     public function mycommande(OrderRepository $orderRepository): Response
  57.     {
  58.         $orders = $orderRepository->findBy(['client' => $this->getUser()],['id' => 'DESC']);
  59.         return $this->render('orders/mycommande.html.twig', [
  60.             'orders' => $orders,
  61.         ]);
  62.     }
  63.     #[Route('/commande', name: '_commande')]
  64.     #[IsGranted('ROLE_USER_ADMIN')]
  65.     public function commande(OrderRepository $orderRepository): Response
  66.     {
  67.         $orders = $orderRepository->findBy([],['id' => 'DESC']);
  68.         return $this->render('orders/commande.html.twig', [
  69.             'orders' => $orders,
  70.         ]);
  71.     }
  72.     //create order from card
  73.     #[Route('/createOrder', name: '_create')]
  74.     #[IsGranted('ROLE_USER')]
  75.     public function createOrder(SessionInterface $sessionInterface,EntityManagerInterface $entityManager): Response
  76.     {
  77.         $card = $sessionInterface->get('card', []);
  78.         if (count($card) > 0) {
  79.             $order = (new Order())
  80.                 ->setClient($this->getUser())
  81.                 ->setCeatedAt(new \DateTimeImmutable())
  82.                 ->setStatus('created')
  83.                 ;
  84.             $entityManager->persist($order);
  85.             $entityManager->flush();
  86.             foreach ($card as $playlistId) {
  87.                 $playlist = $entityManager->getRepository(Playlist::class)->find($playlistId);
  88.                 if (!$playlist) {
  89.                     continue;
  90.                 }
  91.                 $purchace = (new PurchaceOrders())
  92.                     ->setPlaylist($playlist)
  93.                     ->setAmount($playlist->getPrix())
  94.                     ->setOrders($order)
  95.                     ;
  96.                 $entityManager->persist($purchace);
  97.                 $entityManager->flush();
  98.             }
  99.             $sessionInterface->set('card', []);
  100.             return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);
  101.         }
  102.         return $this->redirectToRoute('app_orders_index');
  103.     }
  104.     //order show
  105.     #[Route('/show/{id}', name: '_show')]
  106.     #[IsGranted('ROLE_USER')]
  107.     public function show(Order $order,Request $request, OrangeMoney $orangeMoney,EntityManagerInterface $entityManager,UserRepository $userRepository): Response
  108.     {
  109.         if ($order->getClient() != $this->getUser()) {
  110.             return $this->redirectToRoute('app_orders_index');
  111.         }
  112.         // create form PaymentList
  113.         $form = $this->createForm(PaiementListType::class);
  114.         $form->handleRequest($request);
  115.         if ($form->isSubmitted() && $form->isValid()) {
  116.             $data = $form->getData();
  117.             // calculer le montant total
  118.             $total = 0;
  119.             foreach ($order->getPurchaces() as $purchace) {
  120.                 $total += $purchace->getAmount();
  121.             }
  122.             if ($data['type'] == 'OrangeMoney') {
  123.                 
  124.                 //create paiement with orangemoney
  125.                 $response = $orangeMoney->initiateWebPayment($total, $order->getUid(), $this->generateUrl('app_orders_index', [], true));
  126.                 if ($response['status'] == 201) {
  127.                     $paiement = new Paiement();
  128.                     $paiement->setOrders($order);
  129.                     $paiement->setType('OrangeMoney');
  130.                     $paiement->setAmount($total);
  131.                     $paiement->setStatus('pending');
  132.                     $paiement->setOmPayToken($response['pay_token']);
  133.                     $paiement->setOmNotifToken($response['notif_token']);
  134.                     $entityManager->persist($paiement);
  135.                     $entityManager->flush();
  136.                     $this->addFlash('success', 'Paiement effectué avec succès');
  137.                     return $this->redirect($response['payment_url']);
  138.                 } else {
  139.                     $this->addFlash('error', 'Erreur de paiement');
  140.                 }
  141.                 $order->setStatus('pending');
  142.             } elseif ($data['type'] == 'MVola') {
  143.                 try {
  144.                     $user = $userRepository->findOneBy(['phone' => $this->getUser()->getUserIdentifier()]);
  145.                     
  146.                     if (!$user || !$user->getPhone()) {
  147.                         $this->addFlash('error', 'Numéro de téléphone non trouvé pour l\'utilisateur.');
  148.                         return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);
  149.                     }
  150.                     
  151.                     $customerMsisdn = $user->getPhone();
  152.                     $partnerMsisdn = $this->mvolaApiService->getPartnerMsisdn();
  153.                     // En environnement de développement, MVola impose l'utilisation
  154.                     // de MSISDN de test. On force donc le numéro client sur le MSISDN
  155.                     // de test fourni par Telma pour la sandbox.
  156.                     if ($this->getParameter('kernel.environment') === 'dev') {
  157.                         // Numéros de test officiels MVola (Telma) pour sandbox :
  158.                         // 0343500003, 0343500004
  159.                         // On utilise le premier comme client (débit).
  160.                         $customerMsisdn = '0343500004';
  161.                     }
  162.                     
  163.                     // Convertir le montant en chaîne sans décimales (selon la documentation MVola)
  164.                     $amountString = (string) intval($total);
  165.                     
  166.                     // Préparer la requête DTO
  167.                     $requestDto = new InitiateTransactionRequest();
  168.                     $requestDto->amount = $amountString;
  169.                     $requestDto->currency = 'Ar';
  170.                     $requestDto->setDebitPartyMsisdn($customerMsisdn);
  171.                     $requestDto->setCreditPartyMsisdn($partnerMsisdn);
  172.                     $requestDto->descriptionText = 'Paiement commande ' . $order->getId();
  173.                     $requestDto->requestingOrganisationTransactionReference = 'order_' . $order->getId() . '_' . time();
  174.                     $requestDto->setPartnerName('edena');
  175.                     $requestDto->setForeignCurrency('USD', '1');
  176.                     
  177.                     // Générer l'URL de callback
  178.                     $callbackUrl = $this->generateUrl('app_orders_api_mvola_callback', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
  179.                     
  180.                     $this->logger->info('MVola: Tentative d\'initiation de transaction', [
  181.                         'order_id' => $order->getId(),
  182.                         'amount' => $amountString,
  183.                         'customer' => $customerMsisdn,
  184.                         'partner' => $partnerMsisdn,
  185.                         'callback_url' => $callbackUrl
  186.                     ]);
  187.                     
  188.                     // Initier la transaction
  189.                     $response = $this->mvolaApiService->initiateTransaction($requestDto, $callbackUrl);
  190.                     
  191.                     // Enregistrer le paiement en base
  192.                     $paiement = new Paiement();
  193.                     $paiement->setOrders($order);
  194.                     $paiement->setType('MVola');
  195.                     $paiement->setAmount((int) $total);
  196.                     $paiement->setStatus('pending');
  197.                     // Pour MVola, on stocke le serverCorrelationId dans om_pay_token
  198.                     // car les callbacks et vérifications utilisent ce champ.
  199.                     $paiement->setOmPayToken($response->serverCorrelationId);
  200.                     // On garde aussi une colonne dédiée CorrelationId pour suivi interne.
  201.                     $paiement->setCorrelationId($response->serverCorrelationId);
  202.                     $paiement->setOmNotifToken(null);
  203.                     $entityManager->persist($paiement);
  204.                     
  205.                     // Mettre à jour le statut de la commande
  206.                     $order->setStatus('pending');
  207.                     $entityManager->flush();
  208.                     $this->addFlash('success', 'Paiement MVola initié avec succès. Veuillez valider la transaction sur votre téléphone.');
  209.                     return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);
  210.                     
  211.                 } catch (MVolaApiException $e) {
  212.                     $errorMessage = 'Erreur MVola : ' . $e->getMessage();
  213.                     $errorDetails = [
  214.                         'message' => $e->getMessage(),
  215.                         'code' => $e->getCode(),
  216.                         'file' => $e->getFile(),
  217.                         'line' => $e->getLine(),
  218.                         'trace' => $e->getTraceAsString()
  219.                     ];
  220.                     
  221.                     $this->logger->error('MVola API Exception', $errorDetails);
  222.                     
  223.                     if ($this->getParameter('kernel.environment') === 'dev') {
  224.                         $errorMessage .= ' | Fichier: ' . basename($e->getFile()) . ':' . $e->getLine();
  225.                         if ($e->getErrorData()) {
  226.                             $errorMessage .= ' | Détails: ' . json_encode($e->getErrorData());
  227.                         }
  228.                     }
  229.                     
  230.                     $this->addFlash('error', $errorMessage);
  231.                     return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);
  232.                     
  233.                 } catch (\Exception $e) {
  234.                     $errorDetails = [
  235.                         'message' => $e->getMessage(),
  236.                         'code' => $e->getCode(),
  237.                         'file' => $e->getFile(),
  238.                         'line' => $e->getLine(),
  239.                         'trace' => $e->getTraceAsString()
  240.                     ];
  241.                     
  242.                     $this->logger->error('MVola Exception générique', $errorDetails);
  243.                     
  244.                     $errorMessage = 'Une erreur est survenue lors de l\'initiation du paiement MVola.';
  245.                     if ($this->getParameter('kernel.environment') === 'dev') {
  246.                         $errorMessage .= ' | Erreur: ' . $e->getMessage() . ' | Fichier: ' . basename($e->getFile()) . ':' . $e->getLine();
  247.                     }
  248.                     
  249.                     $this->addFlash('error', $errorMessage);
  250.                     return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);
  251.                 }
  252.             } elseif ($data['type'] == 'AirtelMoney') {
  253.                 try {
  254.                     $user = $userRepository->findOneBy(['phone' => $this->getUser()->getUserIdentifier()]);
  255.                     
  256.                     if (!$user || !$user->getPhone()) {
  257.                         $this->addFlash('error', 'Numéro de téléphone non trouvé pour l\'utilisateur.');
  258.                         return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);
  259.                     }
  260.                     
  261.                     $customerMsisdn = $user->getPhone();
  262.                     $transactionId = 'order_' . $order->getId() . '_' . time();
  263.                     
  264.                     // Générer l'URL de callback
  265.                     $callbackUrl = $this->generateUrl('app_orders_api_airtel_callback', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
  266.                     
  267.                     $this->logger->info('Airtel Money: Tentative d\'initiation de paiement', [
  268.                         'order_id' => $order->getId(),
  269.                         'amount' => $total,
  270.                         'customer' => $customerMsisdn,
  271.                         'transaction_id' => $transactionId,
  272.                         'callback_url' => $callbackUrl
  273.                     ]);
  274.                     
  275.                     // Initier le paiement
  276.                     $response = $this->airtelMoney->initiatePayment(
  277.                         $total,
  278.                         $customerMsisdn,
  279.                         $transactionId,
  280.                         'Commande ' . $order->getId(),
  281.                         $callbackUrl
  282.                     );
  283.                     
  284.                     // Enregistrer le paiement en base
  285.                     $paiement = new Paiement();
  286.                     $paiement->setOrders($order);
  287.                     $paiement->setType('AirtelMoney');
  288.                     $paiement->setAmount((int) $total);
  289.                     $paiement->setStatus('pending');
  290.                     $paiement->setOmPayToken($response['transaction_id']);
  291.                     $paiement->setOmNotifToken(null);
  292.                     $entityManager->persist($paiement);
  293.                     
  294.                     // Mettre à jour le statut de la commande
  295.                     $order->setStatus('pending');
  296.                     $entityManager->flush();
  297.                     $this->addFlash('success', 'Paiement Airtel Money initié avec succès. Veuillez valider la transaction sur votre téléphone.');
  298.                     return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);
  299.                     
  300.                 } catch (AirtelApiException $e) {
  301.                     $errorMessage = 'Erreur Airtel Money : ' . $e->getMessage();
  302.                     $errorDetails = [
  303.                         'message' => $e->getMessage(),
  304.                         'code' => $e->getCode(),
  305.                         'file' => $e->getFile(),
  306.                         'line' => $e->getLine(),
  307.                         'trace' => $e->getTraceAsString()
  308.                     ];
  309.                     
  310.                     $this->logger->error('Airtel Money API Exception', $errorDetails);
  311.                     
  312.                     if ($this->getParameter('kernel.environment') === 'dev') {
  313.                         $errorMessage .= ' | Fichier: ' . basename($e->getFile()) . ':' . $e->getLine();
  314.                         if (method_exists($e, 'getErrorData') && $e->getErrorData()) {
  315.                             $errorMessage .= ' | Détails: ' . json_encode($e->getErrorData());
  316.                         }
  317.                     }
  318.                     
  319.                     $this->addFlash('error', $errorMessage);
  320.                     return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);
  321.                     
  322.                 } catch (\Exception $e) {
  323.                     $errorDetails = [
  324.                         'message' => $e->getMessage(),
  325.                         'code' => $e->getCode(),
  326.                         'file' => $e->getFile(),
  327.                         'line' => $e->getLine(),
  328.                         'trace' => $e->getTraceAsString()
  329.                     ];
  330.                     
  331.                     $this->logger->error('Airtel Money Exception générique', $errorDetails);
  332.                     
  333.                     $errorMessage = 'Une erreur est survenue lors de l\'initiation du paiement Airtel Money.';
  334.                     if ($this->getParameter('kernel.environment') === 'dev') {
  335.                         $errorMessage .= ' | Erreur: ' . $e->getMessage() . ' | Fichier: ' . basename($e->getFile()) . ':' . $e->getLine();
  336.                     }
  337.                     
  338.                     $this->addFlash('error', $errorMessage);
  339.                     return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);
  340.                 }
  341.             } elseif ($data['type'] == 'stripe') {
  342.                 $user = $userRepository->findOneBy(['phone' => $this->getUser()->getUserIdentifier()]);
  343.                 $amountCents = $this->stripePaymentService->convertToCents($total);
  344.                 // Définis les URLs de redirection après paiement
  345.                 $successUrl = $this->generateUrl('app_orders_show', ['id' => $order->getId()], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
  346.                 $cancelUrl = $this->generateUrl('app_orders_index', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);
  347.                 $checkout = $this->stripePaymentService->createCheckoutSession(
  348.                     $amountCents,
  349.                     'usd',
  350.                     $successUrl,
  351.                     $cancelUrl,
  352.                     [
  353.                         'order_id' => $order->getId(),
  354.                         'user_id' => $user->getId()
  355.                     ]
  356.                 );
  357.                 $paiement = new Paiement();
  358.                 $paiement->setOrders($order);
  359.                 $paiement->setType('Stripe');
  360.                 $paiement->setAmount($total);
  361.                 $paiement->setStatus('pending');
  362.                 $paiement->setOmNotifToken('');
  363.                 $paiement->setOmPayToken('');
  364.                 // Tu peux stocker l'ID de session Stripe si tu veux, par exemple :
  365.                 $paiement->setSPaymentIntent($checkout['session_id']); // ou $session->id si tu préfères
  366.                 $entityManager->persist($paiement);
  367.                 $entityManager->flush();
  368.                 // Redirige l'utilisateur vers l'URL de paiement Stripe
  369.                 return $this->redirect($checkout['url']);
  370.             }
  371.             return $this->redirectToRoute('app_orders_index');
  372.         }
  373.         return $this->render('orders/show.html.twig', [
  374.             'order' => $order,
  375.             'form' => $form->createView(),
  376.         ]);
  377.     }
  378.     #[Route('/OM/notif', name: '_notif', methods: ['POST'])]
  379.     public function handleNotification(Request $request, EntityManagerInterface $entityManager): Response
  380.     {
  381.         $data = json_decode($request->getContent(), true);
  382.         if (!isset($data['status'], $data['notif_token'], $data['txnid'])) {
  383.             return $this->json(['error' => 'Invalid payload'], Response::HTTP_BAD_REQUEST);
  384.         }
  385.         /** @var Paiement  */
  386.         $paiement = $entityManager->getRepository(Paiement::class)->findOneBy(['om_notif_token' => $data['notif_token']]);
  387.         if (!$paiement) {
  388.             return $this->json(['error' => 'Payment not found'], Response::HTTP_NOT_FOUND);
  389.         }
  390.         if ($data['status'] === 'SUCCESS') {
  391.             $paiement->setStatus('completed');
  392.             $paiement->getOrders()->setStatus('completed');
  393.         } else {
  394.             $paiement->setStatus('failed');
  395.             $paiement->getOrders()->setStatus('failed');
  396.         }
  397.         $entityManager->flush();
  398.         return $this->json(['message' => 'Notification processed successfully']);
  399.     }
  400.     #[Route('/cancel/{uid}', name: '_cancel')]
  401.     #[IsGranted('ROLE_USER')]
  402.     public function cancelOrder(string $uid, EntityManagerInterface $entityManager): Response
  403.     {
  404.         $order = $entityManager->getRepository(Order::class)->findOneBy(['uid' => $uid]);
  405.         if (!$order || $order->getClient() !== $this->getUser()) {
  406.             $this->addFlash('error', 'Order not found or access denied.');
  407.             return $this->redirectToRoute('app_orders_index');
  408.         }
  409.         if ($order->getStatus() === 'completed') {
  410.             $this->addFlash('error', 'Completed orders cannot be canceled.');
  411.             return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);
  412.         }
  413.         $order->setStatus('canceled');
  414.         $entityManager->flush();
  415.         $this->addFlash('success', 'Order has been canceled successfully.');
  416.         return $this->redirectToRoute('app_orders_index');
  417.     }
  418.     #[Route("/api/mvola/callback", methods: ['PUT', 'POST'], name: '_api_mvola_callback')]
  419.     public function handleCallback(Request $request, EntityManagerInterface $entityManager, PaiementRepository $paiementRepository): JsonResponse
  420.     {
  421.         try {
  422.             // Get callback data
  423.             $data = json_decode($request->getContent(), true);
  424.             
  425.             if (!$data) {
  426.                 throw new MVolaApiException('Invalid callback data');
  427.             }
  428.             
  429.             // Validate and process callback data
  430.             $validatedData = $this->mvolaApiService->handleCallback($data);
  431.             
  432.             // Récupérer le serverCorrelationId depuis les données validées
  433.             $serverCorrelationId = $validatedData['serverCorrelationId'] ?? null;
  434.             
  435.             if (!$serverCorrelationId) {
  436.                 throw new MVolaApiException('Missing serverCorrelationId in callback data');
  437.             }
  438.             
  439.             // Trouver le paiement correspondant
  440.             $paiement = $paiementRepository->findOneBy(['CorrelationId' => $serverCorrelationId, 'type' => 'MVola']);
  441.             
  442.             if (!$paiement) {
  443.                 throw new MVolaApiException('Payment not found for serverCorrelationId: ' . $serverCorrelationId);
  444.             }
  445.             
  446.             // Mettre à jour le statut du paiement et de la commande
  447.             $transactionStatus = $validatedData['transactionStatus'] ?? null;
  448.             
  449.             if ($transactionStatus === 'completed') {
  450.                 $paiement->setStatus('completed');
  451.                 $paiement->getOrders()->setStatus('completed');
  452.             } elseif ($transactionStatus === 'failed') {
  453.                 $paiement->setStatus('failed');
  454.                 $paiement->getOrders()->setStatus('failed');
  455.             } else {
  456.                 // Statut inconnu, on garde pending
  457.                 $paiement->setStatus('pending');
  458.             }
  459.             
  460.             $entityManager->flush();
  461.             
  462.             return $this->json([
  463.                 'success' => true,
  464.                 'message' => 'Callback processed successfully',
  465.             ]);
  466.         } catch (MVolaApiException $e) {
  467.             return $this->json([
  468.                 'error' => true,
  469.                 'message' => $e->getMessage(),
  470.             ], Response::HTTP_BAD_REQUEST);
  471.         } catch (\Exception $e) {
  472.             return $this->json([
  473.                 'error' => true,
  474.                 'message' => 'An error occurred while processing the callback.',
  475.             ], Response::HTTP_INTERNAL_SERVER_ERROR);
  476.         }
  477.     }
  478.     #[Route("/api/airtel/callback", methods: ['POST', 'PUT'], name: '_api_airtel_callback')]
  479.     public function handleAirtelCallback(Request $request, EntityManagerInterface $entityManager, PaiementRepository $paiementRepository): JsonResponse
  480.     {
  481.         try {
  482.             // Get callback data
  483.             $data = json_decode($request->getContent(), true);
  484.             
  485.             if (!$data) {
  486.                 throw new AirtelApiException('Invalid callback data');
  487.             }
  488.             
  489.             // Validate and process callback data
  490.             $validatedData = $this->airtelMoney->handleCallback($data);
  491.             
  492.             // Récupérer le transaction_id depuis les données validées
  493.             $transactionId = $validatedData['transaction_id'] ?? null;
  494.             
  495.             if (!$transactionId) {
  496.                 throw new AirtelApiException('Missing transaction_id in callback data');
  497.             }
  498.             
  499.             // Trouver le paiement correspondant
  500.             $paiement = $paiementRepository->findOneBy(['om_pay_token' => $transactionId, 'type' => 'AirtelMoney']);
  501.             
  502.             if (!$paiement) {
  503.                 throw new AirtelApiException('Payment not found for transaction_id: ' . $transactionId);
  504.             }
  505.             
  506.             // Mettre à jour le statut du paiement et de la commande
  507.             $transactionStatus = $validatedData['status'] ?? null;
  508.             
  509.             if ($transactionStatus === 'TS' || $transactionStatus === 'SUCCESS') {
  510.                 $paiement->setStatus('completed');
  511.                 $paiement->getOrders()->setStatus('completed');
  512.             } elseif ($transactionStatus === 'TF' || $transactionStatus === 'FAILED') {
  513.                 $paiement->setStatus('failed');
  514.                 $paiement->getOrders()->setStatus('failed');
  515.             } else {
  516.                 // Statut inconnu, on garde pending
  517.                 $paiement->setStatus('pending');
  518.             }
  519.             
  520.             $entityManager->flush();
  521.             
  522.             return $this->json([
  523.                 'success' => true,
  524.                 'message' => 'Callback processed successfully',
  525.             ]);
  526.         } catch (AirtelApiException $e) {
  527.             return $this->json([
  528.                 'error' => true,
  529.                 'message' => $e->getMessage(),
  530.             ], Response::HTTP_BAD_REQUEST);
  531.         } catch (\Exception $e) {
  532.             return $this->json([
  533.                 'error' => true,
  534.                 'message' => 'An error occurred while processing the callback.',
  535.             ], Response::HTTP_INTERNAL_SERVER_ERROR);
  536.         }
  537.     }
  538.    
  539.     #[Route('/api/mvola/merchant-pay/transaction/{transactionId}', name: 'api_mvola_merchant_pay_transaction_details', methods: ['GET'])]
  540.     public function getTransactionDetails(Request $request, string $transactionId): JsonResponse
  541.     {
  542.         try {
  543.             // Validate API key from request
  544.             $this->validateApiKey($request);
  545.             
  546.             // Call service
  547.             $response = $this->mvolaApiService->getTransactionDetails($transactionId);
  548.             
  549.             return $this->json($response);
  550.         } catch (MVolaApiException $e) {
  551.             return $this->json([
  552.                 'error' => true,
  553.                 'message' => $e->getMessage(),
  554.                 'details' => $e->getErrorData(),
  555.             ], Response::HTTP_BAD_REQUEST);
  556.         } catch (\Exception $e) {
  557.             return $this->json([
  558.                 'error' => true,
  559.                 'message' => 'An error occurred while processing your request.',
  560.             ], Response::HTTP_INTERNAL_SERVER_ERROR);
  561.         }
  562.     }
  563.     #[Route('/api/mvola/merchant-pay/status/{serverCorrelationId}', name: 'api_mvola_merchant_pay_transaction_status', methods: ['GET'])]
  564.     public function getTransactionStatus(Request $request, string $serverCorrelationId): JsonResponse
  565.     {
  566.         try {
  567.             // Validate API key from request
  568.             $this->validateApiKey($request);
  569.             
  570.             // Call service
  571.             $response = $this->mvolaApiService->getTransactionStatus($serverCorrelationId);
  572.             
  573.             return $this->json($response);
  574.         } catch (MVolaApiException $e) {
  575.             return $this->json([
  576.                 'error' => true,
  577.                 'message' => $e->getMessage(),
  578.                 'details' => $e->getErrorData(),
  579.             ], Response::HTTP_BAD_REQUEST);
  580.         } catch (\Exception $e) {
  581.             return $this->json([
  582.                 'error' => true,
  583.                 'message' => 'An error occurred while processing your request.',
  584.             ], Response::HTTP_INTERNAL_SERVER_ERROR);
  585.         }
  586.     }
  587.     /**
  588.      * Validate API key from request
  589.      * 
  590.      * @param Request $request
  591.      * @throws MVolaApiException If API key is invalid
  592.      */
  593.     private function validateApiKey(Request $request): void
  594.     {
  595.         $apiKey = $request->headers->get('X-API-Key');
  596.         $expectedApiKey = $this->getParameter('app.api_key');
  597.         
  598.         if (!$apiKey || $apiKey !== $expectedApiKey) {
  599.             throw new MVolaApiException('Invalid or missing API key', Response::HTTP_UNAUTHORIZED);
  600.         }
  601.     }
  602.     #[Route('/stripe/webhook', name: 'stripe_webhook', methods: ['POST'])]
  603.     public function handleWebhook(Request $request,StripePaymentService $stripePaymentService,PaiementRepository $paiementRepository,EntityManagerInterface $entityManager): JsonResponse
  604.     {
  605.         try {
  606.             $result = $stripePaymentService->handleWebhook($request);
  607.             if ($result['event_type'] === 'payment_intent.succeeded') {
  608.                 $paymentIntentId = $result['event_id'];
  609.                 $paiement = $paiementRepository->findOneBy(['SPaymentIntent' => $paymentIntentId]);
  610.                 if ($paiement) {
  611.                     $paiement->setStatus('paid');
  612.                     $entityManager->flush();
  613.                 }
  614.             }
  615.             if ($result['event_type'] === 'payment_intent.canceled') {
  616.                 $paymentIntentId = $result['event_id'];
  617.                 $paiement = $paiementRepository->findOneBy(['SPaymentIntent' => $paymentIntentId]);
  618.                 if ($paiement) {
  619.                     $paiement->setStatus('canceled');
  620.                     $entityManager->flush();
  621.                 }
  622.             }
  623.             if ($result['event_type'] === 'payment_intent.failed') {
  624.                 $paymentIntentId = $result['event_id'];
  625.                 $paiement = $paiementRepository->findOneBy(['SPaymentIntent' => $paymentIntentId]);
  626.                 if ($paiement) {
  627.                     $paiement->setStatus('failed');
  628.                     $entityManager->flush();
  629.                 }
  630.             }
  631.             return $this->json($result);
  632.         } catch (\Exception $e) {
  633.             return $this->json([
  634.                 'error' => 'Webhook processing failed',
  635.                 'message' => $e->getMessage()
  636.             ], 400);
  637.         }
  638.     }
  639. }