Repository: PrestaShop/PrestaShop (PHP)

Issue:
Delete discounts in bulk
# Context
Back office user can delete in bulk from the discount list.
A deleted discount must be remove from the list, the db and is no longer applied (if it was the case).

# Mockup

![Image](https://github.com/user-attachments/assets/818387ff-28c5-4493-9d2c-f7ad346bb693)

# Tests
- delete via bulk action => selection must be removed from the list (and the db) + discount are no longer applied



Fix (patch):
diff --git a/src/Adapter/Discount/CommandHandler/BulkDeleteDiscountsHandler.php b/src/Adapter/Discount/CommandHandler/BulkDeleteDiscountsHandler.php
new file mode 100644
index 0000000000000..bf878cbc75e28
--- /dev/null
+++ b/src/Adapter/Discount/CommandHandler/BulkDeleteDiscountsHandler.php
@@ -0,0 +1,88 @@
+<?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)
+ */
+
+namespace PrestaShop\PrestaShop\Adapter\Discount\CommandHandler;
+
+use PrestaShop\PrestaShop\Adapter\Discount\Repository\DiscountRepository;
+use PrestaShop\PrestaShop\Core\CommandBus\Attributes\AsCommandHandler;
+use PrestaShop\PrestaShop\Core\Domain\AbstractBulkCommandHandler;
+use PrestaShop\PrestaShop\Core\Domain\Discount\Command\BulkDeleteDiscountsCommand;
+use PrestaShop\PrestaShop\Core\Domain\Discount\CommandHandler\BulkDeleteDiscountsHandlerInterface;
+use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\BulkDiscountException;
+use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\CannotDeleteDiscountException;
+use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\DiscountException;
+use PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject\DiscountId;
+use PrestaShop\PrestaShop\Core\Domain\Exception\BulkCommandExceptionInterface;
+
+#[AsCommandHandler]
+final class BulkDeleteDiscountsHandler extends AbstractBulkCommandHandler implements BulkDeleteDiscountsHandlerInterface
+{
+    public function __construct(
+        private readonly DiscountRepository $discountRepository,
+    ) {
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * @throws CannotDeleteDiscountException
+     */
+    public function handle(BulkDeleteDiscountsCommand $command): void
+    {
+        $this->handleBulkAction($command->getDiscountIds(), DiscountException::class);
+    }
+
+    /**
+     * @param DiscountId $id
+     * @param BulkDeleteDiscountsCommand $command
+     *
+     * @return void
+     */
+    protected function handleSingleAction(mixed $id, mixed $command): void
+    {
+        $this->discountRepository->delete($id);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function buildBulkException(array $caughtExceptions): BulkCommandExceptionInterface
+    {
+        return new BulkDiscountException(
+            $caughtExceptions,
+            'Errors occurred during discount bulk change status action',
+            BulkDiscountException::FAILED_BULK_DELETE
+        );
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function supports($id): bool
+    {
+        return $id instanceof DiscountId;
+    }
+}
diff --git a/src/Core/Domain/Discount/Command/BulkDeleteDiscountsCommand.php b/src/Core/Domain/Discount/Command/BulkDeleteDiscountsCommand.php
new file mode 100644
index 0000000000000..6644fa4987bce
--- /dev/null
+++ b/src/Core/Domain/Discount/Command/BulkDeleteDiscountsCommand.php
@@ -0,0 +1,77 @@
+<?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)
+ */
+
+namespace PrestaShop\PrestaShop\Core\Domain\Discount\Command;
+
+use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\DiscountConstraintException;
+use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\DiscountException;
+use PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject\DiscountId;
+
+class BulkDeleteDiscountsCommand
+{
+    /**
+     * @var DiscountId[]
+     */
+    private array $discountIds;
+
+    /**
+     * @param int[] $discountIds
+     *
+     * @throws DiscountConstraintException
+     * @throws DiscountException
+     */
+    public function __construct(array $discountIds)
+    {
+        $this->setDiscountIds($discountIds);
+    }
+
+    /**
+     * @return DiscountId[]
+     */
+    public function getDiscountIds(): array
+    {
+        return $this->discountIds;
+    }
+
+    /**
+     * @param int[] $discountIds
+     *
+     * @throws DiscountConstraintException
+     * @throws DiscountException
+     */
+    private function setDiscountIds(array $discountIds): self
+    {
+        if (empty($discountIds)) {
+            throw new DiscountConstraintException('Missing Discount data for bulk deleting', DiscountConstraintException::INVALID_ID);
+        }
+
+        foreach ($discountIds as $discountId) {
+            $this->discountIds[] = new DiscountId((int) $discountId);
+        }
+
+        return $this;
+    }
+}
diff --git a/src/Core/Domain/Discount/CommandHandler/BulkDeleteDiscountsHandlerInterface.php b/src/Core/Domain/Discount/CommandHandler/BulkDeleteDiscountsHandlerInterface.php
new file mode 100644
index 0000000000000..5f7e68a0bd7c8
--- /dev/null
+++ b/src/Core/Domain/Discount/CommandHandler/BulkDeleteDiscountsHandlerInterface.php
@@ -0,0 +1,37 @@
+<?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)
+ */
+
+namespace PrestaShop\PrestaShop\Core\Domain\Discount\CommandHandler;
+
+use PrestaShop\PrestaShop\Core\Domain\Discount\Command\BulkDeleteDiscountsCommand;
+
+interface BulkDeleteDiscountsHandlerInterface
+{
+    /**
+     * @param BulkDeleteDiscountsCommand $command
+     */
+    public function handle(BulkDeleteDiscountsCommand $command): void;
+}
diff --git a/src/Core/Grid/Definition/Factory/DiscountGridDefinitionFactory.php b/src/Core/Grid/Definition/Factory/DiscountGridDefinitionFactory.php
index 4672e85e55eba..20fd8ac99d7ce 100644
--- a/src/Core/Grid/Definition/Factory/DiscountGridDefinitionFactory.php
+++ b/src/Core/Grid/Definition/Factory/DiscountGridDefinitionFactory.php
@@ -286,6 +286,8 @@ protected function getBulkActions()
                         'submit_route' => 'admin_discount_bulk_disable_status',
                     ])
             )
