Current File : //var/www/vinorea/src/PrestaShopBundle/Controller/Admin/Sell/Catalog/Product/ProductController.php
<?php
/**
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.md.
 * It is also available through the world-wide-web at this URL:
 * https://opensource.org/licenses/OSL-3.0
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the future. If you wish to customize PrestaShop for your
 * needs please refer to https://devdocs.prestashop.com/ for more information.
 *
 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
 * @copyright Since 2007 PrestaShop SA and Contributors
 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
 */

declare(strict_types=1);

namespace PrestaShopBundle\Controller\Admin\Sell\Catalog\Product;

use Exception;
use PrestaShop\PrestaShop\Adapter\Product\Repository\ProductRepository;
use PrestaShop\PrestaShop\Adapter\Shop\Context;
use PrestaShop\PrestaShop\Adapter\Shop\Url\ProductPreviewProvider;
use PrestaShop\PrestaShop\Core\Domain\Product\Command\BulkDeleteProductCommand;
use PrestaShop\PrestaShop\Core\Domain\Product\Command\BulkDuplicateProductCommand;
use PrestaShop\PrestaShop\Core\Domain\Product\Command\BulkUpdateProductStatusCommand;
use PrestaShop\PrestaShop\Core\Domain\Product\Command\DeleteProductCommand;
use PrestaShop\PrestaShop\Core\Domain\Product\Command\DuplicateProductCommand;
use PrestaShop\PrestaShop\Core\Domain\Product\Command\UpdateProductCommand;
use PrestaShop\PrestaShop\Core\Domain\Product\Command\UpdateProductsPositionsCommand;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\BulkProductException;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\CannotBulkDeleteProductException;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\CannotDeleteProductException;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\InvalidProductTypeException;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductConstraintException;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductException;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException;
use PrestaShop\PrestaShop\Core\Domain\Product\FeatureValue\Exception\DuplicateFeatureValueAssociationException;
use PrestaShop\PrestaShop\Core\Domain\Product\FeatureValue\Exception\InvalidAssociatedFeatureException;
use PrestaShop\PrestaShop\Core\Domain\Product\Query\GetProductForEditing;
use PrestaShop\PrestaShop\Core\Domain\Product\Query\SearchProductsForAssociation;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductForAssociation;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductForEditing;
use PrestaShop\PrestaShop\Core\Domain\Product\SpecificPrice\Exception\SpecificPriceConstraintException;
use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId;
use PrestaShop\PrestaShop\Core\Domain\Shop\Exception\ShopAssociationNotFound;
use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint;
use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopId;
use PrestaShop\PrestaShop\Core\FeatureFlag\FeatureFlagSettings;
use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\Builder\FormBuilderInterface;
use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\Handler\FormHandlerInterface;
use PrestaShop\PrestaShop\Core\Grid\Definition\Factory\GridDefinitionFactoryInterface;
use PrestaShop\PrestaShop\Core\Grid\Definition\Factory\ProductGridDefinitionFactory;
use PrestaShop\PrestaShop\Core\Search\Filters\ProductFilters;
use PrestaShopBundle\Component\CsvResponse;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use PrestaShopBundle\Entity\AdminFilter;
use PrestaShopBundle\Entity\ProductDownload;
use PrestaShopBundle\Entity\Repository\FeatureFlagRepository;
use PrestaShopBundle\Form\Admin\Sell\Product\Category\CategoryFilterType;
use PrestaShopBundle\Form\Admin\Type\ShopSelectorType;
use PrestaShopBundle\Security\Annotation\AdminSecurity;
use PrestaShopBundle\Security\Annotation\DemoRestricted;
use PrestaShopBundle\Security\Voter\PageVoter;
use PrestaShopBundle\Service\Grid\ResponseBuilder;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;

/**
 * Admin controller for the Product pages using the Symfony architecture:
 * - product list (display, search)
 * - product form (creation, edition)
 * - ...
 *
 * Some component displayed in this form are based on ajax request which might implemented
 * in another Controller.
 *
 * This controller is a re-migration of the initial ProductController which was the first
 * one to be migrated but doesn't meet the standards of the recently migrated controller.
 * The retro-compatibility is dropped for the legacy Admin pages, the former hook are no longer
 * managed for backward compatibility, new hooks need to be used in the modules, migration process
 * is detailed in the devdoc. (@todo add devdoc link when ready?)
 */
class ProductController extends FrameworkBundleAdminController
{
    /**
     * Used to validate connected user authorizations.
     */
    private const PRODUCT_CONTROLLER_PERMISSION = 'ADMINPRODUCTS_';

    /**
     * @var ProductRepository
     */
    private $productRepository;

    /**
     * @param ProductRepository $productRepository
     */
    public function __construct(ProductRepository $productRepository)
    {
        $this->productRepository = $productRepository;
    }

    /**
     * Shows products listing.
     *
     * @AdminSecurity("is_granted('create', request.get('_legacy_controller')) || is_granted('update', request.get('_legacy_controller')) || is_granted('read', request.get('_legacy_controller'))")
     *
     * @param Request $request
     * @param ProductFilters $filters
     *
     * @return Response
     */
    public function indexAction(Request $request, ProductFilters $filters): Response
    {
        if ($this->shouldRedirectToV1()) {
            return $this->redirectToRoute('admin_product_catalog');
        }

        $productGridFactory = $this->get('prestashop.core.grid.factory.product');
        $productGrid = $productGridFactory->getGrid($filters);

        $filteredCategoryId = null;
        if (isset($filters->getFilters()['id_category'])) {
            $filteredCategoryId = (int) $filters->getFilters()['id_category'];
        }
        $categoriesForm = $this->createForm(CategoryFilterType::class, $filteredCategoryId, [
            'action' => $this->generateUrl('admin_products_grid_category_filter'),
        ]);

        return $this->render('@PrestaShop/Admin/Sell/Catalog/Product/index.html.twig', [
            'categoryFilterForm' => $categoriesForm->createView(),
            'productGrid' => $this->presentGrid($productGrid),
            'enableSidebar' => true,
            'layoutHeaderToolbarBtn' => $this->getProductToolbarButtons($request->get('_legacy_controller')),
            'help_link' => $this->generateSidebarLink('AdminProducts'),
        ]);
    }

