<?php
/**
 * @brief		Billing Agreement Model
 * @author		<a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) Invision Power Services, Inc.
 * @license		https://www.invisioncommunity.com/legal/standards/
 * @package		Invision Community
 * @subpackage	Nexus
 * @since		16 Dec 2015
 */

namespace IPS\nexus\Customer;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Billing Agreement Model
 */
class _BillingAgreement extends \IPS\Patterns\ActiveRecord
{
	/**
	 * @brief	Billing Agreement is active and will charge automatically
	 */
	const STATUS_ACTIVE		= 'active';

	/**
	 * @brief	Billing Agreement has been suspended but can be reactivated
	 */
	const STATUS_SUSPENDED	= 'suspended';

	/**
	 * @brief	Billing Agreement has been canceled
	 */
	const STATUS_CANCELED	= 'canceled';
	
	/**
	 * @brief	Multiton Store
	 */
	protected static $multitons;

	/**
	 * @brief	Database Table
	 */
	public static $databaseTable = 'nexus_billing_agreements';
	
	/**
	 * @brief	Database Prefix
	 */
	public static $databasePrefix = 'ba_';
	
	/**
	 * Construct ActiveRecord from database row
	 *
	 * @param	array	$data							Row from database table
	 * @param	bool	$updateMultitonStoreIfExists	Replace current object in multiton store if it already exists there?
	 * @return	static
	 */
	public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
	{
		$gateway = \IPS\nexus\Gateway::load( $data['ba_method'] );
		$classname = 'IPS\nexus\Gateway\\' . $gateway->gateway . '\\BillingAgreement';
		
		/* Initiate an object */
		$obj = new $classname;
		$obj->_new = FALSE;
		
		/* Import data */
		foreach ( $data as $k => $v )
		{
			if( static::$databasePrefix AND mb_strpos( $k, static::$databasePrefix ) === 0 )
			{
				$k = \substr( $k, \strlen( static::$databasePrefix ) );
			}

			$obj->_data[ $k ] = $v;
		}
		$obj->changed = array();
		
		/* Init */
		if ( method_exists( $obj, 'init' ) )
		{
			$obj->init();
		}
				
		/* Return */
		return $obj;
	}
	
	/**
	 * Load and check permissions
	 *
	 * @param	mixed	$id	ID to load
	 * @return	\IPS\Content\Item
	 * @throws	\OutOfRangeException
	 */
	public static function loadAndCheckPerms( $id )
	{
		$obj = static::load( $id );
		
		if ( !$obj->canView() )
		{
			throw new \OutOfRangeException;
		}

		return $obj;
	}
	
	/**
	 * Member can view?
	 *
	 * @param	\IPS\Member|NULL	$member	The member to check for, or NULL for currently logged in member
	 * @return	bool
	 */
	public function canView( \IPS\Member $member = NULL )
	{
		$member = $member ?: \IPS\Member::loggedIn();
		
		return $this->member->member_id === $member->member_id or array_key_exists( $member->member_id, iterator_to_array( $this->member->alternativeContacts( array( 'billing=1' ) ) ) );
	}
	
	/**
	 * Get member
	 *
	 * @return	\IPS\Member
	 */
	public function get_member()
	{
		return \IPS\nexus\Customer::load( $this->_data['member'] );
	}
	
	/**
	 * Set member
	 *
	 * @param	\IPS\Member	$member	Member
	 * @return	void
	 */
	public function set_member( \IPS\Member $member )
	{
		$this->_data['member'] = $member->member_id;
	}
	
	/**
	 * Get payment gateway
	 *
	 * @return	\IPS\nexus\Gateway
	 */
	public function get_method()
	{
		return \IPS\nexus\Gateway::load( $this->_data['method'] );
	}
	
	/**
	 * Set payment gateway
	 *
	 * @param	\IPS\nexus\Gateway	$gateway	Payment gateway
	 * @return	void
	 */
	public function set_method( \IPS\nexus\Gateway $gateway )
	{
		$this->_data['method'] = $gateway->id;
	}
	
	/**
	 * Get start date
	 *
	 * @return	\IPS\DateTime
	 */
	public function get_started()
	{
		return \IPS\DateTime::ts( $this->_data['started'] );
	}
	
	/**
	 * Set next payment date
	 *
	 * @param	\IPS\DateTime	$date	The invoice date
	 * @return	void
	 */
	public function set_next_cycle( \IPS\DateTime $date = NULL )
	{
		$this->_data['next_cycle'] = $date ? $date->getTimestamp() : NULL;
	}
	
	/**
	 * Get next payment date
	 *
	 * @return	\IPS\DateTime
	 */
	public function get_next_cycle()
	{
		return $this->_data['next_cycle'] ? \IPS\DateTime::ts( $this->_data['next_cycle'] ) : NULL;
	}
	
	/**
	 * Set start date
	 *
	 * @param	\IPS\DateTime	$date	The invoice date
	 * @return	void
	 */
	public function set_started( \IPS\DateTime $date )
	{
		$this->_data['started'] = $date->getTimestamp();
	}