-        ;
+            ->add(
+                $this->buildBulkDeleteAction('admin_discount_bulk_delete')
+            );
     }
 }
diff --git a/src/PrestaShopBundle/Controller/Admin/Sell/Catalog/DiscountController.php b/src/PrestaShopBundle/Controller/Admin/Sell/Catalog/DiscountController.php
index 7859d356879c1..1189576d0c734 100644
--- a/src/PrestaShopBundle/Controller/Admin/Sell/Catalog/DiscountController.php
+++ b/src/PrestaShopBundle/Controller/Admin/Sell/Catalog/DiscountController.php
@@ -29,6 +29,7 @@
 use Exception;
 use PrestaShop\PrestaShop\Core\Domain\CartRule\Command\ToggleCartRuleStatusCommand;
 use PrestaShop\PrestaShop\Core\Domain\CartRule\Exception\CartRuleException;
+use PrestaShop\PrestaShop\Core\Domain\Discount\Command\BulkDeleteDiscountsCommand;
 use PrestaShop\PrestaShop\Core\Domain\Discount\Command\BulkUpdateDiscountsStatusCommand;
 use PrestaShop\PrestaShop\Core\Domain\Discount\Command\DeleteDiscountCommand;
 use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\CannotUpdateDiscountException;
@@ -314,4 +315,33 @@ protected function bulkUpdateStatus(Request $request, bool $enable): RedirectRes
 
         return $this->redirectToRoute('admin_discounts_index');
     }
+
+    /**
+     * Processes bulk discounts deleting.
+     *
+     * @param Request $request
+     *
+     * @return RedirectResponse
+     */
+    #[DemoRestricted(redirectRoute: 'admin_discounts_index')]
+    #[AdminSecurity("is_granted('delete', request.get('_legacy_controller'))", redirectRoute: 'admin_discounts_index', message: 'You do not have permission to delete this.')]
+    public function bulkDeleteAction(Request $request): RedirectResponse
+    {
+        try {
+            $discountIds = $this->getBulkDiscountsFromRequest($request);
+
+            $command = new BulkDeleteDiscountsCommand($discountIds);
+
+            $this->dispatchCommand($command);
+
+            $this->addFlash(
+                'success',
+                $this->trans('The selection has been successfully deleted.', [], 'Admin.Notifications.Success')
+            );
+        } catch (DiscountException $e) {
+            $this->addFlash('error', $this->getErrorMessageForException($e, $this->getErrorMessages($e)));
+        }
+
+        return $this->redirectToRoute('admin_discounts_index');
+    }
 }
diff --git a/src/PrestaShopBundle/Resources/config/routing/admin/sell/catalog/discount.yml b/src/PrestaShopBundle/Resources/config/routing/admin/sell/catalog/discount.yml
index 84621b1719689..e036a662effff 100644
--- a/src/PrestaShopBundle/Resources/config/routing/admin/sell/catalog/discount.yml
+++ b/src/PrestaShopBundle/Resources/config/routing/admin/sell/catalog/discount.yml
@@ -68,6 +68,15 @@ admin_discount_bulk_disable_status:
     _legacy_feature_flag: discount
     _legacy_link: AdminCartRules:statuscart_rule
 
+admin_discount_bulk_delete:
+  path: /bulk-delete
+  methods: [ POST ]
+  defaults:
+    _controller: PrestaShopBundle\Controller\Admin\Sell\Catalog\DiscountController::bulkDeleteAction
+    _legacy_controller: AdminCartRules
+    _legacy_feature_flag: discount
+    _legacy_link: AdminCartRules:submitBulkdeletecart_rule
+
 admin_discounts_delete:
   path: /{discountId}/delete
   methods: [ POST, DELETE ]
diff --git a/src/PrestaShopBundle/Resources/config/services/adapter/discount.yml b/src/PrestaShopBundle/Resources/config/services/adapter/discount.yml
index 98aaabb022c06..4d195d67bcb3a 100644
--- a/src/PrestaShopBundle/Resources/config/services/adapter/discount.yml
+++ b/src/PrestaShopBundle/Resources/config/services/adapter/discount.yml
@@ -18,6 +18,9 @@ services:
   PrestaShop\PrestaShop\Adapter\Discount\CommandHandler\BulkUpdateDiscountsStatusHandler:
     autoconfigure: true
 
+  PrestaShop\PrestaShop\Adapter\Discount\CommandHandler\BulkDeleteDiscountsHandler:
+    autoconfigure: true
+
   PrestaShop\PrestaShop\Adapter\Discount\Update\Filler\DiscountFiller:
     autoconfigure: true