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

	class AffiliatesEventHandler extends kDBEventHandler {

		/**
		 * Allows to override standard permission mapping
		 *
		 * @return void
		 * @access protected
		 * @see kEventHandler::$permMapping
		 */
		protected function mapPermissions()
		{
			parent::mapPermissions();

			$permissions = Array (
				'OnItemBuild' => Array ('self' => true),
			);

			$this->permMapping = array_merge($this->permMapping, $permissions);
		}

		/**
		 * Checks user permission to execute given $event
		 *
		 * @param kEvent $event
		 * @return bool
		 * @access public
		 */
		public function CheckPermission(kEvent $event)
		{
			if ( $event->Name == 'OnBecomeAffiliate' || $event->Name == 'OnChangePaymentType' ) {
				return $this->Application->LoggedIn() && $this->Application->ConfigValue('Comm_RegisterAsAffiliate');
			}

			return parent::CheckPermission($event);
		}

		/**
		 * Allows to get ID of affiliate record, associated with currently logged-in user
		 *
		 * @param kEvent $event
		 * @return int
		 * @access public
		 */
		public function getPassedID(kEvent $event)
		{
			if ( $event->Special == 'user' ) {
				$event->setEventParam('raise_warnings', 0);

				$sql = 'SELECT ' . $this->Application->getUnitOption($event->Prefix, 'IDField') . '
						FROM ' . $this->Application->getUnitOption($event->Prefix, 'TableName') . '
						WHERE PortalUserId = ' . (int)$this->Application->RecallVar('user_id');
				$id = $this->Conn->GetOne($sql);

				if ( $id ) {
					return $id;
				}
			}

			return parent::getPassedID($event);
		}

		/**
		 * Generate new affiliate code
		 *
		 * @param kEvent $event
		 * @return string
		 */
		function generateAffiliateCode($event)
		{
			// accepts 1 - 36
			$number_length = 11;
			$num_chars = Array(	'1'=>'a','2'=>'b','3'=>'c','4'=>'d','5'=>'e','6'=>'f',
								'7'=>'g','8'=>'h','9'=>'i','10'=>'j','11'=>'k','12'=>'l',
								'13'=>'m','14'=>'n','15'=>'o','16'=>'p','17'=>'q','18'=>'r',
								'19'=>'s','20'=>'t','21'=>'u','22'=>'v','23'=>'w','24'=>'x',
								'25'=>'y','26'=>'z','27'=>'0','28'=>'1','29'=>'2','30'=>'3',
								'31'=>'4','32'=>'5','33'=>'6','34'=>'7','35'=>'8','36'=>'9');

			$ret = '';
			for($i=1; $i<=$number_length; $i++)
			{
				mt_srand((double)microtime() * 1000000);
				$num = mt_rand(1,36);
				$ret .= $num_chars[$num];
			}
			$ret = strtoupper($ret);

			$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
			$table = $this->Application->getUnitOption($event->Prefix, 'TableName');

			$sql = 'SELECT %s FROM %s WHERE AffiliateCode = %s';
			$code_found = $this->Conn->GetOne( sprintf($sql, $id_field, $table, $this->Conn->qstr($ret) ) );
			if($code_found) return $this->generateAffiliateCode($event);

			return $ret;
		}

		/**
		 * Creates new affiliate code when new affiliate is created
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemCreate(kEvent $event)
		{
			parent::OnBeforeItemCreate($event);

			/** @var kDBItem $object */
			$object = $event->getObject();

			$object->SetDBField('AffiliateCode', $this->generateAffiliateCode($event));

			if ( $object->getFormName() == 'registration' ) {
				if ( $this->Application->LoggedIn() ) {
					$object->SetDBField('PortalUserId', $this->Application->RecallVar('user_id'));
				}

				$object->SetDBField('AffiliatePlanId', $this->_getPrimaryAffiliatePlan());
			}
		}

		/**
		 * Ensures, that user can only update his affiliate record
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeItemUpdate(kEvent $event)
		{
			parent::OnBeforeItemUpdate($event);

			if ( !$this->Application->isAdmin ) {
				/** @var kDBItem $object */
				$object = $event->getObject();

				$object->SetDBField('PortalUserId', $object->GetOriginalField('PortalUserId'));

				if ( $object->GetDBField('PortalUserId') != $this->Application->RecallVar('user_id') ) {
					$object->SetError('PortalUserId', 'not_owner');
				}
			}
		}

		/**
		 * [HOOK] Stores affiliate id using method from Config (session or cookie) if correct code is present in url
		 *
		 * @param kEvent $event
		 * @return bool
		 */
		function OnStoreAffiliate($event)
		{
			if ( defined('IS_INSTALL') && IS_INSTALL ) {
				return;
			}

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

			$affiliate_storage_method = $this->Application->ConfigValue('Comm_AffiliateStorageMethod');

			$affiliate = $this->Application->GetVar('affiliate');
			if ( $affiliate ) {
				$object->Load($affiliate, 'AffiliateCode');
			}
			elseif ( $affiliate_storage_method == 2 ) {
				$affiliate_id = $this->Application->GetVar('affiliate_id');
				$object->Load($affiliate_id);
			}

			if ( $object->isLoaded() && ($object->GetDBField('Status') == 1) ) {
				// user is found with such email
				/** @var UsersItem $affiliate_user */
				$affiliate_user = $this->Application->recallObject('u.affiliate', null, Array ('skip_autoload' => true));

				$affiliate_user->Load($object->GetDBField('PortalUserId'));

				if ( $affiliate_user->GetDBField('Status') == 1 ) {
					$affiliate_id = $object->GetDBField('AffiliateId');
					$this->Application->setVisitField('AffiliateId', $affiliate_id);

					if ( $affiliate_storage_method == 1 ) {
						$this->Application->StoreVar('affiliate_id', $affiliate_id); // per session
					}
					else {
						// in cookie
						$this->Application->Session->SetCookie('affiliate_id', $affiliate_id, $this->getCookieExpiration());
					}
				}
			}
		}

		/**
		 * Returns affiliate cookie expiration date
		 *
		 * @return int
		 */
		function getCookieExpiration()
		{
			$expire = $this->Application->ConfigValue('Comm_AffiliateCookieDuration'); // days
			return adodb_mktime() + $expire * 24 * 60 * 60;
		}

		/**
		 * Calculate what amount is earned by affiliate based on it's affiliate plan & store it
		 *
		 * @param kEvent $event
		 * @author Alex
		 */
		function OnOrderApprove($event)
		{
			/** @var OrdersItem $order */
			$order = $this->Application->recallObject($event->getEventParam('Order_PrefixSpecial'));

			$affiliate_id = $order->GetDBField('AffiliateId');
			if ( !$affiliate_id ) {
				return false;
			}

			/** @var kDBItem $object */
			$object = $event->getObject(Array ('ship_autoload' => true));

			if ( $object->Load($affiliate_id) ) {
				/** @var kDBItem $affiliate_plan */
				$affiliate_plan = $this->Application->recallObject('ap', null, Array ('skip_autoload' => true));

				$affiliate_plan->Load($object->GetDBField('AffiliatePlanId'));

				if ( $affiliate_plan->isLoaded() ) {
					$sql = 'SELECT SUM(Quantity) FROM %s WHERE OrderId = %s';
					$orderitems_table = $this->Application->getUnitOption('orditems', 'TableName');
					$items_sold = $this->Conn->GetOne(sprintf($sql, $orderitems_table, $order->GetID()));

					$object->SetDBField('AccumulatedAmount', $object->GetDBField('AccumulatedAmount') + $order->GetDBField('TotalAmount'));
					$object->SetDBField('ItemsSold', $object->GetDBField('ItemsSold') + $items_sold);

					switch ($affiliate_plan->GetDBField('PlanType')) {
						case 1: // by amount
							$value = $object->GetDBField('AccumulatedAmount');
							break;

						case 2: // by items sold (count)
							$value = $object->GetDBField('ItemsSold');
							break;
					}

					$apb_table = $this->Application->getUnitOption('apbrackets', 'TableName');
					$sql = 'SELECT Percent FROM %1$s WHERE (%2$s >= FromAmount) AND ( (%2$s <= ToAmount) OR (ToAmount = -1) ) AND (AffiliatePlanId = %3$s)';
					$commission_percent = $this->Conn->GetOne(sprintf($sql, $apb_table, $this->Conn->qstr($value), $affiliate_plan->GetID()));

					// process only orders of current affiliate from period start to this order date
					$period_ends = $order->GetDBField('OrderDate');
					$period_starts = $this->getPeriodStartTS($period_ends, $affiliate_plan->GetDBField('ResetInterval'));

					$sql = 'SELECT AffiliateCommission, (SubTotal+ShippingCost+VAT) AS TotalAmount, OrderId
							FROM ' . $order->TableName . '
							WHERE OrderDate >= %s AND OrderDate <= %s AND AffiliateId = ' . $affiliate_id;

					$amount_to_pay_before = 0;
					$amount_to_pay_after = 0;
					$order_update_sql = 'UPDATE ' . $order->TableName . ' SET AffiliateCommission = %s WHERE ' . $order->IDField . ' = %s';
					$orders = $this->Conn->Query(sprintf($sql, $period_starts, $period_ends), 'OrderId');
					if ( $orders ) {
						foreach ($orders as $order_id => $order_data) {
							$amount_to_pay_before += $order_data['AffiliateCommission'];
							$commission = $order_data['TotalAmount'] * ($commission_percent / 100);
							$this->Conn->Query(sprintf($order_update_sql, $this->Conn->qstr($commission), $order_id));
							$amount_to_pay_after += $commission;
						}


					}
					$object->SetDBField('AmountToPay', $object->GetDBField('AmountToPay') - $amount_to_pay_before + $amount_to_pay_after);
					$object->SetDBField('LastOrderDate_date', $order->GetDBField('OrderDate_date'));
					$object->SetDBField('LastOrderDate_time', $order->GetDBField('OrderDate_time'));
					$object->Update();

					$order->SetDBField('AffiliateCommission', $commission); // set last commission to this order, because ApproveEvent was called for him
				}
			}
		}

		/**
		 * [HOOK] Validates affiliate fields on user registration form
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnValidateAffiliate($event)
		{
			if ( $this->Application->GetVar('RegisterAsAffiliate') != 'on' || $event->MasterEvent->status != kEvent::erSUCCESS ) {
				return;
			}

			/** @var kDBItem $object */
			$object = $event->getObject( Array('form_name' => 'registration', 'skip_autoload' => true) );

			$object->setID(0);
			$field_values = $this->getSubmittedFields($event);
			$object->SetFieldsFromHash($field_values);
			$event->setEventParam('form_data', $field_values);

			if ( !$object->Validate() ) {
				/** @var kDBItem $user */
				$user = $event->MasterEvent->getObject();

				$user->Validate();

				$event->MasterEvent->status = kEvent::erFAIL;
			}
		}

		/**
		 * [AFTER HOOK] to u:OnCreate
		 *
		 * @param kEvent $event
		 */
		function OnRegisterAffiliate($event)
		{
			if ( $this->Application->GetVar('RegisterAsAffiliate') != 'on' || $event->MasterEvent->status != kEvent::erSUCCESS ) {
				return;
			}

			/** @var kDBItem $object */
			$object = $event->getObject();

			/** @var UsersItem $user */
			$user = $event->MasterEvent->getObject();

			$object->SetDBField('PortalUserId', $user->GetID());

			if ( $object->Create() ) {
				$this->Application->emailUser('AFFILIATE.REGISTER', $user->GetID());
				$this->Application->emailAdmin('AFFILIATE.REGISTER');
			}
		}

		/**
		 * Returns primary affiliate plan
		 *
		 * @return int
		 * @access protected
		 */
		protected function _getPrimaryAffiliatePlan()
		{
			$sql = 'SELECT AffiliatePlanId
					FROM ' . $this->Application->getUnitOption('ap', 'TableName') . '
					WHERE IsPrimary = 1';

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

		/**
		 * Creates affiliate record for logged-in user
		 *
		 * @param kEvent $event
		 */
		function OnBecomeAffiliate($event)
		{
			/** @var UsersItem $object */
			$object = $event->getObject( Array('form_name' => 'registration', 'skip_autoload' => true) );

			$event->CallSubEvent('OnCreate');

			if ( $event->status == kEvent::erSUCCESS ) {
				$event->SetRedirectParam('opener', 's');

				$next_template = $this->Application->GetVar('next_template');

				if ( $next_template ) {
					$event->redirect = $next_template;
				}
			}
		}

		/**
		 * Change affiliate payment type of affiliate record associated with logged-in user
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnChangePaymentType($event)
		{
			$event->CallSubEvent('OnUpdate');

			if ( $event->status == kEvent::erSUCCESS ) {
				/** @var kDBItem $object */
				$object = $event->getObject();

				$this->Application->emailUser('AFFILIATE.PAYMENT.TYPE.CHANGED', $object->GetDBField('PortalUserId'));
				$this->Application->emailAdmin('AFFILIATE.PAYMENT.TYPE.CHANGED');

				$next_template = $this->Application->GetVar('next_template');

				if ( $next_template ) {
					$event->redirect = $this->Application->GetVar('next_template');
				}

				$event->SetRedirectParam('opener', 's');
			}
		}

		/**
		 * If new payments made, then send email about that
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function OnBeforeDeleteFromLive(kEvent $event)
		{
			parent::OnBeforeDeleteFromLive($event);

			/** @var kDBItem $payment_object */
			$payment_object = $this->Application->recallObject('apayments', 'apayments', Array ('skip_autoload' => true));

			$id = $event->getEventParam('id');
			$ap_table = $this->Application->getUnitOption('apayments', 'TableName');

			$sql = 'SELECT AffiliatePaymentId
					FROM ' . $ap_table . '
					WHERE AffiliateId = ' . $id;
			$live_ids = $this->Conn->GetCol($sql);

			$sql = 'SELECT AffiliatePaymentId
					FROM ' . $payment_object->TableName . '
					WHERE AffiliateId = ' . $id;
			$temp_ids = $this->Conn->GetCol($sql);

			$new_ids = array_diff($temp_ids, $live_ids);

			foreach ($new_ids as $payment_id) {
				$payment_object->Load($payment_id);
				$this->Application->emailUser('AFFILIATE.PAYMENT', $payment_object->GetDBField('PortalUserId'));
				$this->Application->emailAdmin('AFFILIATE.PAYMENT');
			}

			/** @var kDBItem $object */
			$object = $event->getObject();

			$passed_id = $event->getEventParam('id');

			if ( $object->GetID() != $passed_id ) {
				$object->Load($passed_id);
			}

			$sql = 'SELECT Status
					FROM ' . $this->Application->getUnitOption($event->Prefix, 'TableName') . '
					WHERE ' . $object->IDField . ' = ' . $object->GetID();
			$old_status = $this->Conn->GetOne($sql);

			if ( $old_status == 2 && $object->GetDBField('Status') == 1 ) {
				$this->Application->emailUser('AFFILIATE.REGISTRATION.APPROVED', $object->GetDBField('PortalUserId'));
				$this->Application->emailAdmin('AFFILIATE.REGISTRATION.APPROVED');
			}
		}

		/**
		 * [HOOK] Resets statistics (accumulated amount & items sold) for affiliates based on ResetInterval in their plan
		 *
		 * @param kEvent $event
		 * @author Alex
		 */
		function OnResetStatistics($event)
		{
			if ( defined('IS_INSTALL') && IS_INSTALL ) {
				return;
			}

			$intervals = Array (86400 => 'la_day', 604800 => 'la_week', 2628000 => 'la_month', 7884000 => 'la_quartely', 31536000 => 'la_year');

			$affiliates_table = $this->Application->getUnitOption($event->Prefix, 'TableName');
			$affiliate_plan_table = $this->Application->getUnitOption('ap', 'TableName');

			$base_time = adodb_mktime();
			$where_clause = Array ();

			foreach ($intervals as $interval_length => $interval_description) {
				$start_timestamp = $this->getPeriodStartTS($base_time, $interval_length);
				$where_clause[] = 'ap.ResetInterval = ' . $interval_length . ' AND LastOrderDate < ' . $start_timestamp;
			}

			$sql = 'SELECT AffiliateId
					FROM ' . $affiliates_table . ' a
					LEFT JOIN ' . $affiliate_plan_table . ' ap ON a.AffiliatePlanId = ap.AffiliatePlanId
					WHERE (' . implode(') OR (', $where_clause) . ')';
			$affiliate_ids = $this->Conn->GetCol($sql);

			if ( !$affiliate_ids ) {
				return;
			}

			if ( defined('DEBUG_MODE') && DEBUG_MODE && $this->Application->isDebugMode() ) {
				$this->Application->Debugger->appendHTML('Affiliates Pending Totals Reset: ');
				$this->Application->Debugger->dumpVars($affiliate_ids);
			}

			$fields_hash = Array (
				'AccumulatedAmount' => 0,
				'ItemsSold' => 0,
				'LastOrderDate' => $base_time,
			);

			$this->Conn->doUpdate($fields_hash, $affiliates_table, 'AffiliateId IN (' . implode(',', $affiliate_ids) . ')');
		}

		/**
		 * Returns calendar period start timestamp based on current timestamp ($base_time) and $period_length
		 *
		 * @param int $base_time
		 * @param int $period_length
		 * @return int
		 * @author Alex
		 */
		function getPeriodStartTS($base_time, $period_length)
		{
			$start_timestamp = 0;

			switch ($period_length) {
				case 86400: // day
					$start_timestamp = adodb_mktime(0, 0, 0, adodb_date('m', $base_time), adodb_date('d', $base_time), adodb_date('Y', $base_time));
					break;

				case 604800: // week
					$day_seconds = 86400;
					$first_week_day = $this->Application->ConfigValue('FirstDayOfWeek');
					$morning = adodb_mktime(0, 0, 0, adodb_date('m', $base_time), adodb_date('d', $base_time), adodb_date('Y', $base_time));
					$week_day = adodb_date('w', $morning);
					if ( $week_day == $first_week_day ) {
						// if it is already first week day, then don't search for previous week day
						$day_diff = 0;
					}
					else {
						// this way, because sunday is 0, but not 7 as it should be
						$day_diff = $week_day != 0 ? $week_day - $first_week_day : 7 - $first_week_day;
					}
					$start_timestamp = $morning - $day_diff * $day_seconds;
					break;

				case 2628000: // month
					$start_timestamp = adodb_mktime(0, 0, 0, adodb_date('m', $base_time), 1, adodb_date('Y', $base_time));
					break;

				case 7884000: // quartal
					$first_quartal_month = (ceil(adodb_date('m', $base_time) / 3) - 1) * 3 + 1;
					$start_timestamp = adodb_mktime(0, 0, 0, $first_quartal_month, 1, adodb_date('Y', $base_time));
					break;

				case 31536000:
					$start_timestamp = adodb_mktime(0, 0, 0, 1, 1, adodb_date('Y', $base_time));
					break;
			}

			return $start_timestamp;
		}

		/**
		 * Apply same processing to each item being selected in grid
		 *
		 * @param kEvent $event
		 * @return void
		 * @access protected
		 */
		protected function iterateItems(kEvent $event)
		{
			if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
				$event->status = kEvent::erFAIL;
				return;
			}

			/** @var kDBItem $object */
			$object = $event->getObject(Array ('skip_autoload' => true));

			$ids = $this->StoreSelectedIDs($event);

			if ( $ids ) {
				$status_field = $object->getStatusField();

				foreach ($ids as $id) {
					$object->Load($id);

					switch ($event->Name) {
						case 'OnMassApprove':
							$object->SetDBField($status_field, 1);
							break;

						case 'OnMassDecline':
							$object->SetDBField($status_field, 0);
							break;

						case 'OnMassMoveUp':
							$object->SetDBField('Priority', $object->GetDBField('Priority') + 1);
							break;

						case 'OnMassMoveDown':
							$object->SetDBField('Priority', $object->GetDBField('Priority') - 1);
							break;
					}

					if ( $object->Update() ) {
						switch ($event->Name) {
							case 'OnMassApprove':
								$this->Application->emailUser('AFFILIATE.REGISTRATION.APPROVED', $object->GetDBField('PortalUserId'));
								$this->Application->emailAdmin('AFFILIATE.REGISTRATION.APPROVED');
								break;
							case 'OnMassDecline':
								$this->Application->emailUser('AFFILIATE.REGISTRATION.DENIED', $object->GetDBField('PortalUserId'));
								$this->Application->emailAdmin('AFFILIATE.REGISTRATION.DENIED');
								break;
						}

						$event->SetRedirectParam('opener', 's'); //stay!
					}
				}
			}
		}

		/**
		 * Checks that user in affiliate record matches current user
		 * (non permission-based)
		 *
		 * @param kEvent $event
		 * @return bool
		 * @access protected
		 */
		protected function checkItemStatus(kEvent $event)
		{
			if ( $this->Application->isAdminUser ) {
				return true;
			}

			/** @var kDBItem $object */
			$object = $event->getObject();

			if ( !$object->isLoaded() ) {
				return true;
			}

			return $object->GetDBField('PortalUserId') == $this->Application->RecallVar('user_id');
		}
	}