    /**
     * Process Grid search, but we need to add the category filter which is handled independently.
     *
     * @param Request $request
     *
     * @AdminSecurity("is_granted('create', request.get('_legacy_controller')) || is_granted('update', request.get('_legacy_controller')) || is_granted('read', request.get('_legacy_controller'))")
     *
     * @return RedirectResponse
     */
    public function searchGridAction(Request $request)
    {
        /** @var GridDefinitionFactoryInterface $definitionFactory */
        $definitionFactory = $this->get('prestashop.core.grid.definition.factory.product');

        $filterId = ProductGridDefinitionFactory::GRID_ID;

        $adminFilter = $this->getGridAdminFilter();
        if (isset($adminFilter)) {
            $currentFilters = json_decode($adminFilter->getFilter(), true);
            if (!empty($currentFilters['filters']['id_category'])) {
                $request->query->add([
                    'product[filters][id_category]' => $currentFilters['filters']['id_category'],
                ]);
            }
        }

        /** @var ResponseBuilder $responseBuilder */
        $responseBuilder = $this->get('prestashop.bundle.grid.response_builder');

        return $responseBuilder->buildSearchResponse(
            $definitionFactory,
            $request,
            $filterId,
            'admin_products_index',
            ['product[filters][id_category]']
        );
    }

    /**
     * Reset filters for the grid only (category is kept, it can be cleared via another dedicated action)
     *
     * @AdminSecurity("is_granted('create', request.get('_legacy_controller')) || is_granted('update', request.get('_legacy_controller')) || is_granted('read', request.get('_legacy_controller'))")
     *
     * @return JsonResponse
     */
    public function resetGridSearchAction(): JsonResponse
    {
        $adminFilter = $this->getGridAdminFilter();
        if (isset($adminFilter)) {
            $adminFiltersRepository = $this->get('prestashop.core.admin.admin_filter.repository');
            $currentFilters = json_decode($adminFilter->getFilter(), true);

            // This reset action only reset the filters from the Grid, we keep the filter by category if it was present (we still reset to page 1 though)
            if (!empty($currentFilters['filters']['id_category'])) {
                $adminFilter->setFilter(json_encode([
                    'filters' => [
                        'id_category' => $currentFilters['filters']['id_category'],
                    ],
                    'offset' => 0,
                ]));
                $adminFiltersRepository->updateFilter($adminFilter);
            } else {
                $adminFiltersRepository->unsetFilters($adminFilter);
            }
        }

        return new JsonResponse();
    }

    /**
     * Apply the category filter and redirect to list on first page.
     *
     * @AdminSecurity("is_granted('create', request.get('_legacy_controller')) || is_granted('update', request.get('_legacy_controller')) || is_granted('read', request.get('_legacy_controller'))")
     *
     * @return RedirectResponse
     */
    public function gridCategoryFilterAction(Request $request): RedirectResponse
    {
        $filteredCategoryId = $request->request->get('category_filter');
        $adminFilter = $this->getGridAdminFilter();
        if (isset($adminFilter)) {
            $adminFiltersRepository = $this->get('prestashop.core.admin.admin_filter.repository');
            $currentFilters = json_decode($adminFilter->getFilter(), true);
            if (empty($filteredCategoryId)) {
                unset($currentFilters['filters']['id_category']);
            } else {
                $currentFilters['filters']['id_category'] = $filteredCategoryId;
            }
            $currentFilters['offset'] = 0;
            $adminFilter->setFilter(json_encode($currentFilters));
            $adminFiltersRepository->updateFilter($adminFilter);
        }

        return $this->redirectToRoute('admin_products_index');
    }

    /**
     * Shows products shop details.
     *
     * @AdminSecurity("is_granted('create', request.get('_legacy_controller')) || is_granted('update', request.get('_legacy_controller')) || is_granted('read', request.get('_legacy_controller'))")
     *
     * @param ProductFilters $filters
     * @param int $productId
     * @param int|null $shopGroupId
     *
     * @return Response
     */
    public function productShopPreviewsAction(ProductFilters $filters, int $productId, ?int $shopGroupId): Response
    {
        $shopConstraint = !empty($shopGroupId) ? ShopConstraint::shopGroup($shopGroupId) : ShopConstraint::allShops();
        $gridFactory = $this->get('prestashop.core.grid.factory.product.shops');
        $filters = new ProductFilters(
            $shopConstraint,
            [
                'filters' => [
                    'id_product' => [
                        'min_field' => $productId,
                        'max_field' => $productId,
                    ],
                ],
            ],
            $filters->getFilterId()
        );
        $grid = $gridFactory->getGrid($filters);

        return $this->render('@PrestaShop/Admin/Sell/Catalog/Product/shop_previews.html.twig', [
            'shopDetailsGrid' => $this->presentGrid($grid),
        ]);
    }

    /**
     * @AdminSecurity("is_granted('read', 'AdminProducts')")
     *
     * @return Response
     */
    public function lightListAction(ProductFilters $filters, Request $request): Response
    {
        $gridFactory = $this->get('prestashop.core.grid.factory.product_light');
        $grid = $gridFactory->getGrid($filters);

        return $this->render('@PrestaShop/Admin/Sell/Catalog/Product/light_list.html.twig', [
            'lightDisplay' => $request->query->has('liteDisplaying'),
            'productLightGrid' => $this->presentGrid($grid),
        ]);
    }

    /**
     * The redirection URL is generation thanks to the ProductPreviewProvider however it can't be used in the grid
     * since the LinkRowAction expects a symfony route, so this action is merely used as a proxy for symfony routing
     * and redirects to the appropriate product preview url.
     *
     * @AdminSecurity("is_granted('read', 'AdminProducts')")
     *
     * @return RedirectResponse
     */
    public function previewAction(int $productId, ?int $shopId): RedirectResponse
    {
        $shopConstraint = !empty($shopId) ? ShopConstraint::shop($shopId) : ShopConstraint::allShops();
        /** @var ProductForEditing $productForEditing */
        $productForEditing = $this->getQueryBus()->handle(new GetProductForEditing(
            $productId,
            $shopConstraint,
            $this->getContextLangId()
        ));

        if (null === $shopId) {
            $shopId = $this->productRepository->getProductDefaultShopId(new ProductId($productId))->getValue();
        }

        /** @var ProductPreviewProvider $previewUrlProvider */
        $previewUrlProvider = $this->get('prestashop.adapter.shop.url.product_preview_provider');
        $previewUrl = $previewUrlProvider->getUrl($productId, $productForEditing->isActive(), $shopId);

        return $this->redirect($previewUrl);
    }

