Skip to content

7 Rebate Policy

F-WebShop

In this guide we will explore 3 available built in ways to define price for specific user/product and combination of two or more.

There is possibility to override product price in custom plugin, which is described in chapter "Integration with erp".

1. Define same product price for all users

If you only want to define price for product that is same for every user(logged and anonymous user) than this is the way to go. You only need to retrieve(or create new) product entity from database and set current price.

<?php

use Alligator\Model\ProductBundle\Entity\Product;

    ...

    // creating new product
    $product = new Product();
    // or retrieve from database
    $product = $productRepository->findOneById(/** productId */);

    ...

    // set price to product entity
    $product->setCurrentPrice(/** new product price */);

    // you can set price on action too
    $product->setOnAction(true);
    $product->setPriceOnAction(/** new price on action */);

    ...

You can also create new (default) pricelist and define prices for products there(although this will have same result, but will work slightly slower).

<?php

use Alligator\Model\ProductBundle\Entity\Product;
use Alligator\Model\PricelistBundle\Entity\Pricelist;
use Alligator\Model\PricelistBundle\Entity\PricelistItem;

    ...

    // first you will need to create default pricelist
    $defaultPricelist = new Pricelist();

    // and then save pricelist item
    $pricelistItem = new PricelistItem();
    $pricelistItem->setProduct(/** reference to database product */);
    $pricelistItem->setPricelist($defaultPricelist);
    $pricelistItem->setPrice(/** new price */);
    $pricelistItem->setActionPrice(/** new action price if exist */);

    // next update price on product entity - field pricelistItemsArray
    $pricelistItemArray = $product->getPricelistItemsArray() ?: [];
    foreach ($pricelistItemArray as $key => $value) {
        if ($pricelistItem->getId() == $value['id']) {
            $pricelistItemArray[$key][$field] = $pricelistItem->$getter();
            $found = true;
            break;
        }
    }
    if (!$found) {
        $pricelistItemArray[] = [
            'id'                 => (string) $pricelistItem->getId(),
            'pricelistId'        => (string) $pricelistItem->getPricelist()->getId(),
            'pricelistDiscount'  => $pricelistItem->getPricelist()->getDiscount(),
            'pricelistDiscounts' => $pricelistItem->getPricelist()->getDiscounts(),
            'isDefault'          => $pricelistItem->getPricelist()->isDefault() ? '1' : '0',
            'price'              => $pricelistItem->getPrice(),
            'actionPrice'        => $pricelistItem->getActionPrice() ?: 0,
        ];
    }
    $product->setPricelistItemsArray($pricelistItemArray);

    // and if it is default pricelist update fields
    $product->setCurrentPrice($pricelistItem->getPrice());
    $product->setPriceOnAction($pricelistItem->getActionPrice());
    $product->setOnAction($pricelistItem->getActionPrice() > 0);
    $product->setDefaultPricelistDiscount($pricelistItem->getPricelist()->getDiscount());

    // and if price is changed for product dispatch event
    $this->container->get('alligator.product.event.container')->dispatchProductPriceChangedEvent($product, $oldPrice, $lowerPrice, [], $pricelistReference);

Method responsible for handling changing price for pricelist item/product is done in PricelistItemManager class in handlePersist method.

<?php

/**
 * Class PricelistItemManager
 */
class PricelistItemManager implements BasicEntityManagerInterface
{

    ...

