<?php
/**
* @version   $Id: order_manager.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!');

	/**
	 * Manages order contents
	 *
	 */
	class OrderManager extends kBase {

		protected $errorMessages = Array (
            1 => 'state_changed',
            2 => 'qty_unavailable',
            3 => 'outofstock',
            4 => 'invalid_code',
            5 => 'code_expired',
            6 => 'min_qty',
            7 => 'code_removed',
            8 => 'code_removed_automatically',
            9 => 'changed_after_login',
            10 => 'coupon_applied',
            104 => 'invalid_gc_code',
            105 => 'gc_code_expired',
            107 => 'gc_code_removed',
            108 => 'gc_code_removed_automatically',
            110 => 'gift_certificate_applied',
         );

		/**
		 * Order, used in calculator
		 *
		 * @var OrdersItem
		 */
		protected $order = null;

		/**
		 * Order calculator instance
		 *
		 * @var OrderCalculator
		 */
		protected $calculator = null;

		/**
		 * Operations to be performed on order items later
		 *
		 * @var Array
		 */
		protected $operations = Array ();

		/**
		 * Totals override
		 *
		 * @var Array
		 */
		protected $totalsOverride = Array ();

		public function __construct()
		{
			parent::__construct();

			$this->calculator = $this->Application->makeClass('OrderCalculator');
			$this->calculator->setManager($this);

			$this->reset();
		}

		/**
		 * Sets order to be used in calculator
		 *
		 * @param OrdersItem $order
		 */
		public function setOrder(&$order)
		{
			$this->order =& $order;

			$this->reset();
		}

		function reset()
		{
			$this->operations = Array ();
			$this->totalsOverride = Array ();

			$this->calculator->reset();
		}

		public function resetOperationTotals()
		{
			$this->totalsOverride = Array ();
		}

		/**
		 * 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 public
		 */
		public function setError($error_type, $error_code, $product_id = null)
		{
			$this->order->setCheckoutError($error_type, $error_code, $product_id);
		}

		/**
		 * Gets error count
		 *
		 * @return int
		 * @access public
		 */
		public function getErrorCount()
		{
			$errors = $this->Application->RecallVar('checkout_errors');

			if ( !$errors ) {
				return 0;
			}

			return count( unserialize($errors) );
		}

		/**
		 * Returns order object reference
		 *
		 * @return OrdersItem
		 */
		public function &getOrder()
		{
			return $this->order;
		}

		/**
		 * Calculates given order
		 *
		 */
		public function calculate()
		{
			$this->calculator->calculate();

			$changed = $this->applyOperations() || ($this->getErrorCount() > 0);
			$this->setOrderTotals();

			return $changed;
		}

		public function addOperation($item, $backorder_flag, $qty, $price, $cost, $discount_info, $order_item_id = 0)
		{
			$operation = Array (
				'ProductId' => $item['ProductId'],
				'BackOrderFlag' => $backorder_flag,
				'Quantity' => $qty,
				'Price' => $price,
				'Cost' => $cost,
				'DiscountInfo' => $discount_info,
				'OrderItemId' => $order_item_id,
				'OptionsSalt' => $item['OptionsSalt'],
				'ItemData' => $item['ItemData'],
				'PackageNum' => array_key_exists('PackageNum', $item) ? $item['PackageNum'] : 1,
			);

			$this->operations[] = $operation;
		}

		/**
		 * Returns total based on added operations
		 *
		 * @param string $type
		 * @return float
		 */
		public function getOperationTotal($type)
		{
			if ( isset($this->totalsOverride[$type]) ) {
				return $this->totalsOverride[$type];
			}

			$ret = 0;

			foreach ($this->operations as $operation) {
				if ($type == 'SubTotalFlat') {
					$ret += $operation['Quantity'] * $operation['Price'];
				}
				elseif ($type == 'CostTotal') {
					$ret += $operation['Quantity'] * $operation['Cost'];
				}
				elseif ($type == 'SubTotal') {
					$ret += $operation['Quantity'] * $operation['DiscountInfo'][2]; // discounted price
				}
				elseif ($type == 'CouponDiscount') {
					$ret += $operation['DiscountInfo'][3];
				}
			}

			return $ret;
		}

		public function setOperationTotal($type, $value)
		{
			$this->totalsOverride[$type] = $value;
		}

		/**
		 * Apply scheduled operations
		 *
		 */
		public function applyOperations()
		{
			$ret = false;

			/** @var kDBItem $order_item */
			$order_item = $this->Application->recallObject('orditems.-item', null, Array('skip_autoload' => true));

			foreach ($this->operations as $operation) {
				$item = $this->getOrderItemByOperation($operation);
				$item_id = $item['OrderItemId'];

				if ($item_id) { // if Product already exists in the order
					if ( $this->noChangeRequired($item, $operation) ) {
						continue;
					}

					$order_item->Load($item_id);

					if ($operation['Quantity'] > 0) { // Update Price by _TOTAL_ qty
						$item_data = $order_item->GetDBField('ItemData');
						$item_data = $item_data ? unserialize($item_data) : Array ();
						$item_data['DiscountId'] = $operation['DiscountInfo'][0];
						$item_data['DiscountType'] = $operation['DiscountInfo'][1];


						$fields_hash = Array (
							'Quantity' => $operation['Quantity'],
							'FlatPrice' => $operation['Price'],
							'Price' => $operation['DiscountInfo'][2],
							'Cost' => $operation['Cost'],
							'ItemData' => serialize($item_data),
						);

						$order_item->SetDBFieldsFromHash($fields_hash);
						$order_item->Update();
					}
					else { // delete products with 0 qty
						$order_item->Delete();
					}
				}
				elseif ($operation['Quantity'] > 0) {
					// if we are adding product
					// discounts are saved from OrdersEvetnHandler::AddItemToOrder method
					$item_data = $operation['ItemData'];
					$item_data = $item_data ? unserialize($item_data) : Array ();
					$item_data['DiscountId'] = $operation['DiscountInfo'][0];
					$item_data['DiscountType'] = $operation['DiscountInfo'][1];

					$fields_hash = Array (
						'ProductId' => $operation['ProductId'],
						'ProductName' => $this->getProductField( $operation['ProductId'], 'Name' ),
						'Quantity' => $operation['Quantity'],
						'FlatPrice' => $operation['Price'],
						'Price' => $operation['DiscountInfo'][2],
						'Cost' => $operation['Cost'],
						'Weight' => $this->getProductField( $operation['ProductId'], 'Weight' ),
						'OrderId' => $this->order->GetID(),
						'BackOrderFlag' => $operation['BackOrderFlag'],
						'ItemData' => serialize($item_data),
						'PackageNum' => $operation['PackageNum'],
						'OptionsSalt' => $operation['OptionsSalt'],
					);

					$order_item->SetDBFieldsFromHash($fields_hash);
					$order_item->Create();
				}
				else {
					// item requiring to set qty to 0, meaning already does not exist
					continue;
				}

				$ret = true;
			}

			return $ret;
		}

		/**
		 * Sets order fields, containing total values
		 *
		 */
		public function setOrderTotals()
		{
			$sub_total = $this->getOperationTotal('SubTotal');
			$this->order->SetDBField('SubTotal', $sub_total);

			$cost_total = $this->getOperationTotal('CostTotal');
			$this->order->SetDBField('CostTotal', $cost_total);

			$sub_total_flat = $this->getOperationTotal('SubTotalFlat');
			$this->order->SetDBField('DiscountTotal', $sub_total_flat - $sub_total);

			$coupon_discount = $this->getOperationTotal('CouponDiscount');
			$this->order->SetDBField('CouponDiscount', $coupon_discount);
		}

		/**
		 * Returns exising order item data, based on operation details
		 *
		 * @param Array $operation
		 * @return Array
		 */
		protected function getOrderItemByOperation($operation)
		{
			if ( $operation['OrderItemId'] ) {
				$where_clause = Array (
					'OrderItemId = ' . $operation['OrderItemId'],
				);
			}
			else {
				$where_clause = Array (
					'OrderId = ' . $this->order->GetID(),
					'ProductId = ' . $operation['ProductId'],
					'BackOrderFlag ' . ($operation['BackOrderFlag'] ? ' >= 1' : ' = 0'),
					'OptionsSalt = ' . $operation['OptionsSalt'],
				);
			}

			$sql = 'SELECT OrderItemId, Quantity, FlatPrice, Price, BackOrderFlag, ItemData
					FROM ' . $this->getTable('orditems') . '
					WHERE (' . implode(') AND (', $where_clause) . ')';

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

		/**
		 * Checks, that there are no database changes required to order item from operation
		 *
		 * @param Array $item
		 * @param Array $operation
		 * @return bool
		 */
		protected function noChangeRequired($item, $operation)
		{
			$item_data = $item['ItemData'] ? unserialize( $item['ItemData'] ) : Array ();

			$conditions = Array (
				$operation['Quantity'] > 0,
				$item['Quantity'] == $operation['Quantity'],
				round($item['FlatPrice'], 3) == round($operation['Price'], 3),
				round($item['Price'], 3) == round($operation['DiscountInfo'][2], 3),
				(string)getArrayValue($item_data, 'DiscountType') == $operation['DiscountInfo'][1],
				(int)getArrayValue($item_data, 'DiscountId') == $operation['DiscountInfo'][0],
			);

			foreach ($conditions as $condition) {
				if (!$condition) {
					return false;
				}
			}

			return true;
		}

		/**
		 * Returns product name by id
		 *
		 * @param int $product_id
		 * @param string $field
		 * @return string
		 */
		protected function getProductField($product_id, $field)
		{
			/** @var kCatDBItem $product */
			$product = $this->Application->recallObject('p', null, Array ('skip_autoload' => true));

			if ( !$product->isLoaded() || ($product->GetID() != $product_id) ) {
				$product->Load($product_id);
			}

			return $field == 'Name' ? $product->GetField($field) : $product->GetDBField($field);
		}

		/**
		 * Returns table name according to order temp mode
		 *
		 * @param string $prefix
		 * @return string
		 */
		public function getTable($prefix)
		{
			$table_name = $this->Application->getUnitOption($prefix, 'TableName');

			if ( $this->order->IsTempTable() ) {
				return $this->Application->GetTempName($table_name, 'prefix:' . $this->order->Prefix);
			}

			return $table_name;
		}

		/**
		 * Adds product to order
		 *
		 * @param kCatDBItem $product
		 * @param string $item_data
		 * @param int $qty
		 * @param int $package_num
		 */
		public function addProduct(&$product, $item_data, $qty = null, $package_num = null)
		{
			if ( !isset($qty) ) {
				$qty = 1;
			}

			$item = $this->getItemFromProduct($product, $item_data);
			$order_item = $this->getOrderItem($item);

			if ( $this->calculator->canBeGrouped($item, $item) && $order_item ) {
				$qty += $order_item['Quantity'];
			}

			$item['OrderItemId'] = $order_item ? $order_item['OrderItemId'] : 0;

			if ( isset($package_num) ) {
				$item['PackageNum'] = $package_num;
			}

			$this->calculator->addProduct($item, $product, $qty);
			$this->applyOperations();
		}

		/**
		 * Returns virtual $item based on given product
		 *
		 * @param kCatDBItem $product
		 * @param string $item_data
		 * @return Array
		 */
		protected function getItemFromProduct(&$product, $item_data)
		{
			$item_data_array = unserialize($item_data);

			$options = isset($item_data_array['Options']) ? $item_data_array['Options'] : false;
			$options_salt = $options ? $this->calculator->generateOptionsSalt($options) : 0;

			$item = Array (
				'ProductId' => $product->GetID(),
				'OptionsSalt' => $options_salt,
				'ItemData' => $item_data,
				'Type' => $product->GetDBField('Type'),
				'OrderItemId' => 0,
			);

			return $item;
		}

		/**
		 * Returns OrderItem formed from $item
		 *
		 * @param Array $item
		 * @return Array
		 */
		protected function getOrderItem($item)
		{
			$where_clause = Array (
				'OrderId = ' . $this->order->GetID(),
				'ProductId = ' . $item['ProductId'],
			);

			if ( $item['OptionsSalt'] ) {
				$where_clause[] = 'OptionsSalt = ' . $item['OptionsSalt'];
			}

			$sql = 'SELECT Quantity, OrderItemId
					FROM ' . $this->getTable('orditems') . '
					WHERE (' . implode(') AND (', $where_clause) . ')';

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