	/**
	 * Suspend
	 *
	 * @return	void
	 * @throws	\DomainException
	 */
	public function refresh()
	{
		try
		{
			while( true )
			{
				$this->checkForLatestTransaction();
			}
		}
		catch( \OutOfRangeException | \UnderflowException $e )
		{
			return;
		}
	}	

	/**
	 * Suspend
	 *
	 * @return	void
	 * @throws	\DomainException
	 */
	public function suspend()
	{
		$this->doSuspend();
		
		$this->member->log( 'billingagreement', array( 'type' => 'suspend', 'id' => $this->id, 'gw_id' => $this->gw_id ) );
		$this->next_cycle = NULL;
		$this->save();
	}
	
	/**
	 * Reactivate
	 *
	 * @return	void
	 * @throws	\DomainException
	 */
	public function reactivate()
	{
		$this->doReactivate();
		
		$this->member->log( 'billingagreement', array( 'type' => 'reactivate', 'id' => $this->id, 'gw_id' => $this->gw_id ) );
		$this->next_cycle = $this->nextPaymentDate();
		$this->save();
	}
	
	/**
	 * Cancel
	 *
	 * @return	void
	 * @throws	\DomainException
	 */
	public function cancel()
	{
		$this->doCancel();
		
		$this->member->log( 'billingagreement', array( 'type' => 'cancel', 'id' => $this->id, 'gw_id' => $this->gw_id ) );
		$this->next_cycle = NULL;
		$this->canceled = TRUE;	
		$this->save();
	}
	
	/**
	 * Front-End URL
	 *
	 * @return	\IPS\Http\Url
	 */
	public function url()
	{
		return \IPS\Http\Url::internal( "app=nexus&module=clients&controller=billingagreements&do=view&id={$this->id}", 'front', 'clientsbillingagreement' );
	}
	
	/**
	 * ACP URL
	 *
	 * @return	\IPS\Http\Url
	 */
	public function acpUrl()
	{
		return \IPS\Http\Url::internal( "app=nexus&module=payments&controller=billingagreements&id={$this->id}", 'admin' );
	}

	/**
	 * Check for latest unclaimed transactions
	 *
	 * @return	void
	 */
	public function checkForLatestTransaction()
	{
		/* Fetch the latest unclaimed transaction */
		try
		{
			$transaction = $this->latestUnclaimedTransaction();
		}
		/* If there isn't one yet, it might just be that PayPal hasn't taken it yet. Let it try for up to 5 days before giving up, which is how long PayPal will try for if there's an issue. */
		catch ( \OutOfRangeException $e )
		{
			if ( $this->next_cycle->getTimestamp() > ( time() - ( 86400 * 5 ) ) )
			{
				throw new \UnderflowException;
			}
			else
			{
				/* We are here because we could not find any unclaimed transactions and it has been at least 5 days since we expected to. Verify the next cycle before proceeding because it may not have been set properly in the past so before we say there's a problem let's check. */
				$nextPaymentDate = $this->nextPaymentDate();

				if( $nextPaymentDate->getTimestamp() > time() )
				{
					$this->next_cycle = $nextPaymentDate;
					$this->save();

					throw new \UnderflowException;
				}

				throw $e;
			}
		}

		/* Get purchases */
		$purchases = new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_purchases', array( 'ps_billing_agreement=?', $this->id ) ), 'IPS\nexus\Purchase' );

		/* Generate an invoice */
		$invoice = new \IPS\nexus\Invoice;
		$invoice->system = TRUE;
		$invoice->currency = $transaction->amount->currency;
		$invoice->member = $this->member;
		$invoice->billaddress = $this->member->primaryBillingAddress();
		foreach ( $purchases as $purchase )
		{
			$invoice->addItem( \IPS\nexus\Invoice\Item\Renewal::create( $purchase ) );
		}
		$invoice->save();

		/* Assign the transaction to it */
		$transaction->invoice = $invoice;
		$transaction->save();
		$transaction->approve();
		$invoice->status = $transaction->invoice->status;

		/* Log */
		$invoice->member->log( 'transaction', array(
			'type' => 'paid',
			'status' => \IPS\nexus\Transaction::STATUS_PAID,
			'id' => $transaction->id,
			'invoice_id' => $invoice->id,
			'invoice_title' => $invoice->title,
			'automatic' => TRUE,
		), FALSE );

		/* Update the purchase */
		if ( $invoice->status !== $invoice::STATUS_PAID )
		{
			foreach ( $purchases as $purchase )
			{
				$purchase->invoice_pending = $invoice;
				$purchase->save();
			}
		}

		/* Send notification */
		$invoice->sendNotification();

		/* Update billing agreement */
		if ( $invoice->status === $invoice::STATUS_PAID )
		{
			$this->next_cycle = $this->nextPaymentDate();
		}
		else
		{
			$this->next_cycle = NULL;
		}
		$this->save();
	}
}