    /**
     * Save pricelist item - price for product for pricelist
     * If pricelist is default update product and pav
     */
    public function handlePersist($data)
    {
        /** @var PricelistItem $pricelistItem */
        $pricelistItem = $this->repository->getPricelistItemByProductAndList($data);
        $field = $data['field'];
        $setter = 'set'.ucfirst($field);
        $getter = 'get'.ucfirst($field);
        $pricelistItem->$setter($data['value']);
        /** @var Product $product */
        $product = $this->getProductReference($data['productId']);
        /** @var Pricelist $pricelistReference */
        $pricelistReference = $this->getPricelistReference($data['pricelistId']);

        if ($pricelistItem->getId()) {
            $pricelistItem = $this->repository->edit($pricelistItem);
        } else {
            $pricelistItem->setProduct($product);
            $pricelistItem->setPricelist($pricelistReference);
            $pricelistItem = $this->repository->save($pricelistItem);
        }

        /** If pricelistItem is saved successfully, update product($pricelistItemsArray) */
        if ($pricelistItemId = $pricelistItem->getId()) {
            $found = false;
            $pricelistItemArray = $product->getPricelistItemsArray() ?: [];

            foreach ($pricelistItemArray as $key => $value) {
                if ($pricelistItemId == $value['id']) {
                    $pricelistItemArray[$key][$field] = $pricelistItem->$getter();
                    $found = true;
                    break;
                }
            }

            if (!$found) {
                $pricelistItemArray[] = [
                    'id'                 => (string) $pricelistItemId,
                    'pricelistId'        => (string) $pricelistItem->getPricelist()->getId(),
                    'pricelistDiscount'  => $pricelistItem->getPricelist()->getDiscount(),
                    'pricelistDiscounts' => $pricelistItem->getPricelist()->getDiscounts(),
                    'isDefault'          => $pricelistItem->getPricelist()->isDefault() ? '1' : '0',
                    'price'              => $pricelistItem->getPrice(),
                    'actionPrice'        => $pricelistItem->getActionPrice() ?: 0,
                ];
            }

            /** If pricelist is default, set current price to product */
            if ($pricelistReference->isDefault()) {
                $lowerPrice = false;
                $oldPrice = false;
                /** $pricelistItem->getActionPrice() > 0 -> IF TRUE THEN PRODUCT IS ON ACTION */
                if ($pricelistItem->getActionPrice() > 0) {
                    if (!$product->isOnAction()) {
                        $lowerPrice = $product->getCurrentPrice() > $pricelistItem->getActionPrice();
                        $oldPrice = $product->getCurrentPrice();
                    } elseif (($currentPriceValue = $pricelistItem->getActionPrice()) != $product->getPriceOnAction()) {
                        $lowerPrice = $product->getPriceOnAction() > $currentPriceValue;
                        $oldPrice = $product->getPriceOnAction();
                    }
                } elseif ($product->isOnAction()) {
                    $lowerPrice = $product->getPriceOnAction() > $pricelistItem->getPrice();
                    $oldPrice = $product->getPriceOnAction();
                } elseif (($currentPriceValue = $pricelistItem->getPrice()) != $product->getCurrentPrice()) {
                    $lowerPrice = $product->getCurrentPrice() > $currentPriceValue;
                    $oldPrice = $product->getCurrentPrice();
                }

                $product->setCurrentPrice($pricelistItem->getPrice());
                $product->setPriceOnAction($pricelistItem->getActionPrice());
                $product->setOnAction($pricelistItem->getActionPrice() > 0);
                $product->setDefaultPricelistDiscount($pricelistItem->getPricelist()->getDiscount());

                $pavUpdate = null;
                /** @var ProductAttributeValues $pav */
                foreach ($product->getProductAttributesValues() as $pav) {
                    if ($pav->getDefault()) {
                        $pav->setCost($pricelistItem->getPrice());
                        $pricelistItem->getActionPrice() > 0 ? $pav->setPriceOnAction($pricelistItem->getActionPrice()) : false;
                        $pavUpdate = $pav;
                        break;
                    }
                }

                if ($pavUpdate) {
                    $this->repository->flush($pavUpdate);
                }

                $this->container->get('alligator.product.event.container')->dispatchProductPriceChangedEvent($product, $oldPrice, $lowerPrice, [], $pricelistReference);
            }

            /** Save changed to product */
            $product->setPricelistItemsArray($pricelistItemArray);
            $this->repository->flush($product);

            $this->container->get('empire_promo_products_plugin.promo_products_manager')->deleteResultCaches();
        }

        return $pricelistItem;
    }

    ...

}

2. Define different product prices for different users

In case you have multiple pricelists, you can define specific pricelist that will every user have(if not specified, default pricelist will be taken into consideration).

Additional to defining specific pricelist for user, you can define specific discount on pricelist for that user too. This discount is stored on User entity on userSpecificPricelists property and on UserSpecificPricelist entity.

<?php

