src/Controller/GroupSessionController/UserGroupSessionController.php line 177

Open in your IDE?
  1. <?php
  2. namespace App\Controller\GroupSessionController;
  3. use App\Entity\Child;
  4. use App\Entity\Gender;
  5. use App\Entity\Session;
  6. use App\Entity\SessionOrder;
  7. // use App\Entity\StripePlanForGroup; // StripePlanForGroup no longer needed — Stripe price created dynamically
  8. use App\Entity\Subject;
  9. use App\Entity\TrainingReport;
  10. use App\Entity\User;
  11. use App\Notification\EmailNotification;
  12. use App\Notification\SlackClient;
  13. use App\Order\OrderHelper;
  14. use App\Repository\DiscountRepository;
  15. use App\Repository\HolidayRepository;
  16. use App\Repository\LevelTestRepository;
  17. use App\Repository\SessionRegistrationDateRepository;
  18. use App\Repository\SessionRepository;
  19. // use App\Repository\StripePlanForGroupRepository; // StripePlanForGroup no longer needed
  20. use App\Repository\SubjectRepository;
  21. use App\Service\ActiveCampaignHelper;
  22. use App\Service\Currency\CurrencyChangeService;
  23. use App\Service\Stripe\StripeMultiCurrencyService;
  24. use App\StripeClient;
  25. use Doctrine\ORM\EntityManagerInterface;
  26. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  27. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
  28. use Stripe\Invoice;
  29. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  30. use Symfony\Component\HttpFoundation\RedirectResponse;
  31. use Symfony\Component\HttpFoundation\Request;
  32. use Symfony\Component\HttpFoundation\Response;
  33. use Symfony\Component\HttpFoundation\Session\Session as SessionSymfo;
  34. use Symfony\Component\Routing\Annotation\Route;
  35. /**
  36. * @Route("/session")
  37. */
  38. class UserGroupSessionController extends AbstractController
  39. {
  40. private $em;
  41. private $stripeClient;
  42. private $emailNotification;
  43. private $slackClient;
  44. private $activeCampaignHelper;
  45. private $stripeMultiCurrencyService;
  46. public function __construct(EntityManagerInterface $em, StripeClient $stripeClient, EmailNotification $emailNotification, SlackClient $slackClient,
  47. ActiveCampaignHelper $activeCampaignHelper, StripeMultiCurrencyService $stripeMultiCurrencyService
  48. ) {
  49. $this->em = $em;
  50. $this->stripeClient = $stripeClient;
  51. $this->emailNotification = $emailNotification;
  52. $this->slackClient = $slackClient;
  53. $this->activeCampaignHelper = $activeCampaignHelper;
  54. $this->stripeMultiCurrencyService = $stripeMultiCurrencyService;
  55. }
  56. //Voir
  57. /**
  58. * @Route("/{gender}/{tag}", name="session_index", defaults={"tag": "alKaamil"}, methods={"GET"})
  59. * @ParamConverter("gender", options={"mapping": {"gender" : "name"}})
  60. * @param Gender $gender
  61. * @param $tag
  62. * @param SubjectRepository $subjectRepository
  63. * @param SessionRepository $sessionRepository
  64. * @param HolidayRepository $holidayRepository
  65. * @param SessionRegistrationDateRepository $sessionRegistrationDateRepository
  66. * @param LevelTestRepository $levelTestRepository
  67. *
  68. * @return Response
  69. */
  70. public function index(
  71. Gender $gender,
  72. $tag,
  73. SubjectRepository $subjectRepository,
  74. SessionRepository $sessionRepository,
  75. HolidayRepository $holidayRepository,
  76. SessionRegistrationDateRepository $sessionRegistrationDateRepository,
  77. LevelTestRepository $levelTestRepository,
  78. Request $request,
  79. DiscountRepository $discountRepository
  80. ): Response {
  81. $parameter = $request->query->get('d');
  82. $discount = $discountRepository->findOneBy([
  83. 'urlParameter' => $parameter,
  84. ]);
  85. $subject = $subjectRepository->findOneBy([
  86. 'tagName' => $tag,
  87. ]);
  88. if ($discount) {
  89. if ($discount->getSubject() == $subject) {
  90. $isDiscount = true;
  91. $discountAmount = $discount->getAmount();
  92. $discountType = $discount->getType();
  93. } else {
  94. $isDiscount = false;
  95. $discountAmount = 0;
  96. $discountType = null;
  97. }
  98. } else {
  99. $isDiscount = false;
  100. $discountAmount = 0;
  101. $discountType = null;
  102. }
  103. if ( ! $subject) {
  104. $this->addFlash('error', "Le programme demandé n'existe pas");
  105. return $this->redirectToRoute('session_index', [
  106. 'gender' => $gender,
  107. '_locale' => $request->getLocale(),
  108. ]);
  109. }
  110. $sessions = $sessionRepository->openSessionByOrderSubjectGender(
  111. $subject->getTagName(),
  112. $gender->getName()
  113. );
  114. $holidays = $holidayRepository->findAll();
  115. $registrationDate = $sessionRegistrationDateRepository->findOneBy([
  116. 'subject' => $subject,
  117. ]);
  118. $levelTestInfo = $levelTestRepository->findOneBy([
  119. 'type' => "info",
  120. 'subject' => $subject,
  121. ]);
  122. return $this->render('session/index.html.twig', [
  123. 'sessions' => $sessions,
  124. 'gender' => $gender,
  125. 'local' => $this->getParameter('locale'),
  126. 'holidays' => $holidays,
  127. 'subject' => $subject,
  128. 'registrationDate' => $registrationDate,
  129. 'levelTest' => $levelTestInfo,
  130. 'isDiscount' => $isDiscount,
  131. 'discountAmount' => $discountAmount,
  132. 'discountType' => $discountType,
  133. 'parameter' => $parameter,
  134. ]);
  135. }
  136. //Acheter
  137. /**
  138. * @Route("/order/purchase/{id}", name="session_purchase", requirements={"id":"\d+"}), methods={"GET","POST"})
  139. * @ParamConverter("session", options={"id" : "id"})
  140. * @Security("is_granted('ROLE_USER')")
  141. * @param Session $session
  142. * @param Request $request
  143. * @param OrderHelper $orderHelper
  144. * @param SessionSymfo $sessionSymfo
  145. * @param StripePlanForGroupRepository $stripePlanForGroupRepository
  146. *
  147. * @return Response
  148. */
  149. public function purchase(Session $session, Request $request, OrderHelper $orderHelper, SessionSymfo $sessionSymfo,
  150. DiscountRepository $discountRepository, CurrencyChangeService $currencyChangeService
  151. ): Response {
  152. if ($session->getIsClose()) {
  153. $this->addFlash('error', 'Les inscriptions pour cette session sont fermées.');
  154. $referer = $request->headers->get('referer');
  155. if ($referer) {
  156. return $this->redirect($referer);
  157. }
  158. return $this->redirectToRoute('user_dashboard', ['_locale' => $request->getLocale()]);
  159. }
  160. $isDiscount = false;
  161. $sessionPrice = $session->getPrice();
  162. $parameter = $request->query->get('d');
  163. $discount = $discountRepository->findOneBy([
  164. 'urlParameter' => $parameter,
  165. ]);
  166. if ($discount && $discount->getSubject() == $session->getLevel()->getSubject()) {
  167. $isDiscount = true;
  168. $discountAmount = $discount->getAmount();
  169. $discountType = $discount->getType();
  170. if ($discountType == 'fixe') {
  171. $sessionPrice = $sessionPrice - $discountAmount;
  172. } else {
  173. $deduction = ($sessionPrice - $discountAmount) / 100;
  174. $sessionPrice = $sessionPrice - $deduction;
  175. }
  176. }
  177. // StripePlanForGroup no longer needed — Stripe price is created dynamically in createSubscription()
  178. // The stripeId from StripePlanForGroup was unused in the multicurrency flow
  179. $planId = null;
  180. $user = $this->getUser();
  181. $gender = $session->getTeacher()->getUserId()->getGender();
  182. if ($sessionSymfo->get('code-group')) {
  183. $stripeCoupon = $sessionSymfo->get('code-group');
  184. $code = $stripeCoupon->amount_off / 100;
  185. } else {
  186. $stripeCoupon = false;
  187. $code = 0;
  188. }
  189. $currencyInfo = $currencyChangeService->getCurrencyChangeForView($user);
  190. $currency = $currencyInfo['currencyCode'] ? $currencyInfo['currencyCode'] : 'EUR';
  191. $convertedAmount = null;
  192. if ($currency !== 'EUR') {
  193. try {
  194. $convertedAmount = $currencyChangeService->convertAmount($sessionPrice, $currency);
  195. } catch (\Exception $e) {
  196. // If conversion fails, fallback to EUR
  197. $currency = 'EUR';
  198. $convertedAmount = $sessionPrice;
  199. }
  200. } else {
  201. $convertedAmount = $sessionPrice;// If currency is EUR, use the base price
  202. }
  203. $error = false;
  204. if ($request->isMethod('POST')) {
  205. $token = $request->request->get('stripeToken');
  206. $participant = $request->request->get('child-select');
  207. $paymentType = $request->request->get('paymentType');
  208. if ($session->getSeat() > 0) {
  209. try {
  210. if ($paymentType == "twoTime") {
  211. // Paiement en deux fois avec support multicurrency : montant par échéance = total / 2
  212. $amountPerInstallment = $convertedAmount / 2;
  213. $invoice = $orderHelper->chargeCustomerForInstallement(
  214. $token,
  215. $user,
  216. $planId,
  217. $currency,
  218. $amountPerInstallment,
  219. $stripeCoupon
  220. );
  221. } else {
  222. if ($session->getPrice() == 0) {
  223. // Free session - set currency info from user's currency
  224. $paymentInfo = [
  225. 'currency' => $currency,
  226. 'amountPaid' => 0,
  227. 'amountReceivedUsd' => null,
  228. 'fees' => 0,
  229. ];
  230. $sessionOrder = $this->addSessionOrderToBdd($session, $user, "gratuit", 0, null, null, null, null, false, $participant, $paymentInfo);
  231. $this->addFlash('success', 'Inscription completée ! Un mail vous a été envoyé, vérifiez vos courriers indésirables (SPAM) si vous ne l\'avez pas reçu.');
  232. return $this->redirectToRoute(
  233. 'confirmation_purchase',
  234. [
  235. 'userId' => $user->getId(),
  236. 'orderType' => 'group',
  237. 'orderId' => $sessionOrder->getId(),
  238. '_locale' => $request->getLocale(),
  239. ]
  240. );
  241. } else {
  242. $invoice = $orderHelper->chargeCustomerSession($token, $user, $session, $stripeCoupon, $convertedAmount,$currency);
  243. }
  244. }
  245. } catch (\Stripe\Exception\CardException $e) {
  246. //error_log("A payment error occurred: {$e->getError()->message}");
  247. $error = 'Il y a un problème pour charger votre carte: '.$e->getError()->message;
  248. } catch (\Stripe\Exception\InvalidRequestException $e) {
  249. $error = 'Il y a un problème pour charger votre carte: '.$e->getError()->message;
  250. } catch (\Exception $e) {
  251. //error_log("Another problem occurred, maybe unrelated to Stripe.");
  252. $error = 'Il y a un problème pour charger votre carte: '.$e;
  253. }
  254. if ( ! $error and $invoice->paid == true) {
  255. $sessionOrder = $this->prepareCreationSessionOrderWithInvoice($invoice, $user, $session, $participant, $paymentType);
  256. $this->addFlash('success', 'Inscription completée ! Un mail vous a été envoyé, vérifiez vos courriers indésirables (SPAM) si vous ne l\'avez pas reçu.');
  257. $this->activeCampaignHelper->addOrder($sessionOrder);
  258. $this->activeCampaignHelper->updateCustomFieldLastLevelTestForContact($sessionOrder->getStudents()->getEmail(), $session);
  259. return $this->redirectToRoute(
  260. 'confirmation_purchase',
  261. [
  262. 'userId' => $user->getId(),
  263. 'orderType' => 'group',
  264. 'orderId' => $sessionOrder->getId(),
  265. '_locale' => $request->getLocale(),
  266. ]
  267. );
  268. } else {
  269. $this->addFlash('error', $error);
  270. return $this->redirectToRoute('session_purchase', [
  271. 'id' => $session->getId(),
  272. '_locale' => $request->getLocale(),
  273. ]);
  274. }
  275. } else {
  276. $this->addFlash('error', "Le groupe est complet, merci d'en choisir un autre");
  277. return $this->redirectToRoute('session_index', [
  278. 'gender' => $gender,
  279. '_locale' => $request->getLocale(),
  280. ]);
  281. }
  282. }
  283. return $this->render('session/purchase.html.twig', [
  284. 'stripe_public_key' => $this->getParameter('stripe_public_key'),
  285. 'session' => $session,
  286. 'currentUser' => $user,
  287. 'errorCard' => $error,
  288. 'code' => $code,
  289. // 'plan' => $plan, // StripePlanForGroup no longer needed
  290. 'price' => $convertedAmount,
  291. 'isDiscount' => $isDiscount,
  292. 'currency' => $currency,
  293. ]);
  294. }
  295. /**
  296. * @Route("/order/success/takallam", name="session_success_takallam", methods={"GET","POST"})
  297. */
  298. public function successTakallam(Request $request)
  299. {
  300. return $this->render('session/registration-takallam-success.twig');
  301. }
  302. /**
  303. * @Route("/coupon", name="session_coupon_add", methods={"POST"})
  304. * @Security("is_granted('ROLE_USER')")
  305. * @param Request $request
  306. * @param StripeClient $stripeClient
  307. * @param SessionSymfo $sessionSymfo
  308. *
  309. * @return RedirectResponse
  310. */
  311. public function addCouponAction(Request $request, StripeClient $stripeClient, SessionSymfo $sessionSymfo): RedirectResponse
  312. {
  313. $referer = $request->headers->get('referer');
  314. $code = $request->request->get('code');
  315. if ( ! $code) {
  316. $this->addFlash('error', 'Vous n\'avez entré aucun coupon');
  317. return $this->redirect($referer);
  318. }
  319. try {
  320. $stripeCoupon = $stripeClient->findCoupon($code);
  321. } catch (\Stripe\Exception\InvalidRequestException $e) {
  322. $this->addFlash('error', 'Coupon Invalide');
  323. return $this->redirect($referer);
  324. }
  325. if ($stripeCoupon->percent_off != null) {
  326. $this->addFlash('error', 'Coupon invalide pour ces cours');
  327. return $this->redirect($referer);
  328. }
  329. if ( ! $stripeCoupon->valid) {
  330. $this->addFlash('error', 'Coupon expiré');
  331. return $this->redirect($referer);
  332. }
  333. $sessionSymfo->set('code-group', $stripeCoupon);
  334. return $this->redirect($referer);
  335. }
  336. /**
  337. * @param Invoice $invoice
  338. * @param User $user
  339. * @param Session $session
  340. * @param $participant
  341. * @param $paymentType
  342. */
  343. private function prepareCreationSessionOrderWithInvoice(Invoice $invoice, User $user, Session $session, $participant, $paymentType)
  344. {
  345. $stripeCustomerId = $invoice->customer;
  346. $stripeCustomer = \Stripe\Customer::retrieve($stripeCustomerId);
  347. // Extract payment information from invoice (currency, amounts, fees)
  348. $paymentInfo = $this->stripeMultiCurrencyService->extractPaymentInfoFromInvoice($invoice);
  349. if ($paymentType == "twoTime") {
  350. $stripeSubscriptionId = $invoice->subscription;
  351. $stripeSubscription = \Stripe\Subscription::retrieve($stripeSubscriptionId);
  352. $subscriptionStatus = $stripeSubscription->status;
  353. if ($subscriptionStatus == 'active') {
  354. // For two-time payments, the paid amount is the total (both payments)
  355. $paidAmount = ($invoice->amount_paid / 100) * 2;
  356. $nextPaymentTimeStamp = $stripeSubscription->current_period_end;
  357. // For two-time payments, adjust payment info to reflect total amount
  358. $paymentInfo['amountPaid'] = $paidAmount;
  359. // USD amount and fees should also be doubled for two-time payments
  360. if ($paymentInfo['amountReceivedUsd'] !== null) {
  361. $paymentInfo['amountReceivedUsd'] = $paymentInfo['amountReceivedUsd'] * 2;
  362. }
  363. if ($paymentInfo['fees'] !== null) {
  364. $paymentInfo['fees'] = $paymentInfo['fees'] * 2;
  365. }
  366. } else {
  367. $this->addFlash('error', "Votre paiement n'a pas abouti");
  368. return $this->redirectToRoute('session_purchase', [
  369. 'id' => $session->getId(),
  370. '_locale' => $user->getLanguage()->getPole(),
  371. ]);
  372. }
  373. } else {
  374. $paidAmount = $invoice->amount_paid / 100;
  375. $stripeSubscription = null;
  376. $nextPaymentTimeStamp = null;
  377. $stripeSubscriptionId = null;
  378. }
  379. $cardDetails = $this->stripeClient->retrieveSource($stripeCustomerId, $stripeCustomer->default_source);
  380. $isTwoTime = $paymentType == "twoTime";
  381. $cardBrand = $cardDetails->brand;
  382. $cardLastDigits = $cardDetails->last4;
  383. $order = $this->addSessionOrderToBdd(
  384. $session,
  385. $user,
  386. $stripeCustomerId,
  387. $paidAmount,
  388. $cardBrand,
  389. $cardLastDigits,
  390. $nextPaymentTimeStamp,
  391. $stripeSubscriptionId,
  392. $isTwoTime,
  393. $participant,
  394. $paymentInfo
  395. );
  396. return $order;
  397. }
  398. private function addSessionOrderToBdd(Session $session, User $user, $stripeCustomerId, $paidAmount, $cardBrand, $cardLastDigit, $nextPaymentTimeStamp, $stripeSubscriptionId,
  399. bool $isTwoTime, $participant, array $paymentInfo = null
  400. ) {
  401. $childRepo = $this->em->getRepository(Child::class);
  402. if ($participant) {
  403. $participant = $childRepo->findOneBy(['id' => $participant]);
  404. } else {
  405. $participant = null;
  406. }
  407. $order = new SessionOrder();
  408. $order->setSession($session)
  409. ->setStripeCustomerId($stripeCustomerId)
  410. ->setAmountPaid($paidAmount)
  411. ->setIsActif(true)
  412. ->setCanceledByStudent(false)
  413. ->setCanceledByUs(false)
  414. ->setCardBrand($cardBrand)
  415. ->setCardLast4($cardLastDigit)
  416. ->setIsTwoTime($isTwoTime)
  417. ->setNextPaymentTimeStamp($nextPaymentTimeStamp)
  418. ->setStripeSubscriptionId($stripeSubscriptionId)
  419. ->setParticipant($participant)
  420. ->setStudents($user)
  421. ->setPaymentNb(1);
  422. // Set multi-currency payment information if available
  423. if ($paymentInfo !== null) {
  424. $order->setPaymentCurrency($paymentInfo['currency'])
  425. ->setAmountPaidLocalCurrency($paymentInfo['amountPaid'])
  426. ->setAmountReceivedUsd($paymentInfo['amountReceivedUsd'])
  427. ->setStripeFees($paymentInfo['fees']);
  428. }
  429. $session->decrementSeat();
  430. $this->em->persist($session);
  431. $this->em->persist($order);
  432. $this->em->flush();
  433. $program = $order->getSession()->getLevel()->getProgramUrls();
  434. $this->emailNotification->notifyStudentSessionOrder($order, $user, $program);
  435. //$this->slackClient->notifySecretariatGroupSessionInscription($order, $user);
  436. $this->createTakallamTrainingReportIfNeeded($order);
  437. return $order;
  438. }
  439. private function createTakallamTrainingReportIfNeeded(SessionOrder $sessionOrder): void
  440. {
  441. $subject = $sessionOrder->getSession()->getLevel()->getSubject();
  442. if ($subject->getTagName() !== 'takallam') {
  443. return;
  444. }
  445. $trainingReport = new TrainingReport();
  446. $trainingReport
  447. ->setUser($sessionOrder->getStudents())
  448. ->setParticipant($sessionOrder->getParticipant())
  449. ->setProgram($subject)
  450. ->setLevel($sessionOrder->getSession()->getLevel())
  451. ->setTeacher($sessionOrder->getSession()->getTeacher())
  452. ->setRelatedToGroupCourse($sessionOrder)
  453. ->setHadMidExam(false)
  454. ->setIsReadyToSend(false)
  455. ->setIsSent(false)
  456. ->setAskAt(new \DateTime());
  457. $this->em->persist($trainingReport);
  458. $this->em->flush();
  459. }
  460. }