src/Controller/OrdersController.php line 51
<?phpnamespace App\Controller;use App\Dto\MerchantPay\InitiateTransactionRequest;use App\Entity\Order;use App\Entity\Paiement;use App\Entity\Playlist;use App\Services\OrangeMoney;use App\Entity\PurchaceOrders;use App\Form\PaiementListType;use Symfony\Component\Uid\UuidV3;use App\Exception\MVolaApiException;use App\Repository\OrderRepository;use App\Repository\PaiementRepository;use App\Repository\PlaylistRepository;use App\Repository\UserRepository;use App\Services\MVolaApiService;use Doctrine\ORM\EntityManagerInterface;use Symfony\Component\HttpFoundation\Request;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Routing\Annotation\Route;use Symfony\Component\HttpFoundation\JsonResponse;use Symfony\Component\Security\Http\Attribute\IsGranted;use Symfony\Component\HttpFoundation\Session\SessionInterface;use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;use Symfony\Component\Serializer\SerializerInterface;use App\Services\StripePaymentService;use App\Services\AirtelMoney;use App\Exception\AirtelApiException;use Psr\Log\LoggerInterface;#[Route('/orders', name: 'app_orders')]class OrdersController extends AbstractController{public function __construct(private MVolaApiService $mvolaApiService,private SerializerInterface $serializer,private StripePaymentService $stripePaymentService,private AirtelMoney $airtelMoney,private LoggerInterface $logger) {}#[Route('/', name: '_index')]#[IsGranted('ROLE_USER')]public function index(SessionInterface $sessionInterface,PlaylistRepository $playlistRepository): Response{$card = $sessionInterface->get('card', []);$playlists = $playlistRepository->findBy(['id' => $card]);return $this->render('orders/index.html.twig', ['playlists' => $playlists,]);}#[Route('/mycommande', name: '_mycommande')]#[IsGranted('ROLE_USER')]public function mycommande(OrderRepository $orderRepository): Response{$orders = $orderRepository->findBy(['client' => $this->getUser()],['id' => 'DESC']);return $this->render('orders/mycommande.html.twig', ['orders' => $orders,]);}#[Route('/commande', name: '_commande')]#[IsGranted('ROLE_USER_ADMIN')]public function commande(OrderRepository $orderRepository): Response{$orders = $orderRepository->findBy([],['id' => 'DESC']);return $this->render('orders/commande.html.twig', ['orders' => $orders,]);}//create order from card#[Route('/createOrder', name: '_create')]#[IsGranted('ROLE_USER')]public function createOrder(SessionInterface $sessionInterface,EntityManagerInterface $entityManager): Response{$card = $sessionInterface->get('card', []);if (count($card) > 0) {$order = (new Order())->setClient($this->getUser())->setCeatedAt(new \DateTimeImmutable())->setStatus('created');$entityManager->persist($order);$entityManager->flush();foreach ($card as $playlistId) {$playlist = $entityManager->getRepository(Playlist::class)->find($playlistId);if (!$playlist) {continue;}$purchace = (new PurchaceOrders())->setPlaylist($playlist)->setAmount($playlist->getPrix())->setOrders($order);$entityManager->persist($purchace);$entityManager->flush();}$sessionInterface->set('card', []);return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);}return $this->redirectToRoute('app_orders_index');}//order show#[Route('/show/{id}', name: '_show')]#[IsGranted('ROLE_USER')]public function show(Order $order,Request $request, OrangeMoney $orangeMoney,EntityManagerInterface $entityManager,UserRepository $userRepository): Response{if ($order->getClient() != $this->getUser()) {return $this->redirectToRoute('app_orders_index');}// create form PaymentList$form = $this->createForm(PaiementListType::class);$form->handleRequest($request);if ($form->isSubmitted() && $form->isValid()) {$data = $form->getData();// calculer le montant total$total = 0;foreach ($order->getPurchaces() as $purchace) {$total += $purchace->getAmount();}if ($data['type'] == 'OrangeMoney') {//create paiement with orangemoney$response = $orangeMoney->initiateWebPayment($total, $order->getUid(), $this->generateUrl('app_orders_index', [], true));if ($response['status'] == 201) {$paiement = new Paiement();$paiement->setOrders($order);$paiement->setType('OrangeMoney');$paiement->setAmount($total);$paiement->setStatus('pending');$paiement->setOmPayToken($response['pay_token']);$paiement->setOmNotifToken($response['notif_token']);$entityManager->persist($paiement);$entityManager->flush();$this->addFlash('success', 'Paiement effectué avec succès');return $this->redirect($response['payment_url']);} else {$this->addFlash('error', 'Erreur de paiement');}$order->setStatus('pending');} elseif ($data['type'] == 'MVola') {try {$user = $userRepository->findOneBy(['phone' => $this->getUser()->getUserIdentifier()]);if (!$user || !$user->getPhone()) {$this->addFlash('error', 'Numéro de téléphone non trouvé pour l\'utilisateur.');return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);}$customerMsisdn = $user->getPhone();$partnerMsisdn = $this->mvolaApiService->getPartnerMsisdn();// En environnement de développement, MVola impose l'utilisation// de MSISDN de test. On force donc le numéro client sur le MSISDN// de test fourni par Telma pour la sandbox.if ($this->getParameter('kernel.environment') === 'dev') {// Numéros de test officiels MVola (Telma) pour sandbox :// 0343500003, 0343500004// On utilise le premier comme client (débit).$customerMsisdn = '0343500004';}// Convertir le montant en chaîne sans décimales (selon la documentation MVola)$amountString = (string) intval($total);// Préparer la requête DTO$requestDto = new InitiateTransactionRequest();$requestDto->amount = $amountString;$requestDto->currency = 'Ar';$requestDto->setDebitPartyMsisdn($customerMsisdn);$requestDto->setCreditPartyMsisdn($partnerMsisdn);$requestDto->descriptionText = 'Paiement commande ' . $order->getId();$requestDto->requestingOrganisationTransactionReference = 'order_' . $order->getId() . '_' . time();$requestDto->setPartnerName('edena');$requestDto->setForeignCurrency('USD', '1');// Générer l'URL de callback$callbackUrl = $this->generateUrl('app_orders_api_mvola_callback', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);$this->logger->info('MVola: Tentative d\'initiation de transaction', ['order_id' => $order->getId(),'amount' => $amountString,'customer' => $customerMsisdn,'partner' => $partnerMsisdn,'callback_url' => $callbackUrl]);// Initier la transaction$response = $this->mvolaApiService->initiateTransaction($requestDto, $callbackUrl);// Enregistrer le paiement en base$paiement = new Paiement();$paiement->setOrders($order);$paiement->setType('MVola');$paiement->setAmount((int) $total);$paiement->setStatus('pending');// Pour MVola, on stocke le serverCorrelationId dans om_pay_token// car les callbacks et vérifications utilisent ce champ.$paiement->setOmPayToken($response->serverCorrelationId);// On garde aussi une colonne dédiée CorrelationId pour suivi interne.$paiement->setCorrelationId($response->serverCorrelationId);$paiement->setOmNotifToken(null);$entityManager->persist($paiement);// Mettre à jour le statut de la commande$order->setStatus('pending');$entityManager->flush();$this->addFlash('success', 'Paiement MVola initié avec succès. Veuillez valider la transaction sur votre téléphone.');return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);} catch (MVolaApiException $e) {$errorMessage = 'Erreur MVola : ' . $e->getMessage();$errorDetails = ['message' => $e->getMessage(),'code' => $e->getCode(),'file' => $e->getFile(),'line' => $e->getLine(),'trace' => $e->getTraceAsString()];$this->logger->error('MVola API Exception', $errorDetails);if ($this->getParameter('kernel.environment') === 'dev') {$errorMessage .= ' | Fichier: ' . basename($e->getFile()) . ':' . $e->getLine();if ($e->getErrorData()) {$errorMessage .= ' | Détails: ' . json_encode($e->getErrorData());}}$this->addFlash('error', $errorMessage);return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);} catch (\Exception $e) {$errorDetails = ['message' => $e->getMessage(),'code' => $e->getCode(),'file' => $e->getFile(),'line' => $e->getLine(),'trace' => $e->getTraceAsString()];$this->logger->error('MVola Exception générique', $errorDetails);$errorMessage = 'Une erreur est survenue lors de l\'initiation du paiement MVola.';if ($this->getParameter('kernel.environment') === 'dev') {$errorMessage .= ' | Erreur: ' . $e->getMessage() . ' | Fichier: ' . basename($e->getFile()) . ':' . $e->getLine();}$this->addFlash('error', $errorMessage);return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);}} elseif ($data['type'] == 'AirtelMoney') {try {$user = $userRepository->findOneBy(['phone' => $this->getUser()->getUserIdentifier()]);if (!$user || !$user->getPhone()) {$this->addFlash('error', 'Numéro de téléphone non trouvé pour l\'utilisateur.');return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);}$customerMsisdn = $user->getPhone();$transactionId = 'order_' . $order->getId() . '_' . time();// Générer l'URL de callback$callbackUrl = $this->generateUrl('app_orders_api_airtel_callback', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);$this->logger->info('Airtel Money: Tentative d\'initiation de paiement', ['order_id' => $order->getId(),'amount' => $total,'customer' => $customerMsisdn,'transaction_id' => $transactionId,'callback_url' => $callbackUrl]);// Initier le paiement$response = $this->airtelMoney->initiatePayment($total,$customerMsisdn,$transactionId,'Commande ' . $order->getId(),$callbackUrl);// Enregistrer le paiement en base$paiement = new Paiement();$paiement->setOrders($order);$paiement->setType('AirtelMoney');$paiement->setAmount((int) $total);$paiement->setStatus('pending');$paiement->setOmPayToken($response['transaction_id']);$paiement->setOmNotifToken(null);$entityManager->persist($paiement);// Mettre à jour le statut de la commande$order->setStatus('pending');$entityManager->flush();$this->addFlash('success', 'Paiement Airtel Money initié avec succès. Veuillez valider la transaction sur votre téléphone.');return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);} catch (AirtelApiException $e) {$errorMessage = 'Erreur Airtel Money : ' . $e->getMessage();$errorDetails = ['message' => $e->getMessage(),'code' => $e->getCode(),'file' => $e->getFile(),'line' => $e->getLine(),'trace' => $e->getTraceAsString()];$this->logger->error('Airtel Money API Exception', $errorDetails);if ($this->getParameter('kernel.environment') === 'dev') {$errorMessage .= ' | Fichier: ' . basename($e->getFile()) . ':' . $e->getLine();if (method_exists($e, 'getErrorData') && $e->getErrorData()) {$errorMessage .= ' | Détails: ' . json_encode($e->getErrorData());}}$this->addFlash('error', $errorMessage);return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);} catch (\Exception $e) {$errorDetails = ['message' => $e->getMessage(),'code' => $e->getCode(),'file' => $e->getFile(),'line' => $e->getLine(),'trace' => $e->getTraceAsString()];$this->logger->error('Airtel Money Exception générique', $errorDetails);$errorMessage = 'Une erreur est survenue lors de l\'initiation du paiement Airtel Money.';if ($this->getParameter('kernel.environment') === 'dev') {$errorMessage .= ' | Erreur: ' . $e->getMessage() . ' | Fichier: ' . basename($e->getFile()) . ':' . $e->getLine();}$this->addFlash('error', $errorMessage);return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);}} elseif ($data['type'] == 'stripe') {$user = $userRepository->findOneBy(['phone' => $this->getUser()->getUserIdentifier()]);$amountCents = $this->stripePaymentService->convertToCents($total);// Définis les URLs de redirection après paiement$successUrl = $this->generateUrl('app_orders_show', ['id' => $order->getId()], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);$cancelUrl = $this->generateUrl('app_orders_index', [], \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_URL);$checkout = $this->stripePaymentService->createCheckoutSession($amountCents,'usd',$successUrl,$cancelUrl,['order_id' => $order->getId(),'user_id' => $user->getId()]);$paiement = new Paiement();$paiement->setOrders($order);$paiement->setType('Stripe');$paiement->setAmount($total);$paiement->setStatus('pending');$paiement->setOmNotifToken('');$paiement->setOmPayToken('');// Tu peux stocker l'ID de session Stripe si tu veux, par exemple :$paiement->setSPaymentIntent($checkout['session_id']); // ou $session->id si tu préfères$entityManager->persist($paiement);$entityManager->flush();// Redirige l'utilisateur vers l'URL de paiement Stripereturn $this->redirect($checkout['url']);}return $this->redirectToRoute('app_orders_index');}return $this->render('orders/show.html.twig', ['order' => $order,'form' => $form->createView(),]);}#[Route('/OM/notif', name: '_notif', methods: ['POST'])]public function handleNotification(Request $request, EntityManagerInterface $entityManager): Response{$data = json_decode($request->getContent(), true);if (!isset($data['status'], $data['notif_token'], $data['txnid'])) {return $this->json(['error' => 'Invalid payload'], Response::HTTP_BAD_REQUEST);}/** @var Paiement */$paiement = $entityManager->getRepository(Paiement::class)->findOneBy(['om_notif_token' => $data['notif_token']]);if (!$paiement) {return $this->json(['error' => 'Payment not found'], Response::HTTP_NOT_FOUND);}if ($data['status'] === 'SUCCESS') {$paiement->setStatus('completed');$paiement->getOrders()->setStatus('completed');} else {$paiement->setStatus('failed');$paiement->getOrders()->setStatus('failed');}$entityManager->flush();return $this->json(['message' => 'Notification processed successfully']);}#[Route('/cancel/{uid}', name: '_cancel')]#[IsGranted('ROLE_USER')]public function cancelOrder(string $uid, EntityManagerInterface $entityManager): Response{$order = $entityManager->getRepository(Order::class)->findOneBy(['uid' => $uid]);if (!$order || $order->getClient() !== $this->getUser()) {$this->addFlash('error', 'Order not found or access denied.');return $this->redirectToRoute('app_orders_index');}if ($order->getStatus() === 'completed') {$this->addFlash('error', 'Completed orders cannot be canceled.');return $this->redirectToRoute('app_orders_show', ['id' => $order->getId()]);}$order->setStatus('canceled');$entityManager->flush();$this->addFlash('success', 'Order has been canceled successfully.');return $this->redirectToRoute('app_orders_index');}#[Route("/api/mvola/callback", methods: ['PUT', 'POST'], name: '_api_mvola_callback')]public function handleCallback(Request $request, EntityManagerInterface $entityManager, PaiementRepository $paiementRepository): JsonResponse{try {// Get callback data$data = json_decode($request->getContent(), true);if (!$data) {throw new MVolaApiException('Invalid callback data');}// Validate and process callback data$validatedData = $this->mvolaApiService->handleCallback($data);// Récupérer le serverCorrelationId depuis les données validées$serverCorrelationId = $validatedData['serverCorrelationId'] ?? null;if (!$serverCorrelationId) {throw new MVolaApiException('Missing serverCorrelationId in callback data');}// Trouver le paiement correspondant$paiement = $paiementRepository->findOneBy(['CorrelationId' => $serverCorrelationId, 'type' => 'MVola']);if (!$paiement) {throw new MVolaApiException('Payment not found for serverCorrelationId: ' . $serverCorrelationId);}// Mettre à jour le statut du paiement et de la commande$transactionStatus = $validatedData['transactionStatus'] ?? null;if ($transactionStatus === 'completed') {$paiement->setStatus('completed');$paiement->getOrders()->setStatus('completed');} elseif ($transactionStatus === 'failed') {$paiement->setStatus('failed');$paiement->getOrders()->setStatus('failed');} else {// Statut inconnu, on garde pending$paiement->setStatus('pending');}$entityManager->flush();return $this->json(['success' => true,'message' => 'Callback processed successfully',]);} catch (MVolaApiException $e) {return $this->json(['error' => true,'message' => $e->getMessage(),], Response::HTTP_BAD_REQUEST);} catch (\Exception $e) {return $this->json(['error' => true,'message' => 'An error occurred while processing the callback.',], Response::HTTP_INTERNAL_SERVER_ERROR);}}#[Route("/api/airtel/callback", methods: ['POST', 'PUT'], name: '_api_airtel_callback')]public function handleAirtelCallback(Request $request, EntityManagerInterface $entityManager, PaiementRepository $paiementRepository): JsonResponse{try {// Get callback data$data = json_decode($request->getContent(), true);if (!$data) {throw new AirtelApiException('Invalid callback data');}// Validate and process callback data$validatedData = $this->airtelMoney->handleCallback($data);// Récupérer le transaction_id depuis les données validées$transactionId = $validatedData['transaction_id'] ?? null;if (!$transactionId) {throw new AirtelApiException('Missing transaction_id in callback data');}// Trouver le paiement correspondant$paiement = $paiementRepository->findOneBy(['om_pay_token' => $transactionId, 'type' => 'AirtelMoney']);if (!$paiement) {throw new AirtelApiException('Payment not found for transaction_id: ' . $transactionId);}// Mettre à jour le statut du paiement et de la commande$transactionStatus = $validatedData['status'] ?? null;if ($transactionStatus === 'TS' || $transactionStatus === 'SUCCESS') {$paiement->setStatus('completed');$paiement->getOrders()->setStatus('completed');} elseif ($transactionStatus === 'TF' || $transactionStatus === 'FAILED') {$paiement->setStatus('failed');$paiement->getOrders()->setStatus('failed');} else {// Statut inconnu, on garde pending$paiement->setStatus('pending');}$entityManager->flush();return $this->json(['success' => true,'message' => 'Callback processed successfully',]);} catch (AirtelApiException $e) {return $this->json(['error' => true,'message' => $e->getMessage(),], Response::HTTP_BAD_REQUEST);} catch (\Exception $e) {return $this->json(['error' => true,'message' => 'An error occurred while processing the callback.',], Response::HTTP_INTERNAL_SERVER_ERROR);}}#[Route('/api/mvola/merchant-pay/transaction/{transactionId}', name: 'api_mvola_merchant_pay_transaction_details', methods: ['GET'])]public function getTransactionDetails(Request $request, string $transactionId): JsonResponse{try {// Validate API key from request$this->validateApiKey($request);// Call service$response = $this->mvolaApiService->getTransactionDetails($transactionId);return $this->json($response);} catch (MVolaApiException $e) {return $this->json(['error' => true,'message' => $e->getMessage(),'details' => $e->getErrorData(),], Response::HTTP_BAD_REQUEST);} catch (\Exception $e) {return $this->json(['error' => true,'message' => 'An error occurred while processing your request.',], Response::HTTP_INTERNAL_SERVER_ERROR);}}#[Route('/api/mvola/merchant-pay/status/{serverCorrelationId}', name: 'api_mvola_merchant_pay_transaction_status', methods: ['GET'])]public function getTransactionStatus(Request $request, string $serverCorrelationId): JsonResponse{try {// Validate API key from request$this->validateApiKey($request);// Call service$response = $this->mvolaApiService->getTransactionStatus($serverCorrelationId);return $this->json($response);} catch (MVolaApiException $e) {return $this->json(['error' => true,'message' => $e->getMessage(),'details' => $e->getErrorData(),], Response::HTTP_BAD_REQUEST);} catch (\Exception $e) {return $this->json(['error' => true,'message' => 'An error occurred while processing your request.',], Response::HTTP_INTERNAL_SERVER_ERROR);}}/*** Validate API key from request** @param Request $request* @throws MVolaApiException If API key is invalid*/private function validateApiKey(Request $request): void{$apiKey = $request->headers->get('X-API-Key');$expectedApiKey = $this->getParameter('app.api_key');if (!$apiKey || $apiKey !== $expectedApiKey) {throw new MVolaApiException('Invalid or missing API key', Response::HTTP_UNAUTHORIZED);}}#[Route('/stripe/webhook', name: 'stripe_webhook', methods: ['POST'])]public function handleWebhook(Request $request,StripePaymentService $stripePaymentService,PaiementRepository $paiementRepository,EntityManagerInterface $entityManager): JsonResponse{try {$result = $stripePaymentService->handleWebhook($request);if ($result['event_type'] === 'payment_intent.succeeded') {$paymentIntentId = $result['event_id'];$paiement = $paiementRepository->findOneBy(['SPaymentIntent' => $paymentIntentId]);if ($paiement) {$paiement->setStatus('paid');$entityManager->flush();}}if ($result['event_type'] === 'payment_intent.canceled') {$paymentIntentId = $result['event_id'];$paiement = $paiementRepository->findOneBy(['SPaymentIntent' => $paymentIntentId]);if ($paiement) {$paiement->setStatus('canceled');$entityManager->flush();}}if ($result['event_type'] === 'payment_intent.failed') {$paymentIntentId = $result['event_id'];$paiement = $paiementRepository->findOneBy(['SPaymentIntent' => $paymentIntentId]);if ($paiement) {$paiement->setStatus('failed');$entityManager->flush();}}return $this->json($result);} catch (\Exception $e) {return $this->json(['error' => 'Webhook processing failed','message' => $e->getMessage()], 400);}}}