use Alligator\Model\PricelistBundle\Entity\UserSpecificPricelist;

    ...

    $userSpecificPricelist = new UserSpecificPricelist();
    $userSpecificPricelist->setPriority(0);
    $userSpecificPricelist->setUser(/** user reference */);
    $userSpecificPricelist->setPricelist(/** pricelist reference */);

    $userSpecificPricelist->setDiscount(/** discount value for 1 product */);
    $userSpecificPricelist->setDiscounts(array(
        /** array with keys id, discount, discounts, isUserSpecific and currency and corresponding values */
    ));

    ...

This logic is done in UserSpecificPricelistManager class in editUserSpecificPricelist method.

<?php

/**
 * Class UserSpecificPricelistManager
 */
class UserSpecificPricelistManager implements BasicEntityManagerInterface
{
    ...

    /**
     * Edit user specific pricelists
     */
    public function editUserSpecificPricelist($data, $userId, $json)
    {
        $jsonArray = [];
        /** @var UserSpecificPricelist $userSpecific */
        foreach ($json as $userSpecific) {
            $pricelistId = $userSpecific['pricelist']['id'];
            /** @var Pricelist $pricelist */
            $pricelist   = $this->pricelistManager->getPricelistForId($pricelistId);

            $jsonArray[] = [
                'id'       => (string) $pricelistId,
                'discount' => $userSpecific['pricelist']['discount'],
                'discounts' => $userSpecific['pricelist']['discounts'],
                'isUserSpecific' => $userSpecific['pricelist']['isUserSpecific'] ? 1 : 0,
                'currency' => $pricelist->getCurrency(),
            ];
        }

        return $this->repository->editUserSpecificPricelist($data, $userId, $jsonArray);
    }

    ...
}

3. Define different product price for group of products

Define different product price for group of products

In some cases you will need to group multiple products together and give them specific discount(percent or fixed discount). In order to do this, you will first need to create product group and add some products(you can do this by selecting products manually, selecting category or setting parameters that product should math).

<?php

use Alligator\Model\ProductBundle\Entity\ProductGroup;
use Alligator\Model\ProductBundle\Entity\ProductGroupDiscountUser;

    ...

    // initiate product group
    $productGroup = new ProductGroup();

    // define basic information
    $productGroup->setName($key);
    $productGroup->setPriority(1);
    $productGroup->setIsActive(true);

    // define which products should be added to group
    $productGroup->addCategory(/** add all products from provided category */);
    $productGroup->addBrand(/** add all products that belong to provided brand */);
    $productGroup->setCustomProducts(/** array of ids of custom selected products */);
    $productGroup->setProducts(/** array of all ids of products in group */);

    // set percent discount
    $productGroup->setDiscounts(/** array of discounts */);
    $productGroup->setBaseDiscount(/** discount for 1 product */);

    // set fixed discount
    $productGroup->setFixedDiscountsArray(/** array of discounts */);
    $productGroup->setBaseFixedDiscount(/** discount for 1 product */);      

    ...

Define different product price for group of products depending on user

Sometimes, depending on logged user and his current pricelist, you may want to give different discount for products in group.

<?php

use Alligator\Model\ProductBundle\Entity\ProductGroupDiscountUser;

    ...

    // initiate product group discount user
    $productGroupDiscount = new ProductGroupDiscountUser();

    // set base information
    $productGroupDiscount->setProductGroup(/** product group reference */);
    $productGroupDiscount->setBaseDiscount(/** discount for 1 product */);
    $productGroupDiscount->setDiscounts(/** array of discounts */);

    // define users and pricelist for discount
    $productGroupDiscount->setUsers(/** users references */);
    $productGroupDiscount->setLimitToPricelist(/** pricelist references */);

    ...

Define correlation between discounts on pricelist and product group

You can customize how program will calculate price if product is located in one or more groups. Currently you can chose between 3 options(chose max discount, sum of all discounts or calculate cascade discount).

<?php

