<?php
/**
* @version   $Id: order_calculator.php 16516 2017-01-20 14:12:22Z alex $
* @package   In-Commerce
* @copyright   Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license   Commercial License
* This software is protected by copyright law and international treaties.
* Unauthorized reproduction or unlicensed usage of the code of this program,
* or any portion of it may result in severe civil and criminal penalties,
* and will be prosecuted to the maximum extent possible under the law
* See http://www.in-portal.org/commercial-license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	/**
	 * Performs order price calculations
	 *
	 */
	class OrderCalculator extends kBase {

		/**
		 * Order manager instance
		 *
		 * @var OrderManager
		 */
		protected $manager = null;

		/**
		 * Items, associated with current order
		 *
		 * @var Array
		 */
		protected $items = Array ();

		/**
		 * Creates new clean instance of calculator
		 *
		 */
		public function __construct()
		{
			parent::__construct();

			$this->reset();
		}

		/**
		 * Sets order manager instance to calculator
		 *
		 * @param OrderManager $manager
		 */
		public function setManager(&$manager)
		{
			$this->manager =& $manager;
		}

		public function reset()
		{
			$this->items = Array ();
		}

		/**
		 * Returns order object used in order manager
		 *
		 * @return OrdersItem
		 */
		protected function &getOrder()
		{
			$order =& $this->manager->getOrder();

			return $order;
		}

		/**
		 * Sets checkout error
		 *
		 * @param int $error_type = {product,coupon,gc}
		 * @param int $error_code
		 * @param int $product_id - {ProductId}:{OptionsSalt}:{BackOrderFlag}:{FieldName}
		 * @return void
		 * @access protected
		 */
		protected function setError($error_type, $error_code, $product_id = null)
		{
			$this->manager->setError($error_type, $error_code, $product_id);
		}

		/**
		 * Perform order calculations and prepares operations for order manager
		 *
		 */
		public function calculate()
		{
			$this->queryItems();
			$this->groupItems();

			$this->generateOperations();
			$this->applyWholeOrderFlatDiscount();
		}

		/**
		 * Groups order items, when requested
		 *
		 * @return Array
		 */
		protected function groupItems()
		{
			$skipped_items = Array ();

			foreach ($this->items as $item_id => $item_data) {
				if ( in_array($item_id, $skipped_items) ) {
					continue;
				}

				$group_items = $this->getItemsToGroupWith($item_id);

				if (!$group_items) {
					continue;
				}

				foreach ($group_items as $group_item_id) {
					$this->items[$item_id]['Quantity'] += $this->items[$group_item_id]['Quantity'];
					$this->items[$group_item_id]['Quantity'] = 0;
				}

				$skipped_items = array_merge($skipped_items, $group_items);
			}
		}

		/**
		 * Returns order item ids, that can be grouped with given order item id
		 *
		 * @param int $target_item_id
		 * @return Array
		 * @see OrderCalculator::canBeGrouped
		 */
		protected function getItemsToGroupWith($target_item_id)
		{
			$ret = Array ();

			foreach ($this->items as $item_id => $item_data) {
				if ( $this->canBeGrouped($this->items[$item_id], $this->items[$target_item_id]) ) {
					$ret[] = $item_id;
				}
			}

			return array_diff($ret, Array ($target_item_id));
		}

		/**
		 * Checks if 2 given order items can be grouped together
		 *
		 * @param Array $src_item
		 * @param Array $dst_item
		 * @return bool
		 */
		public function canBeGrouped($src_item, $dst_item)
		{
			if ($dst_item['Type'] != PRODUCT_TYPE_TANGIBLE) {
				return false;
			}

			return ($src_item['ProductId'] == $dst_item['ProductId']) && ($src_item['OptionsSalt'] == $dst_item['OptionsSalt']);
		}

		/**
		 * Retrieves order contents from database
		 *
		 */
		protected function queryItems()
		{
			$poc_table = $this->Application->getUnitOption('poc', 'TableName');

			$query = '	SELECT 	oi.ProductId, oi.OptionsSalt, oi.ItemData, oi.Quantity,
								IF(p.InventoryStatus = ' . ProductInventory::BY_OPTIONS . ', poc.QtyInStock, p.QtyInStock) AS QtyInStock,
								p.QtyInStockMin, p.BackOrder, p.InventoryStatus,
								p.Type, oi.OrderItemId
						FROM ' . $this->getTable('orditems') . ' AS oi
						LEFT JOIN ' . TABLE_PREFIX . 'Products AS p ON oi.ProductId = p.ProductId
						LEFT JOIN ' . $poc_table . ' poc ON (poc.CombinationCRC = oi.OptionsSalt) AND (oi.ProductId = poc.ProductId)
						WHERE oi.OrderId = ' . $this->getOrder()->GetID();

			$this->items = $this->Conn->Query($query, 'OrderItemId');
		}

		/**
		 * Generates operations and returns true, when something was changed
		 *
		 * @return bool
		 */
		protected function generateOperations()
		{
			$this->manager->resetOperationTotals();

			foreach ($this->items as $item) {
				$this->ensureMinQty($item);

				$to_order = $back_order = 0;
				$available = $this->getAvailableQty($item);

				if ( $this->allowBackordering($item) ) {
					// split order into order & backorder
					if ($item['BackOrder'] == ProductBackorder::ALWAYS) {
						$to_order = $available = 0;
						$back_order = $item['Quantity'];
					}
					elseif ($item['BackOrder'] == ProductBackorder::AUTO) {
						$to_order = $available;
						$back_order = $item['Quantity'] - $available;
					}

					$qty = $to_order + $back_order;

					$price = $this->getPlainProductPrice($item, $qty);
					$cost = $this->getProductCost($item, $qty);
					$discount_info = $this->getDiscountInfo( $item['ProductId'], $price, $qty );

					$this->manager->addOperation($item, 0, $to_order, $price, $cost, $discount_info);
					$this->manager->addOperation($item, 1, $back_order, $price, $cost, $discount_info);
				}
				else {
					// store as normal order (and remove backorder)
					// we could get here with backorder=never then we should order only what's available
					$to_order = min($item['Quantity'], $available);

					$price = $this->getPlainProductPrice($item, $to_order);
					$cost = $this->getProductCost($item, $to_order);
					$discount_info = $this->getDiscountInfo( $item['ProductId'], $price, $to_order );

					$this->manager->addOperation($item, 0, $to_order, $price, $cost, $discount_info, $item['OrderItemId']);
					$this->manager->addOperation($item, 1, 0, $price, $cost, $discount_info); // remove backorder record

					if ($to_order < $item['Quantity']) {
						// ordered less, then requested -> inform user
						if ( $to_order > 0 ) {
							$this->setError(OrderCheckoutErrorType::PRODUCT, OrderCheckoutError::QTY_UNAVAILABLE, $item['ProductId'] . ':' . $item['OptionsSalt'] . ':0:Quantity');
						}
						else {
							$this->setError(OrderCheckoutErrorType::PRODUCT, OrderCheckoutError::QTY_OUT_OF_STOCK, $item['ProductId'] . ':' . $item['OptionsSalt'] . ':0:Quantity');
						}
					}
				}
			}
		}

		/**
		 * Adds product to order (not to db)
		 *
		 * @param Array $item
		 * @param kCatDBItem $product
		 * @param int $qty
		 */
		public function addProduct($item, &$product, $qty)
		{
			$this->updateItemDataFromProduct($item, $product);

			$price = $this->getPlainProductPrice($item, $qty);
			$cost = $this->getProductCost($item, $qty);
			$discount_info = $this->getDiscountInfo( $item['ProductId'], $price, $qty );

			$this->manager->addOperation( $item, 0, $qty, $price, $cost, $discount_info, $item['OrderItemId'] );
		}

		/**
		 * Apply whole order flat discount after sub-total been calculated
		 *
		 */
		protected function applyWholeOrderFlatDiscount()
		{
			$sub_total_flat = $this->manager->getOperationTotal('SubTotalFlat');
			$flat_discount = min( $sub_total_flat, $this->getWholeOrderPlainDiscount($global_discount_id) );
			$coupon_flat_discount = min( $sub_total_flat, $this->getWholeOrderCouponDiscount() );

			if ($coupon_flat_discount && $coupon_flat_discount > $flat_discount) {
				$global_discount_type = 'coupon';
				$flat_discount = $coupon_flat_discount;
				$global_discount_id = $coupon_id;
			}
			else {
				$global_discount_type = 'discount';
			}

			$sub_total = $this->manager->getOperationTotal('SubTotal');

			if ($sub_total_flat - $sub_total < $flat_discount) {
				// individual item discounts together are smaller when order flat discount
				$this->manager->setOperationTotal('CouponDiscount', $flat_discount == $coupon_flat_discount ? $flat_discount : 0);
				$this->manager->setOperationTotal('SubTotal', $sub_total_flat - $flat_discount);

				// replace discount for each operation
				foreach ($this->operations as $index => $operation) {
					$discounted_price = ($operation['Price'] / $sub_total_flat) * $sub_total;
					$this->operations[$index]['DiscountInfo'] = Array ($global_discount_id, $global_discount_type, $discounted_price, 0);
				}
			}
		}

		/**
		 * Returns discount information for given product price and qty
		 *
		 * @param int $product_id
		 * @param float $price
		 * @param int $qty
		 * @return Array
		 */
		protected function getDiscountInfo($product_id, $price, $qty)
		{
			$discounted_price = $this->getDiscountedProductPrice($product_id, $price, $discount_id);
			$couponed_price = $this->getCouponDiscountedPrice($product_id, $price);

			if ($couponed_price < $discounted_price) {
				$discount_type = 'coupon';
				$discount_id = $coupon_id;

				$discounted_price =	$couponed_price;
				$coupon_discount = ($price - $couponed_price) * $qty;
			}
			else {
				$coupon_discount = 0;
				$discount_type = 'discount';
			}

			return Array ($discount_id, $discount_type, $discounted_price, $coupon_discount);
		}

		/**
		 * Returns product qty, available for ordering
		 *
		 * @param Array $item
		 * @return int
		 */
		protected function getAvailableQty($item)
		{
			if ( $item['InventoryStatus'] == ProductInventory::DISABLED ) {
				// always available
				return $item['Quantity'] * 2;
			}

			return max(0, $item['QtyInStock'] - $item['QtyInStockMin']);
		}

		/**
		 * Checks, that product in given order item can be backordered
		 *
		 * @param Array $item
		 * @return bool
		 */
		protected function allowBackordering($item)
		{
			if ($item['BackOrder'] == ProductBackorder::ALWAYS) {
				return true;
			}

			$available = $this->getAvailableQty($item);
			$backordering = $this->Application->ConfigValue('Comm_Enable_Backordering');

			return $backordering && ($item['Quantity'] > $available) && ($item['BackOrder'] == ProductBackorder::AUTO);
		}

		/**
		 * Make sure, that user can't order less, then minimal required qty of product
		 *
		 * @param Array $item
		 */
		protected function ensureMinQty(&$item)
		{
			$sql = 'SELECT MIN(MinQty)
					FROM ' . TABLE_PREFIX . 'ProductsPricing
					WHERE ProductId = ' . $item['ProductId'];
			$min_qty = max(1, $this->Conn->GetOne($sql));

			$qty = $item['Quantity'];

			if ($qty > 0 && $qty < $min_qty) {
				// qty in cart increased to meat minimal qry requirements of given product
				$this->setError(OrderCheckoutErrorType::PRODUCT, OrderCheckoutError::QTY_CHANGED_TO_MINIMAL, $item['ProductId'] . ':' . $item['OptionsSalt'] . ':0:Quantity');

				$item['Quantity'] = $min_qty;
			}
		}

		/**
		 * Return product price for given qty, taking no discounts into account
		 *
		 * @param Array $item
		 * @param int $qty
		 * @return float
		 */
		public function getPlainProductPrice($item, $qty)
		{
			$item_data = $this->getItemData($item);

			if ( isset($item_data['ForcePrice']) ) {
				return $item_data['ForcePrice'];
			}

			$pricing_id = $this->getPriceBracketByQty($item, $qty);

			$sql = 'SELECT Price
					FROM ' . TABLE_PREFIX . 'ProductsPricing
					WHERE PriceId = ' . $pricing_id;
			$price = (float)$this->Conn->GetOne($sql);

			if ( isset($item_data['Options']) ) {
				$price += $this->getOptionPriceAddition($price, $item_data);
				$price = $this->getCombinationPriceOverride($price, $item_data);
			}

			return max($price, 0);
		}

		/**
		 * Return product cost for given qty, taking no discounts into account
		 *
		 * @param Array $item
		 * @param int $qty
		 * @return float
		 */
		public function getProductCost($item, $qty)
		{
			$pricing_id = $this->getPriceBracketByQty($item, $qty);

			$sql = 'SELECT Cost
					FROM ' . TABLE_PREFIX . 'ProductsPricing
					WHERE PriceId = ' . $pricing_id;

			return (float)$this->Conn->GetOne($sql);
		}

		/**
		 * Return product price for given qty, taking no discounts into account
		 *
		 * @param Array $item
		 * @param int $qty
		 * @return float
		 */
		protected function getPriceBracketByQty($item, $qty)
		{
			$orderby_clause = '';
			$where_clause = Array ();
			$product_id = $item['ProductId'];

			if ( $this->usePriceBrackets($item) ) {
				$user_id = $this->getOrder()->GetDBField('PortalUserId');

				$where_clause = Array (
					'GroupId IN (' . $this->Application->getUserGroups($user_id) . ')',
					'pp.ProductId = ' . $product_id,
					'pp.MinQty <= ' . $qty,
					$qty . ' < pp.MaxQty OR pp.MaxQty = -1',
				);

				$orderby_clause = $this->getPriceBracketOrderClause($user_id);
			}
			else {
				$item_data = $this->getItemData($item);

				$where_clause = Array(
					'pp.ProductId = ' . $product_id,
					'pp.PriceId = ' . $this->getPriceBracketFromRequest($product_id, $item_data),
				);
			}

			$sql = 'SELECT pp.PriceId
					FROM ' . TABLE_PREFIX . 'ProductsPricing AS pp
					LEFT JOIN ' . TABLE_PREFIX . 'Products AS p ON p.ProductId = pp.ProductId
					WHERE (' . implode(') AND (', $where_clause) . ')';

			if ($orderby_clause) {
				$sql .= ' ORDER BY ' . $orderby_clause;
			}

			return (float)$this->Conn->GetOne($sql);
		}

		/**
		 * Checks if price brackets should be used in price calculations
		 *
		 * @param Array $item
		 * @return bool
		 */
		protected function usePriceBrackets($item)
		{
			return $item['Type'] == PRODUCT_TYPE_TANGIBLE;
		}

		/**
		 * Return product pricing id for given product.
		 * If not passed - return primary pricing ID
		 *
		 * @param int $product_id
		 * @return int
		 */
		public function getPriceBracketFromRequest($product_id, $item_data)
		{
			if ( !is_array($item_data) ) {
				$item_data = unserialize($item_data);
			}

			// remembered pricing during checkout
			if ( isset($item_data['PricingId']) && $item_data['PricingId'] ) {
				return $item_data['PricingId'];
			}

			// selected pricing from product detail page
			$price_id = $this->Application->GetVar('pr_id');

			if ($price_id) {
				return $price_id;
			}

			$sql = 'SELECT PriceId
					FROM ' . TABLE_PREFIX . 'ProductsPricing
					WHERE ProductId = ' . $product_id . ' AND IsPrimary = 1';

			return $this->Conn->GetOne($sql);
		}

		/**
		 * Returns order clause for price bracket selection based on configration
		 *
		 * @param int $user_id
		 * @return string
		 */
		protected function getPriceBracketOrderClause($user_id)
		{
			if ($this->Application->ConfigValue('Comm_PriceBracketCalculation') == 1) {
				// if we have to stick to primary group, then its pricing will go first,
				// but if there is no pricing for primary group, then next optimal will be taken
				$primary_group = $this->getUserPrimaryGroup($user_id);

				return '( IF(GroupId = ' . $primary_group . ', 1, 2) ) ASC, pp.Price ASC';
			}

			return 'pp.Price ASC';
		}

		/**
		 * Returns addition to product price based on used product option
		 *
		 * @param float $price
		 * @param Array $item_data
		 * @return float
		 */
		protected function getOptionPriceAddition($price, $item_data)
		{
			$addition = 0;

			/** @var kProductOptionsHelper $opt_helper */
			$opt_helper = $this->Application->recallObject('kProductOptionsHelper');

			foreach ($item_data['Options'] as $opt => $val) {
				$sql = 'SELECT *
						FROM ' . TABLE_PREFIX . 'ProductOptions
						WHERE ProductOptionId = ' . $opt;
				$data = $this->Conn->GetRow($sql);

				$parsed = $opt_helper->ExplodeOptionValues($data);

				if ( !$parsed ) {
					continue;
				}

				if ( is_array($val) ) {
					foreach ($val as $a_val) {
						$addition += $this->formatPrice($a_val, $price, $parsed);
					}
				}
				else {
					$addition += $this->formatPrice($val, $price, $parsed);
				}
			}

			return $addition;
		}

		protected function formatPrice($a_val, $price, $parsed)
		{
			$a_val = kUtil::unescape($a_val, kUtil::ESCAPE_HTML); // TODO: Not sure why we're unescaping.

			$addition = 0;
			$conv_prices = $parsed['Prices'];
			$conv_price_types = $parsed['PriceTypes'];

			if ( isset($conv_prices[$a_val]) && $conv_prices[$a_val] ) {
				if ($conv_price_types[$a_val] == '$') {
					$addition += $conv_prices[$a_val];
				}
				elseif ($conv_price_types[$a_val] == '%') {
					$addition += $price * $conv_prices[$a_val] / 100;
				}
			}

			return $addition;
		}

		/**
		 * Returns product price after applying combination price override
		 *
		 * @param float $price
		 * @param Array $item_data
		 * @return float
		 */
		protected function getCombinationPriceOverride($price, $item_data)
		{
			$combination_salt = $this->generateOptionsSalt( $item_data['Options'] );

			if (!$combination_salt) {
				return $price;
			}

			$sql = 'SELECT *
					FROM ' . TABLE_PREFIX . 'ProductOptionCombinations
					WHERE CombinationCRC = ' . $combination_salt;
			$combination = $this->Conn->GetRow($sql);

			if (!$combination) {
				return $price;
			}

			switch ( $combination['PriceType'] ) {
				case OptionCombinationPriceType::EQUALS:
					return $combination['Price'];
					break;

				case OptionCombinationPriceType::FLAT:
					return $price + $combination['Price'];
					break;

				case OptionCombinationPriceType::PECENT:
					return $price * (1 + $combination['Price'] / 100);
					break;
			}

			return $price;
		}

		/**
		 * Generates salt for given option set
		 *
		 * @param Array $options
		 * @return int
		 */
		public function generateOptionsSalt($options)
		{
			/** @var kProductOptionsHelper $opt_helper */
			$opt_helper = $this->Application->recallObject('kProductOptionsHelper');

			return $opt_helper->OptionsSalt($options, true);
		}

		/**
		 * Return product price for given qty, taking possible discounts into account
		 *
		 * @param int $product_id
		 * @param int $price
		 * @param int $discount_id
		 * @return float
		 */
		public function getDiscountedProductPrice($product_id, $price, &$discount_id)
		{
			$discount_id = 0;
			$user_id = $this->getOrder()->GetDBField('PortalUserId');

			$join_clause = Array (
				'd.DiscountId = di.DiscountId',
				'di.ItemType = ' . DiscountItemType::PRODUCT . ' OR (di.ItemType = ' . DiscountItemType::WHOLE_ORDER . ' AND d.Type = ' . DiscountType::PERCENT . ')',
				'd.Status = ' . STATUS_ACTIVE,
				'd.GroupId IN (' . $this->Application->getUserGroups($user_id) . ')',
				'd.Start IS NULL OR d.Start < ' . $this->getOrder()->GetDBField('OrderDate'),
				'd.End IS NULL OR d.End > ' . $this->getOrder()->GetDBField('OrderDate'),
			);

			$sql = 'SELECT
						ROUND(CASE d.Type
							WHEN ' . DiscountType::FLAT . ' THEN ' . $price . ' - d.Amount
							WHEN ' . DiscountType::PERCENT . ' THEN ' . $price . ' * (1 - d.Amount / 100)
							ELSE ' . $price . '
						END, 4), d.DiscountId
					FROM ' . TABLE_PREFIX . 'Products AS p
					LEFT JOIN ' . TABLE_PREFIX . 'ProductsDiscountItems AS di ON (di.ItemResourceId = p.ResourceId) OR (di.ItemType = ' . DiscountItemType::WHOLE_ORDER . ')
					LEFT JOIN ' . TABLE_PREFIX . 'ProductsDiscounts AS d ON (' . implode(') AND (', $join_clause) . ')
					WHERE (p.ProductId = ' . $product_id . ') AND (d.DiscountId IS NOT NULL)';
			$pricing = $this->Conn->GetCol($sql, 'DiscountId');

			if (!$pricing) {
				return $price;
			}

			// get minimal price + discount
			$discounted_price = min($pricing);
			$pricing = array_flip($pricing);
			$discount_id = $pricing[$discounted_price];

			// optimal discount, but prevent negative price
			return max( min($discounted_price, $price), 0 );
		}

		public function getWholeOrderPlainDiscount(&$discount_id)
		{
			$discount_id = 0;
			$user_id = $this->getOrder()->GetDBField('PortalUserId');

			$join_clause = Array (
				'd.DiscountId = di.DiscountId',
				'di.ItemType = ' . DiscountItemType::WHOLE_ORDER . ' AND d.Type = ' . DiscountType::FLAT,
				'd.Status = ' . STATUS_ACTIVE,
				'd.GroupId IN (' . $this->Application->getUserGroups($user_id) . ')',
				'd.Start IS NULL OR d.Start < ' . $this->getOrder()->GetDBField('OrderDate'),
				'd.End IS NULL OR d.End > ' . $this->getOrder()->GetDBField('OrderDate'),
			);

			$sql = 'SELECT d.Amount AS Discount, d.DiscountId
					FROM ' . TABLE_PREFIX . 'ProductsDiscountItems AS di
					LEFT JOIN ' . TABLE_PREFIX . 'ProductsDiscounts AS d ON (' . implode(') AND (', $join_clause) . ')
					WHERE d.DiscountId IS NOT NULL';
			$pricing = $this->Conn->GetCol($sql, 'DiscountId');

			if (!$pricing) {
				return 0;
			}

			$discounted_price = max($pricing);
			$pricing = array_flip($pricing);
			$discount_id = $pricing[$discounted_price];

			return max($discounted_price, 0);
		}

		public function getCouponDiscountedPrice($product_id, $price)
		{
			if ( !$this->getCoupon() ) {
				return $price;
			}

			$join_clause = Array (
				'c.CouponId = ci.CouponId',
				'ci.ItemType = ' . CouponItemType::PRODUCT . ' OR (ci.ItemType = ' . CouponItemType::WHOLE_ORDER . ' AND c.Type = ' . CouponType::PERCENT . ')',
			);

			$sql = 'SELECT
						MIN(
							ROUND(CASE c.Type
								WHEN ' . CouponType::FLAT . ' THEN ' . $price . ' - c.Amount
								WHEN ' . CouponType::PERCENT . ' THEN ' . $price . ' * (1 - c.Amount / 100)
								ELSE ' . $price . '
							END, 4)
						)
					FROM ' . TABLE_PREFIX . 'Products AS p
					LEFT JOIN ' . TABLE_PREFIX . 'ProductsCouponItems AS ci ON (ci.ItemResourceId = p.ResourceId) OR (ci.ItemType = ' . CouponItemType::WHOLE_ORDER . ')
					LEFT JOIN ' . TABLE_PREFIX . 'ProductsCoupons AS c ON (' . implode(') AND (', $join_clause) . ')
					WHERE p.ProductId = ' . $product_id . ' AND ci.CouponId = ' . $this->getCoupon() . '
					GROUP BY p.ProductId';

			$coupon_price = $this->Conn->GetOne($sql);

			if ($coupon_price === false) {
				return $price;
			}

			return max( min($price, $coupon_price), 0 );
		}

		public function getWholeOrderCouponDiscount()
		{
			if ( !$this->getCoupon() ) {
				return 0;
			}

			$where_clause = Array (
				'ci.CouponId = ' . $this->getCoupon(),
				'ci.ItemType = ' . CouponItemType::WHOLE_ORDER,
				'c.Type = ' . CouponType::FLAT,
			);

			$sql = 'SELECT Amount
					FROM ' . TABLE_PREFIX . 'ProductsCouponItems AS ci
					LEFT JOIN ' . TABLE_PREFIX . 'ProductsCoupons AS c ON c.CouponId = ci.CouponId
					WHERE (' . implode(') AND (', $where_clause) . ')';

			return $this->Conn->GetOne($sql);
		}

		protected function getCoupon()
		{
			return $this->getOrder()->GetDBField('CouponId');
		}

		/**
		 * Returns primary group of given user
		 *
		 * @param int $user_id
		 * @return int
		 */
		protected function getUserPrimaryGroup($user_id)
		{
			if ($user_id > 0) {
				$sql = 'SELECT PrimaryGroupId
						FROM ' . TABLE_PREFIX . 'Users
						WHERE PortalUserId = ' . $user_id;
				return $this->Conn->GetOne($sql);
			}

			return $this->Application->ConfigValue('User_LoggedInGroup');
		}

		/**
		 * Returns ItemData associated with given order item
		 *
		 * @param Array $item
		 * @return Array
		 */
		protected function getItemData($item)
		{
			$item_data = $item['ItemData'];

			if ( is_array($item_data) ) {
				return $item_data;
			}

			return $item_data ? unserialize($item_data) : Array ();
		}

		/**
		 * Sets ItemData according to product
		 *
		 * @param Array $item
		 * @param kCatDBItem $product
		 */
		protected function updateItemDataFromProduct(&$item, &$product)
		{
			$item_data = $this->getItemData($item);
			$item_data['IsRecurringBilling'] = $product->GetDBField('IsRecurringBilling');

			// it item is processed in order using new style, then put such mark in orderitem record
			$processing_data = $product->GetDBField('ProcessingData');

			if ($processing_data) {
				$processing_data = unserialize($processing_data);

				if ( isset($processing_data['HasNewProcessing']) ) {
					$item_data['HasNewProcessing'] = 1;
				}
			}

			$item['ItemData'] = serialize($item_data);
		}

		/**
		 * Returns table name according to order temp mode
		 *
		 * @param string $prefix
		 * @return string
		 */
		protected function getTable($prefix)
		{
			return $this->manager->getTable($prefix);
		}
	}
