2

I have the following task:

"Whenever a client is in a certain customer group, he needs to see a different price for the product in question."

Now, we all know we need to use tier prices for this (Magentos' official documentation also states this), but most of us will know that this also comes with the neat little feature of Magento that it will always show the best deal to the customer.

So above solutions works when a customer groups' price is lower than the original one, but in my situation the price of the customer group is higher than the original price.

For example, I have a product that costs €6.00. But a client in customer group X needs to pay €11.00 for it.

So the most obvious thing to do now is to manipulate the price at run-time, to bypass this behaviour. I've done this plenty of times in Magento 1 (never in Magento 2), and one thing I've learned is that this technique should be considered as a last resort.

Things that need to be considered are then:

  • Price on the product detail page.
  • Price in the grid.
  • Price filter (when searching / sorting).
  • Tax values.
  • Other rules (like discount rules and stuff).
  • Transforming Product Model to Quote Item.
  • Order creation from the admin.
  • Etc.

Now for my question: What's the best way to do this?

I'm currently trying the approach by catching the catalog_product_load_after-event, and manipulate the product model by using the setPrice()-method:

/**
 * Class CatalogProductLoadAfter
 */
class CatalogProductLoadAfter implements ObserverInterface
{
    /**
     * @var Session
     */
    protected $customerSession;

    /**
     * CatalogProductLoadAfter constructor.
     */
    public function __construct(
        Session $customerSession
    ) {
        $this->customerSession = $customerSession;
    }

    /**
     * @param Observer $observer
     * @return mixed
     */
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        if ($this->customerSession->isLoggedIn()) {
            $product = $observer->getEvent()->getData('product');

            $tierPrices = $product->getTierPrices();

            foreach ($tierPrices as $tierPrice) {
                if (
                    $tierPrice->getCustomerGroupId() === $this->customerSession->getCustomerGroupId() &&
                    (int) $tierPrice->getQty() === 1
                ) {
                    $product->setPrice($tierPrice->getValue());
                }
            }
        }
    }
}

I know in Magento 1 you had the setCustomPrice()-method, but this seems to be no longer existent in Magento 2.

I'm also using the checkout_cart_product_add_after-event to manipulate the price on the quote item in a similar fashion:

if ($this->customerSession->isLoggedIn()) {
    $quoteItem = $observer->getEvent()->getData('quote_item');
    $quoteItem = $quoteItem->getParentItem() ?: $quoteItem;
    $tierPrices = $quoteItem->getProduct()->getTierPrices();

    foreach ($tierPrices as $tierPrice) {
        if (
            $tierPrice->getCustomerGroupId() === $this->customerSession->getCustomerGroupId() &&
            (int) $tierPrice->getQty() === 1
        ) {
            $quoteItem->setData('custom_price', $tierPrice->getValue());
            $quoteItem->setData('original_custom_price', $tierPrice->getValue());
            $quoteItem->getProduct()->setData('is_super_mode', true);
        }
    }
}

For the price in the grid / collections I'm manipulating the price indexing process to change the indexed prices (since those are the prices used when building collections):

di.xml:

<!--
    Add plugin to price indexer to change prices for customer groups:
-->
<type name="Magento\Catalog\Model\Indexer\Product\Price">
    <plugin name="change_price_for_customer_group"
            type="Vendor\Module\Plugin\Magento\Catalog\Model\Indexer\Product\Price"/>
</type>

Price.php:

/**
 * Class Price
 */
class Price
{
    /**
     * @var \Magento\Framework\DB\Adapter\AdapterInterface
     */
    protected $connection;

    /**
     * Price constructor.
     */
    public function __construct(
        ResourceConnection $resource
    ) {
        $this->connection = $resource->getConnection();
    }

    /**
     * @param \Magento\Catalog\Model\Indexer\Product\Price $subject
     * @param callable $proceed
     * @param int $id
     */
    public function aroundExecuteRow(\Magento\Catalog\Model\Indexer\Product\Price $subject, callable $proceed, int $id)
    {
        $proceed($id);
        $this->updateIndexedPrice($id);
    }

    /**
     * @param \Magento\Catalog\Model\Indexer\Product\Price $subject
     * @param callable $proceed
     * @param array $ids
     */
    public function aroundExecuteList(
        \Magento\Catalog\Model\Indexer\Product\Price $subject,
        callable $proceed,
        array $ids
    ) {
        $proceed($ids);
        foreach ($ids as $id) {
            $this->updateIndexedPrice($id);
        }
    }

    /**
     * @param \Magento\Catalog\Model\Indexer\Product\Price $subject
     * @param callable $proceed
     * @param array $ids
     */
    public function aroundExecute(
        \Magento\Catalog\Model\Indexer\Product\Price $subject,
        callable $proceed,
        array $ids
    ) {
        $proceed($ids);
        foreach ($ids as $id) {
            $this->updateIndexedPrice($id);
        }
    }

    /**
     * @param \Magento\Catalog\Model\Indexer\Product\Price $subject
     */
    public function afterExecuteFull(
        \Magento\Catalog\Model\Indexer\Product\Price $subject
    ) {
        // Fetch all product ID's:
        $ids = $this->connection->fetchCol(
            $this->connection->select()
                ->distinct()
                ->from($this->connection->getTableName('catalog_product_index_price'), 'entity_id')
        );
        foreach ($ids as $id) {
            $this->updateIndexedPrice($id);
        }
    }

    /**
     * Update the price for a single product
     * @param int $id
     */
    protected function updateIndexedPrice(int $id)
    {
        $this->updateIndexedPriceInDatabase($id, 'catalog_product_index_price');
        $this->updateIndexedPriceInDatabase($id, 'catalog_product_index_price_idx');
    }

    /**
     * @param int $id
     * @param string $tableName
     */
    protected function updateIndexedPriceInDatabase(int $id, string $tableName)
    {
        $rows = $this->connection->fetchAll(
            $this->connection->select()
                ->from($this->connection->getTableName($tableName))
                ->where('entity_id = ?', $id),
            [],
            \PDO::FETCH_ASSOC
        );

        foreach ($rows as $row) {
            if (!empty($row['tier_price']) && 
                (float) $row['tier_price'] > (float) $row['price']) {
                // Update all indexed prices:
                $this->connection->update(
                    $this->connection->getTableName($tableName),
                    [
                        'price'       => $row['tier_price'],
                        'final_price' => $row['tier_price'],
                        'min_price'   => $row['tier_price'],
                        'max_price'   => $row['tier_price']
                    ],
                    [
                        'entity_id = ?'         => $id,
                        'customer_group_id = ?' => $row['customer_group_id'],
                        'website_id = ?'        => $row['website_id'],
                        'tax_class_id = ?'      => $row['tax_class_id']
                    ]
                );
            }
        }
    }
}

Regardless of the other tasks I'm about to have (like dealing with special prices and price rules), what are other people experiences on this? Do you use a similar approach? Would you suggest something totally different? I'd really like to know what's the proper way how to handle this task.

  • Did you consider removing the "tier prices must be lower than original price" rule somehow? I remember doing something similar in M1, but don't have the details right now. – Fabian Schmengler Jan 28 '17 at 1:03
  • @Fabian: no, I tracked it down to it being a clause in the generated SQL query, but it's a very deep nestled function. – Giel Berkers Jan 28 '17 at 6:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.