use Alligator\Model\SettingsBundle\Entity\ProductSettings;

    ...

    // retrieve current product settings
    $productSettings = $productSettingsRepository->getCurrentProductSettings();

    // and set desire discount type
    $productSettings->setCalculateMultipleGroupDiscounts(ProductSettingsManager::DISCOUNT_TYPE_SUM);
    $productSettings->setCalculatePricelistGroupDiscounts(ProductSettingsManager::DISCOUNT_TYPE_SUM);

    ...

4. Calculating price for product

Calculating price for product

Every time before product is added to cart, price for that product is calculated based on many parameters.

First we need to retrieve base price for product. This is done by retrieving base price for product based on pricelist selected for logged user(if no pricelist is defined for user or no user is logged in, default pricelist will be taken into consideration).

Next step is to calculate adjustments for specific order item. This is done based on discounts defined on pricelists, for user or product groups.

Calculating price for product(in code)

First calculation is done in getProductPrice method of class CheckoutManager and it retrieves price for product based on pricelist defined for user.

<?php

namespace Alligator\Business\Manager\Checkout;

use Alligator\Model\ProductBundle\Entity\Product;
use Alligator\Model\OrderBundle\Entity\Order;

/**
 * Class CheckoutManager
 */
class CheckoutManager implements CheckoutManagerInterface, BasicEntityManagerInterface, JQGridInterface
{

    ...

    /**
     * Retrieve price for provided product and order
     * @param Product $productDB
     * @param Order   $order
     *
     * @return float|null
     */
    public function getProductPrice($productDB, $order)
    {
        $defaultCurrency = SessionParamListener::CURRENCY_DEFAULT_MONEY;

        if (!is_null($order->getCurrentPricelist()) && \is_array($productDB->getPricelistItemsArray())) {
            /** @var Pricelist $currentPricelist */
            $currentPricelist = $order->getCurrentPricelist();
            foreach ($productDB->getPricelistItemsArray() as $productPricelistJSON) {
                if ($currentPricelist->getId() == $productPricelistJSON['pricelistId']) {
                    $price = $productPricelistJSON['actionPrice'] ?: $productPricelistJSON['price'];

                    if ($currentPricelist->getCurrency() == $defaultCurrency) {
                        return $price;
                    }

                    return $this->eventContainer->convertMoney($price, $currentPricelist->getCurrency(), $defaultCurrency);
                }
            }
        }

        /** @var User $user */
        if (($user = $this->getLoggedUser()) && \is_array($user->getPricelistItemsArray()) && \is_array($productDB->getPricelistItemsArray())) {
            foreach ($user->getPricelistItemsArray() as $userPricelistJSON) {
                foreach ($productDB->getPricelistItemsArray() as $productPricelistJSON) {
                    if ($userPricelistJSON['id'] == $productPricelistJSON['pricelistId']) {
                        $defaultCurrency = SessionParamListener::CURRENCY_DEFAULT_MONEY;
                        $price = $productPricelistJSON['actionPrice'] ?: $productPricelistJSON['price'];

                        if ($userPricelistJSON['currency'] == $defaultCurrency) {
                            return $price;
                        }

                        return $this->eventContainer->convertMoney($price, $userPricelistJSON['currency'], $defaultCurrency);
                    }
                }
            }
        }

        return $productDB->getPriceOnAction() ?: $productDB->getCurrentPrice();
    }

    ...

}

Second calculation is done in checkOrderItemAdjustments method of class CheckoutManager and it calculates adjustments for product based on discounts for pricelist/product groups.

<?php

namespace Alligator\Business\Manager\Checkout;

use Alligator\Model\ProductBundle\Entity\Product;
use Alligator\Model\OrderBundle\Entity\Order;

/**
 * Class CheckoutManager
 */
class CheckoutManager implements CheckoutManagerInterface, BasicEntityManagerInterface, JQGridInterface
{

    ...

