src/Controller/UIController.php line 1109

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Address;
  4. use App\Entity\Brand;
  5. use App\Entity\Cart;
  6. use App\Entity\CartItem;
  7. use App\Entity\User;
  8. use App\Entity\Order;
  9. use App\Entity\PaymentMethod;
  10. use App\Entity\Product;
  11. use App\Entity\Category;
  12. use App\Entity\ShippingMethod;
  13. use App\Entity\Shop;
  14. use App\Entity\ShopCategory;
  15. use App\Form\AddressType;
  16. use App\Form\KycFormType;
  17. use App\Form\RegistrationFormType;
  18. use App\Repository\AddressRepository;
  19. use App\Repository\ProductRepository;
  20. use App\Security\EmailVerifier;
  21. use App\Service\NotificationService;
  22. use App\Service\RecommendationService;
  23. use App\Service\ProductComparisonService;
  24. use App\Service\ViewTrackingService;
  25. use App\Service\ShopFollowService;
  26. use App\Service\WishlistService;
  27. use App\Service\MonCashService;
  28. use App\Service\DropshipService;
  29. use App\Service\GiftCardService;
  30. use App\Form\GiftCardPurchaseType;
  31. use App\Entity\GiftCard;
  32. use DateTimeImmutable;
  33. use Doctrine\ORM\EntityManagerInterface;
  34. use LogicException;
  35. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  36. use Symfony\Bridge\Twig\Mime\TemplatedEmail;
  37. use Symfony\Component\Mime\Address as EmailAddress;
  38. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  39. use Symfony\Component\HttpFoundation\JsonResponse;
  40. use Symfony\Component\HttpFoundation\Request;
  41. use Symfony\Component\HttpFoundation\Response;
  42. use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
  43. use Symfony\Component\Routing\Annotation\Route;
  44. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  45. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  46. use Symfony\Component\Mailer\MailerInterface;
  47. use Symfony\Contracts\Translation\TranslatorInterface;
  48. use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
  49. use App\Form\ResetPasswordRequestFormType;
  50. use App\Form\ResetPasswordFormType;
  51. use App\Repository\ShopFollowRepository;
  52. use App\Service\PointService;
  53. use App\Service\ReferralService;
  54. #[Route('/'name'ui_')]
  55. class UIController extends AbstractController
  56. {
  57.     private EmailVerifier $emailVerifier;
  58.     private EntityManagerInterface $entityManager;
  59.     private WishlistService $wishlistService;
  60.     private ProductComparisonService $comparisonService;
  61.     private MonCashService $monCashService;
  62.     private DropshipService $dropshipService;
  63.     private RecommendationService $recommendationService;
  64.     public function __construct(EmailVerifier $emailVerifier,
  65.     EntityManagerInterface $entityManager,
  66.     WishlistService $wishlistService,
  67.     ProductComparisonService $comparisonService,
  68.     MonCashService $monCashService,
  69.     DropshipService $dropshipService,
  70.     RecommendationService $recommendationService,
  71.     private PointService $pointService,
  72.     private ReferralService $referralService,
  73.     private NotificationService $notificationService
  74.     )
  75.     {
  76.         $this->emailVerifier $emailVerifier;
  77.         $this->entityManager $entityManager;
  78.         $this->wishlistService $wishlistService;
  79.         $this->comparisonService $comparisonService;
  80.         $this->monCashService $monCashService;
  81.         $this->dropshipService $dropshipService;
  82.         $this->recommendationService $recommendationService;
  83.     }
  84.     #[Route('/api/search/suggest'name'api_search_suggest'methods: ['GET'])]
  85.     public function searchSuggest(Request $requestEntityManagerInterface $em): JsonResponse
  86.     {
  87.         $query trim((string) $request->query->get('q'''));
  88.         $limitPerType 5;
  89.         if ($query === '') {
  90.             return $this->json(['success' => true'results' => []]);
  91.         }
  92.         $results = [];
  93.         // Products
  94.         $products $em->createQueryBuilder()
  95.             ->select('p')
  96.             ->from(Product::class, 'p')
  97.             ->where('p.isActive = :active')
  98.             ->andWhere('LOWER(p.name) LIKE :q')
  99.             ->setParameter('active'true)
  100.             ->setParameter('q''%' mb_strtolower($query) . '%')
  101.             ->setMaxResults($limitPerType)
  102.             ->getQuery()
  103.             ->getResult();
  104.         foreach ($products as $product) {
  105.             $results[] = [
  106.                 'type' => 'product',
  107.                 'label' => $product->getName(),
  108.                 'url' => $this->generateUrl('ui_product_show', ['slug' => $product->getSlug()])
  109.             ];
  110.         }
  111.         // Shops
  112.         $shops $em->createQueryBuilder()
  113.             ->select('s')
  114.             ->from(Shop::class, 's')
  115.             ->where('s.isActive = :active')
  116.             ->andWhere('LOWER(s.name) LIKE :q')
  117.             ->setParameter('active'true)
  118.             ->setParameter('q''%' mb_strtolower($query) . '%')
  119.             ->setMaxResults($limitPerType)
  120.             ->getQuery()
  121.             ->getResult();
  122.         foreach ($shops as $shop) {
  123.             $results[] = [
  124.                 'type' => 'shop',
  125.                 'label' => $shop->getName(),
  126.                 'url' => $this->generateUrl('ui_shop_show', ['slug' => $shop->getSlug()])
  127.             ];
  128.         }
  129.         // Categories
  130.         $categories $em->createQueryBuilder()
  131.             ->select('c')
  132.             ->from(Category::class, 'c')
  133.             ->where('c.isActive = :active')
  134.             ->andWhere('LOWER(c.name) LIKE :q')
  135.             ->setParameter('active'true)
  136.             ->setParameter('q''%' mb_strtolower($query) . '%')
  137.             ->setMaxResults($limitPerType)
  138.             ->getQuery()
  139.             ->getResult();
  140.         foreach ($categories as $category) {
  141.             $results[] = [
  142.                 'type' => 'category',
  143.                 'label' => $category->getName(),
  144.                 'url' => $this->generateUrl('ui_listing', ['category' => $category->getSlug()])
  145.             ];
  146.         }
  147.         // Brands
  148.         $brands $em->createQueryBuilder()
  149.             ->select('b')
  150.             ->from(Brand::class, 'b')
  151.             ->where('b.isActive = :active')
  152.             ->andWhere('LOWER(b.name) LIKE :q')
  153.             ->setParameter('active'true)
  154.             ->setParameter('q''%' mb_strtolower($query) . '%')
  155.             ->setMaxResults($limitPerType)
  156.             ->getQuery()
  157.             ->getResult();
  158.         foreach ($brands as $brand) {
  159.             $results[] = [
  160.                 'type' => 'brand',
  161.                 'label' => $brand->getName(),
  162.                 'url' => $this->generateUrl('ui_listing', ['brand' => $brand->getSlug()])
  163.             ];
  164.         }
  165.         return $this->json([
  166.             'success' => true,
  167.             'results' => $results,
  168.         ]);
  169.     }
  170.     #[Route('/'name'home')]
  171.     public function index(EntityManagerInterface $entityManagerViewTrackingService $viewTrackingService): Response
  172.     {
  173.         // Récupérer les données pour la page d'accueil
  174.         // Produits en vedette pour le caroussel de bannière
  175.         $featuredProducts $entityManager->getRepository(Product::class)->findBy([
  176.             'isFeatured' => true,
  177.             'isActive' => true
  178.         ], ['publishedAt' => 'DESC'], 10);
  179.         $latestProducts $entityManager->getRepository(Product::class)->findBy([
  180.             'isActive' => true
  181.         ], ['publishedAt' => 'DESC'], 12);
  182.         // Limiter à 8 catégories les plus importantes (par quantité de produits)
  183.         $allCategories $entityManager->getRepository(Category::class)->findBy([
  184.             'isActive' => true
  185.         ], ['name' => 'ASC']);
  186.         
  187.         // Trier par nombre de produits
  188.         usort($allCategories, function($a$b) {
  189.             $countA $a->getProducts()->count();
  190.             $countB $b->getProducts()->count();
  191.             return $countB <=> $countA;
  192.         });
  193.         
  194.         $categories array_slice($allCategories08);
  195.         // Limiter à 6 catégories de boutiques
  196.         $shopCategories $entityManager->getRepository(\App\Entity\ShopCategory::class)->findBy([
  197.             'isActive' => true,
  198.             'parent' => null
  199.         ], ['position' => 'ASC'], 6);
  200.         $user $this->getUser();
  201.         $recommendedProducts = [];
  202.         $recentlyViewedProducts $viewTrackingService->getRecentlyViewedProducts(8);
  203.         if ($user instanceof User) {
  204.             $recommendedProducts $this->recommendationService->getPersonalizedRecommendations($user12);
  205.         } else {
  206.             $recommendedProducts $entityManager->getRepository(Product::class)->findBy(
  207.                 ['isActive' => true],
  208.                 ['viewCount' => 'DESC'],
  209.                 12
  210.             );
  211.         }
  212.         // Statistiques pour le dashboard
  213.         $stats = [
  214.             'totalProducts' => $entityManager->getRepository(Product::class)->count(['isActive' => true]),
  215.             'totalShops' => $entityManager->getRepository(Shop::class)->count(['isActive' => true]),
  216.             'totalCategories' => $entityManager->getRepository(Category::class)->count(['isActive' => true]),
  217.         ];
  218.         // Toutes les catégories pour le bouton "Voir tout"
  219.         $allCategories $entityManager->getRepository(Category::class)->findBy([
  220.             'isActive' => true
  221.         ], ['name' => 'ASC']);
  222.         
  223.         // Toutes les catégories de boutiques pour le bouton "Voir tout"
  224.         $allShopCategories $entityManager->getRepository(\App\Entity\ShopCategory::class)->findBy([
  225.             'isActive' => true,
  226.             'parent' => null
  227.         ], ['position' => 'ASC']);
  228.         
  229.         return $this->render('home/index.html.twig', [
  230.             'current_menu' => 'home',
  231.             'featuredProducts' => $featuredProducts,
  232.             'latestProducts' => $latestProducts,
  233.             'categories' => $categories,
  234.             'allCategories' => $allCategories,
  235.             'shopCategories' => $shopCategories,
  236.             'allShopCategories' => $allShopCategories,
  237.             'stats' => $stats,
  238.             'recommendedProducts' => $recommendedProducts,
  239.             'recentlyViewedProducts' => $recentlyViewedProducts,
  240.         ]);
  241.     }
  242.     #[Route('/newsletter/subscribe'name'newsletter_subscribe'methods: ['POST'])]
  243.     public function newsletterSubscribe(Request $request): JsonResponse
  244.     {
  245.         $email $request->request->get('email');
  246.         if (!$email || !filter_var($emailFILTER_VALIDATE_EMAIL)) {
  247.             return $this->json(['success' => false'message' => 'Adresse email invalide'], 400);
  248.         }
  249.         // On va sauvegarder l'email dans la table NewsletterSubscriber (à créer si pas déjà là)
  250.         $em $this->getDoctrine()->getManager();
  251.         $existing $em->getRepository(\App\Entity\NewsletterSubscriber::class)->findOneBy(['email' => $email]);
  252.         if ($existing) {
  253.             return $this->json([
  254.                 'success' => false,
  255.                 'message' => 'Cet email est déjà inscrit à notre newsletter.'
  256.             ], 409);
  257.         }
  258.         $subscriber = new \App\Entity\NewsletterSubscriber();
  259.         $subscriber->setEmail($email);
  260.         $subscriber->setSubscribedAt(new \DateTimeImmutable());
  261.         $em->persist($subscriber);
  262.         $em->flush();
  263.         return $this->json([
  264.             'success' => true,
  265.             'message' => 'Merci pour votre inscription à notre newsletter !'
  266.         ]);
  267.     }
  268.     #[Route('/shops'name'shops_list')]
  269.     public function shopsList(Request $requestEntityManagerInterface $emShopFollowRepository $shopFollowRepository): Response
  270.     {
  271.         $categorySlug $request->query->get('category');
  272.         $page $request->query->getInt('page'1);
  273.         $limit 12;
  274.         $offset = ($page 1) * $limit;
  275.         $qb $em->getRepository(Shop::class)->createQueryBuilder('s')
  276.             ->where('s.isActive = :active')
  277.             ->setParameter('active'true);
  278.         if ($categorySlug) {
  279.             $qb->leftJoin('s.shopCategory''sc')
  280.                ->andWhere('sc.slug = :categorySlug')
  281.                ->setParameter('categorySlug'$categorySlug);
  282.         }
  283.         // Compter le total avant de limiter
  284.         $totalShops = (clone $qb)->select('COUNT(s.id)')
  285.             ->getQuery()
  286.             ->getSingleScalarResult();
  287.         
  288.         $shops $qb->orderBy('s.createdAt''DESC')
  289.             ->setFirstResult($offset)
  290.             ->setMaxResults($limit)
  291.             ->getQuery()
  292.             ->getResult();
  293.         $totalPages ceil($totalShops $limit);
  294.         
  295.         // Si c'est une requête AJAX, retourner JSON
  296.         if ($request->isXmlHttpRequest()) {
  297.             $user $this->getUser();
  298.             $shopsData = [];
  299.             foreach ($shops as $shop) {
  300.                 $isFollowing false;
  301.                 if ($user) {
  302.                     $isFollowing $shopFollowRepository->isUserFollowingShop($user$shop);
  303.                 }
  304.                 
  305.                 $shopsData[] = [
  306.                     'id' => $shop->getId(),
  307.                     'name' => $shop->getName(),
  308.                     'slug' => $shop->getSlug(),
  309.                     'logo' => $shop->getLogo(),
  310.                     'description' => $shop->getDescription(),
  311.                     'isVerified' => $shop->isVerified(),
  312.                     'category' => $shop->getShopCategory() ? $shop->getShopCategory()->getName() : null,
  313.                     'productsCount' => $shop->getActiveProductsCount(),
  314.                     'followersCount' => $shop->getActiveFollowersCount(),
  315.                     'viewCount' => $shop->getViewCount(),
  316.                     'following' => $isFollowing,
  317.                 ];
  318.             }
  319.             return $this->json([
  320.                 'success' => true,
  321.                 'shops' => $shopsData,
  322.                 'hasMore' => $page $totalPages,
  323.                 'currentPage' => $page,
  324.                 'totalPages' => $totalPages,
  325.             ]);
  326.         }
  327.         $shopCategories $em->getRepository(ShopCategory::class)->findBy(['isActive' => true], ['name' => 'ASC']);
  328.         
  329.         // Récupérer la catégorie sélectionnée si un slug est fourni
  330.         $selectedCategoryEntity null;
  331.         if ($categorySlug) {
  332.             $selectedCategoryEntity $em->getRepository(ShopCategory::class)->findOneBy(['slug' => $categorySlug'isActive' => true]);
  333.         }
  334.         
  335.         // Récupérer les IDs des boutiques suivies par l'utilisateur connecté (pour optimiser l'affichage)
  336.         $followedShopIds = [];
  337.         $user $this->getUser();
  338.         if ($user) {
  339.             $followedShops $shopFollowRepository->createQueryBuilder('sf')
  340.                 ->select('s.id')
  341.                 ->join('sf.shop''s')
  342.                 ->where('sf.user = :user')
  343.                 ->andWhere('sf.isActive = :active')
  344.                 ->setParameter('user'$user)
  345.                 ->setParameter('active'true)
  346.                 ->getQuery()
  347.                 ->getResult();
  348.             
  349.             $followedShopIds array_column($followedShops'id');
  350.         }
  351.         
  352.         return $this->render('home/shops_list.html.twig', [
  353.             'current_menu' => 'shops',
  354.             'shops' => $shops,
  355.             'shopCategories' => $shopCategories,
  356.             'selectedCategory' => $selectedCategoryEntity,
  357.             'selectedCategorySlug' => $categorySlug,
  358.             'current_page' => $page,
  359.             'total_pages' => $totalPages,
  360.             'total_shops' => $totalShops,
  361.             'shops_per_page' => $limit,
  362.             'followed_shop_ids' => $followedShopIds,
  363.         ]);
  364.     }
  365.     #[Route('/listing'name'listing')]
  366.     public function listing(Request $requestEntityManagerInterface $emShopFollowRepository $shopFollowRepository): Response
  367.     {
  368.         $categorySlug $request->query->get('category');
  369.         $brandSlug $request->query->get('brand');
  370.         $sortBy $request->query->get('sort''newest');
  371.         $priceMin $request->query->get('price_min');
  372.         $priceMax $request->query->get('price_max');
  373.         $page $request->query->getInt('page'1);
  374.         $searchQuery $request->query->get('q');
  375.         
  376.         // Récupérer les catégories
  377.         $categories $em->getRepository(Category::class)->findBy(['isActive' => true], ['name' => 'ASC']);
  378.         
  379.         // Récupérer les marques
  380.         $brands $em->getRepository(Brand::class)->findBy(['isActive' => true], ['name' => 'ASC']);
  381.         
  382.         // Construire la requête pour les produits
  383.         $qb $em->getRepository(Product::class)->createQueryBuilder('p')
  384.             ->where('p.isActive = :active')
  385.             ->setParameter('active'true);
  386.         
  387.         // Filtre par catégorie
  388.         if ($categorySlug) {
  389.             $qb->andWhere('p.category = :category')
  390.                ->setParameter('category'$em->getRepository(Category::class)->findOneBy(['slug' => $categorySlug]));
  391.         }
  392.         // Recherche par mot-clé
  393.         if ($searchQuery) {
  394.             $qb->andWhere('p.name LIKE :searchQuery OR p.description LIKE :searchQuery')
  395.                ->setParameter('searchQuery''%' $searchQuery '%');
  396.         }
  397.         
  398.         // Filtre par marque
  399.         if ($brandSlug) {
  400.             if ($brandSlug === 'non-specifie') {
  401.                 $qb->andWhere('p.brand IS NULL');
  402.             } else {
  403.                 $qb->andWhere('p.brand = :brand')
  404.                    ->setParameter('brand'$em->getRepository(Brand::class)->findOneBy(['slug' => $brandSlug]));
  405.             }
  406.         }
  407.         
  408.         // Filtre par prix
  409.         if ($priceMin) {
  410.             $qb->andWhere('p.price >= :priceMin')
  411.                ->setParameter('priceMin'$priceMin);
  412.         }
  413.         if ($priceMax) {
  414.             $qb->andWhere('p.price <= :priceMax')
  415.                ->setParameter('priceMax'$priceMax);
  416.         }
  417.         
  418.         // Tri
  419.         switch ($sortBy) {
  420.             case 'rank':
  421.             case 'best_seller':
  422.                 // Trier par ranking Amazon (si catégorie sélectionnée)
  423.                 if ($categorySlug) {
  424.                     $category $em->getRepository(Category::class)->findOneBy(['slug' => $categorySlug]);
  425.                     if ($category) {
  426.                         $qb->leftJoin('App\Entity\ProductRanking''pr''WITH'
  427.                             'pr.product = p AND pr.category = :rankingCategory')
  428.                             ->setParameter('rankingCategory'$category)
  429.                             ->orderBy('pr.rank''ASC')
  430.                             ->addOrderBy('pr.score''DESC');
  431.                         break;
  432.                     }
  433.                 }
  434.                 // Fallback: tri par score global (ventes + vues + notes)
  435.                 $qb->orderBy('p.salesCount''DESC')
  436.                    ->addOrderBy('p.viewCount''DESC')
  437.                    ->addOrderBy('p.averageRating''DESC');
  438.                 break;
  439.             case 'price_asc':
  440.                 $qb->orderBy('p.price''ASC');
  441.                 break;
  442.             case 'price_desc':
  443.                 $qb->orderBy('p.price''DESC');
  444.                 break;
  445.             case 'name_asc':
  446.                 $qb->orderBy('p.name''ASC');
  447.                 break;
  448.             case 'name_desc':
  449.                 $qb->orderBy('p.name''DESC');
  450.                 break;
  451.             case 'popular':
  452.                 $qb->orderBy('p.viewCount''DESC');
  453.                 break;
  454.             default:
  455.                 $qb->orderBy('p.publishedAt''DESC');
  456.         }
  457.         
  458.         // Pagination
  459.         $productsPerPage 12;
  460.         $offset = ($page 1) * $productsPerPage;
  461.         $qb->setFirstResult($offset)
  462.            ->setMaxResults($productsPerPage);
  463.         
  464.         $products $qb->getQuery()->getResult();
  465.         
  466.         // Compter le total pour la pagination
  467.         $totalQuery = clone $qb;
  468.         $totalQuery->setFirstResult(0)->setMaxResults(null);
  469.         $totalProducts count($totalQuery->getQuery()->getResult());
  470.         $totalPages ceil($totalProducts $productsPerPage);
  471.         
  472.         // Récupérer les deals de la semaine (produits avec un prix barré, limités à 9)
  473.         $dealsQb $em->getRepository(Product::class)->createQueryBuilder('p')
  474.             ->where('p.isActive = :active')
  475.             ->andWhere('p.compareAtPrice > p.price')
  476.             ->setParameter('active'true)
  477.             ->orderBy('p.publishedAt''DESC')
  478.             ->setMaxResults(9);
  479.         $dealsOfTheWeek $dealsQb->getQuery()->getResult();
  480.         
  481.         // S'il n'y a pas assez de deals, on complète avec des produits vedettes
  482.         if (count($dealsOfTheWeek) < 9) {
  483.             $needed count($dealsOfTheWeek);
  484.             $featuredQb $em->getRepository(Product::class)->createQueryBuilder('p')
  485.                 ->where('p.isActive = :active')
  486.                 ->andWhere('p.isFeatured = :featured')
  487.                 ->setParameter('active'true)
  488.                 ->setParameter('featured'true);
  489.                 
  490.             if (count($dealsOfTheWeek) > 0) {
  491.                 $existingIds array_map(function($p) { return $p->getId(); }, $dealsOfTheWeek);
  492.                 $featuredQb->andWhere('p.id NOT IN (:existingIds)')
  493.                            ->setParameter('existingIds'$existingIds);
  494.             }
  495.             
  496.             $featuredQb->setMaxResults($needed);
  497.             $extraDeals $featuredQb->getQuery()->getResult();
  498.             $dealsOfTheWeek array_merge($dealsOfTheWeek$extraDeals);
  499.         }
  500.         
  501.         return $this->render('home/listing.html.twig', [
  502.             'current_menu' => 'listing',
  503.             'products' => $products,
  504.             'categories' => $categories,
  505.             'brands' => $brands,
  506.             'currentCategory' => $categorySlug,
  507.             'currentBrand' => $brandSlug,
  508.             'currentSort' => $sortBy,
  509.             'currentPage' => $page,
  510.             'totalPages' => $totalPages,
  511.             'totalProducts' => $totalProducts,
  512.             'priceMin' => $priceMin,
  513.             'priceMax' => $priceMax,
  514.             'searchQuery' => $searchQuery,
  515.             'dealsOfTheWeek' => $dealsOfTheWeek,
  516.         ]);
  517.     }
  518.     #[Route('/api/products/featured'name'api_products_featured'methods: ['GET'])]
  519.     public function getFeaturedProducts(Request $requestEntityManagerInterface $em): JsonResponse
  520.     {
  521.         $page $request->query->getInt('page'1);
  522.         $limit $request->query->getInt('limit'10);
  523.         $offset = ($page 1) * $limit;
  524.         $featuredProducts $em->getRepository(Product::class)->findBy(
  525.             ['isFeatured' => true'isActive' => true],
  526.             ['publishedAt' => 'DESC'],
  527.             $limit,
  528.             $offset
  529.         );
  530.         $total $em->getRepository(Product::class)->count([
  531.             'isFeatured' => true,
  532.             'isActive' => true
  533.         ]);
  534.         $products = [];
  535.         foreach ($featuredProducts as $product) {
  536.             $products[] = [
  537.                 'id' => $product->getId(),
  538.                 'name' => $product->getName(),
  539.                 'slug' => $product->getSlug(),
  540.                 'price' => $product->getPrice(),
  541.                 'description' => $product->getDescription() ? substr($product->getDescription(), 0120) . '...' null,
  542.                 'image' => $product->getImages()[0] ?? null,
  543.                 'shop' => $product->getShop() ? [
  544.                     'name' => $product->getShop()->getName(),
  545.                     'slug' => $product->getShop()->getSlug()
  546.                 ] : null,
  547.                 'averageRating' => $product->getAverageRating(),
  548.                 'reviewCount' => $product->getReviewCount(),
  549.                 'viewCount' => $product->getViewCount(),
  550.                 'url' => $this->generateUrl('ui_product_show', ['slug' => $product->getSlug()])
  551.             ];
  552.         }
  553.         return $this->json([
  554.             'success' => true,
  555.             'products' => $products,
  556.             'pagination' => [
  557.                 'page' => $page,
  558.                 'limit' => $limit,
  559.                 'total' => $total,
  560.                 'hasMore' => ($offset $limit) < $total
  561.             ]
  562.         ]);
  563.     }
  564.     #[Route('/api/products/filter'name'api_products_filter'methods: ['GET'])]
  565.     public function filterProducts(Request $requestEntityManagerInterface $em): JsonResponse
  566.     {
  567.         $categorySlug $request->query->get('category');
  568.         $brandSlug $request->query->get('brand');
  569.         $sortBy $request->query->get('sort''newest');
  570.         $priceMin $request->query->get('price_min');
  571.         $priceMax $request->query->get('price_max');
  572.         $page $request->query->getInt('page'1);
  573.         
  574.         // Nouveaux filtres avancés
  575.         $shopSlug $request->query->get('shop');
  576.         $isFeatured $request->query->get('featured');
  577.         $isDigital $request->query->get('digital');
  578.         $stockStatus $request->query->get('stock_status');
  579.         $ratingMin $request->query->get('rating_min');
  580.         $weightMin $request->query->get('weight_min');
  581.         $weightMax $request->query->get('weight_max');
  582.         $color $request->query->get('color');
  583.         $size $request->query->get('size');
  584.         $material $request->query->get('material');
  585.         $condition $request->query->get('condition');
  586.         $availability $request->query->get('availability');
  587.         $searchQuery $request->query->get('q');
  588.         
  589.         // Construire la requête pour les produits
  590.         $qb $em->getRepository(Product::class)->createQueryBuilder('p')
  591.             ->where('p.isActive = :active')
  592.             ->setParameter('active'true);
  593.         
  594.         // Filtre par catégorie
  595.         if ($categorySlug) {
  596.             $qb->andWhere('p.category = :category')
  597.                ->setParameter('category'$em->getRepository(Category::class)->findOneBy(['slug' => $categorySlug]));
  598.         }
  599.         // Recherche par mot-clé
  600.         if ($searchQuery) {
  601.             $qb->andWhere('p.name LIKE :searchQuery OR p.description LIKE :searchQuery')
  602.                ->setParameter('searchQuery''%' $searchQuery '%');
  603.         }
  604.         
  605.         // Filtre par marque
  606.         if ($brandSlug) {
  607.             if ($brandSlug === 'non-specifie') {
  608.                 $qb->andWhere('p.brand IS NULL');
  609.             } else {
  610.                 $qb->andWhere('p.brand = :brand')
  611.                    ->setParameter('brand'$em->getRepository(Brand::class)->findOneBy(['slug' => $brandSlug]));
  612.             }
  613.         }
  614.         
  615.         // Filtre par boutique
  616.         if ($shopSlug) {
  617.             $qb->andWhere('p.shop = :shop')
  618.                ->setParameter('shop'$em->getRepository(Shop::class)->findOneBy(['slug' => $shopSlug]));
  619.         }
  620.         
  621.         // Filtre par prix
  622.         if ($priceMin) {
  623.             $qb->andWhere('p.price >= :priceMin')
  624.                ->setParameter('priceMin'$priceMin);
  625.         }
  626.         if ($priceMax) {
  627.             $qb->andWhere('p.price <= :priceMax')
  628.                ->setParameter('priceMax'$priceMax);
  629.         }
  630.         
  631.         // Filtre par produit vedette
  632.         if ($isFeatured !== null) {
  633.             $qb->andWhere('p.isFeatured = :featured')
  634.                ->setParameter('featured'$isFeatured === 'true' || $isFeatured === '1');
  635.         }
  636.         
  637.         // Filtre par produit numérique
  638.         if ($isDigital !== null) {
  639.             $qb->andWhere('p.isDigital = :digital')
  640.                ->setParameter('digital'$isDigital === 'true' || $isDigital === '1');
  641.         }
  642.         
  643.         // Filtre par statut de stock
  644.         if ($stockStatus) {
  645.             $qb->andWhere('p.stockStatus = :stockStatus')
  646.                ->setParameter('stockStatus'$stockStatus);
  647.         }
  648.         
  649.         // Filtre par note minimale
  650.         if ($ratingMin) {
  651.             $qb->andWhere('p.averageRating >= :ratingMin')
  652.                ->setParameter('ratingMin'$ratingMin);
  653.         }
  654.         
  655.         // Filtre par poids
  656.         if ($weightMin) {
  657.             $qb->andWhere('p.weight >= :weightMin')
  658.                ->setParameter('weightMin'$weightMin);
  659.         }
  660.         if ($weightMax) {
  661.             $qb->andWhere('p.weight <= :weightMax')
  662.                ->setParameter('weightMax'$weightMax);
  663.         }
  664.         
  665.         // Filtres par attributs (stockés en base)
  666.         if ($color) {
  667.             $qb->andWhere('p.color = :color')
  668.                ->setParameter('color'$color);
  669.         }
  670.         
  671.         if ($size) {
  672.             $qb->andWhere('p.size = :size')
  673.                ->setParameter('size'$size);
  674.         }
  675.         
  676.         if ($material) {
  677.             $qb->andWhere('p.material = :material')
  678.                ->setParameter('material'$material);
  679.         }
  680.         
  681.         if ($condition) {
  682.             $qb->andWhere('p.itemCondition = :condition')
  683.                ->setParameter('condition'$condition);
  684.         }
  685.         
  686.         // Filtre par disponibilité
  687.         if ($availability === 'in_stock') {
  688.             $qb->andWhere('p.stock > 0');
  689.         } elseif ($availability === 'out_of_stock') {
  690.             $qb->andWhere('p.stock = 0');
  691.         } elseif ($availability === 'low_stock') {
  692.             $qb->andWhere('p.stock > 0 AND p.stock <= p.minStockAlert');
  693.         }
  694.         
  695.         // Tri
  696.         switch ($sortBy) {
  697.             case 'price_asc':
  698.                 $qb->orderBy('p.price''ASC');
  699.                 break;
  700.             case 'price_desc':
  701.                 $qb->orderBy('p.price''DESC');
  702.                 break;
  703.             case 'name_asc':
  704.                 $qb->orderBy('p.name''ASC');
  705.                 break;
  706.             case 'name_desc':
  707.                 $qb->orderBy('p.name''DESC');
  708.                 break;
  709.             case 'popular':
  710.                 $qb->orderBy('p.viewCount''DESC');
  711.                 break;
  712.             case 'rank':
  713.             case 'best_seller':
  714.                 // Trier par ranking Amazon (si catégorie sélectionnée)
  715.                 if ($categorySlug) {
  716.                     $category $em->getRepository(Category::class)->findOneBy(['slug' => $categorySlug]);
  717.                     if ($category) {
  718.                         $qb->leftJoin('App\Entity\ProductRanking''pr''WITH'
  719.                             'pr.product = p AND pr.category = :rankingCategory')
  720.                             ->setParameter('rankingCategory'$category)
  721.                             ->orderBy('pr.rank''ASC')
  722.                             ->addOrderBy('pr.score''DESC');
  723.                         break;
  724.                     }
  725.                 }
  726.                 // Fallback: tri par score global
  727.                 $qb->orderBy('p.salesCount''DESC')
  728.                    ->addOrderBy('p.viewCount''DESC')
  729.                    ->addOrderBy('p.averageRating''DESC');
  730.                 break;
  731.             case 'rating':
  732.                 $qb->orderBy('p.averageRating''DESC');
  733.                 break;
  734.             case 'sales':
  735.                 $qb->orderBy('p.salesCount''DESC');
  736.                 break;
  737.             case 'weight_asc':
  738.                 $qb->orderBy('p.weight''ASC');
  739.                 break;
  740.             case 'weight_desc':
  741.                 $qb->orderBy('p.weight''DESC');
  742.                 break;
  743.             default:
  744.                 $qb->orderBy('p.publishedAt''DESC');
  745.         }
  746.         
  747.         // Pagination
  748.         $productsPerPage 12;
  749.         $offset = ($page 1) * $productsPerPage;
  750.         $qb->setFirstResult($offset)
  751.            ->setMaxResults($productsPerPage);
  752.         
  753.         $products $qb->getQuery()->getResult();
  754.         
  755.         // Compter le total pour la pagination
  756.         $totalQuery = clone $qb;
  757.         $totalQuery->setFirstResult(0)->setMaxResults(null);
  758.         $totalProducts count($totalQuery->getQuery()->getResult());
  759.         $totalPages ceil($totalProducts $productsPerPage);
  760.         
  761.         // Récupérer les marques disponibles pour cette catégorie
  762.         $availableBrands = [];
  763.         if ($categorySlug) {
  764.             $category $em->getRepository(Category::class)->findOneBy(['slug' => $categorySlug]);
  765.             if ($category) {
  766.                 $brands $em->getRepository(Brand::class)->findBrandsByCategory($categorySlug);
  767.                 foreach ($brands as $brandData) {
  768.                     $brand $brandData[0];
  769.                     $availableBrands[] = [
  770.                         'id' => $brand->getId(),
  771.                         'name' => $brand->getName(),
  772.                         'slug' => $brand->getSlug(),
  773.                         'productCount' => $brandData['productCount']
  774.                     ];
  775.                 }
  776.                 
  777.                 // Ajouter l'option "Non spécifié" pour les produits sans marque
  778.                 $productsWithoutBrand $em->getRepository(Product::class)->createQueryBuilder('p')
  779.                     ->where('p.category = :category')
  780.                     ->andWhere('p.isActive = :active')
  781.                     ->andWhere('p.brand IS NULL')
  782.                     ->setParameter('category'$category)
  783.                     ->setParameter('active'true)
  784.                     ->select('COUNT(p.id)')
  785.                     ->getQuery()
  786.                     ->getSingleScalarResult();
  787.                 
  788.                 if ($productsWithoutBrand 0) {
  789.                     $availableBrands[] = [
  790.                         'id' => null,
  791.                         'name' => 'Non spécifié',
  792.                         'slug' => 'non-specifie',
  793.                         'productCount' => $productsWithoutBrand
  794.                     ];
  795.                 }
  796.             }
  797.         }
  798.         
  799.         // Récupérer les boutiques disponibles
  800.         $availableShops = [];
  801.         if ($categorySlug) {
  802.             $shops $em->getRepository(Shop::class)->createQueryBuilder('s')
  803.                 ->select('s, COUNT(p.id) as productCount')
  804.                 ->leftJoin('s.products''p')
  805.                 ->leftJoin('p.category''c')
  806.                 ->where('s.isActive = :active')
  807.                 ->andWhere('c.slug = :categorySlug')
  808.                 ->setParameter('active'true)
  809.                 ->setParameter('categorySlug'$categorySlug)
  810.                 ->groupBy('s.id')
  811.                 ->having('productCount > 0')
  812.                 ->orderBy('s.name''ASC')
  813.                 ->getQuery()
  814.                 ->getResult();
  815.             
  816.             foreach ($shops as $shopData) {
  817.                 $shop $shopData[0];
  818.                 $availableShops[] = [
  819.                     'id' => $shop->getId(),
  820.                     'name' => $shop->getName(),
  821.                     'slug' => $shop->getSlug(),
  822.                     'productCount' => $shopData['productCount']
  823.                 ];
  824.             }
  825.         }
  826.         
  827.         // Récupérer les attributs disponibles (couleurs, tailles, etc.)
  828.         $availableAttributes $this->getAvailableAttributes($em$categorySlug$brandSlug);
  829.         
  830.         return $this->json([
  831.             'success' => true,
  832.             'products' => $this->renderView('home/_products_list.html.twig', [
  833.                 'products' => $products
  834.             ]),
  835.             'pagination' => [
  836.                 'currentPage' => $page,
  837.                 'totalPages' => $totalPages,
  838.                 'totalProducts' => $totalProducts
  839.             ],
  840.             'availableBrands' => $availableBrands,
  841.             'availableShops' => $availableShops,
  842.             'availableAttributes' => $availableAttributes
  843.         ]);
  844.     }
  845.     /**
  846.      * Récupère les attributs disponibles pour les filtres
  847.      */
  848.     private function getAvailableAttributes(EntityManagerInterface $em, ?string $categorySlug, ?string $brandSlug): array
  849.     {
  850.         $qb $em->getRepository(Product::class)->createQueryBuilder('p')
  851.             ->where('p.isActive = :active')
  852.             ->setParameter('active'true);
  853.         
  854.         if ($categorySlug) {
  855.             $qb->andWhere('p.category = :category')
  856.                ->setParameter('category'$em->getRepository(Category::class)->findOneBy(['slug' => $categorySlug]));
  857.         }
  858.         
  859.         if ($brandSlug) {
  860.             if ($brandSlug === 'non-specifie') {
  861.                 $qb->andWhere('p.brand IS NULL');
  862.             } else {
  863.                 $qb->andWhere('p.brand = :brand')
  864.                    ->setParameter('brand'$em->getRepository(Brand::class)->findOneBy(['slug' => $brandSlug]));
  865.             }
  866.         }
  867.         
  868.         $products $qb->getQuery()->getResult();
  869.         
  870.         // Récupérer la catégorie pour déterminer quels filtres sont pertinents
  871.         $category null;
  872.         if ($categorySlug) {
  873.             $category $em->getRepository(Category::class)->findOneBy(['slug' => $categorySlug]);
  874.         }
  875.         
  876.         $attributes = [
  877.             'colors' => [],
  878.             'sizes' => [],
  879.             'materials' => [],
  880.             'conditions' => []
  881.         ];
  882.         
  883.         foreach ($products as $product) {
  884.             $productAttributes $product->getAttributes();
  885.             
  886.             // Couleurs
  887.             $color $product->getColor() ?? ($productAttributes['color'] ?? null);
  888.             if ($color) {
  889.                 if (!isset($attributes['colors'][$color])) $attributes['colors'][$color] = 0;
  890.                 $attributes['colors'][$color]++;
  891.             }
  892.             
  893.             // Tailles
  894.             $size $product->getSize() ?? ($productAttributes['size'] ?? null);
  895.             if ($size) {
  896.                 if (!isset($attributes['sizes'][$size])) $attributes['sizes'][$size] = 0;
  897.                 $attributes['sizes'][$size]++;
  898.             }
  899.             
  900.             // Matériaux
  901.             $material $product->getMaterial() ?? ($productAttributes['material'] ?? null);
  902.             if ($material) {
  903.                 if (!isset($attributes['materials'][$material])) $attributes['materials'][$material] = 0;
  904.                 $attributes['materials'][$material]++;
  905.             }
  906.             
  907.             // Conditions
  908.             $condition $product->getItemCondition() ?? ($productAttributes['condition'] ?? null);
  909.             if ($condition) {
  910.                 if (!isset($attributes['conditions'][$condition])) $attributes['conditions'][$condition] = 0;
  911.                 $attributes['conditions'][$condition]++;
  912.             }
  913.         }
  914.         
  915.         // Déterminer quels filtres sont pertinents selon la catégorie
  916.         $categoryName $category strtolower($category->getName()) : '';
  917.         $relevantFilters = [
  918.             'colors' => true,
  919.             'sizes' => true,
  920.             'materials' => true,
  921.             'conditions' => true
  922.         ];
  923.         
  924.         // Catégories où certains filtres ne sont pas pertinents
  925.         $alimentaryKeywords = ['aliment''food''nourriture''boisson''drink''repas''meal'];
  926.         $hasAlimentaryKeyword false;
  927.         foreach ($alimentaryKeywords as $keyword) {
  928.             if (strpos($categoryName$keyword) !== false) {
  929.                 $hasAlimentaryKeyword true;
  930.                 break;
  931.             }
  932.         }
  933.         
  934.         if ($hasAlimentaryKeyword) {
  935.             // Pour les produits alimentaires : pas de couleur, pas de taille standard
  936.             $relevantFilters['colors'] = false;
  937.             $relevantFilters['sizes'] = false// Sauf si volume/poids
  938.         }
  939.         
  940.         // Convertir en format array simple et filtrer selon la pertinence
  941.         $result = [];
  942.         foreach ($attributes as $type => $values) {
  943.             if (!$relevantFilters[$type]) {
  944.                 $result[$type] = []; // Vide si non pertinent
  945.                 continue;
  946.             }
  947.             
  948.             $result[$type] = [];
  949.             foreach ($values as $value => $count) {
  950.                 $result[$type][] = [
  951.                     'value' => $value,
  952.                     'count' => $count
  953.                 ];
  954.             }
  955.             // Trier par nombre de produits décroissant
  956.             usort($result[$type], function($a$b) {
  957.                 return $b['count'] - $a['count'];
  958.             });
  959.         }
  960.         
  961.         // Ajouter metadata sur la pertinence des filtres
  962.         $result['_metadata'] = [
  963.             'relevantFilters' => $relevantFilters,
  964.             'categoryName' => $category $category->getName() : null
  965.         ];
  966.         
  967.         return $result;
  968.     }
  969.     #[Route('/api/brands'name'api_brands'methods: ['GET'])]
  970.     public function getBrands(Request $requestEntityManagerInterface $em): JsonResponse
  971.     {
  972.         $categorySlug $request->query->get('category');
  973.         $search $request->query->get('search');
  974.         $limit $request->query->getInt('limit'20);
  975.         if ($search) {
  976.             $brands $em->getRepository(Brand::class)->createQueryBuilder('b')
  977.                 ->where('b.isActive = :active')
  978.                 ->andWhere('b.name LIKE :search')
  979.                 ->setParameter('active'true)
  980.                 ->setParameter('search''%' $search '%')
  981.                 ->setMaxResults($limit)
  982.                 ->getQuery()
  983.                 ->getResult();
  984.         } elseif ($categorySlug) {
  985.             $category $em->getRepository(Category::class)->findOneBy(['slug' => $categorySlug]);
  986.             if ($category) {
  987.                 $brands $em->getRepository(Brand::class)->createQueryBuilder('b')
  988.                     ->leftJoin('b.products''p')
  989.                     ->where('b.isActive = :active')
  990.                     ->andWhere('p.category = :category')
  991.                     ->setParameter('active'true)
  992.                     ->setParameter('category'$category)
  993.                     ->setMaxResults($limit)
  994.                     ->getQuery()
  995.                     ->getResult();
  996.             } else {
  997.                 $brands = [];
  998.             }
  999.         } else {
  1000.             $brands $em->getRepository(Brand::class)->findBy(['isActive' => true], ['name' => 'ASC'], $limit);
  1001.         }
  1002.         $brandsData = [];
  1003.         foreach ($brands as $brand) {
  1004.             $brandsData[] = [
  1005.                 'id' => $brand->getId(),
  1006.                 'name' => $brand->getName(),
  1007.                 'slug' => $brand->getSlug(),
  1008.                 'description' => $brand->getDescription(),
  1009.                 'logo' => $brand->getLogo(),
  1010.                 'website' => $brand->getWebsite(),
  1011.                 'productCount' => $brand->getActiveProductsCount(),
  1012.             ];
  1013.         }
  1014.         return $this->json([
  1015.             'success' => true,
  1016.             'brands' => $brandsData,
  1017.         ]);
  1018.     }
  1019.     #[Route('/listing/{slug}'name'product_show')]
  1020.     public function productShow(string $slugEntityManagerInterface $emViewTrackingService $viewTrackingServiceRequest $request): Response
  1021.     {
  1022.         $product $em->getRepository(Product::class)->findOneBy(['slug' => $slug]);
  1023.         $user $this->getUser();
  1024.         $viewTrackingService->trackProductView($product$user instanceof User $user null);
  1025.         // Récupérer les statistiques du produit
  1026.         $productStats $viewTrackingService->getProductViewStats($product);
  1027.         // Vérifier si l'utilisateur arrive via un lien d'affiliation
  1028.         $session $request->getSession();
  1029.         $dropshipReferral null;
  1030.         if ($session->has('dropship_referral')) {
  1031.             $referralData $session->get('dropship_referral');
  1032.             // Vérifier que c'est bien pour ce produit et que le timestamp est récent (moins de 5 minutes)
  1033.             if ($referralData['productId'] === $product->getId() && (time() - $referralData['timestamp']) < 300) {
  1034.                 // Ne pas afficher le modal si l'utilisateur est l'affilié lui-même
  1035.                 $currentUser $this->getUser();
  1036.                 $isOwnLink $currentUser && isset($referralData['affiliateId']) && 
  1037.                             $currentUser->getId() === $referralData['affiliateId'];
  1038.                 
  1039.                 if (!$isOwnLink) {
  1040.                     $dropshipReferral $referralData;
  1041.                 } else {
  1042.                     // Nettoyer la session si c'est le propre lien de l'utilisateur
  1043.                     $session->remove('dropship_referral');
  1044.                 }
  1045.             } else {
  1046.                 // Nettoyer la session si ce n'est pas pour ce produit ou si c'est trop ancien
  1047.                 $session->remove('dropship_referral');
  1048.             }
  1049.         }
  1050.         $youMightAlsoLike $this->recommendationService->getYouMightAlsoLike(
  1051.             $user instanceof User $user null,
  1052.             $product,
  1053.             8
  1054.         );
  1055.         return $this->render('home/single-product.html.twig', [
  1056.             'current_menu' => 'listing',
  1057.             'product' => $product,
  1058.             'productStats' => $productStats,
  1059.             'dropshipReferral' => $dropshipReferral,
  1060.             'youMightAlsoLike' => $youMightAlsoLike,
  1061.         ]);
  1062.     }
  1063.     #[Route('/blog'name'blog')]
  1064.     public function blog(): Response
  1065.     {
  1066.         return $this->render('home/blog.html.twig', [
  1067.             'current_menu' => 'blog',
  1068.         ]);
  1069.     }
  1070.     #[Route('/contact'name'contact')]
  1071.     public function contact(): Response
  1072.     {
  1073.         return $this->render('home/contact.html.twig', [
  1074.             'current_menu' => 'contact',
  1075.         ]);
  1076.     }
  1077.     #[Route('/help'name'help')]
  1078.     public function help(): Response
  1079.     {
  1080.         return $this->render('home/help.html.twig', [
  1081.             'current_menu' => 'help',
  1082.         ]);
  1083.     }
  1084.     #[Route('/faq'name'faq')]
  1085.     public function faq(): Response
  1086.     {
  1087.         return $this->render('home/faq.html.twig', [
  1088.             'current_menu' => 'faq',
  1089.         ]);
  1090.     }
  1091.     #[Route('/privacy'name'privacy')]
  1092.     public function privacy(): Response
  1093.     {
  1094.         return $this->render('home/privacy.html.twig', [
  1095.             'current_menu' => 'privacy',
  1096.         ]);
  1097.     }
  1098.     #[Route('/terms'name'terms')]
  1099.     public function terms(): Response
  1100.     {
  1101.         return $this->render('home/terms.html.twig', [
  1102.             'current_menu' => 'terms',
  1103.         ]);
  1104.     }
  1105.     #[Route('/conditions-generales-vente'name'terms_of_sale')]
  1106.     public function termsOfSale(): Response
  1107.     {
  1108.         return $this->render('home/terms_of_sale.html.twig', [
  1109.             'current_menu' => 'terms',
  1110.         ]);
  1111.     }
  1112.     #[Route('/cart/add'name'cart_add'methods: ['POST'])]
  1113.     public function cartAdd(Request $requestEntityManagerInterface $em): JsonResponse
  1114.     {
  1115.         $productId = (int) $request->request->get('productId');
  1116.         $qty max(1, (int) $request->request->get('qty'1));
  1117.         $product $em->getRepository(Product::class)->find($productId);
  1118.         if (!$product) return new JsonResponse(['ok' => false'message' => 'Produit introuvable'], 404);
  1119.         $user $this->getUser();
  1120.         $session $request->getSession();
  1121.         $sessionId $session->getId();
  1122.         // Chercher le panier existant
  1123.         $cart null;
  1124.         if ($user) {
  1125.             // Utilisateur connecté - chercher son panier actif
  1126.             $cart $em->getRepository(Cart::class)->findOneBy([
  1127.                 'user' => $user,
  1128.                 'isActive' => true
  1129.             ]);
  1130.         } else {
  1131.             // Utilisateur non connecté - chercher par session
  1132.             $cart $em->getRepository(Cart::class)->findOneBy([
  1133.                 'sessionId' => $sessionId,
  1134.                 'isActive' => true,
  1135.                 'user' => null
  1136.             ]);
  1137.         }
  1138.         if (!$cart) {
  1139.             // Créer un nouveau panier
  1140.             $cart = new Cart();
  1141.             if ($user) {
  1142.                 $cart->setUser($user);
  1143.             } else {
  1144.                 $cart->setSessionId($sessionId);
  1145.             }
  1146.             $em->persist($cart);
  1147.         }
  1148.         // Vérifier si le produit existe déjà dans le panier
  1149.         $existingItem $cart->getItemByProduct($product);
  1150.         if ($existingItem) {
  1151.             $existingItem->incrementQuantity($qty);
  1152.         } else {
  1153.             $cartItem = new CartItem();
  1154.             $cartItem->setProduct($product);
  1155.             $cartItem->setQuantity($qty);
  1156.             $cart->addItem($cartItem);
  1157.             $em->persist($cartItem);
  1158.         }
  1159.         $em->flush();
  1160.         // Gérer la conversion d'affiliation si présente
  1161.         $session $request->getSession();
  1162.         if ($session->has('dropship_referral')) {
  1163.             $referralData $session->get('dropship_referral');
  1164.             // Vérifier que c'est bien pour ce produit
  1165.             if ($referralData['productId'] === $product->getId()) {
  1166.                 // Vérifier que l'utilisateur n'est pas l'affilié lui-même
  1167.                 $currentUser $this->getUser();
  1168.                 $isOwnLink $currentUser && isset($referralData['affiliateId']) && 
  1169.                             $currentUser->getId() === $referralData['affiliateId'];
  1170.                 
  1171.                 if (!$isOwnLink) {
  1172.                     try {
  1173.                         $this->dropshipService->processConversion(
  1174.                             $referralData['code'],
  1175.                             $product->getPrice() * $qty,
  1176.                             $currentUser
  1177.                         );
  1178.                         // Nettoyer la session après conversion
  1179.                         $session->remove('dropship_referral');
  1180.                     } catch (\Exception $e) {
  1181.                         // Log l'erreur mais ne bloque pas l'ajout au panier
  1182.                         error_log('Erreur lors de la conversion dropship: ' $e->getMessage());
  1183.                     }
  1184.                 } else {
  1185.                     // Nettoyer la session si c'est le propre lien de l'utilisateur
  1186.                     $session->remove('dropship_referral');
  1187.                 }
  1188.             }
  1189.         }
  1190.         $totalQty $cart->getItemCount();
  1191.         return new JsonResponse(['ok' => true'totalQty' => $totalQty]);
  1192.     }
  1193.     #[Route('/cart/update'name'cart_update'methods: ['POST'])]
  1194.     public function cartUpdate(Request $requestEntityManagerInterface $em): JsonResponse
  1195.     {
  1196.         $productId = (int) $request->request->get('productId');
  1197.         $qty max(0, (int) $request->request->get('qty'1));
  1198.         $user $this->getUser();
  1199.         $session $request->getSession();
  1200.         $sessionId $session->getId();
  1201.         // Trouver le panier
  1202.         $cart null;
  1203.         if ($user) {
  1204.             $cart $em->getRepository(Cart::class)->findOneBy([
  1205.                 'user' => $user,
  1206.                 'isActive' => true
  1207.             ]);
  1208.         } else {
  1209.             $cart $em->getRepository(Cart::class)->findOneBy([
  1210.                 'sessionId' => $sessionId,
  1211.                 'isActive' => true,
  1212.                 'user' => null
  1213.             ]);
  1214.         }
  1215.         if (!$cart) {
  1216.             return new JsonResponse(['ok' => false'message' => 'Panier non trouvé'], 404);
  1217.         }
  1218.         $product $em->getRepository(Product::class)->find($productId);
  1219.         if (!$product) {
  1220.             return new JsonResponse(['ok' => false'message' => 'Produit non trouvé'], 404);
  1221.         }
  1222.         if ($qty === 0) {
  1223.             // Supprimer l'article du panier
  1224.             $item $cart->getItemByProduct($product);
  1225.             if ($item) {
  1226.                 $cart->removeItem($item);
  1227.                 $em->remove($item);
  1228.             }
  1229.         } else {
  1230.             // Mettre à jour ou ajouter l'article
  1231.             $existingItem $cart->getItemByProduct($product);
  1232.             if ($existingItem) {
  1233.                 $existingItem->setQuantity($qty);
  1234.             } else {
  1235.                 $cartItem = new CartItem();
  1236.                 $cartItem->setProduct($product);
  1237.                 $cartItem->setQuantity($qty);
  1238.                 $cart->addItem($cartItem);
  1239.                 $em->persist($cartItem);
  1240.             }
  1241.         }
  1242.         $em->flush();
  1243.         $subtotal $cart->getTotalAmount();
  1244.         $totalQty $cart->getItemCount();
  1245.         return new JsonResponse([
  1246.             'ok' => true,
  1247.             'subtotal' => $subtotal,
  1248.             'totalQty' => $totalQty
  1249.         ]);
  1250.     }
  1251.     #[Route('/cart'name'cart')]
  1252.     public function cart(Request $requestEntityManagerInterface $em): Response
  1253.     {
  1254.         $user $this->getUser();
  1255.         $session $request->getSession();
  1256.         $sessionId $session->getId();
  1257.         // Récupérer le panier depuis la base de données
  1258.         $cart null;
  1259.         if ($user) {
  1260.             $cart $em->getRepository(Cart::class)->findOneBy([
  1261.                 'user' => $user,
  1262.                 'isActive' => true
  1263.             ]);
  1264.         } else {
  1265.             $cart $em->getRepository(Cart::class)->findOneBy([
  1266.                 'sessionId' => $sessionId,
  1267.                 'isActive' => true,
  1268.                 'user' => null
  1269.             ]);
  1270.         }
  1271.         $items = [];
  1272.         $subtotal 0.0;
  1273.         if ($cart) {
  1274.             $cartItems $cart->getItems();
  1275.             foreach ($cartItems as $cartItem) {
  1276.                 $product $cartItem->getProduct();
  1277.                 $image = ($product && $product->getImages() && count($product->getImages()) > 0)
  1278.                     ? $product->getImages()[0]
  1279.                     : null;
  1280.                 $items[] = [
  1281.                     'id' => $product $product->getId() : 0,
  1282.                     'name' => $product $product->getName() : 'Produit inconnu',
  1283.                     'price' => $cartItem->getUnitPrice(),
  1284.                     'qty' => $cartItem->getQuantity(),
  1285.                     'image' => $image,
  1286.                     'slug' => $product $product->getSlug() : '',
  1287.                     'cartItem' => $cartItem
  1288.                 ];
  1289.             }
  1290.             $subtotal = (float) $cart->getTotalAmount();
  1291.         }
  1292.         return $this->render('home/cart.html.twig', [
  1293.             'current_menu' => 'cart',
  1294.             'items' => $items,
  1295.             'subtotal' => $subtotal,
  1296.         ]);
  1297.     }
  1298.     #[Route('/cart/remove'name'cart_remove'methods: ['POST'])]
  1299.     public function cartRemove(Request $requestEntityManagerInterface $em): JsonResponse
  1300.     {
  1301.         $productId = (int) $request->request->get('productId');
  1302.         $user $this->getUser();
  1303.         $session $request->getSession();
  1304.         $sessionId $session->getId();
  1305.         // Trouver le panier
  1306.         $cart null;
  1307.         if ($user) {
  1308.             $cart $em->getRepository(Cart::class)->findOneBy([
  1309.                 'user' => $user,
  1310.                 'isActive' => true
  1311.             ]);
  1312.         } else {
  1313.             $cart $em->getRepository(Cart::class)->findOneBy([
  1314.                 'sessionId' => $sessionId,
  1315.                 'isActive' => true,
  1316.                 'user' => null
  1317.             ]);
  1318.         }
  1319.         if (!$cart) {
  1320.             return new JsonResponse(['ok' => false'message' => 'Panier non trouvé'], 404);
  1321.         }
  1322.         $product $em->getRepository(Product::class)->find($productId);
  1323.         if (!$product) {
  1324.             return new JsonResponse(['ok' => false'message' => 'Produit non trouvé'], 404);
  1325.         }
  1326.         // Trouver et supprimer l'article du panier
  1327.         $item $cart->getItemByProduct($product);
  1328.         if ($item) {
  1329.             $cart->removeItem($item);
  1330.             $em->remove($item);
  1331.             $em->flush();
  1332.             $subtotal = (float) $cart->getTotalAmount();
  1333.             $totalQty $cart->getItemCount();
  1334.             return new JsonResponse([
  1335.                 'ok' => true,
  1336.                 'subtotal' => $subtotal,
  1337.                 'totalQty' => $totalQty
  1338.             ]);
  1339.         }
  1340.         return new JsonResponse(['ok' => false'message' => 'Article non trouvé dans le panier'], 404);
  1341.     }
  1342.     #[Route('/checkout'name'checkout')]
  1343.     public function checkout(EntityManagerInterface $em): Response
  1344.     {
  1345.         $user $this->getUser();
  1346.         $session $this->container->get('request_stack')->getSession();
  1347.         $sessionId $session->getId();
  1348.         // Récupérer le panier depuis la base de données
  1349.         $cart null;
  1350.         if ($user) {
  1351.             $cart $em->getRepository(Cart::class)->findOneBy([
  1352.                 'user' => $user,
  1353.                 'isActive' => true
  1354.             ]);
  1355.         } else {
  1356.             $cart $em->getRepository(Cart::class)->findOneBy([
  1357.                 'sessionId' => $sessionId,
  1358.                 'isActive' => true,
  1359.                 'user' => null
  1360.             ]);
  1361.         }
  1362.         if (!$cart || $cart->getItems()->isEmpty()) {
  1363.             return $this->redirectToRoute('ui_cart');
  1364.         }
  1365.         // Récupérer les items du panier avec les détails des produits
  1366.         $items = [];
  1367.         $subtotal 0.0;
  1368.         foreach ($cart->getItems() as $cartItem) {
  1369.             $product $cartItem->getProduct();
  1370.             $image = ($product && $product->getImages() && count($product->getImages()) > 0)
  1371.                 ? $product->getImages()[0]
  1372.                 : null;
  1373.             $items[] = [
  1374.                 'id' => $product $product->getId() : 0,
  1375.                 'name' => $product $product->getName() : 'Produit inconnu',
  1376.                 'price' => (float) $cartItem->getUnitPrice(),
  1377.                 'qty' => $cartItem->getQuantity(),
  1378.                 'image' => $image,
  1379.                 'slug' => $product $product->getSlug() : '',
  1380.                 'total' => (float) $cartItem->getTotalPrice()
  1381.             ];
  1382.             $subtotal += (float) $cartItem->getTotalPrice();
  1383.         }
  1384.         // Récupérer les adresses de l'utilisateur
  1385.         $addresses = [];
  1386.         if ($user) {
  1387.             $addresses $em->getRepository(Address::class)->findBy(['user' => $user]);
  1388.         }
  1389.         // Récupérer les moyens de paiement actifs
  1390.         $paymentMethods $em->getRepository(PaymentMethod::class)->findBy(['isActive' => true], ['sortOrder' => 'ASC']);
  1391.         // Récupérer les moyens de livraison actifs
  1392.         $shippingMethods $em->getRepository(ShippingMethod::class)->findBy(['isActive' => true], ['sortOrder' => 'ASC']);
  1393.         return $this->render('home/checkout.html.twig', [
  1394.             'current_menu' => 'cart',
  1395.             'items' => $items,
  1396.             'subtotal' => $subtotal,
  1397.             'addresses' => $addresses,
  1398.             'paymentMethods' => $paymentMethods,
  1399.             'shippingMethods' => $shippingMethods,
  1400.             'user' => $user,
  1401.             'cart' => $cart,
  1402.         ]);
  1403.     }
  1404.     #[Route('/checkout/process-moncash'name'checkout_process_moncash'methods: ['POST''GET'])]
  1405.     public function processMonCashPayment(Request $requestEntityManagerInterface $em): JsonResponse|Response
  1406.     {
  1407.         // Si c'est une requête GET avec checkStatus, vérifier le statut
  1408.         if ($request->isMethod('GET') && $request->query->has('checkStatus')) {
  1409.             return $this->checkMonCashPaymentStatus($request$em);
  1410.         }
  1411.         
  1412.         // Si c'est une requête GET sans checkStatus, rediriger vers le checkout
  1413.         if ($request->isMethod('GET')) {
  1414.             $this->addFlash('info''Cette page n\'est accessible que via le processus de paiement.');
  1415.             return $this->redirectToRoute('ui_checkout');
  1416.         }
  1417.         $user $this->getUser();
  1418.         if (!$user) {
  1419.             return new JsonResponse(['ok' => false'message' => 'Vous devez être connecté pour effectuer un paiement'], 401);
  1420.         }
  1421.         $rawContent $request->getContent();
  1422.         error_log('MonCash Request Raw Content: ' $rawContent);
  1423.         
  1424.         $data json_decode($rawContenttrue);
  1425.         
  1426.         if (json_last_error() !== JSON_ERROR_NONE) {
  1427.             error_log('MonCash JSON Decode Error: ' json_last_error_msg());
  1428.             return new JsonResponse([
  1429.                 'ok' => false
  1430.                 'message' => 'Données JSON invalides: ' json_last_error_msg(),
  1431.                 'raw_content' => substr($rawContent0200)
  1432.             ], 400);
  1433.         }
  1434.         
  1435.         error_log('MonCash Request Data: ' json_encode($data));
  1436.         
  1437.         if (!isset($data['amount'])) {
  1438.             error_log('MonCash Error: Amount missing in request data');
  1439.             return new JsonResponse([
  1440.                 'ok' => false
  1441.                 'message' => 'Montant manquant',
  1442.                 'received_data' => array_keys($data ?? [])
  1443.             ], 400);
  1444.         }
  1445.         $moncashNumber = isset($data['moncashNumber']) ? preg_replace('/\s+/'''$data['moncashNumber']) : '';
  1446.         $moncashHolderName $data['moncashHolderName'] ?? '';
  1447.         $amount = (float) $data['amount'];
  1448.         
  1449.         // Sauvegarder les données de commande dans la session
  1450.         if (isset($data['orderData'])) {
  1451.             $session $request->getSession();
  1452.             $session->set('moncash_order_data'$data['orderData']);
  1453.         }
  1454.         
  1455.         error_log('MonCash: Amount = ' $amount ', MonCashNumber = ' . ($moncashNumber ?: 'empty'));
  1456.         // Validation du numéro MonCash seulement s'il est fourni (format haïtien: 509XXXXXXXX)
  1457.         if (!empty($moncashNumber) && !preg_match('/^509[0-9]{8}$/'$moncashNumber)) {
  1458.             return new JsonResponse(['ok' => false'message' => 'Numéro MonCash invalide. Format attendu : 509 XX XXX XXX'], 400);
  1459.         }
  1460.         // Validation du montant
  1461.         if ($amount <= 0) {
  1462.             error_log('MonCash Error: Invalid amount ' $amount);
  1463.             return new JsonResponse(['ok' => false'message' => 'Montant invalide'], 400);
  1464.         }
  1465.         // Récupérer le panier
  1466.         error_log('MonCash: Looking for cart for user ID: ' $user->getId());
  1467.         $cart $em->getRepository(Cart::class)->findOneBy([
  1468.             'user' => $user,
  1469.             'isActive' => true
  1470.         ]);
  1471.         error_log('MonCash Cart Check - User ID: ' $user->getId() . ', Cart Found: ' . ($cart 'Yes (ID: ' $cart->getId() . ')' 'No'));
  1472.         
  1473.         if (!$cart) {
  1474.             error_log('MonCash Error: No active cart found for user ' $user->getId());
  1475.             return new JsonResponse([
  1476.                 'ok' => false
  1477.                 'message' => 'Panier vide ou introuvable. Veuillez ajouter des produits à votre panier.',
  1478.                 'user_id' => $user->getId()
  1479.             ], 400);
  1480.         }
  1481.         
  1482.         if ($cart->getItems()->isEmpty()) {
  1483.             error_log('MonCash Error: Cart found but empty for user ' $user->getId() . ', Cart ID: ' $cart->getId());
  1484.             return new JsonResponse([
  1485.                 'ok' => false
  1486.                 'message' => 'Votre panier est vide. Veuillez ajouter des produits avant de finaliser votre commande.',
  1487.                 'cart_id' => $cart->getId()
  1488.             ], 400);
  1489.         }
  1490.         
  1491.         error_log('MonCash Cart Items Count: ' $cart->getItems()->count());
  1492.         try {
  1493.             // Créer une transaction MonCash en attente
  1494.             $transaction = new \App\Entity\MonCashTransaction();
  1495.             $transactionId 'TXN-' strtoupper(uniqid()) . '-' time();
  1496.             $transaction->setTransactionId($transactionId);
  1497.             $transaction->setUser($user);
  1498.             $transaction->setAmount((string) $amount);
  1499.             $transaction->setStatus('pending');
  1500.             $transaction->setMoncashNumber($moncashNumber);
  1501.             $transaction->setMoncashHolderName($moncashHolderName);
  1502.             
  1503.             // Sauvegarder les données de commande en JSON
  1504.             $orderData $data['orderData'] ?? [];
  1505.             $transaction->setOrderData(json_encode($orderData));
  1506.             
  1507.             $em->persist($transaction);
  1508.             $em->flush();
  1509.             
  1510.             // Vérifier les credentials MonCash avant d'appeler le service
  1511.             $apiKey getenv('MONCASH_API_KEY') ?: ($_ENV['MONCASH_API_KEY'] ?? '');
  1512.             $apiSecret getenv('MONCASH_API_SECRET') ?: ($_ENV['MONCASH_API_SECRET'] ?? '');
  1513.             
  1514.             error_log('=== MONCASH CREDENTIALS CHECK ===');
  1515.             error_log('MONCASH_API_KEY: ' . (!empty($apiKey) ? 'Present (' substr($apiKey08) . '...)' 'MISSING'));
  1516.             error_log('MONCASH_API_SECRET: ' . (!empty($apiSecret) ? 'Present (' substr($apiSecret08) . '...)' 'MISSING'));
  1517.             error_log('getenv(MONCASH_API_KEY): ' . (getenv('MONCASH_API_KEY') ?: 'empty'));
  1518.             error_log('$_ENV[MONCASH_API_KEY]: ' . ($_ENV['MONCASH_API_KEY'] ?? 'not set'));
  1519.             
  1520.             // Intégration avec l'API MonCash
  1521.             // Utiliser le service MonCash pour initier le paiement et obtenir l'URL du portail
  1522.             error_log('MonCash: Calling initiatePayment with amount: ' $amount ', transactionId: ' $transactionId);
  1523.             
  1524.             $paymentResult $this->monCashService->initiatePayment([
  1525.                 'amount' => $amount,
  1526.                 'phone' => $moncashNumber ?: null,
  1527.                 'orderId' => $transactionId// Utiliser l'ID de transaction comme orderId
  1528.                 'description' => 'Paiement commande MaketOu'
  1529.             ]);
  1530.             
  1531.             error_log('MonCash initiatePayment result: ' json_encode($paymentResult));
  1532.             if (!$paymentResult['success']) {
  1533.                 $errorMessage $paymentResult['message'] ?? 'Erreur lors de l\'initialisation du paiement MonCash';
  1534.                 
  1535.                 // Améliorer le message d'erreur pour l'erreur 401 (credentials invalides)
  1536.                 if (isset($paymentResult['response']) && is_array($paymentResult['response'])) {
  1537.                     $apiResponse $paymentResult['response'];
  1538.                     if (isset($apiResponse['status']) && $apiResponse['status'] == 401) {
  1539.                         $errorMessage 'Erreur d\'authentification avec l\'API MonCash (401 Unauthorized). ' .
  1540.                                        'Vos credentials MonCash (MONCASH_API_KEY et MONCASH_API_SECRET) sont invalides ou incorrects. ' .
  1541.                                        'Vérifiez qu\'ils sont corrects dans votre fichier .env et qu\'il n\'y a pas d\'espaces ou de caractères invisibles.';
  1542.                     }
  1543.                 }
  1544.                 
  1545.                 // Logger l'erreur pour le débogage
  1546.                 error_log('MonCash Payment Initiation Failed: ' $errorMessage);
  1547.                 error_log('Payment Result: ' json_encode($paymentResult));
  1548.                 error_log('Transaction ID: ' $transactionId);
  1549.                 error_log('Amount: ' $amount);
  1550.                 
  1551.                 $transaction->setStatus('failed');
  1552.                 $transaction->setErrorMessage($errorMessage);
  1553.                 $em->flush();
  1554.                 
  1555.                 // Retourner une réponse détaillée pour le débogage
  1556.                 $errorResponse = [
  1557.                     'ok' => false,
  1558.                     'message' => $errorMessage,
  1559.                     'transactionId' => $transactionId,
  1560.                     'amount' => $amount
  1561.                 ];
  1562.                 
  1563.                 // Ajouter les détails de l'erreur si disponibles
  1564.                 if (isset($paymentResult['response'])) {
  1565.                     $errorResponse['api_response'] = $paymentResult['response'];
  1566.                 }
  1567.                 
  1568.                 return new JsonResponse($errorResponse400);
  1569.             }
  1570.             // Mettre à jour la transaction avec les informations de paiement
  1571.             if (isset($paymentResult['paymentId'])) {
  1572.                 $transaction->setPaymentToken($paymentResult['paymentId']);
  1573.             }
  1574.             
  1575.             $transaction->setMoncashResponse(json_encode($paymentResult));
  1576.             $em->flush();
  1577.             // Retourner l'URL du portail MonCash pour affichage dans le modal
  1578.             if (isset($paymentResult['portalUrl'])) {
  1579.                 // Convertir l'URL relative en URL absolue si nécessaire
  1580.                 $portalUrl $paymentResult['portalUrl'];
  1581.                 if (strpos($portalUrl'http') !== 0) {
  1582.                     // C'est une URL relative, la convertir en absolue
  1583.                     $portalUrl $request->getSchemeAndHttpHost() . $portalUrl;
  1584.                 }
  1585.                 
  1586.                 return new JsonResponse([
  1587.                     'ok' => true,
  1588.                     'portalUrl' => $portalUrl,
  1589.                     'paymentId' => $paymentResult['paymentId'] ?? null,
  1590.                     'transactionId' => $transactionId,
  1591.                     'simulation' => false
  1592.                 ]);
  1593.             }
  1594.             return new JsonResponse([
  1595.                 'ok' => false,
  1596.                 'message' => 'Erreur: URL du portail MonCash non reçue'
  1597.             ], 400);
  1598.         } catch (\Exception $e) {
  1599.             return new JsonResponse([
  1600.                 'ok' => false,
  1601.                 'message' => 'Erreur lors de l\'initialisation du paiement: ' $e->getMessage()
  1602.             ], 500);
  1603.         }
  1604.     }
  1605.     /**
  1606.      * Vérifier le statut d'un paiement MonCash
  1607.      */
  1608.     private function checkMonCashPaymentStatus(Request $requestEntityManagerInterface $em): JsonResponse
  1609.     {
  1610.         $transactionId $request->query->get('checkStatus');
  1611.         if (!$transactionId) {
  1612.             return new JsonResponse(['ok' => false'message' => 'Transaction ID manquant'], 400);
  1613.         }
  1614.         $user $this->getUser();
  1615.         if (!$user) {
  1616.             return new JsonResponse(['ok' => false'message' => 'Vous devez être connecté'], 401);
  1617.         }
  1618.         $transaction $em->getRepository(\App\Entity\MonCashTransaction::class)->findOneBy(['transactionId' => $transactionId]);
  1619.         if (!$transaction) {
  1620.             return new JsonResponse(['ok' => false'message' => 'Transaction non trouvée'], 404);
  1621.         }
  1622.         // Vérifier que la transaction appartient à l'utilisateur
  1623.         if ($transaction->getUser()->getId() !== $user->getId()) {
  1624.             return new JsonResponse(['ok' => false'message' => 'Accès non autorisé'], 403);
  1625.         }
  1626.         // Si la transaction est déjà complétée, retourner la commande
  1627.         if ($transaction->getStatus() === 'completed' && $transaction->getOrder()) {
  1628.             return new JsonResponse([
  1629.                 'ok' => true,
  1630.                 'orderCreated' => true,
  1631.                 'orderId' => $transaction->getOrder()->getId(),
  1632.                 'orderNumber' => $transaction->getOrder()->getOrderNumber(),
  1633.                 'redirectUrl' => $this->generateUrl('ui_account_orders')
  1634.             ]);
  1635.         }
  1636.         // Vérifier le statut avec MonCash
  1637.         if ($transaction->getPaymentToken()) {
  1638.             $statusResult $this->monCashService->checkPaymentStatus($transaction->getPaymentToken());
  1639.             
  1640.             if ($statusResult['success'] && isset($statusResult['status'])) {
  1641.                 $moncashStatus strtolower($statusResult['status']);
  1642.                 
  1643.                 if (in_array($moncashStatus, ['completed''success''approved''paid'])) {
  1644.                     // Paiement réussi, créer la commande
  1645.                     $cart $em->getRepository(Cart::class)->findOneBy([
  1646.                         'user' => $transaction->getUser(),
  1647.                         'isActive' => true
  1648.                     ]);
  1649.                     
  1650.                     if ($cart) {
  1651.                         $result $this->createOrderFromTransaction($transaction$em$cart);
  1652.                         $resultData json_decode($result->getContent(), true);
  1653.                         return new JsonResponse($resultData);
  1654.                     }
  1655.                 } elseif (in_array($moncashStatus, ['failed''cancelled''rejected'])) {
  1656.                     $transaction->setStatus('failed');
  1657.                     $transaction->setErrorMessage('Paiement refusé par MonCash');
  1658.                     $em->flush();
  1659.                 }
  1660.             }
  1661.         }
  1662.         return new JsonResponse([
  1663.             'ok' => true,
  1664.             'orderCreated' => false,
  1665.             'status' => $transaction->getStatus()
  1666.         ]);
  1667.     }
  1668.     /**
  1669.      * Créer une commande à partir d'une transaction MonCash
  1670.      */
  1671.     private function createOrderFromTransaction(\App\Entity\MonCashTransaction $transactionEntityManagerInterface $emCart $cart): JsonResponse
  1672.     {
  1673.         try {
  1674.             $user $transaction->getUser();
  1675.             $orderData json_decode($transaction->getOrderData(), true);
  1676.             
  1677.             $shippingMethodId $orderData['shippingMethod'] ?? null;
  1678.             $deliveryAddressId $orderData['deliveryAddress'] ?? null;
  1679.             $orderNotes $orderData['orderNotes'] ?? '';
  1680.             $giftCardCode $orderData['giftCardCode'] ?? null;
  1681.             $giftCardDiscount $orderData['giftCardDiscount'] ?? 0;
  1682.             // Calculer les totaux en additionnant les items du panier
  1683.             // IMPORTANT: Calculer le sous-total en additionnant les totalPrice de chaque item
  1684.             // pour garantir la cohérence avec les OrderItems créés
  1685.             $subtotal 0.0;
  1686.             foreach ($cart->getItems() as $cartItem) {
  1687.                 $subtotal += (float) $cartItem->getTotalPrice();
  1688.             }
  1689.             
  1690.             $shippingMethod $shippingMethodId $em->getRepository(ShippingMethod::class)->find($shippingMethodId) : null;
  1691.             $shippingAmount $shippingMethod ? (float) $shippingMethod->getPrice() : 0.0;
  1692.             $totalBeforeTax $subtotal $shippingAmount $giftCardDiscount;
  1693.             $taxAmount $totalBeforeTax 0.01;
  1694.             $totalAmount $totalBeforeTax $taxAmount;
  1695.             // Créer la commande
  1696.             $order = new Order();
  1697.             $order->setOrderNumber('ORD-' strtoupper(uniqid()));
  1698.             $order->setCustomer($user);
  1699.             $order->setSubtotal((string) $subtotal);
  1700.             $order->setTaxAmount((string) $taxAmount);
  1701.             $order->setShippingAmount((string) $shippingAmount);
  1702.             $order->setDiscountAmount((string) $giftCardDiscount);
  1703.             $order->setTotalAmount((string) $totalAmount);
  1704.             $order->setCurrency('HTG');
  1705.             $order->setStatus('pending');
  1706.             $order->setPaymentStatus('paid');
  1707.             $order->setPaymentMethod('MonCash');
  1708.             $order->setShippingMethod($shippingMethod $shippingMethod->getName() : null);
  1709.             $order->setNotes($orderNotes);
  1710.             $order->setOrderedAt(new \DateTimeImmutable('now'));
  1711.             // Ajouter l'adresse de livraison
  1712.             if ($deliveryAddressId) {
  1713.                 $address $em->getRepository(Address::class)->find($deliveryAddressId);
  1714.                 if ($address) {
  1715.                     $fullAddress sprintf(
  1716.                         '%s, %s, %s, %s%s',
  1717.                         $address->getStreet(),
  1718.                         $address->getCity(),
  1719.                         $address->getState(),
  1720.                         $address->getCountry(),
  1721.                         $address->getZipCode() ? ' ' $address->getZipCode() : ''
  1722.                     );
  1723.                     $order->setShippingAddress($fullAddress);
  1724.                     $order->setBillingAddress($fullAddress);
  1725.                 }
  1726.             }
  1727.             // Ajouter les items de la commande
  1728.             foreach ($cart->getItems() as $cartItem) {
  1729.                 $orderItem = new \App\Entity\OrderItem();
  1730.                 $orderItem->setOrder($order);
  1731.                 $orderItem->setProduct($cartItem->getProduct());
  1732.                 $orderItem->setQuantity($cartItem->getQuantity());
  1733.                 $orderItem->setUnitPrice((string) $cartItem->getUnitPrice());
  1734.                 $orderItem->setTotalPrice((string) $cartItem->getTotalPrice());
  1735.                 $em->persist($orderItem);
  1736.             }
  1737.             // Appliquer la carte cadeau si fournie
  1738.             if ($giftCardCode && $giftCardDiscount 0) {
  1739.                 $giftCard $em->getRepository(\App\Entity\GiftCard::class)->findOneBy(['code' => $giftCardCode]);
  1740.                 if ($giftCard) {
  1741.                     $newBalance max(0, (float) $giftCard->getBalance() - $giftCardDiscount);
  1742.                     $giftCard->setBalance((string) $newBalance);
  1743.                     $giftCard->setLastUsedAt(new \DateTimeImmutable('now'));
  1744.                     $em->persist($giftCard);
  1745.                 }
  1746.             }
  1747.             // Désactiver le panier
  1748.             $cart->setIsActive(false);
  1749.             // Lier la transaction à la commande
  1750.             $transaction->setOrder($order);
  1751.             $transaction->setStatus('completed');
  1752.             $em->persist($order);
  1753.             $em->persist($transaction);
  1754.             $em->flush();
  1755.             // Envoyer des notifications
  1756.             // Notification pour le client
  1757.             $this->notificationService->createOrderCreatedNotification($user$order);
  1758.             
  1759.             // Notifications pour les vendeurs (un par boutique)
  1760.             $shopsNotified = [];
  1761.             foreach ($order->getItems() as $orderItem) {
  1762.                 $product $orderItem->getProduct();
  1763.                 $shop $product->getShop();
  1764.                 if ($shop && !in_array($shop->getId(), $shopsNotified)) {
  1765.                     $shopOwner $shop->getManager()->first();
  1766.                     if ($shopOwner) {
  1767.                         $this->notificationService->createOrderReceivedNotification($shopOwner$order$shop);
  1768.                         $shopsNotified[] = $shop->getId();
  1769.                     }
  1770.                 }
  1771.             }
  1772.             return new JsonResponse([
  1773.                 'ok' => true,
  1774.                 'message' => 'Paiement MonCash traité avec succès',
  1775.                 'orderId' => $order->getId(),
  1776.                 'orderNumber' => $order->getOrderNumber(),
  1777.                 'redirectUrl' => $this->generateUrl('ui_account_orders'),
  1778.                 'orderCreated' => true
  1779.             ]);
  1780.         } catch (\Exception $e) {
  1781.             $transaction->setStatus('failed');
  1782.             $transaction->setErrorMessage($e->getMessage());
  1783.             $em->flush();
  1784.             
  1785.             return new JsonResponse([
  1786.                 'ok' => false,
  1787.                 'message' => 'Erreur lors de la création de la commande: ' $e->getMessage()
  1788.             ], 500);
  1789.         }
  1790.     }
  1791.     #[Route('/checkout/moncash-simulation'name'checkout_moncash_simulation'methods: ['GET'])]
  1792.     public function moncashSimulation(Request $requestEntityManagerInterface $em): Response
  1793.     {
  1794.         $user $this->getUser();
  1795.         if (!$user) {
  1796.             $this->addFlash('error''Vous devez être connecté pour effectuer un paiement.');
  1797.             return $this->redirectToRoute('ui_app_login');
  1798.         }
  1799.         // Récupérer les paramètres de la simulation
  1800.         $token $request->query->get('token');
  1801.         $amount $request->query->get('amount');
  1802.         $orderId $request->query->get('orderId');
  1803.         $transactionId $request->query->get('transactionId');
  1804.         if (!$token || !$amount) {
  1805.             $this->addFlash('error''Paramètres de simulation invalides.');
  1806.             return $this->redirectToRoute('ui_checkout');
  1807.         }
  1808.         // Récupérer les données de la commande depuis la session
  1809.         $session $request->getSession();
  1810.         $orderData $session->get('moncash_order_data', []);
  1811.         if (empty($orderData)) {
  1812.             $this->addFlash('error''Données de commande non trouvées. Veuillez recommencer le processus de paiement.');
  1813.             return $this->redirectToRoute('ui_checkout');
  1814.         }
  1815.         try {
  1816.             // Récupérer le panier
  1817.             $cart $em->getRepository(Cart::class)->findOneBy([
  1818.                 'user' => $user,
  1819.                 'isActive' => true
  1820.             ]);
  1821.             if (!$cart || $cart->getItems()->isEmpty()) {
  1822.                 $this->addFlash('error''Panier vide.');
  1823.                 return $this->redirectToRoute('ui_checkout');
  1824.             }
  1825.             // Récupérer les données de la commande
  1826.             $shippingMethodId $orderData['shippingMethod'] ?? null;
  1827.             $deliveryAddressId $orderData['deliveryAddress'] ?? null;
  1828.             $orderNotes $orderData['orderNotes'] ?? '';
  1829.             $giftCardCode $orderData['giftCardCode'] ?? null;
  1830.             $giftCardDiscount $orderData['giftCardDiscount'] ?? 0;
  1831.             // Calculer les totaux en additionnant les items du panier
  1832.             // IMPORTANT: Calculer le sous-total en additionnant les totalPrice de chaque item
  1833.             // pour garantir la cohérence avec les OrderItems créés
  1834.             $subtotal 0.0;
  1835.             foreach ($cart->getItems() as $cartItem) {
  1836.                 $subtotal += (float) $cartItem->getTotalPrice();
  1837.             }
  1838.             
  1839.             $shippingMethod $shippingMethodId $em->getRepository(ShippingMethod::class)->find($shippingMethodId) : null;
  1840.             $shippingAmount $shippingMethod ? (float) $shippingMethod->getPrice() : 0.0;
  1841.             $totalBeforeTax $subtotal $shippingAmount $giftCardDiscount;
  1842.             $taxAmount $totalBeforeTax 0.01;
  1843.             $totalAmount $totalBeforeTax $taxAmount;
  1844.             // Créer la commande
  1845.             $order = new Order();
  1846.             $order->setOrderNumber('ORD-' strtoupper(uniqid()));
  1847.             $order->setCustomer($user);
  1848.             $order->setSubtotal((string) $subtotal);
  1849.             $order->setTaxAmount((string) $taxAmount);
  1850.             $order->setShippingAmount((string) $shippingAmount);
  1851.             $order->setDiscountAmount((string) $giftCardDiscount);
  1852.             $order->setTotalAmount((string) $totalAmount);
  1853.             $order->setCurrency('HTG');
  1854.             $order->setStatus('pending');
  1855.             $order->setPaymentStatus('paid');
  1856.             $order->setPaymentMethod('MonCash');
  1857.             $order->setShippingMethod($shippingMethod $shippingMethod->getName() : null);
  1858.             $order->setNotes($orderNotes);
  1859.             $order->setOrderedAt(new \DateTimeImmutable('now'));
  1860.             // Ajouter l'adresse de livraison
  1861.             if ($deliveryAddressId) {
  1862.                 $address $em->getRepository(Address::class)->find($deliveryAddressId);
  1863.                 if ($address) {
  1864.                     $fullAddress sprintf(
  1865.                         '%s, %s, %s, %s%s',
  1866.                         $address->getStreet(),
  1867.                         $address->getCity(),
  1868.                         $address->getState(),
  1869.                         $address->getCountry(),
  1870.                         $address->getZipCode() ? ' ' $address->getZipCode() : ''
  1871.                     );
  1872.                     $order->setShippingAddress($fullAddress);
  1873.                     $order->setBillingAddress($fullAddress);
  1874.                 }
  1875.             }
  1876.             // Ajouter les items de la commande
  1877.             foreach ($cart->getItems() as $cartItem) {
  1878.                 $orderItem = new \App\Entity\OrderItem();
  1879.                 $orderItem->setOrder($order);
  1880.                 $orderItem->setProduct($cartItem->getProduct());
  1881.                 $orderItem->setQuantity($cartItem->getQuantity());
  1882.                 $orderItem->setUnitPrice((string) $cartItem->getUnitPrice());
  1883.                 $orderItem->setTotalPrice((string) $cartItem->getTotalPrice());
  1884.                 $em->persist($orderItem);
  1885.             }
  1886.             // Appliquer la carte cadeau si fournie
  1887.             if ($giftCardCode && $giftCardDiscount 0) {
  1888.                 $giftCard $em->getRepository(\App\Entity\GiftCard::class)->findOneBy(['code' => $giftCardCode]);
  1889.                 if ($giftCard) {
  1890.                     $newBalance max(0, (float) $giftCard->getBalance() - $giftCardDiscount);
  1891.                     $giftCard->setBalance((string) $newBalance);
  1892.                     $giftCard->setLastUsedAt(new \DateTimeImmutable('now'));
  1893.                     $em->persist($giftCard);
  1894.                 }
  1895.             }
  1896.             // Désactiver le panier
  1897.             $cart->setIsActive(false);
  1898.             $em->persist($order);
  1899.             $em->flush();
  1900.             // Envoyer des notifications
  1901.             // Notification pour le client
  1902.             $this->notificationService->createOrderCreatedNotification($user$order);
  1903.             
  1904.             // Notifications pour les vendeurs (un par boutique)
  1905.             $shopsNotified = [];
  1906.             foreach ($order->getItems() as $orderItem) {
  1907.                 $product $orderItem->getProduct();
  1908.                 $shop $product->getShop();
  1909.                 if ($shop && !in_array($shop->getId(), $shopsNotified)) {
  1910.                     $shopOwner $shop->getManager()->first();
  1911.                     if ($shopOwner) {
  1912.                         $this->notificationService->createOrderReceivedNotification($shopOwner$order$shop);
  1913.                         $shopsNotified[] = $shop->getId();
  1914.                     }
  1915.                 }
  1916.             }
  1917.             // Nettoyer la session
  1918.             $session->remove('moncash_order_data');
  1919.             // Envoyer un message de succès à la fenêtre parente si c'est une popup
  1920.             return $this->render('home/moncash-simulation.html.twig', [
  1921.                 'order' => $order,
  1922.                 'success' => true,
  1923.                 'redirectUrl' => $this->generateUrl('ui_account_orders')
  1924.             ]);
  1925.         } catch (\Exception $e) {
  1926.             $this->addFlash('error''Erreur lors de la création de la commande: ' $e->getMessage());
  1927.             return $this->redirectToRoute('ui_checkout');
  1928.         }
  1929.     }
  1930.     #[Route('/account/'name'account_index')]
  1931.     public function accountIndex(): Response
  1932.     {
  1933.         if (!$this->getUser()) {
  1934.             return $this->redirectToRoute('ui_app_login');
  1935.         }
  1936.         return $this->render('account/account.html.twig');
  1937.     }
  1938.     #[Route('/account/recently_viewed'name'account_recently_viewed')]
  1939.     public function accountRecentlyViewed(Request $requestViewTrackingService $viewTrackingService): Response
  1940.     {
  1941.         if (!$this->getUser()) {
  1942.             return $this->redirectToRoute('ui_app_login');
  1943.         }
  1944.         
  1945.         $page $request->query->getInt('page'1);
  1946.         $limit 12;
  1947.         $sortBy $request->query->get('sort''recent');
  1948.         $clear $request->query->get('clear');
  1949.         
  1950.         // Vider l'historique si demandé
  1951.         if ($clear) {
  1952.             $session $request->getSession();
  1953.             $session->remove('viewed_products');
  1954.             $this->addFlash('success''Historique des produits vus vidé avec succès.');
  1955.             return $this->redirectToRoute('account_recently_viewed');
  1956.         }
  1957.         
  1958.         // Récupérer tous les produits récemment vus
  1959.         $allRecentlyViewedProducts $viewTrackingService->getRecentlyViewedProducts(100);
  1960.         
  1961.         // Appliquer le tri
  1962.         switch ($sortBy) {
  1963.             case 'price_asc':
  1964.                 usort($allRecentlyViewedProducts, function($a$b) {
  1965.                     return $a->getPrice() <=> $b->getPrice();
  1966.                 });
  1967.                 break;
  1968.             case 'price_desc':
  1969.                 usort($allRecentlyViewedProducts, function($a$b) {
  1970.                     return $b->getPrice() <=> $a->getPrice();
  1971.                 });
  1972.                 break;
  1973.             case 'name':
  1974.                 usort($allRecentlyViewedProducts, function($a$b) {
  1975.                     return strcmp($a->getName(), $b->getName());
  1976.                 });
  1977.                 break;
  1978.             case 'recent':
  1979.             default:
  1980.                 // Garder l'ordre par défaut (plus récent en premier)
  1981.                 break;
  1982.         }
  1983.         
  1984.         // Pagination
  1985.         $totalProducts count($allRecentlyViewedProducts);
  1986.         $totalPages ceil($totalProducts $limit);
  1987.         $offset = ($page 1) * $limit;
  1988.         $recentlyViewedProducts array_slice($allRecentlyViewedProducts$offset$limit);
  1989.         
  1990.         return $this->render('account/recently_viewed.html.twig', [
  1991.             'recentlyViewedProducts' => $recentlyViewedProducts,
  1992.             'current_page' => $page,
  1993.             'total_pages' => $totalPages,
  1994.             'total_products' => $totalProducts,
  1995.             'sort_by' => $sortBy,
  1996.         ]);
  1997.     }
  1998.     #[Route('/account/followed_shops'name'account_followed_shops')]
  1999.     public function accountFollowedShops(ShopFollowService $shopFollowService): Response
  2000.     {
  2001.         if (!$this->getUser()) {
  2002.             return $this->redirectToRoute('ui_app_login');
  2003.         }
  2004.         
  2005.         // Récupérer les boutiques suivies par l'utilisateur
  2006.         $followedShops $shopFollowService->getFollowedShopsByUser($this->getUser());
  2007.         
  2008.         return $this->render('account/followed_shops.html.twig', [
  2009.             'followedShops' => $followedShops,
  2010.         ]);
  2011.     }
  2012.     #[Route('/account/orders'name'account_orders')]
  2013.     public function accountOrders(EntityManagerInterface $emRequest $request): Response
  2014.     {
  2015.         if (!$this->getUser()) {
  2016.             return $this->redirectToRoute('ui_app_login');
  2017.         }
  2018.         
  2019.         $page $request->query->getInt('page'1);
  2020.         $limit 10;
  2021.         $offset = ($page 1) * $limit;
  2022.         
  2023.         $orders $em->getRepository(Order::class)
  2024.             ->createQueryBuilder('o')
  2025.             ->where('o.customer = :user')
  2026.             ->setParameter('user'$this->getUser())
  2027.             ->orderBy('o.orderedAt''DESC')
  2028.             ->setFirstResult($offset)
  2029.             ->setMaxResults($limit)
  2030.             ->getQuery()
  2031.             ->getResult();
  2032.         
  2033.         $totalOrders $em->getRepository(Order::class)
  2034.             ->createQueryBuilder('o')
  2035.             ->select('COUNT(o.id)')
  2036.             ->where('o.customer = :user')
  2037.             ->setParameter('user'$this->getUser())
  2038.             ->getQuery()
  2039.             ->getSingleScalarResult();
  2040.         
  2041.         $totalPages ceil($totalOrders $limit);
  2042.         
  2043.         return $this->render('account/orders.html.twig', [
  2044.             'orders' => $orders,
  2045.             'current_page' => $page,
  2046.             'total_pages' => $totalPages,
  2047.             'active' => 'orders'
  2048.         ]);
  2049.     }
  2050.     #[Route('/account/orders/{id}'name'account_order_show')]
  2051.     public function accountOrderShow(int $idEntityManagerInterface $em): Response
  2052.     {
  2053.         if (!$this->getUser()) {
  2054.             return $this->redirectToRoute('ui_app_login');
  2055.         }
  2056.         
  2057.         $order $em->getRepository(Order::class)->find($id);
  2058.         
  2059.         if (!$order) {
  2060.             $this->addFlash('error''Commande introuvable.');
  2061.             return $this->redirectToRoute('ui_account_orders');
  2062.         }
  2063.         
  2064.         // Vérifier que la commande appartient à l'utilisateur connecté
  2065.         if ($order->getCustomer()->getId() !== $this->getUser()->getId()) {
  2066.             $this->addFlash('error''Vous n\'avez pas accès à cette commande.');
  2067.             return $this->redirectToRoute('ui_account_orders');
  2068.         }
  2069.         
  2070.         return $this->render('account/order_show.html.twig', [
  2071.             'order' => $order,
  2072.             'active' => 'orders'
  2073.         ]);
  2074.     }
  2075.     #[Route('/account/wishlist'name'account_wishlist')]
  2076.     public function accountWishlist(EntityManagerInterface $em\App\Service\WishlistService $wishlistService): Response
  2077.     {
  2078.         if (!$this->getUser()) {
  2079.             return $this->redirectToRoute('ui_app_login');
  2080.         }
  2081.         
  2082.         $wishlistItems $wishlistService->getWishlistProducts($this->getUser());
  2083.         $products array_map(fn($item) => $item->getProduct(), $wishlistItems);
  2084.         
  2085.         return $this->render('account/wishlist.html.twig', [
  2086.             'wishlist' => $products,
  2087.             'active' => 'wishlist'
  2088.         ]);
  2089.     }
  2090.     #[Route('/account/saved-searches'name'account_saved_searches')]
  2091.     public function accountSavedSearches(): Response
  2092.     {
  2093.         if (!$this->getUser()) {
  2094.             return $this->redirectToRoute('ui_app_login');
  2095.         }
  2096.         
  2097.         // TODO: Implémenter recherches enregistrées
  2098.         return $this->render('account/saved_searches.html.twig', [
  2099.             'searches' => [],
  2100.             'active' => 'search'
  2101.         ]);
  2102.     }
  2103.     #[Route('/account/transactions'name'account_transactions')]
  2104.     public function accountTransactions(EntityManagerInterface $emRequest $request): Response
  2105.     {
  2106.         if (!$this->getUser()) {
  2107.             return $this->redirectToRoute('ui_app_login');
  2108.         }
  2109.         
  2110.         // Les transactions sont basées sur les commandes
  2111.         $orders $em->getRepository(Order::class)
  2112.             ->createQueryBuilder('o')
  2113.             ->where('o.customer = :user')
  2114.             ->setParameter('user'$this->getUser())
  2115.             ->orderBy('o.orderedAt''DESC')
  2116.             ->getQuery()
  2117.             ->getResult();
  2118.         
  2119.         return $this->render('account/transactions.html.twig', [
  2120.             'orders' => $orders,
  2121.             'active' => 'transactions'
  2122.         ]);
  2123.     }
  2124.     #[Route('/account/payment-methods'name'account_payment_methods')]
  2125.     public function accountPaymentMethods(EntityManagerInterface $em): Response
  2126.     {
  2127.         if (!$this->getUser()) {
  2128.             return $this->redirectToRoute('ui_app_login');
  2129.         }
  2130.         
  2131.         $paymentMethods $em->getRepository(PaymentMethod::class)->findBy(['isActive' => true]);
  2132.         
  2133.         return $this->render('account/payment_methods.html.twig', [
  2134.             'paymentMethods' => $paymentMethods,
  2135.             'active' => 'payment'
  2136.         ]);
  2137.     }
  2138.     #[Route('/account/coupons'name'account_coupons')]
  2139.     public function accountCoupons(): Response
  2140.     {
  2141.         if (!$this->getUser()) {
  2142.             return $this->redirectToRoute('ui_app_login');
  2143.         }
  2144.         
  2145.         // TODO: Implémenter système de coupons
  2146.         return $this->render('account/coupons.html.twig', [
  2147.             'coupons' => [],
  2148.             'active' => 'coupons'
  2149.         ]);
  2150.     }
  2151.     #[Route('/account/gift-cards'name'account_gift_cards')]
  2152.     public function accountGiftCards(GiftCardService $giftCardService): Response
  2153.     {
  2154.         if (!$this->getUser()) {
  2155.             return $this->redirectToRoute('ui_app_login');
  2156.         }
  2157.         
  2158.         $user $this->getUser();
  2159.         $giftCards $giftCardService->getUserGiftCards($user);
  2160.         
  2161.         return $this->render('account/gift_cards.html.twig', [
  2162.             'giftCards' => $giftCards,
  2163.             'active' => 'giftcards'
  2164.         ]);
  2165.     }
  2166.     #[Route('/gift-card/purchase'name'gift_card_purchase'methods: ['GET''POST'])]
  2167.     #[IsGranted('ROLE_USER')]
  2168.     public function purchaseGiftCard(Request $requestGiftCardService $giftCardServiceEntityManagerInterface $em): Response
  2169.     {
  2170.         $user $this->getUser();
  2171.         $giftCard = new GiftCard();
  2172.         $form $this->createForm(GiftCardPurchaseType::class, $giftCard);
  2173.         $form->handleRequest($request);
  2174.         if ($form->isSubmitted() && $form->isValid()) {
  2175.             $amount = (float)$giftCard->getInitialAmount();
  2176.             $recipientEmail $giftCard->getRecipientEmail();
  2177.             $recipient null;
  2178.             
  2179.             // Vérifier si l'email correspond à un utilisateur existant
  2180.             if ($recipientEmail) {
  2181.                 $recipient $em->getRepository(User::class)->findOneBy(['email' => $recipientEmail]);
  2182.                 
  2183.                 // Vérifier que le destinataire n'est pas l'acheteur lui-même
  2184.                 if ($recipient && $recipient->getId() === $user->getId()) {
  2185.                     $this->addFlash('error''Vous ne pouvez pas offrir une carte cadeau à vous-même.');
  2186.                     return $this->render('gift_card/purchase.html.twig', [
  2187.                         'form' => $form->createView(),
  2188.                         'current_menu' => 'giftcard'
  2189.                     ]);
  2190.                 }
  2191.             }
  2192.             
  2193.             // Créer la carte cadeau
  2194.             $newGiftCard $giftCardService->createGiftCard(
  2195.                 $amount,
  2196.                 $user,
  2197.                 $recipient// recipient sera défini si email correspond à un utilisateur
  2198.                 $recipientEmail,
  2199.                 $giftCard->getRecipientName(),
  2200.                 $giftCard->getMessage()
  2201.             );
  2202.             $this->addFlash('success''Carte cadeau créée avec succès ! Code: ' $newGiftCard->getCode());
  2203.             
  2204.             // TODO: Envoyer un email au destinataire si email fourni
  2205.             
  2206.             return $this->redirectToRoute('ui_account_gift_cards');
  2207.         }
  2208.         return $this->render('gift_card/purchase.html.twig', [
  2209.             'form' => $form->createView(),
  2210.             'current_menu' => 'giftcard'
  2211.         ]);
  2212.     }
  2213.     #[Route('/gift-card/redeem'name'gift_card_redeem'methods: ['POST'])]
  2214.     #[IsGranted('ROLE_USER')]
  2215.     public function redeemGiftCard(Request $requestGiftCardService $giftCardService): JsonResponse
  2216.     {
  2217.         $code $request->request->get('code''');
  2218.         $code strtoupper(trim($code));
  2219.         $validation $giftCardService->validateGiftCardCode($code$this->getUser());
  2220.         if (!$validation['valid']) {
  2221.             return $this->json([
  2222.                 'success' => false,
  2223.                 'message' => $validation['message']
  2224.             ], 400);
  2225.         }
  2226.         $giftCard $validation['giftCard'];
  2227.         $currentUser $this->getUser();
  2228.         
  2229.         // Vérifier si la carte a un destinataire spécifique
  2230.         if ($giftCard->getRecipient()) {
  2231.             // Si la carte a un destinataire, seul ce destinataire peut l'utiliser
  2232.             if ($giftCard->getRecipient()->getId() !== $currentUser->getId()) {
  2233.                 return $this->json([
  2234.                     'success' => false,
  2235.                     'message' => 'Cette carte cadeau a été offerte à quelqu\'un d\'autre et ne peut être utilisée que par le destinataire.'
  2236.                 ], 403);
  2237.             }
  2238.         } else {
  2239.             // Si pas de destinataire, seul l'acheteur peut l'utiliser
  2240.             if ($giftCard->getPurchasedBy()->getId() !== $currentUser->getId()) {
  2241.                 return $this->json([
  2242.                     'success' => false,
  2243.                     'message' => 'Cette carte cadeau ne vous appartient pas'
  2244.                 ], 403);
  2245.             }
  2246.         }
  2247.         return $this->json([
  2248.             'success' => true,
  2249.             'giftCard' => [
  2250.                 'id' => $giftCard->getId(),
  2251.                 'code' => $giftCard->getCode(),
  2252.                 'balance' => $giftCard->getBalance(),
  2253.                 'currency' => $giftCard->getCurrency()
  2254.             ],
  2255.             'message' => 'Carte cadeau valide'
  2256.         ]);
  2257.     }
  2258.     #[Route('/checkout/complete-with-gift-card'name'checkout_complete_gift_card'methods: ['POST'])]
  2259.     public function completeOrderWithGiftCard(
  2260.         Request $request,
  2261.         EntityManagerInterface $em,
  2262.         GiftCardService $giftCardService
  2263.     ): JsonResponse {
  2264.         $user $this->getUser();
  2265.         if (!$user) {
  2266.             return new JsonResponse(['ok' => false'message' => 'Vous devez être connecté'], 401);
  2267.         }
  2268.         $data json_decode($request->getContent(), true);
  2269.         
  2270.         // Récupérer les données de la commande
  2271.         $shippingMethodId $data['shippingMethod'] ?? null;
  2272.         $deliveryAddressId $data['deliveryAddress'] ?? null;
  2273.         $orderNotes $data['orderNotes'] ?? '';
  2274.         $giftCardCode = isset($data['giftCardCode']) ? strtoupper(trim($data['giftCardCode'])) : null;
  2275.         // Validation des champs requis
  2276.         if (!$shippingMethodId || !$deliveryAddressId) {
  2277.             return new JsonResponse([
  2278.                 'ok' => false,
  2279.                 'message' => 'Adresse de livraison et moyen de livraison requis'
  2280.             ], 400);
  2281.         }
  2282.         // Récupérer le panier
  2283.         $cart $em->getRepository(Cart::class)->findOneBy([
  2284.             'user' => $user,
  2285.             'isActive' => true
  2286.         ]);
  2287.         if (!$cart || $cart->getItems()->isEmpty()) {
  2288.             return new JsonResponse(['ok' => false'message' => 'Panier vide'], 400);
  2289.         }
  2290.         // Récupérer la carte cadeau si fournie
  2291.         $giftCard null;
  2292.         if ($giftCardCode) {
  2293.             $validation $giftCardService->validateGiftCardCode($giftCardCode$user);
  2294.             if (!$validation['valid']) {
  2295.                 return new JsonResponse([
  2296.                     'ok' => false,
  2297.                     'message' => $validation['message']
  2298.                 ], 400);
  2299.             }
  2300.             $giftCard $validation['giftCard'];
  2301.             
  2302.             // Vérifier si la carte a un destinataire spécifique
  2303.             if ($giftCard->getRecipient()) {
  2304.                 // Si la carte a un destinataire, seul ce destinataire peut l'utiliser
  2305.                 if ($giftCard->getRecipient()->getId() !== $user->getId()) {
  2306.                     return new JsonResponse([
  2307.                         'ok' => false,
  2308.                         'message' => 'Cette carte cadeau a été offerte à quelqu\'un d\'autre et ne peut être utilisée que par le destinataire.'
  2309.                     ], 403);
  2310.                 }
  2311.             } else {
  2312.                 // Si pas de destinataire, seul l'acheteur peut l'utiliser
  2313.                 if ($giftCard->getPurchasedBy()->getId() !== $user->getId()) {
  2314.                     return new JsonResponse([
  2315.                         'ok' => false,
  2316.                         'message' => 'Cette carte cadeau ne vous appartient pas'
  2317.                     ], 403);
  2318.                 }
  2319.             }
  2320.         }
  2321.         try {
  2322.             // Calculer les totaux en additionnant les items du panier
  2323.             // IMPORTANT: Calculer le sous-total en additionnant les totalPrice de chaque item
  2324.             // pour garantir la cohérence avec les OrderItems créés
  2325.             $subtotal 0.0;
  2326.             foreach ($cart->getItems() as $cartItem) {
  2327.                 $subtotal += (float) $cartItem->getTotalPrice();
  2328.             }
  2329.             
  2330.             $shippingMethod $em->getRepository(ShippingMethod::class)->find($shippingMethodId);
  2331.             if (!$shippingMethod) {
  2332.                 return new JsonResponse(['ok' => false'message' => 'Moyen de livraison invalide'], 400);
  2333.             }
  2334.             
  2335.             $shippingAmount = (float) $shippingMethod->getPrice();
  2336.             $totalBeforeTax $subtotal $shippingAmount;
  2337.             
  2338.             // Calculer la réduction carte cadeau
  2339.             $giftCardDiscount 0;
  2340.             if ($giftCard) {
  2341.                 $balance = (float) $giftCard->getBalance();
  2342.                 $giftCardDiscount min($balance$totalBeforeTax);
  2343.             }
  2344.             
  2345.             $totalBeforeTaxAfterDiscount $totalBeforeTax $giftCardDiscount;
  2346.             $taxAmount $totalBeforeTaxAfterDiscount 0.01;
  2347.             $totalAmount $totalBeforeTaxAfterDiscount $taxAmount;
  2348.             // Vérifier que le total est bien <= 0
  2349.             if ($totalAmount 0) {
  2350.                 return new JsonResponse([
  2351.                     'ok' => false,
  2352.                     'message' => 'Le montant restant doit être inférieur ou égal à zéro pour utiliser cette méthode'
  2353.                 ], 400);
  2354.             }
  2355.             // Récupérer l'adresse de livraison
  2356.             $address $em->getRepository(Address::class)->find($deliveryAddressId);
  2357.             if (!$address) {
  2358.                 return new JsonResponse(['ok' => false'message' => 'Adresse de livraison invalide'], 400);
  2359.             }
  2360.             // Créer la commande
  2361.             $order = new Order();
  2362.             $order->setOrderNumber('ORD-' strtoupper(uniqid()));
  2363.             $order->setCustomer($user);
  2364.             $order->setSubtotal((string) $subtotal);
  2365.             $order->setTaxAmount((string) $taxAmount);
  2366.             $order->setShippingAmount((string) $shippingAmount);
  2367.             $order->setDiscountAmount((string) $giftCardDiscount);
  2368.             $order->setTotalAmount((string) max(0$totalAmount)); // S'assurer que le total n'est pas négatif
  2369.             $order->setCurrency('HTG');
  2370.             $order->setStatus('pending');
  2371.             $order->setPaymentStatus('paid'); // Paiement complet via carte cadeau
  2372.             $order->setPaymentMethod('Gift Card');
  2373.             $order->setShippingMethod($shippingMethod->getName());
  2374.             $order->setNotes($orderNotes);
  2375.             $order->setOrderedAt(new \DateTimeImmutable('now'));
  2376.             // Ajouter l'adresse de livraison
  2377.             $fullAddress sprintf(
  2378.                 '%s, %s, %s, %s%s',
  2379.                 $address->getStreet(),
  2380.                 $address->getCity(),
  2381.                 $address->getState(),
  2382.                 $address->getCountry(),
  2383.                 $address->getZipCode() ? ' ' $address->getZipCode() : ''
  2384.             );
  2385.             $order->setShippingAddress($fullAddress);
  2386.             $order->setBillingAddress($fullAddress);
  2387.             // Ajouter les items de la commande
  2388.             foreach ($cart->getItems() as $cartItem) {
  2389.                 $orderItem = new \App\Entity\OrderItem();
  2390.                 $orderItem->setOrder($order);
  2391.                 $orderItem->setProduct($cartItem->getProduct());
  2392.                 $orderItem->setQuantity($cartItem->getQuantity());
  2393.                 $orderItem->setUnitPrice((string) $cartItem->getUnitPrice());
  2394.                 $orderItem->setTotalPrice((string) $cartItem->getTotalPrice());
  2395.                 $em->persist($orderItem);
  2396.             }
  2397.             // Désactiver le panier
  2398.             $cart->setIsActive(false);
  2399.             // Persister l'order d'abord pour qu'il ait un ID
  2400.             $em->persist($order);
  2401.             $em->flush();
  2402.             // Utiliser la carte cadeau si applicable (après que l'order soit persisté)
  2403.             // Passer l'EntityManager pour s'assurer que tout utilise le même contexte
  2404.             if ($giftCard && $giftCardDiscount 0) {
  2405.                 $giftCardService->useGiftCard($giftCard$giftCardDiscount$order$em);
  2406.             }
  2407.             // Envoyer des notifications
  2408.             // Notification pour le client
  2409.             $this->notificationService->createOrderCreatedNotification($user$order);
  2410.             
  2411.             // Notifications pour les vendeurs (un par boutique)
  2412.             $shopsNotified = [];
  2413.             foreach ($order->getItems() as $orderItem) {
  2414.                 $product $orderItem->getProduct();
  2415.                 $shop $product->getShop();
  2416.                 if ($shop && !in_array($shop->getId(), $shopsNotified)) {
  2417.                     $shopOwner $shop->getManager()->first();
  2418.                     if ($shopOwner) {
  2419.                         $this->notificationService->createOrderReceivedNotification($shopOwner$order$shop);
  2420.                         $shopsNotified[] = $shop->getId();
  2421.                     }
  2422.                 }
  2423.             }
  2424.             return new JsonResponse([
  2425.                 'ok' => true,
  2426.                 'message' => 'Commande créée avec succès',
  2427.                 'orderId' => $order->getId(),
  2428.                 'orderNumber' => $order->getOrderNumber(),
  2429.                 'redirectUrl' => $this->generateUrl('ui_account_orders')
  2430.             ]);
  2431.         } catch (\Exception $e) {
  2432.             return new JsonResponse([
  2433.                 'ok' => false,
  2434.                 'message' => 'Erreur lors de la création de la commande: ' $e->getMessage()
  2435.             ], 500);
  2436.         }
  2437.     }
  2438.     #[Route('/account/settings'name'account_settings')]
  2439.     public function accountSettings(Request $requestEntityManagerInterface $em): Response
  2440.     {
  2441.         if (!$this->getUser()) {
  2442.             return $this->redirectToRoute('ui_app_login');
  2443.         }
  2444.         
  2445.         $user $this->getUser();
  2446.         
  2447.         if ($request->isMethod('POST')) {
  2448.             // TODO: Gérer la mise à jour des paramètres
  2449.             $this->addFlash('success''Paramètres mis à jour avec succès.');
  2450.             return $this->redirectToRoute('account_settings');
  2451.         }
  2452.         
  2453.         return $this->render('account/settings.html.twig', [
  2454.             'user' => $user,
  2455.             'active' => 'settings'
  2456.         ]);
  2457.     }
  2458.     #[Route('/account/kyc'name'account_kyc')]
  2459.     public function accountKyc(Request $requestEntityManagerInterface $em): Response
  2460.     {
  2461.         if (!$this->getUser()) {
  2462.             return $this->redirectToRoute('ui_app_login');
  2463.         }
  2464.         $userFromToken $this->getUser(); // c’est un UserInterface
  2465.         $userId $userFromToken->getId();
  2466.         $user $em->getRepository(User::class)->find($userId);
  2467.         if (!$user) {
  2468.             throw $this->createNotFoundException('Utilisateur introuvable');
  2469.         }
  2470.         $form $this->createForm(KycFormType::class, $user);
  2471.         $form->handleRequest($request);
  2472.         if ($form->isSubmitted() && $form->isValid()) {
  2473.             if (
  2474.                 $user->getFrontDocumentSubmitted() && $user->getSelfieSubmitted()
  2475.                 && $user->getBackDocumentSubmitted() && $user->getFirstname()
  2476.                 && $user->getLastname() && $user->getPhone() && $user->getGender()
  2477.             ) {
  2478.                 $user->setKycStatus('pending');
  2479.                 $user->setKycSubmittedAt(new DateTimeImmutable('now'));
  2480.                 // Vérifier et ajouter ROLE_SELLER s’il n’existe pas déjà
  2481.                 $roles $user->getRoles();
  2482.                 if (!in_array('ROLE_SELLER'$rolestrue)) {
  2483.                     $roles[] = 'ROLE_SELLER';
  2484.                     $user->setRoles($roles);
  2485.                 }
  2486.             }
  2487.             $em->persist($user);
  2488.             $em->flush();
  2489.             $this->addFlash('success''Vos documents ont été soumis avec succès. En attente de validation.');
  2490.             return $this->redirectToRoute('ui_account_kyc');
  2491.         }
  2492.         return $this->render('account/kyc.html.twig', [
  2493.             'form' => $form->createView(),
  2494.             'user' => $user
  2495.         ]);
  2496.     }
  2497.     #[Route('/kyc/upload/{type}'name'kyc_upload'methods: ['POST'])]
  2498.     public function upload(Request $requeststring $typeEntityManagerInterface $em): JsonResponse
  2499.     {
  2500.         if (!$this->getUser()) {
  2501.             return new JsonResponse(['error' => 'Vous devez être connecté pour uploader des fichiers'], 401);
  2502.         }
  2503.         $file $request->files->get('file');
  2504.         if (!$file) {
  2505.             return new JsonResponse(['error' => 'Aucun fichier n\'a été reçu. Veuillez sélectionner un fichier.'], 400);
  2506.         }
  2507.         // Vérifier les erreurs d'upload PHP
  2508.         if ($file->getError() !== UPLOAD_ERR_OK) {
  2509.             $errorMessages = [
  2510.                 UPLOAD_ERR_INI_SIZE => 'Le fichier dépasse la limite de taille autorisée par le serveur (upload_max_filesize). Taille maximum : ' ini_get('upload_max_filesize'),
  2511.                 UPLOAD_ERR_FORM_SIZE => 'Le fichier dépasse la limite de taille autorisée par le formulaire.',
  2512.                 UPLOAD_ERR_PARTIAL => 'Le fichier n\'a été que partiellement uploadé.',
  2513.                 UPLOAD_ERR_NO_FILE => 'Aucun fichier n\'a été uploadé.',
  2514.                 UPLOAD_ERR_NO_TMP_DIR => 'Le dossier temporaire est manquant.',
  2515.                 UPLOAD_ERR_CANT_WRITE => 'Échec de l\'écriture du fichier sur le disque.',
  2516.                 UPLOAD_ERR_EXTENSION => 'Une extension PHP a arrêté l\'upload du fichier.',
  2517.             ];
  2518.             $errorMessage $errorMessages[$file->getError()] ?? 'Erreur inconnue lors de l\'upload.';
  2519.             return new JsonResponse(['error' => $errorMessage], 400);
  2520.         }
  2521.         // Vérifier la taille du fichier (max 5MB)
  2522.         $maxSize 1024 1024// 5MB
  2523.         if ($file->getSize() > $maxSize) {
  2524.             return new JsonResponse([
  2525.                 'error' => 'Le fichier est trop volumineux. Taille maximum : 5 Mo. Taille du fichier : ' round($file->getSize() / 1024 10242) . ' Mo'
  2526.             ], 400);
  2527.         }
  2528.         // Obtenir l'extension en utilisant getClientOriginalExtension() qui ne nécessite pas d'accès au fichier
  2529.         $extension $file->getClientOriginalExtension();
  2530.         
  2531.         // Si l'extension est vide, utiliser le MIME type
  2532.         if (empty($extension)) {
  2533.             $mimeType $file->getMimeType();
  2534.             $extensionMap = [
  2535.                 'image/jpeg' => 'jpg',
  2536.                 'image/jpg' => 'jpg',
  2537.                 'image/png' => 'png',
  2538.                 'image/gif' => 'gif',
  2539.                 'application/pdf' => 'pdf',
  2540.             ];
  2541.             $extension $extensionMap[$mimeType] ?? 'bin';
  2542.         }
  2543.         
  2544.         // Nettoyer l'extension (enlever les points et caractères spéciaux)
  2545.         $extension strtolower(trim($extension'.'));
  2546.         $extension preg_replace('/[^a-z0-9]/'''$extension);
  2547.         if (empty($extension)) {
  2548.             $extension 'bin';
  2549.         }
  2550.         
  2551.         $filename uniqid() . '.' $extension;
  2552.         
  2553.         // Obtenir le répertoire KYC
  2554.         $kycDirectory $this->getParameter('kyc_directory');
  2555.         
  2556.         // Créer le répertoire s'il n'existe pas
  2557.         if (!is_dir($kycDirectory)) {
  2558.             @mkdir($kycDirectory0755true);
  2559.         }
  2560.         
  2561.         // Déplacer le fichier
  2562.         try {
  2563.             $file->move($kycDirectory$filename);
  2564.         } catch (\Exception $e) {
  2565.             return new JsonResponse([
  2566.                 'error' => 'Erreur lors de l\'upload : ' $e->getMessage()
  2567.             ], 500);
  2568.         }
  2569.         // Mettre à jour l’utilisateur
  2570.         $user $this->getUser();
  2571.         if ($type === 'selfie') {
  2572.             $user->setSelfieSubmitted($filename);
  2573.         } elseif ($type === 'front') {
  2574.             // Tu peux décider recto/verso selon logique
  2575.             $user->setFrontDocumentSubmitted($filename);
  2576.         } elseif ($type === 'back') {
  2577.             // Tu peux décider recto/verso selon logique
  2578.             $user->setBackDocumentSubmitted($filename);
  2579.         }
  2580.         $em->flush();
  2581.         return new JsonResponse(['success' => true'filename' => $filename]);
  2582.     }
  2583.     #[Route('/account/address'name'account_address'methods: ['GET''POST'])]
  2584.     public function indexAddress(AddressRepository $addressRepositoryRequest $requestEntityManagerInterface $entityManager): Response
  2585.     {
  2586.         if (!$this->getUser()) {
  2587.             return $this->redirectToRoute('ui_app_login');
  2588.         }
  2589.         $address = new Address();
  2590.         $form $this->createForm(AddressType::class, $address);
  2591.         $form->handleRequest($request);
  2592.         if ($form->isSubmitted() && $form->isValid()) {
  2593.             $address->setUser($this->getUser());
  2594.             if ($address->isDefault()) {
  2595.                 // Mettre toutes les autres adresses de l'user à false
  2596.                 foreach ($this->getUser()->getAddresses() as $otherAddress) {
  2597.                     $otherAddress->setIsDefault(false);
  2598.                     $entityManager->persist($otherAddress);
  2599.                 }
  2600.             }
  2601.             $entityManager->persist($address);
  2602.             $entityManager->flush();
  2603.             return $this->redirectToRoute('ui_account_address', [], Response::HTTP_SEE_OTHER);
  2604.         }
  2605.         return $this->render('account/address.html.twig', [
  2606.             'addresses' => $addressRepository->findBy(['user' => $this->getUser()]),
  2607.             'address' => $address,
  2608.             'newAddressForm' => $form->createView(),
  2609.         ]);
  2610.     }
  2611.     #[Route('/account/address/{id}'name'account_address_delete'methods: ['POST'])]
  2612.     public function delete(Request $request\App\Entity\Address $addressEntityManagerInterface $entityManager): Response
  2613.     {
  2614.         if ($this->isCsrfTokenValid('delete' $address->getId(), $request->request->get('_token'))) {
  2615.             $entityManager->remove($address);
  2616.             $entityManager->flush();
  2617.         }
  2618.         return $this->redirectToRoute('ui_account_address', [], Response::HTTP_SEE_OTHER);
  2619.     }
  2620.     #[Route('/register'name'app_register')]
  2621.     public function register(
  2622.         Request $request
  2623.         UserPasswordHasherInterface $userPasswordHasher
  2624.         EntityManagerInterface $entityManager
  2625.     ): Response
  2626.     {
  2627.         $user = new User();
  2628.         $form $this->createForm(RegistrationFormType::class, $user);
  2629.         $form->handleRequest($request);
  2630.         // Récupérer le code de parrainage depuis l'URL ou la session
  2631.         $referralCode $request->query->get('ref') ?? $request->getSession()->get('referral_code');
  2632.         if ($form->isSubmitted() && $form->isValid()) {
  2633.             // encode the plain password
  2634.             $user->setPassword(
  2635.                 $userPasswordHasher->hashPassword(
  2636.                     $user,
  2637.                     $form->get('plainPassword')->getData()
  2638.                 )
  2639.             );
  2640.             $user->setCreatedAt(new DateTimeImmutable('now'));
  2641.             $entityManager->persist($user);
  2642.             $entityManager->flush();
  2643.             // Traiter le parrainage si un code est présent
  2644.             if ($referralCode) {
  2645.                 try {
  2646.                     $this->referralService->processReferralSignup($user$referralCode);
  2647.                     $this->addFlash('success''Vous avez été inscrit via un lien de parrainage !');
  2648.                 } catch (\Exception $e) {
  2649.                     // Ne pas bloquer l'inscription si le parrainage échoue
  2650.                     // Log l'erreur si nécessaire
  2651.                 }
  2652.                 // Nettoyer le code de parrainage de la session
  2653.                 $request->getSession()->remove('referral_code');
  2654.             }
  2655.             // generate a signed url and email it to the user
  2656.             $this->emailVerifier->sendEmailConfirmation(
  2657.                 'ui_app_verify_email',
  2658.                 $user,
  2659.                 (new TemplatedEmail())
  2660.                     ->from(new EmailAddress('no-reply@maketou.com''Verification de votre adresse email'))
  2661.                     ->to($user->getEmail())
  2662.                     ->subject('Please Confirm your Email')
  2663.                     ->htmlTemplate('registration/confirmation_email.html.twig')
  2664.             );
  2665.             // do anything else you need here, like send an email
  2666.             return $this->redirectToRoute('ui_app_login');
  2667.         }
  2668.         // Stocker le code de parrainage en session si présent dans l'URL
  2669.         if ($referralCode && !$form->isSubmitted()) {
  2670.             $request->getSession()->set('referral_code'$referralCode);
  2671.         }
  2672.         return $this->render('registration/register.html.twig', [
  2673.             'registrationForm' => $form->createView(),
  2674.             'referralCode' => $referralCode,
  2675.         ]);
  2676.     }
  2677.     #[Route('/verify/email'name'app_verify_email')]
  2678.     public function verifyUserEmail(Request $requestTranslatorInterface $translator): Response
  2679.     {
  2680.         $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
  2681.         // validate email confirmation link, sets User::isVerified=true and persists
  2682.         try {
  2683.             $this->emailVerifier->handleEmailConfirmation($request$this->getUser());
  2684.         } catch (VerifyEmailExceptionInterface $exception) {
  2685.             $this->addFlash('verify_email_error'$translator->trans($exception->getReason(), [], 'VerifyEmailBundle'));
  2686.             return $this->redirectToRoute('ui_app_register');
  2687.         }
  2688.         // @TODO Change the redirect on success and handle or remove the flash message in your templates
  2689.         $this->addFlash('success''Your email address has been verified.');
  2690.         return $this->redirectToRoute('ui_app_register');
  2691.     }
  2692.     #[Route(path'/login'name'app_login')]
  2693.     public function login(AuthenticationUtils $authenticationUtils): Response
  2694.     {
  2695.         // get the login error if there is one
  2696.         $error $authenticationUtils->getLastAuthenticationError();
  2697.         // last username entered by the user
  2698.         $lastUsername $authenticationUtils->getLastUsername();
  2699.         return $this->render('security/login.html.twig', [
  2700.             'last_username' => $lastUsername,
  2701.             'error' => $error,
  2702.         ]);
  2703.     }
  2704.     #[Route('/reset-password'name'app_forgot_password_request')]
  2705.     public function requestPasswordReset(Request $requestEntityManagerInterface $emMailerInterface $mailer): Response
  2706.     {
  2707.         $form $this->createForm(ResetPasswordRequestFormType::class);
  2708.         $form->handleRequest($request);
  2709.         if ($form->isSubmitted() && $form->isValid()) {
  2710.             $email $form->get('email')->getData();
  2711.             $user $em->getRepository(User::class)->findOneBy(['email' => $email]);
  2712.             // Ne pas révéler si l'email existe ou non pour des raisons de sécurité
  2713.             if ($user) {
  2714.                 $resetToken bin2hex(random_bytes(32));
  2715.                 $user->setResetToken($resetToken);
  2716.                 $user->setPasswordRequestedAt(new DateTimeImmutable('now'));
  2717.                 $em->flush();
  2718.                 // Envoyer l'email
  2719.                 $resetUrl $this->generateUrl('ui_app_reset_password', ['token' => $resetToken], UrlGeneratorInterface::ABSOLUTE_URL);
  2720.                 
  2721.                 $email = (new TemplatedEmail())
  2722.                     ->from(new EmailAddress('no-reply@maketou.com''MaketOu'))
  2723.                     ->to($user->getEmail())
  2724.                     ->subject('Réinitialisation de votre mot de passe')
  2725.                     ->htmlTemplate('security/reset_password_email.html.twig')
  2726.                     ->context([
  2727.                         'resetUrl' => $resetUrl,
  2728.                         'user' => $user,
  2729.                     ]);
  2730.                 $mailer->send($email);
  2731.             }
  2732.             // Toujours afficher le même message pour éviter l'énumération d'emails
  2733.             $this->addFlash('success''Si votre adresse email existe dans notre système, vous recevrez un lien pour réinitialiser votre mot de passe.');
  2734.             return $this->redirectToRoute('ui_app_forgot_password_request');
  2735.         }
  2736.         return $this->render('security/reset_password_request.html.twig', [
  2737.             'requestForm' => $form->createView(),
  2738.         ]);
  2739.     }
  2740.     #[Route('/reset-password/{token}'name'app_reset_password')]
  2741.     public function resetPassword(string $tokenRequest $requestUserPasswordHasherInterface $userPasswordHasherEntityManagerInterface $em): Response
  2742.     {
  2743.         $user $em->getRepository(User::class)->findOneBy(['resetToken' => $token]);
  2744.         if (!$user || !$user->isPasswordRequestNonExpired()) {
  2745.             $this->addFlash('error''Le lien de réinitialisation est invalide ou a expiré.');
  2746.             return $this->redirectToRoute('ui_app_forgot_password_request');
  2747.         }
  2748.         $form $this->createForm(ResetPasswordFormType::class);
  2749.         $form->handleRequest($request);
  2750.         if ($form->isSubmitted() && $form->isValid()) {
  2751.             // Encoder le nouveau mot de passe
  2752.             $user->setPassword(
  2753.                 $userPasswordHasher->hashPassword(
  2754.                     $user,
  2755.                     $form->get('plainPassword')->getData()
  2756.                 )
  2757.             );
  2758.             // Effacer le token
  2759.             $user->setResetToken(null);
  2760.             $user->setPasswordRequestedAt(null);
  2761.             $em->flush();
  2762.             $this->addFlash('success''Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter.');
  2763.             return $this->redirectToRoute('ui_app_login');
  2764.         }
  2765.         return $this->render('security/reset_password.html.twig', [
  2766.             'resetForm' => $form->createView(),
  2767.         ]);
  2768.     }
  2769.     #[Route('/shop/{slug}'name'shop_show')]
  2770.     public function shopShow(string $slugProductRepository $productRepositoryViewTrackingService $viewTrackingServiceShopFollowService $shopFollowServiceEntityManagerInterface $em): Response
  2771.     {
  2772.         $shop $em->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  2773.         if (!$shop) {
  2774.             throw $this->createNotFoundException('Boutique non trouvée');
  2775.         }
  2776.         // Tracker la vue de la boutique
  2777.         $viewTrackingService->trackShopView($shop);
  2778.         // Récupérer les produits de cette boutique
  2779.         $products $productRepository->findBy([
  2780.             'shop' => $shop,
  2781.             'isActive' => true
  2782.         ], ['publishedAt' => 'DESC'], 12);
  2783.         // Récupérer les statistiques détaillées via le service
  2784.         $stats $viewTrackingService->getShopViewStats($shop);
  2785.         // Vérifier si l'utilisateur connecté suit cette boutique
  2786.         $isFollowing false;
  2787.         if ($this->getUser()) {
  2788.             $isFollowing $shopFollowService->isUserFollowingShop($this->getUser(), $shop);
  2789.         }
  2790.         // Récupérer les statistiques de follow
  2791.         $followStats $shopFollowService->getShopFollowStats($shop);
  2792.         
  2793.         // Permission d'édition pour le gestionnaire de la boutique
  2794.         $canEdit false;
  2795.         if ($this->getUser()) {
  2796.             $canEdit $shop->getManager()->contains($this->getUser());
  2797.         }
  2798.         return $this->render('home/shop.html.twig', [
  2799.             'shop' => $shop,
  2800.             'products' => $products,
  2801.             'stats' => $stats,
  2802.             'isFollowing' => $isFollowing,
  2803.             'followStats' => $followStats,
  2804.             'canEdit' => $canEdit
  2805.         ]);
  2806.     }
  2807.     #[Route('/account/upload-profile-picture'name'account_upload_profile_picture'methods: ['POST'])]
  2808.     public function uploadProfilePicture(Request $requestEntityManagerInterface $em): JsonResponse
  2809.     {
  2810.         $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
  2811.         
  2812.         $user $this->getUser();
  2813.         if (!$user instanceof User) {
  2814.             return $this->json(['success' => false'message' => 'Utilisateur non trouvé'], 401);
  2815.         }
  2816.         
  2817.         $file $request->files->get('profilePicture');
  2818.         if (!$file) {
  2819.             return $this->json(['success' => false'message' => 'Aucun fichier fourni'], 400);
  2820.         }
  2821.         
  2822.         // Valider le type de fichier
  2823.         $allowedMimeTypes = ['image/jpeg''image/png''image/gif''image/webp'];
  2824.         if (!in_array($file->getMimeType(), $allowedMimeTypes)) {
  2825.             return $this->json(['success' => false'message' => 'Format de fichier non supporté'], 400);
  2826.         }
  2827.         
  2828.         // Valider la taille (max 5MB)
  2829.         if ($file->getSize() > 1024 1024) {
  2830.             return $this->json(['success' => false'message' => 'Le fichier est trop volumineux (max 5MB)'], 400);
  2831.         }
  2832.         
  2833.         // Générer un nom de fichier unique
  2834.         $newFilename 'profile_' uniqid() . '.' $file->guessExtension();
  2835.         $uploadDir $this->getParameter('kernel.project_dir') . '/public/uploads/profiles/';
  2836.         
  2837.         // Créer le dossier s'il n'existe pas
  2838.         if (!is_dir($uploadDir)) {
  2839.             mkdir($uploadDir0755true);
  2840.         }
  2841.         
  2842.         // Supprimer l'ancienne photo si elle existe
  2843.         if ($user->getProfilePicture() && file_exists($this->getParameter('kernel.project_dir') . '/public/' $user->getProfilePicture())) {
  2844.             unlink($this->getParameter('kernel.project_dir') . '/public/' $user->getProfilePicture());
  2845.         }
  2846.         
  2847.         // Déplacer le fichier
  2848.         $file->move($uploadDir$newFilename);
  2849.         
  2850.         // Mettre à jour l'utilisateur
  2851.         $user->setProfilePicture('uploads/profiles/' $newFilename);
  2852.         $em->flush();
  2853.         
  2854.         return $this->json([
  2855.             'success' => true,
  2856.             'message' => 'Photo de profil mise à jour avec succès',
  2857.             'url' => '/uploads/profiles/' $newFilename
  2858.         ]);
  2859.     }
  2860.     #[Route('/api/track-product-view/{id}'name'api_track_product_view'methods: ['POST'])]
  2861.     public function trackProductView(int $idViewTrackingService $viewTrackingServiceEntityManagerInterface $em): Response
  2862.     {
  2863.         $product $em->getRepository(Product::class)->find($id);
  2864.         if (!$product) {
  2865.             return $this->json(['success' => false'message' => 'Produit introuvable'], 404);
  2866.         }
  2867.         // Tracker la vue du produit
  2868.         $viewTrackingService->trackProductView($product);
  2869.         return $this->json([
  2870.             'success' => true,
  2871.             'viewCount' => $product->getViewCount(),
  2872.             'message' => 'Vue enregistrée'
  2873.         ]);
  2874.     }
  2875.     #[Route('/api/track-shop-view/{id}'name'api_track_shop_view'methods: ['POST'])]
  2876.     public function trackShopView(int $idViewTrackingService $viewTrackingServiceEntityManagerInterface $em): Response
  2877.     {
  2878.         $shop $em->getRepository(Shop::class)->find($id);
  2879.         if (!$shop) {
  2880.             return $this->json(['success' => false'message' => 'Boutique introuvable'], 404);
  2881.         }
  2882.         // Tracker la vue de la boutique
  2883.         $viewTrackingService->trackShopView($shop);
  2884.         return $this->json([
  2885.             'success' => true,
  2886.             'viewCount' => $shop->getViewCount(),
  2887.             'message' => 'Vue enregistrée'
  2888.         ]);
  2889.     }
  2890.     #[Route('/api/shop/{id}/follow'name'api_shop_follow'methods: ['POST'])]
  2891.     public function followShop(Shop $shopShopFollowService $shopFollowService): JsonResponse
  2892.     {
  2893.         $user $this->getUser();
  2894.         if (!$user) {
  2895.             return $this->json(['success' => false'message' => 'Vous devez être connecté pour suivre une boutique'], 401);
  2896.         }
  2897.         $result $shopFollowService->followShop($user$shop);
  2898.         return $this->json($result$result['success'] ? 200 400);
  2899.     }
  2900.     #[Route('/api/shop/{id}/unfollow'name'api_shop_unfollow'methods: ['POST'])]
  2901.     public function unfollowShop(Shop $shopShopFollowService $shopFollowService): JsonResponse
  2902.     {
  2903.         $user $this->getUser();
  2904.         if (!$user) {
  2905.             return $this->json(['success' => false'message' => 'Vous devez être connecté pour ne plus suivre une boutique'], 401);
  2906.         }
  2907.         $result $shopFollowService->unfollowShop($user$shop);
  2908.         return $this->json($result$result['success'] ? 200 400);
  2909.     }
  2910.     #[Route('/api/shop/{id}/toggle-follow'name'api_shop_toggle_follow'methods: ['POST'])]
  2911.     public function toggleFollowShop(Shop $shopShopFollowService $shopFollowService): JsonResponse
  2912.     {
  2913.         $user $this->getUser();
  2914.         if (!$user) {
  2915.             return $this->json(['success' => false'message' => 'Vous devez être connecté pour suivre une boutique'], 401);
  2916.         }
  2917.         $result $shopFollowService->toggleFollow($user$shop);
  2918.         return $this->json($result$result['success'] ? 200 400);
  2919.     }
  2920.     #[Route('/api/shop/{id}/followers'name'api_shop_followers'methods: ['GET'])]
  2921.     public function getShopFollowers(Shop $shopShopFollowService $shopFollowService): JsonResponse
  2922.     {
  2923.         $followers $shopFollowService->getShopFollowers($shop);
  2924.         $followStats $shopFollowService->getShopFollowStats($shop);
  2925.         return $this->json([
  2926.             'success' => true,
  2927.             'followers' => $followers,
  2928.             'stats' => $followStats
  2929.         ]);
  2930.     }
  2931.     #[Route('/api/shop/{id}/products/sort'name'api_shop_products_sort'methods: ['POST'])]
  2932.     public function sortShopProducts(int $idRequest $requestEntityManagerInterface $entityManager): JsonResponse
  2933.     {
  2934.         try {
  2935.             $data json_decode($request->getContent(), true);
  2936.             $sortBy $data['sortBy'] ?? '';
  2937.             $shop $entityManager->getRepository(Shop::class)->find($id);
  2938.             if (!$shop) {
  2939.                 return $this->json(['success' => false'message' => 'Boutique non trouvée'], 404);
  2940.             }
  2941.             $queryBuilder $entityManager->createQueryBuilder()
  2942.                 ->select('p')
  2943.                 ->from(Product::class, 'p')
  2944.                 ->where('p.shop = :shop')
  2945.                 ->andWhere('p.isActive = :active')
  2946.                 ->setParameter('shop'$shop)
  2947.                 ->setParameter('active'true);
  2948.             // Appliquer le tri selon le critère sélectionné
  2949.             switch ($sortBy) {
  2950.                 case 'price_asc':
  2951.                     $queryBuilder->orderBy('p.price''ASC');
  2952.                     break;
  2953.                 case 'price_desc':
  2954.                     $queryBuilder->orderBy('p.price''DESC');
  2955.                     break;
  2956.                 case 'newest':
  2957.                     $queryBuilder->orderBy('p.publishedAt''DESC');
  2958.                     break;
  2959.                 case 'popular':
  2960.                     $queryBuilder->orderBy('p.viewCount''DESC');
  2961.                     break;
  2962.                 case 'name_asc':
  2963.                     $queryBuilder->orderBy('p.name''ASC');
  2964.                     break;
  2965.                 case 'name_desc':
  2966.                     $queryBuilder->orderBy('p.name''DESC');
  2967.                     break;
  2968.                 default:
  2969.                     $queryBuilder->orderBy('p.publishedAt''DESC');
  2970.                     break;
  2971.             }
  2972.             $products $queryBuilder->getQuery()->getResult();
  2973.             // Rendre le template des produits
  2974.             $html $this->renderView('home/_products_list.html.twig', [
  2975.                 'products' => $products
  2976.             ]);
  2977.             return $this->json([
  2978.                 'success' => true,
  2979.                 'html' => $html,
  2980.                 'count' => count($products)
  2981.             ]);
  2982.         } catch (\Exception $e) {
  2983.             return $this->json([
  2984.                 'success' => false,
  2985.                 'message' => 'Erreur lors du tri des produits: ' $e->getMessage()
  2986.             ], 500);
  2987.         }
  2988.     }
  2989.     #[Route('/api/wishlist/add/{id}'name'api_wishlist_add'methods: ['POST'])]
  2990.     #[IsGranted('ROLE_USER')]
  2991.     public function addToWishlist(int $id): JsonResponse
  2992.     {
  2993.         $product $this->entityManager->getRepository(Product::class)->find($id);
  2994.         
  2995.         if (!$product) {
  2996.             return $this->json([
  2997.                 'success' => false,
  2998.                 'message' => 'Produit non trouvé'
  2999.             ], 404);
  3000.         }
  3001.         $result $this->wishlistService->addToWishlist($this->getUser(), $product);
  3002.         
  3003.         return $this->json($result);
  3004.     }
  3005.     #[Route('/api/wishlist/remove/{id}'name'api_wishlist_remove'methods: ['POST'])]
  3006.     #[IsGranted('ROLE_USER')]
  3007.     public function removeFromWishlist(int $id): JsonResponse
  3008.     {
  3009.         $product $this->entityManager->getRepository(Product::class)->find($id);
  3010.         
  3011.         if (!$product) {
  3012.             return $this->json([
  3013.                 'success' => false,
  3014.                 'message' => 'Produit non trouvé'
  3015.             ], 404);
  3016.         }
  3017.         $result $this->wishlistService->removeFromWishlist($this->getUser(), $product);
  3018.         
  3019.         return $this->json($result);
  3020.     }
  3021.     #[Route('/api/wishlist/clear'name'api_wishlist_clear'methods: ['POST'])]
  3022.     #[IsGranted('ROLE_USER')]
  3023.     public function clearWishlist(): JsonResponse
  3024.     {
  3025.         $result $this->wishlistService->clearWishlist($this->getUser());
  3026.         
  3027.         return $this->json($result);
  3028.     }
  3029.     #[Route('/api/wishlist/count'name'api_wishlist_count'methods: ['GET'])]
  3030.     #[IsGranted('ROLE_USER')]
  3031.     public function getWishlistCount(): JsonResponse
  3032.     {
  3033.         $count $this->wishlistService->getWishlistCount($this->getUser());
  3034.         
  3035.         return $this->json([
  3036.             'success' => true,
  3037.             'count' => $count
  3038.         ]);
  3039.     }
  3040.     #[Route('/api/wishlist/status/{id}'name'api_wishlist_status'methods: ['GET'])]
  3041.     #[IsGranted('ROLE_USER')]
  3042.     public function getWishlistStatus(int $id): JsonResponse
  3043.     {
  3044.         $product $this->entityManager->getRepository(Product::class)->find($id);
  3045.         
  3046.         if (!$product) {
  3047.             return $this->json([
  3048.                 'success' => false,
  3049.                 'message' => 'Produit non trouvé'
  3050.             ], 404);
  3051.         }
  3052.         $isInWishlist $this->wishlistService->isInWishlist($this->getUser(), $product);
  3053.         
  3054.         return $this->json([
  3055.             'success' => true,
  3056.             'inWishlist' => $isInWishlist
  3057.         ]);
  3058.     }
  3059.     #[Route('/api/comparison/add/{id}'name'api_comparison_add'methods: ['POST'])]
  3060.     public function addToComparison(int $idRequest $request): JsonResponse
  3061.     {
  3062.         $product $this->entityManager->getRepository(Product::class)->find($id);
  3063.         
  3064.         if (!$product) {
  3065.             return $this->json([
  3066.                 'success' => false,
  3067.                 'message' => 'Produit non trouvé'
  3068.             ], 404);
  3069.         }
  3070.         $currentUser $this->getUser();
  3071.         $user $currentUser instanceof User $currentUser null;
  3072.         $result $this->comparisonService->addToComparison($user$product$request->getSession());
  3073.         
  3074.         return $this->json($result);
  3075.     }
  3076.     #[Route('/api/comparison/remove/{id}'name'api_comparison_remove'methods: ['POST'])]
  3077.     public function removeFromComparison(int $idRequest $request): JsonResponse
  3078.     {
  3079.         $product $this->entityManager->getRepository(Product::class)->find($id);
  3080.         
  3081.         if (!$product) {
  3082.             return $this->json([
  3083.                 'success' => false,
  3084.                 'message' => 'Produit non trouvé'
  3085.             ], 404);
  3086.         }
  3087.         $currentUser $this->getUser();
  3088.         $user $currentUser instanceof User $currentUser null;
  3089.         $result $this->comparisonService->removeFromComparison($user$product$request->getSession());
  3090.         
  3091.         return $this->json($result);
  3092.     }
  3093.     #[Route('/api/comparison/clear'name'api_comparison_clear'methods: ['POST'])]
  3094.     public function clearComparison(Request $request): JsonResponse
  3095.     {
  3096.         $currentUser $this->getUser();
  3097.         $user $currentUser instanceof User $currentUser null;
  3098.         $result $this->comparisonService->clearComparison($user$request->getSession());
  3099.         
  3100.         return $this->json($result);
  3101.     }
  3102.     #[Route('/api/comparison/count'name'api_comparison_count'methods: ['GET'])]
  3103.     public function getComparisonCount(Request $request): JsonResponse
  3104.     {
  3105.         $currentUser $this->getUser();
  3106.         $user $currentUser instanceof User $currentUser null;
  3107.         $count $this->comparisonService->getComparisonCount($user$request->getSession());
  3108.         
  3109.         return $this->json([
  3110.             'success' => true,
  3111.             'count' => $count
  3112.         ]);
  3113.     }
  3114.     #[Route('/api/comparison/status/{id}'name'api_comparison_status'methods: ['GET'])]
  3115.     public function getComparisonStatus(int $idRequest $request): JsonResponse
  3116.     {
  3117.         $product $this->entityManager->getRepository(Product::class)->find($id);
  3118.         
  3119.         if (!$product) {
  3120.             return $this->json([
  3121.                 'success' => false,
  3122.                 'message' => 'Produit non trouvé'
  3123.             ], 404);
  3124.         }
  3125.         $currentUser $this->getUser();
  3126.         $user $currentUser instanceof User $currentUser null;
  3127.         $isInComparison $this->comparisonService->isInComparison($user$product$request->getSession());
  3128.         
  3129.         return $this->json([
  3130.             'success' => true,
  3131.             'inComparison' => $isInComparison
  3132.         ]);
  3133.     }
  3134.     #[Route('/comparison'name'product_comparison'methods: ['GET'])]
  3135.     public function comparisonPage(Request $request): Response
  3136.     {
  3137.         $currentUser $this->getUser();
  3138.         $user $currentUser instanceof User $currentUser null;
  3139.         $shareToken $request->query->get('share');
  3140.         $comparisonData $this->comparisonService->getComparisonData($user$request->getSession(), is_string($shareToken) ? $shareToken null);
  3141.         $comparisonShareUrl null;
  3142.         if (!empty($comparisonData['shareToken'])) {
  3143.             $comparisonShareUrl $this->generateUrl('ui_product_comparison', [], UrlGeneratorInterface::ABSOLUTE_URL)
  3144.                 . '?share=' urlencode((string) $comparisonData['shareToken']);
  3145.         }
  3146.         
  3147.         return $this->render('product/comparison.html.twig', [
  3148.             'comparisonData' => $comparisonData,
  3149.             'comparisonShareUrl' => $comparisonShareUrl,
  3150.             'current_menu' => 'comparison'
  3151.         ]);
  3152.     }
  3153.     #[Route('/api/comparison/data'name'api_comparison_data'methods: ['GET'])]
  3154.     public function getComparisonData(Request $request): JsonResponse
  3155.     {
  3156.         $currentUser $this->getUser();
  3157.         $user $currentUser instanceof User $currentUser null;
  3158.         $comparisonData $this->comparisonService->getComparisonData($user$request->getSession());
  3159.         
  3160.         return $this->json([
  3161.             'success' => true,
  3162.             'data' => $comparisonData
  3163.         ]);
  3164.     }
  3165.     #[Route(path'/logout'name'app_logout')]
  3166.     public function logout(): void
  3167.     {
  3168.         throw new LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
  3169.     }
  3170.     #[Route('account/points'name'account_points'methods: ['GET'])]
  3171.     #[IsGranted('ROLE_USER')]
  3172.     public function indexPoint(Request $request): Response
  3173.     {
  3174.         $user $this->getUser();
  3175.         $page max(1, (int)$request->query->get('page'1));
  3176.         $limit 20;
  3177.         $offset = ($page 1) * $limit;
  3178.         $stats $this->pointService->getStats($user);
  3179.         $transactions $this->pointService->getTransactionHistory($user$limit$offset);
  3180.         $totalTransactions count($transactions);
  3181.         return $this->render('account/points/index.html.twig', [
  3182.             'stats' => $stats,
  3183.             'transactions' => $transactions,
  3184.             'currentPage' => $page,
  3185.             'totalPages' => ceil($totalTransactions $limit),
  3186.             'hasMore' => $totalTransactions >= $limit,
  3187.         ]);
  3188.     }
  3189.     #[Route('/account/points/convert'name'account_points_convert'methods: ['GET''POST'])]
  3190.     #[IsGranted('ROLE_USER')]
  3191.     public function convert(Request $request): Response
  3192.     {
  3193.         $user $this->getUser();
  3194.         $userPoints $this->pointService->getUserPoints($user);
  3195.         
  3196.         if ($request->isMethod('POST')) {
  3197.             $points = (int)$request->request->get('points');
  3198.             $recipientEmail $request->request->get('recipient_email');
  3199.             $recipientName $request->request->get('recipient_name');
  3200.             if ($points 100) {
  3201.                 $this->addFlash('error''Le minimum est de 100 points (1 HTG)');
  3202.                 return $this->redirectToRoute('ui_account_points_convert');
  3203.             }
  3204.             if ($userPoints->getBalance() < $points) {
  3205.                 $this->addFlash('error''Solde de points insuffisant');
  3206.                 return $this->redirectToRoute('ui_account_points_convert');
  3207.             }
  3208.             try {
  3209.                 $giftCard $this->pointService->convertPointsToGiftCard(
  3210.                     $user,
  3211.                     $points,
  3212.                     $recipientEmail ?: null,
  3213.                     $recipientName ?: null
  3214.                 );
  3215.                 $this->addFlash('success'"Carte cadeau de {$giftCard->getInitialAmount()} HTG créée avec succès ! Code: {$giftCard->getCode()}");
  3216.                 return $this->redirectToRoute('ui_account_gift_cards');
  3217.             } catch (\Exception $e) {
  3218.                 $this->addFlash('error'$e->getMessage());
  3219.             }
  3220.         }
  3221.         $stats $this->pointService->getStats($user);
  3222.         return $this->render('account/points/convert.html.twig', [
  3223.             'stats' => $stats,
  3224.         ]);
  3225.     }
  3226. }