src/Controller/SellerController.php line 30

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Shop;
  4. use App\Entity\Product;
  5. use App\Entity\Category;
  6. use App\Entity\ShopPlan;
  7. use Symfony\Bridge\Doctrine\Attribute\MapEntity;
  8. use App\Entity\User;
  9. use App\Form\SellerShopType;
  10. use App\Repository\ShopPlanRepository;
  11. use App\Service\NotificationService;
  12. use Doctrine\ORM\EntityManagerInterface;
  13. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  14. use Symfony\Component\HttpFoundation\JsonResponse;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\Mailer\MailerInterface;
  18. use Symfony\Bridge\Twig\Mime\TemplatedEmail;
  19. use Symfony\Component\Mime\Address as EmailAddress;
  20. use Symfony\Component\Routing\Annotation\Route;
  21. use Symfony\Component\String\Slugger\AsciiSlugger;
  22. use function Symfony\Component\String\s;
  23. #[Route('/seller_space'name'seller_')]
  24. class SellerController extends AbstractController
  25. {
  26.     #[Route('/'name'index')]
  27.     public function indexSeller(): Response
  28.     {
  29.         if (!$this->getUser()) {
  30.             return $this->redirectToRoute('ui_app_login');
  31.         }
  32.         if (!$this->isAuthorized()) {
  33.             return $this->render('misc/access_denied.html.twig');
  34.         }
  35.         return $this->render('seller/index.html.twig', [
  36.             'current_menu' => 'home'
  37.         ]);
  38.     }
  39.     #[Route('/help/how-to-sell'name'help_how_to_sell')]
  40.     public function howToSell(): Response
  41.     {
  42.         return $this->render('seller/help/how_to_sell.html.twig', [
  43.             'current_menu' => 'help'
  44.         ]);
  45.     }
  46.     #[Route('/help/pricing'name'help_pricing')]
  47.     public function pricing(EntityManagerInterface $em): Response
  48.     {
  49.         $shopPlans $em->getRepository(ShopPlan::class)->findAll();
  50.         return $this->render('seller/help/pricing.html.twig', [
  51.             'current_menu' => 'help',
  52.             'shopPlans' => $shopPlans
  53.         ]);
  54.     }
  55.     #[Route('/help/support'name'help_support')]
  56.     public function support(): Response
  57.     {
  58.         return $this->render('seller/help/support.html.twig', [
  59.             'current_menu' => 'help'
  60.         ]);
  61.     }
  62.     #[Route('/shop/new'name'shop_new')]
  63.     public function newShopSeller(Request $requestEntityManagerInterface $entityManagerNotificationService $notificationServiceMailerInterface $mailer): Response
  64.     {
  65.         if (!$this->getUser()) {
  66.             return $this->redirectToRoute('ui_app_login');
  67.         }
  68.         if (!$this->isAuthorized()) {
  69.             return $this->render('misc/access_denied.html.twig');
  70.         }
  71.         $shop = new Shop();
  72.         $form $this->createForm(SellerShopType::class, $shop);
  73.         $form->handleRequest($request);
  74.         if ($form->isSubmitted() && $form->isValid()) {
  75.             $authenticatedUser $this->getUser();
  76.             if (!$authenticatedUser instanceof User) {
  77.                 return $this->redirectToRoute('ui_app_login');
  78.             }
  79.             $userId $authenticatedUser->getId();
  80.             $user $entityManager->getRepository(User::class)->find($userId);
  81.             if (!$user) {
  82.                 throw $this->createNotFoundException('Utilisateur introuvable');
  83.             }
  84.             // Configuration de base
  85.             $shop->setIsActive(true);
  86.             $shop->setCreatedAt(new \DateTimeImmutable('now'));
  87.             $shop->setCurrentEmployees(1);
  88.             
  89.             // Génération automatique du slug si nécessaire
  90.             if ($shop->getName()) {
  91.                 $slugger = new AsciiSlugger();
  92.                 $shop->setSlug(strtolower($slugger->slug($shop->getName())->toString()));
  93.             }
  94.             
  95.             // Initialisation des compteurs
  96.             $shop->setCurrentOrders(0);
  97.             $shop->setCurrentProducts(0);
  98.             $shop->setCurrentCaLimit(0);
  99.             $shop->setCurrentRevenue('0.000');
  100.             $shop->setCurrentStorage(0);
  101.             
  102.             // Initialisation des statistiques
  103.             $shop->setViewCount(0);
  104.             $shop->setFollowerCount(0);
  105.             $shop->setAverageRating(0.0);
  106.             $shop->setReviewCount(0);
  107.             
  108.             // Configuration des statuts par défaut
  109.             if (!$shop->getStatus()) {
  110.                 $shop->setStatus('pending');
  111.             }
  112.             if (!$shop->getVerificationStatus()) {
  113.                 $shop->setVerificationStatus('unverified');
  114.             }
  115.             
  116.             // Configuration des options par défaut (uniquement pour les champs modifiables par le vendeur)
  117.             if ($shop->isIsActive() === null) {
  118.                 $shop->setIsActive(true);
  119.             }
  120.             if ($shop->isAllowReviews() === null) {
  121.                 $shop->setAllowReviews(true);
  122.             }
  123.             if ($shop->isAllowMessages() === null) {
  124.                 $shop->setAllowMessages(true);
  125.             }
  126.             if ($shop->isShowContactInfo() === null) {
  127.                 $shop->setShowContactInfo(true);
  128.             }
  129.             if ($shop->isShowSocialLinks() === null) {
  130.                 $shop->setShowSocialLinks(true);
  131.             }
  132.             
  133.             // Champs gérés uniquement par l'administrateur (ne pas modifier)
  134.             // isVerified, isPremium, status, verificationStatus sont gérés par l'admin
  135.             if (!$shop->getStatus()) {
  136.                 $shop->setStatus('pending');
  137.             }
  138.             if (!$shop->getVerificationStatus()) {
  139.                 $shop->setVerificationStatus('unverified');
  140.             }
  141.             if ($shop->isIsVerified() === null) {
  142.                 $shop->setIsVerified(false);
  143.             }
  144.             if ($shop->isIsPremium() === null) {
  145.                 $shop->setIsPremium(false);
  146.             }
  147.             
  148.             // Gestion des fichiers uploadés
  149.             $logoFile $form->get('logo')->getData();
  150.             if ($logoFile && $logoFile->isValid()) {
  151.                 $maxSize 15 1024 1024// 15MB
  152.                 if ($logoFile->getSize() > $maxSize) {
  153.                     $this->addFlash('error''Le logo est trop volumineux. Taille maximale : 15MB.');
  154.                 } else {
  155.                     $newFilename uniqid().'.'.$logoFile->guessExtension();
  156.                     $logoFile->move(
  157.                         $this->getParameter('kernel.project_dir').'/public/uploads/shops/',
  158.                         $newFilename
  159.                     );
  160.                     $shop->setLogo('uploads/shops/'.$newFilename);
  161.                 }
  162.             }
  163.             
  164.             // Gestion des images de bannière (jusqu'à 7 images, 15MB max chacune)
  165.             $allBannerImages $shop->getAllBannerImages();
  166.             
  167.             // Gestion des nouvelles images multiples de bannière
  168.             $bannerImagesFile $form->get('bannerImages')->getData();
  169.             if ($bannerImagesFile) {
  170.                 $maxSize 15 1024 1024// 15MB
  171.                 foreach ($bannerImagesFile as $file) {
  172.                     if ($file && $file->isValid()) {
  173.                         // Vérifier la taille du fichier
  174.                         if ($file->getSize() > $maxSize) {
  175.                             $this->addFlash('error''Le fichier "' $file->getClientOriginalName() . '" est trop volumineux. Taille maximale : 15MB.');
  176.                             continue;
  177.                         }
  178.                         
  179.                         $newFilename uniqid().'.'.$file->guessExtension();
  180.                         $file->move(
  181.                             $this->getParameter('kernel.project_dir').'/public/uploads/shops/',
  182.                             $newFilename
  183.                         );
  184.                         $allBannerImages[] = 'uploads/shops/'.$newFilename;
  185.                     }
  186.                 }
  187.             }
  188.             // Dédupliquer et limiter à 7 images max
  189.             $allBannerImages array_values(array_unique($allBannerImages));
  190.             if (count($allBannerImages) > 7) {
  191.                 $this->addFlash('error''Maximum 7 images de bannière autorisées. Les images supplémentaires ont été ignorées.');
  192.                 $allBannerImages array_slice($allBannerImages07);
  193.             }
  194.             $shop->setBannerImages($allBannerImages);
  195.             
  196.             // Contraintes basées sur le plan sélectionné
  197.             if ($shop->getPlan()) {
  198.                 $plan $shop->getPlan();
  199.                 $errors = [];
  200.                 
  201.                 // Vérifier les champs requis par le plan
  202.                 if ($plan->isRequireSiretNif() && !$shop->getSiretNif()) {
  203.                     $errors[] = 'Votre plan exige un SIRET/NIF. Veuillez le renseigner.';
  204.                 }
  205.                 
  206.                 if ($plan->isRequireIban() && !$shop->getIban()) {
  207.                     $errors[] = 'Votre plan exige un IBAN. Veuillez le renseigner.';
  208.                 }
  209.                 
  210.                 // Si des erreurs, retourner au formulaire
  211.                 if (!empty($errors)) {
  212.                     foreach ($errors as $error) {
  213.                         $this->addFlash('error'$error);
  214.                     }
  215.                     return $this->render('seller/new_shop_simple.html.twig', [
  216.                         'current_menu' => 'shop',
  217.                         'current' => 'shopNew',
  218.                         'shop' => $shop,
  219.                         'form' => $form->createView()
  220.                     ]);
  221.                 }
  222.             }
  223.             
  224.             $shop->addManager($user);
  225.             $entityManager->persist($shop);
  226.             $entityManager->flush();
  227.             // Créer une notification de félicitations
  228.             $notificationService->createShopCreatedNotification($user$shop);
  229.             // Envoyer un email de félicitations
  230.             try {
  231.                 $email = (new TemplatedEmail())
  232.                     ->from(new EmailAddress('noreply@maketou.com''MaketOu'))
  233.                     ->to(new EmailAddress($user->getEmail(), $user->getFirstname() . ' ' $user->getLastname()))
  234.                     ->subject('Félicitations ! Votre boutique "' $shop->getName() . '" a été créée')
  235.                     ->htmlTemplate('emails/shop_created.html.twig')
  236.                     ->context([
  237.                         'user' => $user,
  238.                         'shop' => $shop,
  239.                     ]);
  240.                 $mailer->send($email);
  241.             } catch (\Exception $e) {
  242.                 // Log l'erreur mais ne bloque pas la création de la boutique
  243.                 // On peut logger l'erreur ici si nécessaire
  244.             }
  245.             $this->addFlash('success''Boutique créée avec succès ! Vous avez reçu un email de confirmation.');
  246.             return $this->redirectToRoute('seller_index', [], Response::HTTP_SEE_OTHER);
  247.         }
  248.         // Récupérer tous les plans pour les contraintes JavaScript
  249.         $shopPlans $entityManager->getRepository(ShopPlan::class)->findAll();
  250.         $plansData = [];
  251.         foreach ($shopPlans as $plan) {
  252.             $plansData[$plan->getId()] = [
  253.                 'id' => $plan->getId(),
  254.                 'name' => $plan->getName(),
  255.                 'requireSiretNif' => $plan->isRequireSiretNif(),
  256.                 'requireIban' => $plan->isRequireIban(),
  257.                 'maxProducts' => $plan->getMaxProducts(),
  258.                 'maxEmployees' => $plan->getMaxEmployees(),
  259.             ];
  260.         }
  261.         
  262.         return $this->render('seller/new_shop_simple.html.twig', [
  263.             'current_menu' => 'shop',
  264.             'current' => 'shopNew',
  265.             'shop' => $shop,
  266.             'form' => $form->createView(),
  267.             'plansData' => $plansData
  268.         ]);
  269.     }
  270.     #[Route('/shop/{slug}/show'name'shop_show')]
  271.     public function showShopSeller(#[MapEntity(mapping: ['slug' => 'slug'])] Shop $shopRequest $requestEntityManagerInterface $em): Response
  272.     {
  273.         if (!$this->getUser()) {
  274.             return $this->redirectToRoute('ui_app_login');
  275.         }
  276.         if (!$this->isAuthorized()) {
  277.             return $this->render('misc/access_denied.html.twig');
  278.         }
  279.         // Vérifier que la boutique appartient à l'utilisateur connecté
  280.         $canEdit $shop->getManager()->contains($this->getUser());
  281.         // Calculer les statistiques dynamiques
  282.         $productsCount $shop->getActiveProductsCount();
  283.         $totalProducts $shop->getProducts()->count();
  284.         $ordersCount $shop->getCurrentOrders() ?? 0;
  285.         $revenue = (float)($shop->getCurrentRevenue() ?? 0);
  286.         
  287.         // Calculer le solde disponible (revenue - dépenses potentielles)
  288.         // Pour l'instant, on utilise le revenue comme solde disponible
  289.         $availableBalance $revenue;
  290.         
  291.         // Calculer la date de dernière mise à jour (date de création ou dernière commande)
  292.         $lastUpdate $shop->getCreatedAt();
  293.         if ($ordersCount 0) {
  294.             // Essayer de trouver la dernière commande via les produits
  295.             // Pour l'instant, on utilise la date de création
  296.         }
  297.         
  298.         // Calculer le temps depuis la dernière mise à jour
  299.         $updateText 'Aujourd\'hui';
  300.         if ($lastUpdate) {
  301.             $now = new \DateTimeImmutable();
  302.             $diff $now->diff($lastUpdate);
  303.             if ($diff->days 0) {
  304.                 if ($diff->days == 1) {
  305.                     $updateText 'Hier';
  306.                 } elseif ($diff->days 7) {
  307.                     $updateText 'Il y a ' $diff->days ' jours';
  308.                 } elseif ($diff->days 30) {
  309.                     $weeks floor($diff->days 7);
  310.                     $updateText 'Il y a ' $weeks ' semaine' . ($weeks 's' '');
  311.                 } else {
  312.                     $months floor($diff->days 30);
  313.                     $updateText 'Il y a ' $months ' mois';
  314.                 }
  315.             }
  316.         }
  317.         // Récupérer les commandes récentes de la boutique (si l'entité Order existe)
  318.         $recentOrders = [];
  319.         try {
  320.             $orderRepo $em->getRepository(\App\Entity\Order::class);
  321.             $recentOrders $orderRepo->createQueryBuilder('o')
  322.                 ->join('o.items''oi')
  323.                 ->join('oi.product''p')
  324.                 ->where('p.shop = :shop')
  325.                 ->setParameter('shop'$shop)
  326.                 ->orderBy('o.orderedAt''DESC')
  327.                 ->setMaxResults(10)
  328.                 ->getQuery()
  329.                 ->getResult();
  330.         } catch (\Exception $e) {
  331.             // Si l'entité Order n'existe pas encore, on continue sans
  332.         }
  333.         // Calculer les statistiques de vues par période (derniers 30 jours)
  334.         $viewsLast30Days 0;
  335.         try {
  336.             // Pour l'instant, on utilise viewCount comme approximation
  337.             $viewsLast30Days = (int)($shop->getViewCount() * 0.3); // Approximation
  338.         } catch (\Exception $e) {
  339.             // Ignorer si erreur
  340.         }
  341.         return $this->render('seller/show_shop.html.twig', [
  342.             'current_menu' => 'shop',
  343.             'slugShop' => $shop->getSlug(),
  344.             'shop' => $shop,
  345.             'canEdit' => $canEdit,
  346.             'recentOrders' => $recentOrders,
  347.             'viewsLast30Days' => $viewsLast30Days,
  348.             'stats' => [
  349.                 'productsCount' => $productsCount,
  350.                 'totalProducts' => $totalProducts,
  351.                 'ordersCount' => $ordersCount,
  352.                 'revenue' => $revenue,
  353.                 'availableBalance' => $availableBalance,
  354.                 'lastUpdate' => $updateText
  355.             ]
  356.         ]);
  357.     }
  358.     #[Route('/shop/{slug}/edit'name'shop_edit')]
  359.     public function editShopSeller(#[MapEntity(mapping: ['slug' => 'slug'])] Shop $shopRequest $requestEntityManagerInterface $entityManager): Response
  360.     {
  361.         if (!$this->getUser()) {
  362.             return $this->redirectToRoute('ui_app_login');
  363.         }
  364.         if (!$this->isAuthorized()) {
  365.             return $this->render('misc/access_denied.html.twig');
  366.         }
  367.         // Vérifier que la boutique appartient à l'utilisateur connecté
  368.         if (!$shop->getManager()->contains($this->getUser())) {
  369.             $this->addFlash('error''Vous n\'avez pas la permission de modifier cette boutique.');
  370.             return $this->redirectToRoute('seller_shop_show', ['slug' => $shop->getSlug()]);
  371.         }
  372.         $form $this->createForm(SellerShopType::class, $shop);
  373.         $form->handleRequest($request);
  374.         if ($form->isSubmitted() && $form->isValid()) {
  375.             // Génération automatique du slug si le nom a changé
  376.             if ($shop->getName()) {
  377.                 $slugger = new AsciiSlugger();
  378.                 $newSlug strtolower($slugger->slug($shop->getName())->toString());
  379.                 // Vérifier si le slug existe déjà pour une autre boutique
  380.                 $existingShop $entityManager->getRepository(Shop::class)->findOneBy(['slug' => $newSlug]);
  381.                 if (!$existingShop || $existingShop->getId() === $shop->getId()) {
  382.                     $shop->setSlug($newSlug);
  383.                 }
  384.             }
  385.             
  386.             // Gestion des fichiers uploadés
  387.             $logoFile $form->get('logo')->getData();
  388.             if ($logoFile && $logoFile->isValid()) {
  389.                 $maxSize 15 1024 1024// 15MB
  390.                 if ($logoFile->getSize() > $maxSize) {
  391.                     $this->addFlash('error''Le logo est trop volumineux. Taille maximale : 15MB.');
  392.                 } else {
  393.                     $newFilename uniqid().'.'.$logoFile->guessExtension();
  394.                     $logoFile->move(
  395.                         $this->getParameter('kernel.project_dir').'/public/uploads/shops/',
  396.                         $newFilename
  397.                     );
  398.                     $shop->setLogo('uploads/shops/'.$newFilename);
  399.                 }
  400.             }
  401.             
  402.             // Gestion des images de bannière (jusqu'à 7 images, 15MB max chacune)
  403.             $allBannerImages $shop->getAllBannerImages();
  404.             
  405.             // Gérer la suppression d'images existantes
  406.             $removeBannerImages $request->request->all('removeBannerImages');
  407.             if ($removeBannerImages) {
  408.                 foreach ($removeBannerImages as $imageToRemove) {
  409.                     $allBannerImages array_filter($allBannerImages, function($img) use ($imageToRemove) {
  410.                         return $img !== $imageToRemove;
  411.                     });
  412.                 }
  413.                 $allBannerImages array_values($allBannerImages);
  414.             }
  415.             
  416.             // Gestion des nouvelles images multiples de bannière
  417.             $bannerImagesFile $form->get('bannerImages')->getData();
  418.             if ($bannerImagesFile) {
  419.                 $maxSize 15 1024 1024// 15MB
  420.                 foreach ($bannerImagesFile as $file) {
  421.                     if ($file && $file->isValid()) {
  422.                         // Vérifier la taille du fichier
  423.                         if ($file->getSize() > $maxSize) {
  424.                             $this->addFlash('error''Le fichier "' $file->getClientOriginalName() . '" est trop volumineux. Taille maximale : 15MB.');
  425.                             continue;
  426.                         }
  427.                         
  428.                         $newFilename uniqid().'.'.$file->guessExtension();
  429.                         $file->move(
  430.                             $this->getParameter('kernel.project_dir').'/public/uploads/shops/',
  431.                             $newFilename
  432.                         );
  433.                         $allBannerImages[] = 'uploads/shops/'.$newFilename;
  434.                     }
  435.                 }
  436.             }
  437.             // Dédupliquer et limiter à 7 images max
  438.             $allBannerImages array_values(array_unique($allBannerImages));
  439.             if (count($allBannerImages) > 7) {
  440.                 $this->addFlash('error''Maximum 7 images de bannière autorisées. Les images supplémentaires ont été ignorées.');
  441.                 $allBannerImages array_slice($allBannerImages07);
  442.             }
  443.             $shop->setBannerImages($allBannerImages);
  444.             
  445.             // Contraintes basées sur le plan sélectionné
  446.             if ($shop->getPlan()) {
  447.                 $plan $shop->getPlan();
  448.                 $errors = [];
  449.                 
  450.                 // Vérifier les champs requis par le plan
  451.                 if ($plan->isRequireSiretNif() && !$shop->getSiretNif()) {
  452.                     $errors[] = 'Votre plan exige un SIRET/NIF. Veuillez le renseigner.';
  453.                 }
  454.                 
  455.                 if ($plan->isRequireIban() && !$shop->getIban()) {
  456.                     $errors[] = 'Votre plan exige un IBAN. Veuillez le renseigner.';
  457.                 }
  458.                 
  459.                 // Si des erreurs, retourner au formulaire
  460.                 if (!empty($errors)) {
  461.                     foreach ($errors as $error) {
  462.                         $this->addFlash('error'$error);
  463.                     }
  464.                     // Récupérer tous les plans pour les contraintes JavaScript
  465.                     $shopPlans $entityManager->getRepository(ShopPlan::class)->findAll();
  466.                     $plansData = [];
  467.                     foreach ($shopPlans as $planItem) {
  468.                         $plansData[$planItem->getId()] = [
  469.                             'id' => $planItem->getId(),
  470.                             'name' => $planItem->getName(),
  471.                             'requireSiretNif' => $planItem->isRequireSiretNif(),
  472.                             'requireIban' => $planItem->isRequireIban(),
  473.                             'maxProducts' => $planItem->getMaxProducts(),
  474.                             'maxEmployees' => $planItem->getMaxEmployees(),
  475.                         ];
  476.                     }
  477.                     return $this->render('seller/new_shop_simple.html.twig', [
  478.                         'current_menu' => 'shop',
  479.                         'current' => 'shopEdit',
  480.                         'shop' => $shop,
  481.                         'form' => $form->createView(),
  482.                         'plansData' => $plansData
  483.                     ]);
  484.                 }
  485.             }
  486.             
  487.             $entityManager->flush();
  488.             $this->addFlash('success''Boutique modifiée avec succès !');
  489.             return $this->redirectToRoute('seller_shop_show', ['slug' => $shop->getSlug()], Response::HTTP_SEE_OTHER);
  490.         }
  491.         // Récupérer tous les plans pour les contraintes JavaScript
  492.         $shopPlans $entityManager->getRepository(ShopPlan::class)->findAll();
  493.         $plansData = [];
  494.         foreach ($shopPlans as $plan) {
  495.             $plansData[$plan->getId()] = [
  496.                 'id' => $plan->getId(),
  497.                 'name' => $plan->getName(),
  498.                 'requireSiretNif' => $plan->isRequireSiretNif(),
  499.                 'requireIban' => $plan->isRequireIban(),
  500.                 'maxProducts' => $plan->getMaxProducts(),
  501.                 'maxEmployees' => $plan->getMaxEmployees(),
  502.             ];
  503.         }
  504.         
  505.         return $this->render('seller/new_shop_simple.html.twig', [
  506.             'current_menu' => 'shop',
  507.             'current' => 'shopEdit',
  508.             'shop' => $shop,
  509.             'form' => $form->createView(),
  510.             'plansData' => $plansData
  511.         ]);
  512.     }
  513.     #[Route('/shop/{slug}/show/products'name'shop_show_products')]
  514.     public function showProductsSeller(#[MapEntity(mapping: ['slug' => 'slug'])] Shop $shopRequest $requestEntityManagerInterface $em): Response
  515.     {
  516.         if (!$this->getUser()) {
  517.             return $this->redirectToRoute('ui_app_login');
  518.         }
  519.         if (!$this->isAuthorized()) {
  520.             return $this->render('misc/access_denied.html.twig');
  521.         }
  522.         $products $shop->getProducts();
  523.         $categories $em->getRepository(Category::class)->findAll();
  524.         $data = [];
  525.         foreach ($products as $product) {
  526.             $tierPrices $product->getTierPrices();
  527.             $hasTierPricing $product->hasTierPricing() || ($tierPrices && count($tierPrices) > 0);
  528.             
  529.             $data[] = [
  530.                 'id' => $product->getId(),
  531.                 'slug' => $product->getSlug(),
  532.                 'image' => $product->getImages()[0] ?? null,
  533.                 'name' => $product->getName(),
  534.                 'price' => $product->getPrice(),
  535.                 'stock' => $product->getStock(),
  536.                 'status' => $product->getStockStatus(),
  537.                 'hasTierPricing' => $hasTierPricing,
  538.             ];
  539.         }
  540.         return $this->render('seller/show_products.html.twig', [
  541.             'current_menu' => 'shop',
  542.             'current' => 'products',
  543.             'productsJson' => json_encode($data),
  544.             'categoriesJson' => json_encode(array_map(static function(Category $c){
  545.                 return ['id' => $c->getId(), 'name' => $c->getName()];
  546.             }, $categories)),
  547.             'slugShop' => $shop->getSlug(),
  548.             'shop' => $shop
  549.         ]);
  550.     }
  551.     #[Route('/shop/{slug}/products/new'name'shop_products_new'methods: ['POST'])]
  552.     public function createProductForShop(#[MapEntity(mapping: ['slug' => 'slug'])] Shop $shopRequest $requestEntityManagerInterface $em): Response
  553.     {
  554.         if (!$this->getUser() || !$this->isAuthorized()) {
  555.             return new JsonResponse(['ok' => false'message' => 'Non autorisé'], 401);
  556.         }
  557.         $product = new Product();
  558.         $name trim((string) $request->request->get('name'));
  559.         $description = (string) $request->request->get('description');
  560.         $price = (float) $request->request->get('price');
  561.         $compareAtPrice $request->request->get('compareAtPrice') !== null ? (float) $request->request->get('compareAtPrice') : null;
  562.         $stock = (int) $request->request->get('stock');
  563.         $minStockAlert $request->request->get('minStockAlert') !== null ? (int) $request->request->get('minStockAlert') : 0;
  564.         $manageStock = (bool) $request->request->get('manageStock');
  565.         $allowBackorders = (bool) $request->request->get('allowBackorders');
  566.         $isFeatured = (bool) $request->request->get('isFeatured');
  567.         $isDigital = (bool) $request->request->get('isDigital');
  568.         $stockStatus = (string) $request->request->get('stockStatus');
  569.         $categoryId = (int) $request->request->get('category');
  570.         if ($name === '' || $price <= || $stock || !$categoryId) {
  571.             return new JsonResponse(['ok' => false'message' => 'Champs requis manquants'], 400);
  572.         }
  573.         $category $em->getRepository(Category::class)->find($categoryId);
  574.         if (!$category) {
  575.             return new JsonResponse(['ok' => false'message' => 'Catégorie invalide'], 400);
  576.         }
  577.         $slugger = new AsciiSlugger();
  578.         $slug strtolower($slugger->slug($name)->toString());
  579.         $sku 'SKU-' strtoupper(substr(md5($name.microtime()), 08));
  580.         $product->setName($name);
  581.         $product->setDescription($description ?: null);
  582.         $product->setPrice($price);
  583.         $product->setCompareAtPrice($compareAtPrice);
  584.         $product->setStock($stock);
  585.         $product->setMinStockAlert($minStockAlert);
  586.         $product->setManageStock($manageStock);
  587.         $product->setAllowBackorders($allowBackorders);
  588.         $product->setIsFeatured($isFeatured);
  589.         $product->setIsDigital($isDigital);
  590.         $product->setStockStatus($stockStatus ?: 'In stock');
  591.         $product->setCategory($category);
  592.         $product->setShop($shop);
  593.         $product->setSlug($slug);
  594.         $product->setSku($sku);
  595.         $product->setIsActive(true);
  596.         $product->setPublishedAt(new \DateTimeImmutable('now'));
  597.         $product->setViewCount(0);
  598.         $product->setSalesCount(0);
  599.         $product->setAverageRating(0);
  600.         $product->setReviewCount(0);
  601.         // Prix de gros (facultatif, à l'initiative du vendeur)
  602.         $enableTierPricing = (bool) $request->request->get('enableTierPricing');
  603.         $tierMins $request->request->all('tierMin');
  604.         $tierPrices $request->request->all('tierPrice');
  605.         $normalizedTiers = [];
  606.         if ($enableTierPricing && is_array($tierMins) && is_array($tierPrices)) {
  607.             $count min(count($tierMins), count($tierPrices));
  608.             for ($i 0$i $count$i++) {
  609.                 $m = (int) ($tierMins[$i] ?? 0);
  610.                 $p = (float) ($tierPrices[$i] ?? 0);
  611.                 if ($m >= && $p 0) {
  612.                     $normalizedTiers[$m] = $p// utiliser la quantité min comme clé pour dédupliquer
  613.                 }
  614.             }
  615.             if (!empty($normalizedTiers)) {
  616.                 ksort($normalizedTiersSORT_NUMERIC);
  617.                 $tiersArray = [];
  618.                 foreach ($normalizedTiers as $minQty => $priceVal) {
  619.                     $tiersArray[] = [ 'min' => (int)$minQty'price' => (float)$priceVal ];
  620.                 }
  621.                 $product->setHasTierPricing(true);
  622.                 $product->setTierPrices($tiersArray);
  623.             } else {
  624.                 $product->setHasTierPricing(false);
  625.                 $product->setTierPrices(null);
  626.             }
  627.         } else {
  628.             $product->setHasTierPricing(false);
  629.             $product->setTierPrices(null);
  630.         }
  631.         // Uploads
  632.         try {
  633.             $projectDir $this->getParameter('kernel.project_dir');
  634.             $baseUploadDir $projectDir '/public/uploads/products/' $shop->getSlug();
  635.             $imagesDir $baseUploadDir '/images';
  636.             $videosDir $baseUploadDir '/videos';
  637.             $documentsDir $baseUploadDir '/documents';
  638.             foreach ([$imagesDir$videosDir$documentsDir] as $dir) {
  639.                 if (!is_dir($dir) && !@mkdir($dir0777true) && !is_dir($dir)) {
  640.                     throw new \RuntimeException('Impossible de créer le dossier: ' $dir);
  641.                 }
  642.             }
  643.             $sluggerFile = new AsciiSlugger();
  644.             $images = [];
  645.             $filesImages $request->files->get('images', []);
  646.             if ($filesImages instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
  647.                 $filesImages = [$filesImages];
  648.             }
  649.             foreach ($filesImages as $file) {
  650.                 if (!$file || !($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) || !$file->isValid()) continue;
  651.                 $ext $file->guessExtension() ?: $file->getClientOriginalExtension() ?: 'bin';
  652.                 $orig pathinfo((string)$file->getClientOriginalName(), PATHINFO_FILENAME);
  653.                 $safeOrig strtolower($sluggerFile->slug($orig)->toString());
  654.                 $unique bin2hex(random_bytes(8)) . '_' str_replace('.''', (string) microtime(true));
  655.                 $filename $safeOrig '_' $unique '.' $ext;
  656.                 $file->move($imagesDir$filename);
  657.                 $images[] = '/uploads/products/' $shop->getSlug() . '/images/' $filename;
  658.             }
  659.             $videos = [];
  660.             $filesVideos $request->files->get('videos', []);
  661.             if ($filesVideos instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
  662.                 $filesVideos = [$filesVideos];
  663.             }
  664.             foreach ($filesVideos as $file) {
  665.                 if (!$file || !($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) || !$file->isValid()) continue;
  666.                 $ext $file->guessExtension() ?: $file->getClientOriginalExtension() ?: 'bin';
  667.                 $orig pathinfo((string)$file->getClientOriginalName(), PATHINFO_FILENAME);
  668.                 $safeOrig strtolower($sluggerFile->slug($orig)->toString());
  669.                 $unique bin2hex(random_bytes(8)) . '_' str_replace('.''', (string) microtime(true));
  670.                 $filename $safeOrig '_' $unique '.' $ext;
  671.                 $file->move($videosDir$filename);
  672.                 $videos[] = '/uploads/products/' $shop->getSlug() . '/videos/' $filename;
  673.             }
  674.             $documents = [];
  675.             $filesDocuments $request->files->get('documents', []);
  676.             if ($filesDocuments instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
  677.                 $filesDocuments = [$filesDocuments];
  678.             }
  679.             foreach ($filesDocuments as $file) {
  680.                 if (!$file || !($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) || !$file->isValid()) continue;
  681.                 $ext $file->guessExtension() ?: $file->getClientOriginalExtension() ?: 'bin';
  682.                 $orig pathinfo((string)$file->getClientOriginalName(), PATHINFO_FILENAME);
  683.                 $safeOrig strtolower($sluggerFile->slug($orig)->toString());
  684.                 $unique bin2hex(random_bytes(8)) . '_' str_replace('.''', (string) microtime(true));
  685.                 $filename $safeOrig '_' $unique '.' $ext;
  686.                 $file->move($documentsDir$filename);
  687.                 $documents[] = '/uploads/products/' $shop->getSlug() . '/documents/' $filename;
  688.             }
  689.         } catch (\Throwable $e) {
  690.             return new JsonResponse(['ok' => false'message' => 'Upload échoué: '.$e->getMessage()], 500);
  691.         }
  692.         $product->setImages($images);
  693.         $product->setVideos($videos ?: null);
  694.         $product->setDocuments($documents ?: null);
  695.         try {
  696.             // Incrémente le compteur de produits de la catégorie sélectionnée
  697.             if ($category) {
  698.                 $category->incrementProductCount();
  699.                 $em->persist($category);
  700.             }
  701.             $em->persist($product);
  702.             $em->flush();
  703.         } catch (\Throwable $e) {
  704.             return new JsonResponse(['ok' => false'message' => 'Erreur enregistrement: '.$e->getMessage()], 500);
  705.         }
  706.         return new JsonResponse([
  707.             'ok' => true,
  708.             'product' => [
  709.                 'id' => $product->getId(),
  710.                 'image' => $images[0] ?? null,
  711.                 'name' => $product->getName(),
  712.                 'price' => $product->getPrice(),
  713.                 'stock' => $product->getStock(),
  714.                 'status' => $product->getStockStatus(),
  715.                 'hasTierPricing' => $product->hasTierPricing(),
  716.             ]
  717.         ]);
  718.     }
  719.     #[Route('/shop/{shopSlug}/products/{id}/show'name'product_show')]
  720.     // Note: Le préfixe 'seller_' fait que la route finale est 'seller_product_show'
  721.     public function showProductSeller(#[MapEntity(mapping: ['shopSlug' => 'slug'])] Shop $shopint $idEntityManagerInterface $em): Response
  722.     {
  723.         if (!$this->getUser() || !$this->isAuthorized()) {
  724.             return $this->redirectToRoute('ui_app_login');
  725.         }
  726.         // Vérifier que la boutique appartient au vendeur
  727.         if (!$shop->getManager()->contains($this->getUser())) {
  728.             $this->addFlash('error''Vous n\'avez pas accès à cette boutique.');
  729.             return $this->redirectToRoute('seller_index');
  730.         }
  731.         $product $em->getRepository(Product::class)->find($id);
  732.         
  733.         if (!$product || $product->getShop()->getId() !== $shop->getId()) {
  734.             throw $this->createNotFoundException('Produit non trouvé');
  735.         }
  736.         // Rendre le template seller pour voir le produit
  737.         return $this->render('seller/product/show.html.twig', [
  738.             'product' => $product,
  739.             'shop' => $shop,
  740.         ]);
  741.     }
  742.     #[Route('/shop/{shopSlug}/products/{id}/edit'name'product_edit'methods: ['GET''POST'])]
  743.     // Note: Le préfixe 'seller_' fait que la route finale est 'seller_product_edit'
  744.     public function editProductSeller(#[MapEntity(mapping: ['shopSlug' => 'slug'])] Shop $shopint $idRequest $requestEntityManagerInterface $em): Response
  745.     {
  746.         if (!$this->getUser() || !$this->isAuthorized()) {
  747.             return $this->redirectToRoute('ui_app_login');
  748.         }
  749.         // Vérifier que la boutique appartient au vendeur
  750.         if (!$shop->getManager()->contains($this->getUser())) {
  751.             $this->addFlash('error''Vous n\'avez pas accès à cette boutique.');
  752.             return $this->redirectToRoute('seller_index');
  753.         }
  754.         $product $em->getRepository(Product::class)->find($id);
  755.         
  756.         if (!$product || $product->getShop()->getId() !== $shop->getId()) {
  757.             throw $this->createNotFoundException('Produit non trouvé');
  758.         }
  759.         // Traitement de la soumission du formulaire (POST)
  760.         if ($request->isMethod('POST')) {
  761.             $name trim((string) $request->request->get('name'));
  762.             $description = (string) $request->request->get('description');
  763.             $price = (float) $request->request->get('price');
  764.             $compareAtPrice $request->request->get('compareAtPrice') !== null && $request->request->get('compareAtPrice') !== '' ? (float) $request->request->get('compareAtPrice') : null;
  765.             $stock = (int) $request->request->get('stock');
  766.             $minStockAlert $request->request->get('minStockAlert') !== null ? (int) $request->request->get('minStockAlert') : 0;
  767.             $manageStock = (bool) $request->request->get('manageStock');
  768.             $allowBackorders = (bool) $request->request->get('allowBackorders');
  769.             $isFeatured = (bool) $request->request->get('isFeatured');
  770.             $isDigital = (bool) $request->request->get('isDigital');
  771.             $stockStatus = (string) $request->request->get('stockStatus');
  772.             $categoryId = (int) $request->request->get('category');
  773.             $isActive = (bool) $request->request->get('isActive');
  774.             if ($name === '' || $price <= || $stock || !$categoryId) {
  775.                 $this->addFlash('error''Champs requis manquants ou invalides.');
  776.                 return $this->redirectToRoute('seller_product_edit', ['shopSlug' => $shop->getSlug(), 'id' => $product->getId()]);
  777.             }
  778.             $category $em->getRepository(Category::class)->find($categoryId);
  779.             if (!$category) {
  780.                 $this->addFlash('error''Catégorie invalide.');
  781.                 return $this->redirectToRoute('seller_product_edit', ['shopSlug' => $shop->getSlug(), 'id' => $product->getId()]);
  782.             }
  783.             // Mettre à jour le slug si le nom a changé
  784.             if ($product->getName() !== $name) {
  785.                 $slugger = new AsciiSlugger();
  786.                 $slug strtolower($slugger->slug($name)->toString());
  787.                 $product->setSlug($slug);
  788.             }
  789.             $product->setName($name);
  790.             $product->setDescription($description ?: null);
  791.             $product->setPrice($price);
  792.             $product->setCompareAtPrice($compareAtPrice);
  793.             $product->setStock($stock);
  794.             $product->setMinStockAlert($minStockAlert);
  795.             $product->setManageStock($manageStock);
  796.             $product->setAllowBackorders($allowBackorders);
  797.             $product->setIsFeatured($isFeatured);
  798.             $product->setIsDigital($isDigital);
  799.             $product->setStockStatus($stockStatus ?: 'In stock');
  800.             $product->setCategory($category);
  801.             $product->setIsActive($isActive);
  802.             // Prix de gros (Tier Pricing)
  803.             $enableTierPricing = (bool) $request->request->get('enableTierPricing');
  804.             $tierMins $request->request->all('tierMin');
  805.             $tierPrices $request->request->all('tierPrice');
  806.             $normalizedTiers = [];
  807.             if ($enableTierPricing && is_array($tierMins) && is_array($tierPrices)) {
  808.                 $count min(count($tierMins), count($tierPrices));
  809.                 for ($i 0$i $count$i++) {
  810.                     $m = (int) ($tierMins[$i] ?? 0);
  811.                     $p = (float) ($tierPrices[$i] ?? 0);
  812.                     if ($m >= && $p 0) {
  813.                         $normalizedTiers[$m] = $p;
  814.                     }
  815.                 }
  816.                 if (!empty($normalizedTiers)) {
  817.                     ksort($normalizedTiersSORT_NUMERIC);
  818.                     $tiersArray = [];
  819.                     foreach ($normalizedTiers as $minQty => $priceVal) {
  820.                         $tiersArray[] = [ 'min' => (int)$minQty'price' => (float)$priceVal ];
  821.                     }
  822.                     $product->setHasTierPricing(true);
  823.                     $product->setTierPrices($tiersArray);
  824.                 } else {
  825.                     $product->setHasTierPricing(false);
  826.                     $product->setTierPrices(null);
  827.                 }
  828.             } else {
  829.                 $product->setHasTierPricing(false);
  830.                 $product->setTierPrices(null);
  831.             }
  832.             // Gestion des fichiers (images, vidéos, documents)
  833.             $images $product->getImages();
  834.             $videos $product->getVideos();
  835.             $documents $product->getDocuments();
  836.             // Upload des nouvelles images
  837.             $uploadedImages $request->files->all('images');
  838.             if (!empty($uploadedImages)) {
  839.                 $productDir $this->getParameter('kernel.project_dir') . '/public/uploads/products/' $product->getId();
  840.                 if (!is_dir($productDir)) {
  841.                     mkdir($productDir0755true);
  842.                 }
  843.                 foreach ($uploadedImages as $file) {
  844.                     if ($file && $file->isValid()) {
  845.                         $filename uniqid() . '.' $file->guessExtension();
  846.                         $file->move($productDir$filename);
  847.                         $images[] = '/uploads/products/' $product->getId() . '/' $filename;
  848.                     }
  849.                 }
  850.             }
  851.             // Upload des nouvelles vidéos
  852.             $uploadedVideos $request->files->all('videos');
  853.             if (!empty($uploadedVideos)) {
  854.                 if ($videos === null$videos = [];
  855.                 $productDir $this->getParameter('kernel.project_dir') . '/public/uploads/products/' $product->getId() . '/videos';
  856.                 if (!is_dir($productDir)) {
  857.                     mkdir($productDir0755true);
  858.                 }
  859.                 foreach ($uploadedVideos as $file) {
  860.                     if ($file && $file->isValid()) {
  861.                         $filename uniqid() . '.' $file->guessExtension();
  862.                         $file->move($productDir$filename);
  863.                         $videos[] = '/uploads/products/' $product->getId() . '/videos/' $filename;
  864.                     }
  865.                 }
  866.             }
  867.             // Upload des nouveaux documents
  868.             $uploadedDocuments $request->files->all('documents');
  869.             if (!empty($uploadedDocuments)) {
  870.                 if ($documents === null$documents = [];
  871.                 $productDir $this->getParameter('kernel.project_dir') . '/public/uploads/products/' $product->getId() . '/documents';
  872.                 if (!is_dir($productDir)) {
  873.                     mkdir($productDir0755true);
  874.                 }
  875.                 foreach ($uploadedDocuments as $file) {
  876.                     if ($file && $file->isValid()) {
  877.                         $filename uniqid() . '.' $file->guessExtension();
  878.                         $file->move($productDir$filename);
  879.                         $documents[] = '/uploads/products/' $product->getId() . '/documents/' $filename;
  880.                     }
  881.                 }
  882.             }
  883.             $product->setImages($images);
  884.             $product->setVideos($videos ?: null);
  885.             $product->setDocuments($documents ?: null);
  886.             try {
  887.                 $em->flush();
  888.                 $this->addFlash('success''Produit modifié avec succès.');
  889.                 return $this->redirectToRoute('seller_product_show', ['shopSlug' => $shop->getSlug(), 'id' => $product->getId()]);
  890.             } catch (\Throwable $e) {
  891.                 $this->addFlash('error''Erreur lors de la modification: ' $e->getMessage());
  892.             }
  893.         }
  894.         // Récupérer les catégories pour le formulaire
  895.         $categories $em->getRepository(Category::class)->findAll();
  896.         // Rendre le template seller pour modifier le produit
  897.         return $this->render('seller/product/edit.html.twig', [
  898.             'product' => $product,
  899.             'shop' => $shop,
  900.             'categories' => $categories,
  901.         ]);
  902.     }
  903.     #[Route('/shop/{shopSlug}/products/{id}/image/delete'name'product_image_delete'methods: ['POST'])]
  904.     public function deleteProductImage(#[MapEntity(mapping: ['shopSlug' => 'slug'])] Shop $shopint $idRequest $requestEntityManagerInterface $em): JsonResponse
  905.     {
  906.         if (!$this->getUser() || !$this->isAuthorized()) {
  907.             return new JsonResponse(['success' => false'message' => 'Non autorisé'], 401);
  908.         }
  909.         // Vérifier que la boutique appartient au vendeur
  910.         if (!$shop->getManager()->contains($this->getUser())) {
  911.             return new JsonResponse(['success' => false'message' => 'Vous n\'avez pas accès à cette boutique.'], 403);
  912.         }
  913.         $product $em->getRepository(Product::class)->find($id);
  914.         
  915.         if (!$product || $product->getShop()->getId() !== $shop->getId()) {
  916.             return new JsonResponse(['success' => false'message' => 'Produit non trouvé'], 404);
  917.         }
  918.         $imagePath $request->request->get('image');
  919.         if (!$imagePath) {
  920.             return new JsonResponse(['success' => false'message' => 'Chemin d\'image manquant'], 400);
  921.         }
  922.         $images $product->getImages();
  923.         $imageIndex array_search($imagePath$images);
  924.         
  925.         if ($imageIndex === false) {
  926.             return new JsonResponse(['success' => false'message' => 'Image non trouvée'], 404);
  927.         }
  928.         // Supprimer l'image du tableau
  929.         unset($images[$imageIndex]);
  930.         $images array_values($images); // Réindexer le tableau
  931.         $product->setImages($images);
  932.         // Supprimer le fichier physique
  933.         $filePath $this->getParameter('kernel.project_dir') . '/public' $imagePath;
  934.         if (file_exists($filePath)) {
  935.             @unlink($filePath);
  936.         }
  937.         try {
  938.             $em->flush();
  939.             return new JsonResponse([
  940.                 'success' => true,
  941.                 'message' => 'Image supprimée avec succès',
  942.                 'remainingImages' => count($images)
  943.             ]);
  944.         } catch (\Throwable $e) {
  945.             return new JsonResponse(['success' => false'message' => 'Erreur lors de la suppression: ' $e->getMessage()], 500);
  946.         }
  947.     }
  948.     #[Route('/check-slug'name'shop_check_slug'methods: ['GET'])]
  949.     public function checkSlug(Request $requestEntityManagerInterface $em): JsonResponse
  950.     {
  951.         $slug $request->query->get('slug');
  952.         if (!$slug) {
  953.             return new JsonResponse(['valid' => false'message' => 'Slug manquant'], 400);
  954.         }
  955.         $exists $em->getRepository(Shop::class)->findOneBy(['slug' => $slug]);
  956.         return new JsonResponse([
  957.             'valid' => $exists false true,
  958.             'message' => $exists 'Ce slug est déjà utilisé' 'Slug disponible'
  959.         ]);
  960.     }
  961.     #[Route('/pricing'name'pricing')]
  962.     public function pricingSeller(ShopPlanRepository $shopPlanRepository): Response
  963.     {
  964.         if (!$this->getUser()) {
  965.             return $this->redirectToRoute('ui_app_login');
  966.         }
  967.         if (!$this->isAuthorized()) {
  968.             return $this->render('misc/access_denied.html.twig');
  969.         }
  970.         $allPlans $shopPlanRepository->findAll();
  971.         return $this->render('seller/pricing.html.twig', [
  972.             'current_menu' => 'pricing',
  973.             'allPlans' => $allPlans
  974.         ]);
  975.     }
  976.     #[Route('/faq'name'faq')]
  977.     public function faqSeller(): Response
  978.     {
  979.         if (!$this->getUser()) {
  980.             return $this->redirectToRoute('ui_app_login');
  981.         }
  982.         if (!$this->isAuthorized()) {
  983.             return $this->render('misc/access_denied.html.twig');
  984.         }
  985.         return $this->render('seller/faq.html.twig', [
  986.             'current_menu' => 'faq'
  987.         ]);
  988.     }
  989.     #[Route('/terms'name'terms')]
  990.     public function terms(): Response
  991.     {
  992.         if (!$this->getUser()) {
  993.             return $this->redirectToRoute('ui_app_login');
  994.         }
  995.         if (!$this->isAuthorized()) {
  996.             return $this->render('misc/access_denied.html.twig');
  997.         }
  998.         return $this->render('seller/terms.html.twig', [
  999.             'current_menu' => 'shop'
  1000.         ]);
  1001.     }
  1002.     #[Route('/privacy'name'privacy')]
  1003.     public function privacy(): Response
  1004.     {
  1005.         if (!$this->getUser()) {
  1006.             return $this->redirectToRoute('ui_app_login');
  1007.         }
  1008.         if (!$this->isAuthorized()) {
  1009.             return $this->render('misc/access_denied.html.twig');
  1010.         }
  1011.         return $this->render('seller/privacy.html.twig', [
  1012.             'current_menu' => 'shop'
  1013.         ]);
  1014.     }
  1015.     private function isAuthorized(): bool
  1016.     {
  1017.         if (!$this->isGranted('IS_AUTHENTICATED_FULLY')) {
  1018.             return false;
  1019.         }
  1020.         $user $this->getUser();
  1021.         return $user && in_array('ROLE_SELLER'$user->getRoles());
  1022.     }
  1023. }