    /**
     * Calculate item adjustments for order
     * @param OrderItem $orderItem
     *
     * @return mixed
     */
    public function checkOrderItemAdjustments($orderItem)
    {
        /** ------ PRICE LIST --------------------------------------------------------------------------------------  */
        $pricelistDiscount = $this->eventContainer->getTotalPricelistDiscount($orderItem->getProduct()->getPricelistItemsArray(), $orderItem->getQuantity());
        $this->changeAdjustmentValues($orderItem, OrderItemInterface::PRODUCT_PRICE_LIST_DISCOUNT, $pricelistDiscount);

        /** ------ GROUPS ------------------------------------------------------------------------------------------- */
        $groupDiscounts          = $this->eventContainer->getTotalGroupDiscount($orderItem->getProduct()->getId(), $orderItem->getQuantity());
        $groupDiscountTotal      = $groupDiscounts['percent'];
        $groupDiscountFixedTotal = $groupDiscounts['fixed'];

        $this->changeAdjustmentValues($orderItem, OrderItemInterface::PRODUCT_GROUP_DISCOUNT, $groupDiscountTotal);
        $this->changeAdjustmentValues($orderItem, OrderItemInterface::PRODUCT_GROUP_FIXED_DISCOUNT, $groupDiscountFixedTotal, true, true);

        /** ------ CHECK ALL DISCOUNTS ------------------------------------------------------------------------------ */
        $pricelistAdjustment      = $orderItem->getAdjustments(OrderItemInterface::PRODUCT_PRICE_LIST_DISCOUNT)->first();
        $groupAdjustment          = $orderItem->getAdjustments(OrderItemInterface::PRODUCT_GROUP_DISCOUNT)->first();
        $groupFixedAdjustment     = $orderItem->getAdjustments(OrderItemInterface::PRODUCT_GROUP_FIXED_DISCOUNT)->first();
        $allAdjustmentTypesActive = false;
        $unitTimesQuantity        = $orderItem->getUnitPrice() * $orderItem->getQuantity();

        /** if only one type of discount exists set it to be active */
        if ($pricelistAdjustment && !$groupFixedAdjustment && !$groupAdjustment) {
            $pricelistAdjustment->setNeutral(false);
        } elseif ($groupAdjustment && !$groupFixedAdjustment && !$pricelistAdjustment) {
            $groupAdjustment->setNeutral(false);
        } elseif ($groupFixedAdjustment && !$pricelistAdjustment && !$groupAdjustment) {
            $groupFixedAdjustment->setNeutral(false);
        }

        /** if all types exist check settings. If discount between groups is set to max we can reduce this case to a simpler one.
            CASE 1) If fixed discount is grated than percent do not use percent discount in further calculations.
            CASE 2) If percent discount is grated than fixed do not use fixed discount in further calculations. */
        if ($pricelistAdjustment && $groupAdjustment && $groupFixedAdjustment) {
            if ($this->eventContainer->getMultipleGroupsDiscountType() === ProductSettingsManager::DISCOUNT_TYPE_MAX) {
                if ($unitTimesQuantity * (1 - $groupDiscountTotal/100) < $unitTimesQuantity - $groupDiscountFixedTotal) {
                    $groupFixedAdjustment->setNeutral(true);
                    $groupFixedAdjustment = false;
                } else {
                    $groupAdjustment->setNeutral(true);
                    $groupAdjustment = false;
                }
            } else {
                $allAdjustmentTypesActive = true;
            }
        }

        /** Group discounts are active but pricelist is not */
        if ($groupAdjustment && $groupFixedAdjustment && !$pricelistAdjustment) {
            if ($this->eventContainer->getMultipleGroupsDiscountType() === ProductSettingsManager::DISCOUNT_TYPE_MAX) {
                if ($unitTimesQuantity * (1 - $groupDiscountTotal/100) < $unitTimesQuantity - $groupDiscountFixedTotal) {
                    $groupAdjustment->setNeutral(false);
                    $groupFixedAdjustment->setNeutral(true);
                } else {
                    $groupAdjustment->setNeutral(true);
                    $groupFixedAdjustment->setNeutral(false);
                }
            } else {
                $groupAdjustment->setNeutral(false);
                $groupFixedAdjustment->setNeutral(false);
            }
        }

        /** Group fixed discount and pricelist discount are active or fixed discount is greater than percent (See case 1.) */
        if ($pricelistAdjustment && $groupFixedAdjustment && !$groupAdjustment) {
            if ($this->eventContainer->getCalculatePricelistGroupDiscounts() === ProductSettingsManager::DISCOUNT_TYPE_MAX) {
                if ($unitTimesQuantity * (1 - $pricelistDiscount/100) < $unitTimesQuantity - $groupDiscountFixedTotal) {
                    $groupAdjustment->setNeutral(false);
                    $groupFixedAdjustment->setNeutral(true);
                } else {
                    $groupAdjustment->setNeutral(true);
                    $groupFixedAdjustment->setNeutral(false);
                }
            } else {
                $pricelistAdjustment->setNeutral(false);
                $groupFixedAdjustment->setNeutral(false);
            }
        }

        /** All discounts are active or fixed discount is less then perecent (see case 2.)
            PRODUCT_PRICE_LIST_AND_GROUP_DISCOUNT is used to calculate final sum of all percent discounts */
        if (true === $allAdjustmentTypesActive || ($pricelistAdjustment && $groupAdjustment && !$groupFixedAdjustment)) {
            switch ($this->eventContainer->getCalculatePricelistGroupDiscounts()) {
                case ProductSettingsManager::DISCOUNT_TYPE_CASCADE:
                    $totalPricelistAndGroupDiscount = 100 - (100 - $groupDiscountTotal) * (100 - $pricelistDiscount) / 100;
                    $groupFixedAdjustment ? $groupFixedAdjustment->setNeutral(false) : false;
                    break;
                case ProductSettingsManager::DISCOUNT_TYPE_MAX:
                    if ($groupFixedAdjustment > $groupDiscountTotal && $groupFixedAdjustment > $pricelistDiscount) {
                        $groupFixedAdjustment ? $groupFixedAdjustment->setNeutral(false) : false;
                        $totalPricelistAndGroupDiscount = 0;
                    } else {
                        $totalPricelistAndGroupDiscount = $groupDiscountTotal < $pricelistDiscount ? $pricelistDiscount : $groupDiscountTotal;
                    }

                    break;
                case ProductSettingsManager::DISCOUNT_TYPE_SUM:
                default:
                    $sum = $groupDiscountTotal + $pricelistDiscount;
                    $totalPricelistAndGroupDiscount = $sum > 100 ? 100 : $sum;
                    $groupFixedAdjustment ? $groupFixedAdjustment->setNeutral(false) : false;
                    break;
            }

            if ($totalPricelistAndGroupDiscount) {
                $groupAdjustment->setNeutral(true);
                $pricelistAdjustment->setNeutral(true);
            }

            $this->changeAdjustmentValues($orderItem, OrderItemInterface::PRODUCT_PRICE_LIST_AND_GROUP_DISCOUNT, $totalPricelistAndGroupDiscount, false);
        } else {
            $orderItem->removeAdjustments(OrderItemInterface::PRODUCT_PRICE_LIST_AND_GROUP_DISCOUNT);
        }

        /** there are no discounts for price and group but there is discount on product for quantity */
        if ((!$pricelistAdjustment && !$groupAdjustment && !$groupFixedAdjustment) && ($qtyRebate = $orderItem->getProduct()->getQuantityRebate())) {
            $adjustments = $orderItem->getAdjustments(OrderItemInterface::PRODUCT_QUANTITY_REBATE);
            if ($adjustments->count() && reset($qtyRebate)['amount'] > $orderItem->getQuantity()) {
                $orderItem->removeAdjustments(OrderItemInterface::PRODUCT_QUANTITY_REBATE);
            } else {
                foreach (array_reverse($qtyRebate) as $rebate) {
                    if ($rebate['amount'] <= $orderItem->getQuantity()) {
                        if ($adjustments->count()) {
                            /** @var Adjustment $productQtyRebate */
                            $productQtyRebate = $adjustments->first();
                            $productQtyRebate->setAmount(-$rebate['rebate']);
                        } else {
                            $productQtyRebate = (new Adjustment())->setLabel(OrderItemInterface::PRODUCT_QUANTITY_REBATE)->setAmount(-$rebate['rebate'])->setNeutral(false);
                            $orderItem->addAdjustment($productQtyRebate);
                        }
                        break;
                    }
                }
            }
        } else {
            $orderItem->removeAdjustments(OrderItemInterface::PRODUCT_QUANTITY_REBATE);
        }

        return $orderItem;
    }

    ...

}