    /**
     * @AdminSecurity("is_granted('create', request.get('_legacy_controller'))", message="You do not have permission to create this.")
     *
     * @param Request $request
     * @param int $productId
     *
     * @return Response
     */
    public function selectProductShopsAction(Request $request, int $productId): Response
    {
        if (!$this->get('prestashop.adapter.shop.context')->isSingleShopContext()) {
            return $this->renderIncompatibleContext($productId);
        }

        $productShopsForm = $this->getProductShopsFormBuilder()->getFormFor($productId);

        try {
            $productShopsForm->handleRequest($request);

            $result = $this->getProductShopsFormHandler()->handleFor($productId, $productShopsForm);

            if ($result->isSubmitted() && $result->isValid()) {
                $this->addFlash('success', $this->trans('Successful update', 'Admin.Notifications.Success'));

                $redirectParams = ['productId' => $productId];
                if ($request->query->has('liteDisplaying')) {
                    $redirectParams['liteDisplaying'] = true;
                }

                return $this->redirectToRoute('admin_products_select_shops', $redirectParams);
            }
        } catch (Exception $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));
        }

        return $this->renderProductShopsForm($productShopsForm, $productId, $request->query->has('liteDisplaying'));
    }

    /**
     * @AdminSecurity("is_granted('create', request.get('_legacy_controller'))", message="You do not have permission to create this.")
     *
     * @param Request $request
     *
     * @return Response
     */
    public function createAction(Request $request): Response
    {
        if ($request->query->has('shopId')) {
            $data['shop_id'] = $request->query->get('shopId');
        } else {
            /** @var Context $shopContext */
            $shopContext = $this->get('prestashop.adapter.shop.context');

            $data['shop_id'] = $shopContext->getContextShopID();
        }
        $productForm = $this->getCreateProductFormBuilder()->getForm($data);

        try {
            $productForm->handleRequest($request);

            $result = $this->getProductFormHandler()->handle($productForm);

            if ($result->isSubmitted() && $result->isValid()) {
                $this->addFlash('success', $this->trans('Successful update', 'Admin.Notifications.Success'));

                $redirectParams = ['productId' => $result->getIdentifiableObjectId()];

                $createdData = $productForm->getData();
                if (!empty($createdData['shop_id'])) {
                    $this->addFlash('success', $this->trans('Your store context has been automatically modified.', 'Admin.Notifications.Success'));

                    // Force shop context switching to selected shop for creation (handled in admin-dev/init.php and/or AdminController)
                    $redirectParams['setShopContext'] = 's-' . $createdData['shop_id'];
                }

                // When this configuration is enabled we pre-fill the online status in the redirected form
                if ((bool) $this->getConfiguration()->get('PS_PRODUCT_ACTIVATION_DEFAULT')) {
                    $redirectParams['forceDefaultActive'] = 1;
                }

                return $this->redirectToRoute('admin_products_edit', $redirectParams);
            }
        } catch (Exception $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));
        }

        return $this->renderCreateProductForm($productForm, $request->query->has('liteDisplaying'));
    }

    /**
     * @AdminSecurity("is_granted('update', request.get('_legacy_controller'))", message="You do not have permission to update this.")
     *
     * @param Request $request
     * @param int $productId
     *
     * @return Response
     */
    public function editAction(Request $request, int $productId): Response
    {
        if ($this->shouldRedirectToV1()) {
            return $this->redirectToRoute('admin_product_form', ['id' => $productId]);
        }

        if ($request->query->get('switchToShop')) {
            $this->addFlash('success', $this->trans('Your store context has been automatically modified.', 'Admin.Notifications.Success'));

            return $this->redirectToRoute('admin_products_edit', [
                'productId' => $productId,
                // Force shop context switching to selected shop for creation (handled in admin-dev/init.php and/or AdminController)
                'setShopContext' => 's-' . $request->query->get('switchToShop'),
            ]);
        }

        if (!$this->get('prestashop.adapter.shop.context')->isSingleShopContext()) {
            return $this->renderIncompatibleContext($productId);
        }

        // When query parameter is present we force the initial value in the form, but only in GET method, or you could never manually set false in the form on submit
        $forceDefaultActive = $request->query->getBoolean('forceDefaultActive') && $request->isMethod(Request::METHOD_GET);

        try {
            $productForm = $this->getEditProductFormBuilder()->getFormFor($productId, [], [
                'product_id' => $productId,
                'shop_id' => (int) $this->getContextShopId(),
                'force_default_active' => $forceDefaultActive,
                // @todo: patch/partial update doesn't work good for now (especially multiple empty values) so we use POST for now
                // 'method' => Request::METHOD_PATCH,
                'method' => Request::METHOD_POST,
            ]);
        } catch (ShopAssociationNotFound $e) {
            return $this->renderMissingAssociation($productId);
        } catch (ProductNotFoundException $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));

            return $this->redirectToRoute('admin_products_index');
        }

        try {
            $productForm->handleRequest($request);
            $result = $this->getProductFormHandler()->handleFor($productId, $productForm);

            if ($result->isSubmitted()) {
                if ($result->isValid()) {
                    $this->addFlash('success', $this->trans('Successful update', 'Admin.Notifications.Success'));

                    return $this->redirectToRoute('admin_products_edit', ['productId' => $productId]);
                } else {
                    // Display root level errors with flash messages
                    foreach ($productForm->getErrors() as $error) {
                        $this->addFlash('error', sprintf(
                            '%s: %s',
                            $error->getOrigin()->getName(),
                            $error->getMessage()
                        ));
                    }
                }
            }
        } catch (Exception $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));
        }

        return $this->renderEditProductForm($productForm, $productId);
    }

    /**
     * @AdminSecurity("is_granted('delete', request.get('_legacy_controller'))", message="You do not have permission to delete this.")
     *
     * @param int $productId
     *
     * @return Response
     */
    public function deleteFromAllShopsAction(int $productId): Response
    {
        try {
            $this->getCommandBus()->handle(new DeleteProductCommand($productId, ShopConstraint::allShops()));
            $this->addFlash(
                'success',
                $this->trans('Successful deletion', 'Admin.Notifications.Success')
            );
        } catch (Exception $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));
        }

        return $this->redirectToRoute('admin_products_index');
    }

    /**
     * @AdminSecurity("is_granted('delete', request.get('_legacy_controller'))", message="You do not have permission to delete this.")
     *
     * @param int $productId
     * @param int $shopId
     *
     * @return Response
     */
    public function deleteFromShopAction(int $productId, int $shopId): Response
    {
        try {
            $this->getCommandBus()->handle(new DeleteProductCommand($productId, ShopConstraint::shop($shopId)));
            $this->addFlash(
                'success',
                $this->trans('Successful deletion', 'Admin.Notifications.Success')
            );
        } catch (Exception $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));
        }

        return $this->redirectToRoute('admin_products_index');
    }

    /**
     * @AdminSecurity("is_granted('delete', request.get('_legacy_controller'))", message="You do not have permission to delete this.")
     *
     * @param int $productId
     * @param int $shopGroupId
     *
     * @return Response
     */
    public function deleteFromShopGroupAction(int $productId, int $shopGroupId): Response
    {
        try {
            $this->getCommandBus()->handle(new DeleteProductCommand($productId, ShopConstraint::shopGroup($shopGroupId)));
            $this->addFlash(
                'success',
                $this->trans('Successful deletion', 'Admin.Notifications.Success')
            );
        } catch (ProductException $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));
        }

        return $this->redirectToRoute('admin_products_index');
    }

    /**
     * @AdminSecurity("is_granted('delete', request.get('_legacy_controller'))", message="You do not have permission to delete this.")
     *
     * @param int $shopId
     *
     * @return Response
     */
    public function bulkDeleteFromShopAction(Request $request, int $shopId): Response
    {
        return $this->bulkDeleteByShopConstraint($request, ShopConstraint::shop($shopId));
    }

    /**
     * @AdminSecurity("is_granted('delete', request.get('_legacy_controller'))", message="You do not have permission to delete this.")
     *
     * @param int $shopGroupId
     *
     * @return Response
     */
    public function bulkDeleteFromShopGroupAction(Request $request, int $shopGroupId): Response
    {
        return $this->bulkDeleteByShopConstraint($request, ShopConstraint::shopGroup($shopGroupId));
    }

    /**
     * @AdminSecurity("is_granted('create', request.get('_legacy_controller'))", message="You do not have permission to create this.")
     *
     * @param int $productId
     *
     * @return Response
     */
    public function duplicateAllShopsAction(int $productId): Response
    {
        return $this->duplicateByShopConstraint($productId, ShopConstraint::allShops());
    }

    /**
     * @AdminSecurity("is_granted('create', request.get('_legacy_controller'))", message="You do not have permission to create this.")
     *
     * @param int $productId
     * @param int $shopId
     *
     * @return Response
     */
    public function duplicateShopAction(int $productId, int $shopId): Response
    {
        return $this->duplicateByShopConstraint($productId, ShopConstraint::shop($shopId));
    }

    /**
     * @AdminSecurity("is_granted('create', request.get('_legacy_controller'))", message="You do not have permission to create this.")
     *
     * @param int $productId
     * @param int $shopGroupId
     *
     * @return Response
     */
    public function duplicateShopGroupAction(int $productId, int $shopGroupId): Response
    {
        return $this->duplicateByShopConstraint($productId, ShopConstraint::shopGroup($shopGroupId));
    }

    /**
     * Toggles product status
     *
     * @AdminSecurity("is_granted('update', request.get('_legacy_controller'))", redirectRoute="admin_products_index")
     *
     * @param int $productId
     * @param int $shopId
     *
     * @return JsonResponse
     */
    public function toggleStatusForShopAction(int $productId, int $shopId): JsonResponse
    {
        $shopConstraint = ShopConstraint::shop($shopId);
        /** @var ProductForEditing $productForEditing */
        $productForEditing = $this->getQueryBus()->handle(new GetProductForEditing(
            $productId,
            $shopConstraint,
            $this->getContextLangId()
        ));

        try {
            $command = new UpdateProductCommand($productId, $shopConstraint);
            $command->setActive(!$productForEditing->isActive());
            $this->getCommandBus()->handle($command);
        } catch (Exception $e) {
            return $this->json([
                'status' => false,
                'message' => $this->getErrorMessageForException($e, $this->getErrorMessages($e)),
            ]);
        }

        return $this->json([
            'status' => true,
            'message' => $this->trans('The status has been successfully updated.', 'Admin.Notifications.Success'),
        ]);
    }

    /**
     * Enable product status for all shops and redirect to product list.
     *
     * @AdminSecurity("is_granted('update', request.get('_legacy_controller'))", redirectRoute="admin_products_index")
     *
     * @param int $productId
     *
     * @return RedirectResponse
     */
    public function enableForAllShopsAction(int $productId): RedirectResponse
    {
        return $this->updateProductStatusByShopConstraint($productId, true, ShopConstraint::allShops());
    }

    /**
     * Enable product status for shop group and redirect to product list.
     *
     * @AdminSecurity("is_granted('update', request.get('_legacy_controller'))", redirectRoute="admin_products_index")
     *
     * @param int $productId
     * @param int $shopGroupId
     *
     * @return RedirectResponse
     */
    public function enableForShopGroupAction(int $productId, int $shopGroupId): RedirectResponse
    {
        return $this->updateProductStatusByShopConstraint($productId, true, ShopConstraint::shopGroup($shopGroupId));
    }

    /**
     * Disable product status for shop group and redirect to product list.
     *
     * @AdminSecurity("is_granted('update', request.get('_legacy_controller'))", redirectRoute="admin_products_index")
     *
     * @param int $productId
     * @param int $shopGroupId
     *
     * @return RedirectResponse
     */
    public function disableForShopGroupAction(int $productId, int $shopGroupId): RedirectResponse
    {
        return $this->updateProductStatusByShopConstraint($productId, false, ShopConstraint::shopGroup($shopGroupId));
    }

    /**
     * Disable product status for all shops and redirect to product list.
     *
     * @AdminSecurity("is_granted('update', request.get('_legacy_controller'))", redirectRoute="admin_products_index")
     *
     * @param int $productId
     *
     * @return RedirectResponse
     */
    public function disableForAllShopsAction(int $productId): RedirectResponse
    {
        return $this->updateProductStatusByShopConstraint($productId, false, ShopConstraint::allShops());
    }

    /**
     * Export filtered products
     *
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     *
     * @param ProductFilters $filters
     *
     * @return CsvResponse
     */
    public function exportAction(ProductFilters $filters)
    {
        $productGridFactory = $this->get('prestashop.core.grid.factory.product');
        $grid = $productGridFactory->getGrid($filters);

        $headers = [
            'id_product' => 'Product ID',
            'image_link' => $this->trans('Image', 'Admin.Global'),
            'name' => $this->trans('Name', 'Admin.Global'),
            'reference' => $this->trans('Reference', 'Admin.Global'),
            'name_category' => $this->trans('Category', 'Admin.Global'),
            'price' => $this->trans('Price (tax excl.)', 'Admin.Catalog.Feature'),
            'price_final' => $this->trans('Price (tax incl.)', 'Admin.Catalog.Feature'),
            'sav_quantity' => $this->trans('Quantity', 'Admin.Global'),
        ];

        $data = [];

        foreach ($grid->getData()->getRecords()->all() as $record) {
            $data[] = [
                'id_product' => $record['id_product'],
                'image_link' => $record['image'],
                'name' => $record['name'],
                'reference' => $record['reference'],
                'name_category' => $record['category'],
                'price' => $record['final_price_tax_excluded'],
                'price_final' => $record['price_tax_included'],
                'sav_quantity' => $record['quantity'],
            ];
        }

        return (new CsvResponse())
            ->setData($data)
            ->setHeadersData($headers)
            ->setFileName('product_' . date('Y-m-d_His') . '.csv');
    }

    /**
     * Updates product position.
     *
     * @AdminSecurity(
     *     "is_granted('update', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     redirectQueryParamsToKeep={"id_category"},
     *     message="You do not have permission to edit this."
     * )
     * @DemoRestricted(
     *     redirectRoute="admin_products_index",
     *     redirectQueryParamsToKeep={"id_category"}
     * )
     *
     * @param Request $request
     *
     * @return RedirectResponse
     */
    public function updatePositionAction(Request $request): RedirectResponse
    {
        try {
            $this->getCommandBus()->handle(
                new UpdateProductsPositionsCommand(
                    $request->request->get('positions'),
                    $request->query->getInt('id_category')
                )
            );
            $this->addFlash('success', $this->trans('Update successful', 'Admin.Notifications.Success'));
        } catch (Exception $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));

            return $this->redirectToRoute('admin_products_index');
        }

        return $this->redirectToRoute('admin_products_index');
    }

    /**
     * Delete products in bulk action.
     *
     * @AdminSecurity(
     *     "is_granted('delete', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     message="You do not have permission to delete this."
     * )
     *
     * @param Request $request
     *
     * @return JsonResponse
     */
    public function bulkDeleteFromAllShopsAction(Request $request): JsonResponse
    {
        try {
            $this->bulkDeleteByShopConstraint($request, ShopConstraint::allShops());
            $this->addFlash(
                'success',
                $this->trans('Successful deletion', 'Admin.Notifications.Success')
            );
        } catch (Exception $e) {
            if ($e instanceof BulkProductException) {
                return $this->jsonBulkErrors($e);
            } else {
                return $this->json(['error' => $this->getErrorMessageForException($e, $this->getErrorMessages($e))], Response::HTTP_BAD_REQUEST);
            }
        }

        return $this->json(['success' => true]);
    }

    /**
     * Enable products in bulk action.
     *
     * @AdminSecurity(
     *     "is_granted('update', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     message="You do not have permission to edit this."
     * )
     *
     * @param Request $request
     *
     * @return JsonResponse
     */
    public function bulkEnableAllShopsAction(Request $request): JsonResponse
    {
        return $this->bulkUpdateProductStatus($request, true, ShopConstraint::allShops());
    }

    /**
     * Enable products in bulk action for a specific shop.
     *
     * @AdminSecurity(
     *     "is_granted('update', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     message="You do not have permission to edit this."
     * )
     *
     * @param Request $request
     * @param int $shopId
     *
     * @return JsonResponse
     */
    public function bulkEnableShopAction(Request $request, int $shopId): JsonResponse
    {
        return $this->bulkUpdateProductStatus($request, true, ShopConstraint::shop($shopId));
    }

    /**
     * Enable products in bulk action for a specific shop group.
     *
     * @AdminSecurity(
     *     "is_granted('update', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     message="You do not have permission to edit this."
     * )
     *
     * @param Request $request
     * @param int $shopGroupId
     *
     * @return JsonResponse
     */
    public function bulkEnableShopGroupAction(Request $request, int $shopGroupId): JsonResponse
    {
        return $this->bulkUpdateProductStatus($request, true, ShopConstraint::shopGroup($shopGroupId));
    }

    /**
     * Disable products in bulk action.
     *
     * @AdminSecurity(
     *     "is_granted('update', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     message="You do not have permission to edit this."
     * )
     *
     * @param Request $request
     *
     * @return JsonResponse
     */
    public function bulkDisableAllShopsAction(Request $request): JsonResponse
    {
        return $this->bulkUpdateProductStatus($request, false, ShopConstraint::allShops());
    }

    /**
     * Disable products in bulk action for a specific shop.
     *
     * @AdminSecurity(
     *     "is_granted('update', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     message="You do not have permission to edit this."
     * )
     *
     * @param Request $request
     * @param int $shopId
     *
     * @return JsonResponse
     */
    public function bulkDisableShopAction(Request $request, int $shopId): JsonResponse
    {
        return $this->bulkUpdateProductStatus($request, false, ShopConstraint::shop($shopId));
    }

    /**
     * Disable products in bulk action for a specific shop group.
     *
     * @AdminSecurity(
     *     "is_granted('update', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     message="You do not have permission to edit this."
     * )
     *
     * @param Request $request
     * @param int $shopGroupId
     *
     * @return JsonResponse
     */
    public function bulkDisableShopGroupAction(Request $request, int $shopGroupId): JsonResponse
    {
        return $this->bulkUpdateProductStatus($request, false, ShopConstraint::shopGroup($shopGroupId));
    }

    /**
     * Duplicate products in bulk action.
     *
     * @AdminSecurity(
     *     "is_granted('create', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     message="You do not have permission to edit this."
     * )
     *
     * @param Request $request
     *
     * @return JsonResponse
     */
    public function bulkDuplicateAllShopsAction(Request $request): JsonResponse
    {
        return $this->bulkDuplicateByShopConstraint($request, ShopConstraint::allShops());
    }

    /**
     * Duplicate products in bulk action for specific shop.
     *
     * @AdminSecurity(
     *     "is_granted('create', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     message="You do not have permission to edit this."
     * )
     *
     * @param Request $request
     * @param int $shopId
     *
     * @return JsonResponse
     */
    public function bulkDuplicateShopAction(Request $request, int $shopId): JsonResponse
    {
        return $this->bulkDuplicateByShopConstraint($request, ShopConstraint::shop($shopId));
    }

    /**
     * Duplicate products in bulk action for specific shop group.
     *
     * @AdminSecurity(
     *     "is_granted('create', request.get('_legacy_controller'))",
     *     redirectRoute="admin_products_index",
     *     message="You do not have permission to edit this."
     * )
     *
     * @param Request $request
     * @param int $shopGroupId
     *
     * @return JsonResponse
     */
    public function bulkDuplicateShopGroupAction(Request $request, int $shopGroupId): JsonResponse
    {
        return $this->bulkDuplicateByShopConstraint($request, ShopConstraint::shopGroup($shopGroupId));
    }

    /**
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))", message="You do not have permission to read this.")
     *
     * Download the content of the virtual product.
     *
     * @param int $virtualProductFileId
     *
     * @return BinaryFileResponse
     */
    public function downloadVirtualFileAction(int $virtualProductFileId): BinaryFileResponse
    {
        $configuration = $this->getConfiguration();
        $download = $this->getDoctrine()
            ->getRepository(ProductDownload::class)
            ->findOneBy([
                'id' => $virtualProductFileId,
            ]);

        $response = new BinaryFileResponse(
            $configuration->get('_PS_DOWNLOAD_DIR_') . $download->getFilename()
        );

        $response->setContentDisposition(
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
            $download->getDisplayFilename()
        );

        return $response;
    }

    /**
     * @AdminSecurity("is_granted(['read'], request.get('_legacy_controller'))")
     *
     * @param Request $request
     * @param string $languageCode
     *
     * @return JsonResponse
     */
    public function searchProductsForAssociationAction(Request $request, string $languageCode): JsonResponse
    {
        $langRepository = $this->get('prestashop.core.admin.lang.repository');
        $lang = $langRepository->getOneByLocaleOrIsoCode($languageCode);
        if (null === $lang) {
            return $this->json([
                'message' => sprintf(
                    'Invalid language code %s was used which matches no existing language in this shop.',
                    $languageCode
                ),
            ], Response::HTTP_BAD_REQUEST);
        }

        $shopId = $this->get('prestashop.adapter.shop.context')->getContextShopID();
        if (empty($shopId)) {
            $shopId = $this->getConfiguration()->getInt('PS_SHOP_DEFAULT');
        }

        try {
            /** @var ProductForAssociation[] $products */
            $products = $this->getQueryBus()->handle(new SearchProductsForAssociation(
                $request->get('query', ''),
                $lang->getId(),
                (int) $shopId,
                (int) $request->get('limit', 20)
            ));
        } catch (ProductConstraintException $e) {
            return $this->json([
                'message' => $e->getMessage(),
            ], Response::HTTP_BAD_REQUEST);
        }

        if (empty($products)) {
            return $this->json([], Response::HTTP_NOT_FOUND);
        }

        return $this->json($this->formatProductsForAssociation($products));
    }

    /**
     * @AdminSecurity("is_granted(['read'], request.get('_legacy_controller'))")
     *
     * @param int $productId
     * @param int $shopId
     *
     * @return JsonResponse
     */
    public function quantityAction(int $productId, int $shopId): JsonResponse
    {
        /** @var ProductForEditing $productForEditing */
        $productForEditing = $this->getQueryBus()->handle(
            new GetProductForEditing($productId, ShopConstraint::shop($shopId), $this->getContextLangId())
        );

        return $this->json(['quantity' => $productForEditing->getStockInformation()->getQuantity()]);
    }

    /**
     * @param ProductForAssociation[] $productsForAssociation
     *
     * @return array
     */
    private function formatProductsForAssociation(array $productsForAssociation): array
    {
        $productsData = [];
        foreach ($productsForAssociation as $productForAssociation) {
            $productName = $productForAssociation->getName();
            if (!empty($productForAssociation->getReference())) {
                $productName .= sprintf(' (ref: %s)', $productForAssociation->getReference());
            }

            $productsData[] = [
                'id' => $productForAssociation->getProductId(),
                'name' => $productName,
                'image' => $productForAssociation->getImageUrl(),
            ];
        }

        return $productsData;
    }

    /**
     * @param FormInterface $productForm
     *
     * @return Response
     */
    private function renderCreateProductForm(FormInterface $productForm, bool $lightDisplay): Response
    {
        return $this->render('@PrestaShop/Admin/Sell/Catalog/Product/create.html.twig', [
            'lightDisplay' => $lightDisplay,
            'showContentHeader' => false,
            'productForm' => $productForm->createView(),
            'helpLink' => $this->generateSidebarLink('AdminProducts'),
            'editable' => $this->isGranted(PageVoter::UPDATE, self::PRODUCT_CONTROLLER_PERMISSION),
        ]);
    }

    /**
     * @param FormInterface $productForm
     * @param int $productId
     *
     * @return Response
     */
    private function renderEditProductForm(FormInterface $productForm, int $productId): Response
    {
        $configuration = $this->getConfiguration();
        $categoryTreeFormBuilder = $this->get('prestashop.core.form.identifiable_object.builder.category_tree_selector_form_builder');

        $moduleDataProvider = $this->get('prestashop.adapter.data_provider.module');
        $statsModule = $moduleDataProvider->findByName('statsproduct');
        $statsLink = null;
        if (!empty($statsModule['active'])) {
            $statsLink = $this->getAdminLink('AdminStats', ['module' => 'statsproduct', 'id_product' => $productId]);
        }

        return $this->render('@PrestaShop/Admin/Sell/Catalog/Product/edit.html.twig', [
            'categoryTreeSelectorForm' => $categoryTreeFormBuilder->getForm()->createView(),
            'showContentHeader' => false,
            'productForm' => $productForm->createView(),
            'statsLink' => $statsLink,
            'helpLink' => $this->generateSidebarLink('AdminProducts'),
            'editable' => $this->isGranted(PageVoter::UPDATE, self::PRODUCT_CONTROLLER_PERMISSION),
            'taxEnabled' => (bool) $configuration->get('PS_TAX'),
            'stockEnabled' => (bool) $configuration->get('PS_STOCK_MANAGEMENT'),
            'isMultistoreActive' => $this->get('prestashop.adapter.multistore_feature')->isActive(),
        ]);
    }

    /**
     * @param FormInterface $productShopsForm
     *
     * @return Response
     */
    private function renderProductShopsForm(FormInterface $productShopsForm, int $productId, bool $lightDisplay): Response
    {
        return $this->render('@PrestaShop/Admin/Sell/Catalog/Product/shops.html.twig', [
            'productId' => $productId,
            'lightDisplay' => $lightDisplay,
            'showContentHeader' => false,
            'productShopsForm' => $productShopsForm->createView(),
            'helpLink' => $this->generateSidebarLink('AdminProducts'),
        ]);
    }

    /**
     * Helper private method to duplicate some products
     *
     * @param Request $request
     * @param ShopConstraint $shopConstraint
     *
     * @return JsonResponse
     */
    private function bulkDuplicateByShopConstraint(Request $request, ShopConstraint $shopConstraint): JsonResponse
    {
        try {
            $this->getCommandBus()->handle(
                new BulkDuplicateProductCommand(
                    $this->getProductIdsFromRequest($request),
                    $shopConstraint
                )
            );
        } catch (Exception $e) {
            if ($e instanceof BulkProductException) {
                return $this->jsonBulkErrors($e);
            } else {
                return $this->json(['error' => $this->getErrorMessageForException($e, $this->getErrorMessages($e))], Response::HTTP_BAD_REQUEST);
            }
        }

        return $this->json(['success' => true]);
    }

    /**
     * Helper private method to duplicate a product
     *
     * @param int $productId
     * @param ShopConstraint $shopConstraint
     *
     * @return Response
     */
    private function duplicateByShopConstraint(int $productId, ShopConstraint $shopConstraint): Response
    {
        try {
            /** @var ProductId $newProductId */
            $newProductId = $this->getCommandBus()->handle(new DuplicateProductCommand(
                $productId,
                $shopConstraint
            ));
            $this->addFlash(
                'success',
                $this->trans('Successful duplication', 'Admin.Notifications.Success')
            );
        } catch (Exception $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));

            return $this->redirectToRoute('admin_products_index');
        }

        return $this->redirectToRoute('admin_products_edit', ['productId' => $newProductId->getValue()]);
    }

    /**
     * Helper private method to delete some products
     *
     * @param Request $request
     * @param ShopConstraint $shopConstraint
     *
     * @return JsonResponse
     */
    private function bulkDeleteByShopConstraint(Request $request, ShopConstraint $shopConstraint): JsonResponse
    {
        try {
            $this->getCommandBus()->handle(new BulkDeleteProductCommand(
                $this->getProductIdsFromRequest($request),
                $shopConstraint
            ));
            $this->addFlash(
                'success',
                $this->trans('Successful deletion', 'Admin.Notifications.Success')
            );
        } catch (Exception $e) {
            if ($e instanceof BulkProductException) {
                return $this->jsonBulkErrors($e);
            } else {
                return $this->json(['error' => $this->getErrorMessageForException($e, $this->getErrorMessages($e))], Response::HTTP_BAD_REQUEST);
            }
        }

        return $this->json(['success' => true]);
    }

    /**
     * @param Request $request
     *
     * @return array<int, int>
     */
    private function getProductIdsFromRequest(Request $request): array
    {
        $productIds = $request->request->get('product_bulk');

        if (is_numeric($productIds)) {
            return [(int) $productIds];
        }

        if (!is_array($productIds)) {
            return [];
        }

        foreach ($productIds as $i => $productId) {
            $productIds[$i] = (int) $productId;
        }

        // Return product IDs ordered
        sort($productIds);

        return $productIds;
    }

    /**
     * @param string $securitySubject
     *
     * @return array<string, array<string, mixed>>
     */
    private function getProductToolbarButtons(string $securitySubject): array
    {
        $toolbarButtons = [];

        // do not show create button if user has no permissions for it
        if (!$this->isGranted(PageVoter::CREATE, $securitySubject)) {
            return $toolbarButtons;
        }

        $toolbarButtons['add'] = [
            'href' => $this->generateUrl('admin_products_create', ['shopId' => $this->getShopIdFromShopContext()]),
            'desc' => $this->trans('New product', 'Admin.Actions'),
            'icon' => 'add_circle_outline',
            'class' => 'btn-primary new-product-button',
            'floating_class' => 'new-product-button',
            'data_attributes' => [
                'modal-title' => $this->trans('Add new product', 'Admin.Catalog.Feature'),
            ],
        ];

        return $toolbarButtons;
    }

    /**
     * Helper private method to update a product's status.
     *
     * @param int $productId
     * @param bool $isEnabled
     * @param ShopConstraint $shopConstraint
     *
     * @return RedirectResponse
     */
    private function updateProductStatusByShopConstraint(int $productId, bool $isEnabled, ShopConstraint $shopConstraint): RedirectResponse
    {
        try {
            $command = new UpdateProductCommand($productId, $shopConstraint);
            $command->setActive($isEnabled);
            $this->getCommandBus()->handle($command);
            $this->addFlash('success', $this->trans('The status has been successfully updated.', 'Admin.Notifications.Success'));
        } catch (Exception $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));
        }

        return $this->redirectToRoute('admin_products_index');
    }

    /**
     * Helper private method to bulk update a product's status.
     *
     * @param Request $request
     * @param bool $newStatus
     * @param ShopConstraint $shopConstraint
     *
     * @return JsonResponse
     */
    private function bulkUpdateProductStatus(Request $request, bool $newStatus, ShopConstraint $shopConstraint): JsonResponse
    {
        try {
            $this->getCommandBus()->handle(
                new BulkUpdateProductStatusCommand(
                    $this->getProductIdsFromRequest($request),
                    $newStatus,
                    $shopConstraint
                )
            );
        } catch (Exception $e) {
            if ($e instanceof BulkProductException) {
                return $this->jsonBulkErrors($e);
            } else {
                return $this->json(['error' => $this->getErrorMessageForException($e, $this->getErrorMessages($e))], Response::HTTP_BAD_REQUEST);
            }
        }

        return $this->json(['success' => true]);
    }

    /**
     * Gets creation form builder.
     *
     * @return FormBuilderInterface
     */
    private function getCreateProductFormBuilder(): FormBuilderInterface
    {
        return $this->get('prestashop.core.form.identifiable_object.builder.create_product_form_builder');
    }

    /**
     * Gets edition form builder.
     *
     * @return FormBuilderInterface
     */
    private function getEditProductFormBuilder(): FormBuilderInterface
    {
        return $this->get('prestashop.core.form.identifiable_object.builder.edit_product_form_builder');
    }

    /**
     * @return FormHandlerInterface
     */
    private function getProductFormHandler(): FormHandlerInterface
    {
        return $this->get('prestashop.core.form.identifiable_object.product_form_handler');
    }

    /**
     * Gets shop association form builder.
     *
     * @return FormBuilderInterface
     */
    private function getProductShopsFormBuilder(): FormBuilderInterface
    {
        return $this->get('prestashop.core.form.identifiable_object.builder.product_shops_form_builder');
    }

    /**
     * @return FormHandlerInterface
     */
    private function getProductShopsFormHandler(): FormHandlerInterface
    {
        return $this->get('prestashop.core.form.identifiable_object.product_shops_form_handler');
    }

    /**
     * Format the bulk exception into an array of errors returned in a JsonResponse.
     *
     * @param BulkProductException $bulkProductException
     *
     * @return JsonResponse
     */
    private function jsonBulkErrors(BulkProductException $bulkProductException): JsonResponse
    {
        $errors = [];
        foreach ($bulkProductException->getBulkExceptions() as $productId => $productException) {
            $errors[] = $this->trans(
                'Error for product %product_id%: %error_message%',
                'Admin.Catalog.Notification',
                [
                    '%product_id%' => $productId,
                    '%error_message%' => $this->getErrorMessageForException($productException, $this->getErrorMessages($productException)),
                ]
            );
        }

        return $this->json(['errors' => $errors], Response::HTTP_BAD_REQUEST);
    }

    /**
     * Gets an error by exception class and its code.
     *
     * @param Exception $e
     *
     * @return array
     */
    private function getErrorMessages(Exception $e): array
    {
        // @todo: all the constraint error messages are missing for now (see ProductConstraintException)
        return [
            CannotDeleteProductException::class => $this->trans(
                'An error occurred while deleting the object.',
                'Admin.Notifications.Error'
            ),
            CannotBulkDeleteProductException::class => $this->trans(
                    'An error occurred while deleting this selection.',
                    'Admin.Notifications.Error'
            ),
            ProductConstraintException::class => [
                ProductConstraintException::INVALID_PRICE => $this->trans(
                    'Product price is invalid',
                    'Admin.Notifications.Error'
                ),
                ProductConstraintException::INVALID_UNIT_PRICE => $this->trans(
                    'Product price per unit is invalid',
                    'Admin.Notifications.Error'
                ),
                ProductConstraintException::INVALID_REDIRECT_TARGET => $this->trans(
                    'When redirecting towards a product you must select a target product.',
                    'Admin.Catalog.Notification'
                ),
                ProductConstraintException::INVALID_ONLINE_DATA => $this->trans(
                    'To put this product online, please enter a name.',
                    'Admin.Catalog.Notification'
                ),
            ],
            DuplicateFeatureValueAssociationException::class => $this->trans(
                'You cannot associate the same feature value more than once.',
                'Admin.Notifications.Error'
            ),
            InvalidAssociatedFeatureException::class => $this->trans(
                'The selected value belongs to another feature.',
                'Admin.Notifications.Error'
            ),
            SpecificPriceConstraintException::class => [
                SpecificPriceConstraintException::DUPLICATE_PRIORITY => $this->trans(
                    'The selected condition must be different in each field to set an order of priority.',
                    'Admin.Notifications.Error'
                ),
            ],
            InvalidProductTypeException::class => [
                InvalidProductTypeException::EXPECTED_NO_EXISTING_PACK_ASSOCIATIONS => $this->trans(
                    'This product cannot be changed into a pack because it is already associated to another pack.',
                    'Admin.Notifications.Error'
                ),
            ],
            ProductNotFoundException::class => $this->trans(
                'The object cannot be loaded (or found).',
                'Admin.Notifications.Error'
            ),
        ];
    }

    /**
     * @param int $productId
     *
     * @return Response
     */
    private function renderMissingAssociation(int $productId): Response
    {
        return $this->renderPreSelectShopPage(
            $productId,
            $this->trans(
                'This product is not associated with the store selected in the multistore header, please select another one.',
                'Admin.Notifications.Info'
            )
        );
    }

    /**
     * @param int $productId
     *
     * @return Response
     */
    private function renderIncompatibleContext(int $productId): Response
    {
        return $this->renderPreSelectShopPage(
            $productId,
            $this->trans(
                'This page is only compatible in a single-store context. Please select a store in the multistore header.',
                'Admin.Notifications.Info'
            )
        );
    }

    /**
     * @param int $productId
     *
     * @return Response
     */
    private function renderPreSelectShopPage(int $productId, string $warningMessage): Response
    {
        return $this->render('@PrestaShop/Admin/Sell/Catalog/Product/pre_select_shop.html.twig', [
            'warningMessage' => $warningMessage,
            'showContentHeader' => false,
            'modalTitle' => $this->trans('Select a store', 'Admin.Catalog.Feature'),
            'shopSelector' => $this->createForm(ShopSelectorType::class),
            'productId' => $productId,
            'productShopIds' => array_map(static function (ShopId $shopId) {
                return $shopId->getValue();
            }, $this->productRepository->getAssociatedShopIds(new ProductId($productId))),
        ]);
    }

    private function getGridAdminFilter(): ?AdminFilter
    {
        if (null === $this->getUser() || null === $this->getContext()->shop || empty($this->getContext()->shop->id)) {
            return null;
        }

        $adminFiltersRepository = $this->get('prestashop.core.admin.admin_filter.repository');
        $employeeId = $this->getUser()->getId();
        $shopId = $this->getContext()->shop->id;

        return $adminFiltersRepository->findByEmployeeAndFilterId($employeeId, $shopId, ProductGridDefinitionFactory::GRID_ID);
    }

    /**
     * @return bool
     */
    private function shouldRedirectToV1(): bool
    {
        return $this->get(FeatureFlagRepository::class)->isDisabled(FeatureFlagSettings::FEATURE_FLAG_PRODUCT_PAGE_V2);
    }

    /**
     * @return int|null
     */
    private function getShopIdFromShopContext(): ?int
    {
        /** @var Context $shopContext */
        $shopContext = $this->get('prestashop.adapter.shop.context');
        $shopId = $shopContext->getContextShopID();

        return !empty($shopId) ? (int) $shopId : null;
    }
}