'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; $order_item = $this->Application->recallObject('orditems.-item', null, Array('skip_autoload' => true)); /* @var $order_item kDBItem */ 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) { $product = $this->Application->recallObject('p', null, Array ('skip_autoload' => true)); /* @var $product kCatDBItem */ 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); } }