src/Controller/UIController.php line 3765

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