<?php
/**
 * @brief		Invoice 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		10 Feb 2014
 */

namespace IPS\nexus;

/* 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;
}

/**
 * Invoice Model
 */
class _Invoice extends \IPS\Patterns\ActiveRecord
{
	const STATUS_PAID		= 'paid';
	const STATUS_PENDING	= 'pend';
	const STATUS_EXPIRED	= 'expd';
	const STATUS_CANCELED	= 'canc';
	
	/**
	 * @brief	Multiton Store
	 */
	protected static $multitons;

	/**
	 * @brief	Database Table
	 */
	public static $databaseTable = 'nexus_invoices';
	
	/**
	 * @brief	Database Prefix
	 */
	public static $databasePrefix = 'i_';
	
	/**
	 * Load and check permissions
	 *
	 * @return	\IPS\Content\Item
	 * @throws	\OutOfRangeException
	 */
	public static function loadAndCheckPerms( $id )
	{
		$obj = static::load( $id );
		
		if ( !$obj->canView() )
		{
			throw new \OutOfRangeException;
		}

		return $obj;
	}
	
	/**
	 * Get statuses
	 *
	 * @return	array
	 */
	public static function statuses()
	{
		$options = array();
		$reflection = new \ReflectionClass( get_called_class() );
		foreach ( $reflection->getConstants() as $k => $v )
		{
			if ( mb_substr( $k, 0, 7 ) === 'STATUS_' )
			{
				$options[ $v ] = "istatus_{$v}";
			}
		}
		return $options;	
	}
	
	/**
	 * Get invoices table
	 *
	 * @param	array	$where	Where clause
	 * @param	string	$ref	Referer
	 * @return	\IPS\Helpers\Table\Db
	 */
	public static function table( $where = array(), \IPS\Http\Url $url, $ref = 't' )
	{
		/* Create the table */
		$table = new \IPS\Helpers\Table\Db( 'nexus_invoices', $url, $where );
		$table->sortBy = $table->sortBy ?: 'i_date';
		
		/* Format Columns */
		$table->include = array( 'i_status', 'i_id', 'i_title', 'i_member', 'i_total', 'i_date' );
		$table->parsers = array(
			'i_status'	=> function( $val )
			{
				return \IPS\Theme::i()->getTemplate('invoices')->status( $val );
			},
			'i_member'	=> function ( $val, $row )
			{
				if ( $val )
				{
					return \IPS\Theme::i()->getTemplate('global')->userLink( \IPS\Member::load( $val ) );
				}
				elseif ( $row['i_guest_data'] )
				{
					$data  = json_decode( $row['i_guest_data'], TRUE );
					return \IPS\nexus\Customer::constructFromData( $data['member'] )->cm_name;
				}
				else
				{
					return \IPS\Theme::i()->getTemplate('global')->userLink( \IPS\Member::load( 0 ) );
				}
			},
			'i_total'	=> function( $val, $row )
			{
				return (string) new \IPS\nexus\Money( $val, $row['i_currency'] );
			},
			'i_date'	=> function( $val )
			{
				return \IPS\DateTime::ts( $val );
			}
		);
				
		/* Buttons */
		$table->rowButtons = function( $row ) use( $ref )
		{
			return array_merge( array(
				'view'	=> array(
					'icon'	=> 'search',
					'title'	=> 'invoice_view',
					'link'	=> \IPS\Http\Url::internal( "app=nexus&module=payments&controller=invoices&do=view&id={$row['i_id']}" )
				),
			), \IPS\nexus\Invoice::constructFromData( $row )->buttons( $ref ) );
		};
		
		return $table;
	}
	
	/**
	 * @brief	Being generated by system?
	 */
	public $system = FALSE;
	
	/**
	 * Set Default Values
	 *
	 * @return	void
	 */
	public function setDefaultValues()
	{
		$this->status = static::STATUS_PENDING;
		$this->items = json_encode( array() );
		$this->date = new \IPS\DateTime;
		$this->renewal_ids = array();
		$this->guest_data = array();
		$this->_data['total'] = 0;
		$this->_data['member'] = 0;
		$this->status_extra = array();
	}
	
	/**
	 * 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' ) ) ) );
	}
		
	/**
	 * Set title
	 *
	 * @param	string
	 * @return	void
	 * @throws	\LengthException
	 */
	public function set_title( $title )
	{
		if ( mb_strlen( $title ) > 128 )
		{
			throw new \LengthException;
		}
		
		$this->_data['title'] = $title;
	}
	
	/**
	 * @brief	Member
	 */
	protected $_member;
	
	/**
	 * Get member
	 *
	 * @return	\IPS\nexus\Customer
	 */
	public function get_member()
	{
		if ( $this->_member === NULL )
		{
			try
			{
				$this->_member = \IPS\nexus\Customer::load( $this->_data['member'] );
				
				/* \IPS\nexus\Customer does not necessarily throw an OutOfRangeException if passed a value of 0 (a guest instance is valid) - throw one here so we can bubble up to get the correct information for guests. */
				if ( !$this->_member->member_id )
				{
					throw new \OutOfRangeException;
				}
			}
			catch ( \OutOfRangeException $e )
			{
				$this->_member = new \IPS\nexus\Customer;
				
				if ( $this->_data['member'] )
				{
					$this->_member->_data['cm_first_name'] = \IPS\Member::loggedIn()->language()->addToStack('deleted_member');
				}
				elseif ( $this->guest_data )
				{
					foreach ( $this->guest_data['member'] as $k => $v )
					{
						$this->_member->_data[ $k ] = $v;
						$this->_member->changed[ $k ] = $v;
					}
				}
			}
		}
		
		return $this->_member;
	}
	
	/**
	 * Set member
	 *
	 * @param	\IPS\Member
	 * @return	void
	 */
	public function set_member( \IPS\Member $member )
	{
		$this->_data['member'] = (int) $member->member_id;
		$this->_member = NULL;
		
		if ( !$this->currency and $member instanceof \IPS\nexus\Customer )
		{
			$this->currency = $member->defaultCurrency();
		}
	}
	
	/**
	 * @brief	Items iterator
	 */
	protected $_itemsIterator;
	
	/**
	 * Get items
	 *
	 * @return	array
	 */
	public function get_items()
	{
		if ( $this->_itemsIterator === NULL )
		{
			/* Decode */
			$itemsJson = json_decode( $this->_data['items'], TRUE ) ?: array();
			
			/* Check we actually have a valid array with items (very old invoices from IP.Subscriptions may not have anything) and
				just create a dummy if so */
			$itemsJson = array_filter( $itemsJson, function( $val )
			{
				return is_array( $val ) and isset( $val['act'] );
			} );
			if ( !$itemsJson and $this->total->amount->isGreaterThanZero() )
			{
				$itemsJson = array( array(
					'act'			=> 'charge',
					'app'			=> 'nexus',
					'type'			=> 'charge',
					'cost'			=> $this->total->amountAsString(),
					'tax'			=> NULL,
					'quantity'		=> 1,
					'itemName'		=> $this->title,
					'itemID'		=> NULL,
					'physical'		=> FALSE,
					'methods'		=> '*',
					'cfields'		=> array(),
					'extra'			=> NULL,
				) );
			}
						
			/* Return it */
			$this->_itemsIterator = new \IPS\nexus\Invoice\ItemsIterator( $itemsJson );
			$this->_itemsIterator->currency = $this->currency;
		}

		return $this->_itemsIterator;
	}
	
	/**
	 * Get total
	 *
	 * @return	\IPS\nexus\Money
	 */
	public function get_total()
	{
		return new \IPS\nexus\Money( $this->_data['total'], $this->currency );
	}
	
	/**
	 * Set total
	 *
	 * @param	\IPS\nexus\Money	$total	The total
	 * @return	void
	 */
	public function set_total( \IPS\nexus\Money $total )
	{
		$this->_data['total'] = $total->amount;
	}
	
	/**
	 * Get date
	 *
	 * @return	\IPS\DateTime
	 */
	public function get_date()
	{
		return \IPS\DateTime::ts( $this->_data['date'] );
	}
	
	/**
	 * Set date
	 *
	 * @param	\IPS\DateTime	$date	The invoice date
	 * @return	void
	 */
	public function set_date( \IPS\DateTime $date )
	{
		$this->_data['date'] = $date->getTimestamp();
	}
	
	/**
	 * Get paid date date
	 *
	 * @return	\IPS\DateTime|NULL
	 */
	public function get_paid()
	{
		return $this->_data['paid'] ? \IPS\DateTime::ts( $this->_data['paid'] ) : NULL;
	}
	
	/**
	 * Set paid date
	 *
	 * @param	\IPS\DateTime	$date	The invoice date
	 * @return	void
	 */
	public function set_paid( \IPS\DateTime $date )
	{
		$this->_data['paid'] = $date->getTimestamp();
	}
	
	/**
	 * Get return URI
	 *
	 * @return	\IPS\Http\Url|NULL
	 */
	public function get_return_uri()
	{
		return $this->_data['return_uri'] ? \IPS\Http\Url::internal( $this->_data['return_uri'] ) : NULL;
	}
	
	/**
	 * Get status information
	 *
	 * @return	mixed
	 */
	public function get_status_extra()
	{
		return json_decode( $this->_data['status_extra'], TRUE );
	}
	
	/**
	 * Set status information
	 *
	 * @param	mixed	$extra	The data
	 * @return	void
	 */
	public function set_status_extra( $extra )
	{
		$this->_data['status_extra'] = json_encode( $extra );
	}
	
	/**
	 * Get renewal IDs
	 *
	 * @return	array
	 */
	public function get_renewal_ids()
	{
		return explode( ',', $this->_data['renewal_ids'] );
	}
	
	/**
	 * Set renewal IDs
	 *
	 * @param	array	$ids	The IDs
	 * @return	void
	 */
	public function set_renewal_ids( $ids )
	{
		array_unique( $ids );
		$ids = array_filter( $ids );
		$this->_data['renewal_ids'] = implode( ',', $ids );
	}
	
	/**
	 * Get shipping address
	 *
	 * @return	\IPS\GeoLocation|NULL
	 */
	public function get_shipaddress()
	{
		if ( !$this->hasPhysicalItems() )
		{
			return NULL;
		}
		
		if ( isset( $this->_data['shipaddress'] ) and $this->_data['shipaddress'] )
		{
			return \IPS\GeoLocation::buildFromJson( $this->_data['shipaddress'] );
		}
		else
		{
			try
			{
				return \IPS\GeoLocation::buildFromJson( \IPS\Db::i()->select( 'address', 'nexus_customer_addresses', array( 'member=? AND primary_shipping=1', $this->member->member_id ) )->first() );
			}
			catch ( \UnderflowException $e )
			{
				return NULL;
			}
		}		
	}
	
	/**
	 * Set shipping address
	 *
	 * @param	\IPS\GeoLocation	$shippingAddress	The shipping address
	 * @return	void
	 */
	public function set_shipaddress( \IPS\GeoLocation $shippingAddress )
	{
		$this->_data['shipaddress'] = json_encode( $shippingAddress );
	}
	
	/**
	 * Get billing address
	 *
	 * @return	\IPS\GeoLocation|NULL
	 */
	public function get_billaddress()
	{
		if ( isset( $this->_data['billaddress'] ) and $this->_data['billaddress'] )
		{
			return \IPS\GeoLocation::buildFromJson( $this->_data['billaddress'] );
		}
		else
		{
			return NULL;
		}		
	}
	
	/**
	 * Set billing address
	 *
	 * @param	\IPS\GeoLocation	$billingAddress	The billing address
	 * @return	void
	 */
	public function set_billaddress( \IPS\GeoLocation $billingAddress )
	{
		$this->_data['billaddress'] = json_encode( $billingAddress );
		$this->_data['billcountry'] = $billingAddress->country;
	}
	
	/**
	 * Get guest data
	 *
	 * @return	mixed
	 */
	public function get_guest_data()
	{
		return $this->_data['guest_data'] ? json_decode( $this->_data['guest_data'], TRUE ) : NULL;
	}
	
	/**
	 * Set guest data
	 *
	 * @param	mixed	$data	The data
	 * @return	void
	 */
	public function set_guest_data( $data )
	{
		$this->_data['guest_data'] = $data ? json_encode( $data ) : NULL;
	}
	
	/**
	 * Set a title automatically based on item names
	 *
	 * @return	void
	 */
	public function setDefaultTitle()
	{
		$titles = array();
		foreach ( $this->items as $item )
		{
			$titles[] = ( $item->name . ( $item->quantity > 1 ? " x{$item->quantity}" : '' ) );
		}
		$thisTitle = implode( ', ', $titles );
		
		if ( mb_strlen( $thisTitle ) > 128 )
		{
			$thisTitle = mb_substr( $thisTitle, 0, 125 ) . '...';
		}

		$this->title = $thisTitle;
	}
	
	/**
	 * Save Changed Columns
	 *
	 * @return	void
	 */
	public function save()
	{		
		if ( !$this->title )
		{
			$this->setDefaultTitle();
		}
		
		if ( $this->_new )
		{
			parent::save();
			$this->member->log( 'invoice', array( 'id' => $this->id, 'title' => $this->title, 'status' => $this->status, 'system' => $this->system ), $this->system ? FALSE : NULL );
		}
		else
		{
			parent::save();
		}						
	}
	
	/**
	 * Delete
	 *
	 * @return	void
	 */
	public function delete()
	{
		\IPS\File::unclaimAttachments( 'nexus_Purchases', NULL, NULL, "invoice-{$this->id}" );
		
		parent::delete();
		
		foreach ( $this->transactions( array( \IPS\nexus\Transaction::STATUS_PENDING, \IPS\nexus\Transaction::STATUS_WAITING, \IPS\nexus\Transaction::STATUS_HELD, \IPS\nexus\Transaction::STATUS_REVIEW, \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING ) ) as $transaction )
		{
			if ( $transaction->status === \IPS\nexus\Transaction::STATUS_PENDING )
			{
				$transaction->delete();
			}
			else
			{
				try
				{
					$transaction->method->void( $transaction );
				}
				catch ( \Exception $e ){}
				
				$extra = $transaction->extra;
				$extra['history'][] = array( 's' => \IPS\nexus\Transaction::STATUS_REFUSED, 'on' => time(), 'note' => 'invoiceDeleted' );
				$transaction->extra = $extra;
				$transaction->status = \IPS\nexus\Transaction::STATUS_REFUSED;
				$transaction->save();
			}
		}
	}
	
	/* !Items */
	
	/**
	 * Add item
	 *
	 * @param	\IPS\nexus\Invoice\Item	$item		The item
	 * @array	int|NULL				$cartKey	Our cart key for this item, or NULL. Used to make sure items are properly associated with their parents if not bought yet.
	 * @return	int
	 */
	public function addItem( \IPS\nexus\Invoice\Item $item, $cartKey = NULL )
	{
		/* Set basic data */
		$data = array(
			'act'			=> $item::$act,
			'app'			=> isset( $item::$application ) ? $item::$application : $item->application,
			'type'			=> isset( $item::$type ) ? $item::$type : $item->type,
			'cost'			=> $item->price->amountAsString(),
			'tax'			=> $item->tax ? $item->tax->id : NULL,
			'quantity'		=> $item->quantity,
			'itemName'		=> $item->name,
			'itemID'		=> $item->id,
			'physical'		=> FALSE,
			'methods'		=> $item->paymentMethodIds ?: '*',
			'cfields'		=> $item->details,
			'extra'			=> $item->extra,
		);
		
		/* Parent? */
		if ( isset( $item->parent ) and $item->parent !== NULL )
		{			
			if ( is_int( $item->parent ) )
			{
				$data['assoc'] = $item->parent;
				$data['assocBought'] = FALSE;
			}
			else
			{
				$data['assoc'] = $item->parent->id;
				$data['assocBought'] = TRUE;
			}
			$data['groupParent'] = $item->groupWithParent;
		}
		
		/* Renewal term? */
		if ( isset( $item->renewalTerm ) )
		{
			$renewTermData = $item->renewalTerm->getTerm();
			$data['renew_term'] = $renewTermData['term'];
			$data['renew_units'] = $renewTermData['unit'];
			$data['renew_cost'] = $item->renewalTerm->cost->amountAsString();
			if ( $item->renewalTerm->gracePeriod )
			{
				$data['grace_period'] = \IPS\DateTime::create()->add( $item->renewalTerm->gracePeriod )->getTimestamp() - time();
			}
		}
		
		/* Expire date? */
		if ( isset( $item->expireDate ) )
		{
			$data['expires'] = $item->expireDate->getTimestamp();
		}
		
		/* Physical? */
		if ( $item->physical )
		{
			$data['physical'] = TRUE;
			$data['shipping'] = $item->shippingMethodIds ?: '*';
			$data['weight'] = $item->weight->kilograms;
			$data['length'] = $item->length->metres;
			$data['width'] = $item->width->metres;
			$data['height'] = $item->height->metres;
		}
		
		/* Pay to someone else? */
		if ( $item->payTo )
		{
			$data['payTo'] = $item->payTo->member_id;
			if ( $item->commission )
			{
				$data['commission'] = $item->commission;
			}
			if ( $item->fee )
			{
				$data['fee'] = $item->fee->amount;
			}
		}
								
		/* Add it */
		$items = json_decode( $this->_data['items'], TRUE );
		if ( $cartKey !== NULL )
		{
			$items[ $cartKey ] = $data;
		}
		else
		{
			$items[] = $data;
		}
		
		/* Get the ID */
		$ids = array_keys( $items );
		$id = array_pop( $ids );

		/* Save it */
		$this->items = json_encode( $items );
		$this->_itemsIterator = NULL;
		
		/* Is it a renewal */
		if ( $item instanceof \IPS\nexus\Invoice\Item\Renewal )
		{
			$renewalIds = $this->renewal_ids;
			$renewalIds[] = $item->id;
			$this->renewal_ids = $renewalIds;
		}

		/* Increase the total */
		$this->recalculateTotal();
		
		/* Return the ID */
		return $id;
	}
	
	/**
	 * Change item
	 *
	 * @param	int		$index	The index
	 * @param	array	$data	Data to set
	 * @return	void
	 */
	public function changeItem( $index, $data )
	{
		$items = json_decode( $this->_data['items'], TRUE );
		foreach ( $data as $k => $v )
		{
			$items[ $index ][ $k ] = $v;
		}
		$this->items = json_encode( $items );
		$this->_itemsIterator = NULL;
	}
	
	/**
	 * Remove item
	 *
	 * @param	int		$index	The index
	 * @return	void
	 */
	public function removeItem( $index )
	{
		$items = json_decode( $this->_data['items'], TRUE );
		unset( $items[ $index ] );
		$this->items = json_encode( $items );
		$this->_itemsIterator = NULL;
		$this->recalculateTotal();
	}
		
	/**
	 * Recalculate the total amount
	 *
	 * @return	void
	 */
	public function recalculateTotal()
	{		
		$summary = $this->summary();		
		$this->total = $summary['total'];
	}
	
	/**
	 * Get summary for invoice tables and so on
	 *
	 * @return	array
	 */
	public function summary()
	{
		$return = array();
		$return['items'] = array();
		$shippingItems = array();
		$subtotal = new \IPS\Math\Number('0');
		$taxableAmounts = array();
		$taxClasses = \IPS\nexus\Tax::roots();
				
		/* Reduce taxable amount from coupons */
		$couponTaxReductions = array();
		foreach ( $this->items as $itemId => $item )
		{
			if ( $item instanceof \IPS\nexus\extensions\nexus\Item\CouponDiscount )
			{				
				if ( isset( $item->extra['type'] ) )
				{
					if ( isset( $item->extra['items'] ) )
					{
						$itemCount = count( $item->extra['items'] );
					}
					else
					{
						$itemCount = 0;
						foreach ( clone $this->items as $k => $_item ) // We have to clone because $items is an iterator, and we're already looping them, but we need a separate loop
						{
							if ( $_item != $item and !( $_item instanceof \IPS\nexus\extensions\nexus\Item\ShippingCharge ) and !( $_item instanceof \IPS\nexus\extensions\nexus\Item\CouponDiscount ) )
							{
								$itemCount++;
							}
						}
					}
					
					foreach ( clone $this->items as $k => $_item ) // We have to clone because $items is an iterator, and we're already looping them, but we need a separate loop
					{
						if ( $_item != $item and !( $_item instanceof \IPS\nexus\extensions\nexus\Item\CouponDiscount ) and !( $_item instanceof \IPS\nexus\extensions\nexus\Item\ShippingCharge ) and ( !isset( $item->extra['items'] ) or in_array( $k, $item->extra['items'] ) ) )
						{
							if ( $item->extra['type'] == 'p' )
							{
								$reductionAmount = $_item->linePrice()->amount->multiply( new \IPS\Math\Number("{$item->extra['value']}") )->divide( new \IPS\Math\Number('100') );
							}
							else
							{
								$reductionAmount = ( new \IPS\Math\Number( "{$item->extra['value']}" ) )->divide( new \IPS\Math\Number( "{$itemCount}" ) )->multiply( new \IPS\Math\Number( '-1' ) );
							}
							
							if ( isset( $couponTaxReductions[ $k ] ) )
							{
								$couponTaxReductions[ $k ] = $couponTaxReductions[ $k ]->add( $reductionAmount );
							}
							else
							{
								$couponTaxReductions[ $k ] = $reductionAmount;
							}
						}
					}
				}
			}
		}
						
		/* Work out the line totals */
		foreach ( $this->items as $k => $item )
		{
			if ( $item instanceof \IPS\nexus\extensions\nexus\Item\ShippingCharge )
			{
				$shippingItems[ $k ] = $item;
			}
			else
			{
				$subtotal = $subtotal->add( $item->linePrice()->amount );
								
				if ( isset( $item->tax ) )
				{
					$taxAmount = $item->linePrice()->amount;
										
					if ( isset( $couponTaxReductions[ $k ] ) )
					{
						$taxAmount = $taxAmount->subtract( $couponTaxReductions[ $k ] );
					}
					
					if ( isset( $taxableAmounts[ $item->tax->id ] ) )
					{
						$taxableAmounts[ $item->tax->id ] = $taxableAmounts[ $item->tax->id ]->add( $taxAmount );
					}
					else
					{
						$taxableAmounts[ $item->tax->id ] = $taxAmount;
					}
				}		
				
				$return['items'][ $k ] = $item;
			}
		}
		
		/* Subtotal (subtotal is caulculated BEFORE shipping is added) */
		$return['subtotal'] = new \IPS\nexus\Money( $subtotal, $this->currency );
		
		/* Add shipping */
		$return['shipping'] = array();
		$shipping = new \IPS\Math\Number('0');
		foreach ( $shippingItems as $k => $item )
		{
			$linePrice = $item->linePrice()->amount;
			$shipping = $shipping->add( $linePrice );
			
			if ( isset( $item->tax ) )
			{
				if ( isset( $taxableAmounts[ $item->tax->id ] ) )
				{
					$taxableAmounts[ $item->tax->id ] = $taxableAmounts[ $item->tax->id ]->add( $linePrice );
				}
				else
				{
					$taxableAmounts[ $item->tax->id ] = $linePrice;
				}
			}
			
			$return['shipping'][ $k ] = $item;
		}
		$return['shippingTotal'] = new \IPS\nexus\Money( $shipping, $this->currency );
				
		/* Now work out the tax */
		$return['tax'] = array();
		$tax = new \IPS\Math\Number('0');
		foreach ( $taxableAmounts as $taxId => $amount )
		{
			$rate = $taxClasses[ $taxId ]->rate( $this->billaddress );
			$amount = $amount->multiply( new \IPS\Math\Number( $rate ) );
			
			$amount = new \IPS\nexus\Money( $amount, $this->currency );
			
			$return['tax'][ $taxId ] = array( 'name' => $taxClasses[ $taxId ]->_title, 'rate' => $rate, 'amount' => $amount );
			$tax = $tax->add( $amount->amount );
		}
		$return['taxTotal'] = new \IPS\nexus\Money( $tax, $this->currency );
		
		/* Add it all together */		
		$return['total'] = new \IPS\nexus\Money( $subtotal->add( $shipping )->add( $tax ), $this->currency );
		
		return $return;			
	}
	
	/* !Information */
	
	/**
	 * Does the invoice contain items that require a billing address?
	 *
	 * @return	bool
	 */
	public function hasItemsRequiringBillingAddress()
	{
		foreach ( $this->items as $item )
		{
			if ( $item->requiresBillingAddress() )
			{
				return TRUE;
			}
		}
		
		return FALSE;
	}
	
	/**
	 * Does the invoice contain physical items that need shipping?
	 *
	 * @return	bool
	 */
	public function hasPhysicalItems()
	{
		foreach ( $this->items as $item )
		{
			if ( $item->physical )
			{
				return TRUE;
			}
		}
		
		return FALSE;
	}
	
	/**
	 * Requires login?
	 *
	 * @return	bool
	 */
	public function requiresLogin()
	{
		foreach ( $this->items as $item )
		{
			if ( $item::$requiresAccount )
			{
				return TRUE;
			}
		}
		
		return FALSE;
	}
	
	/**
	 * Amounts that go to a member
	 *
	 * @return	array
	 */
	public function payToRecipients()
	{
		$recipients = array();
		
		foreach ( $this->items as $item )
		{
			if ( $item->payTo )
			{
				if ( !isset( $recipients[ $item->payTo->member_id ] ) )
				{
					$recipients[ $item->payTo->member_id ] = new \IPS\Math\Number('0');
				}
				$itemRecipientAmounts = $item->recipientAmounts();
				$recipients[ $item->payTo->member_id ] = $recipients[ $item->payTo->member_id ]->add( $itemRecipientAmounts['recipient_final']->amount );
			}
		}

		return $recipients;
	}
	
	/**
	 * Get the commission information
	 *
	 * @param	\IPS\Math\Number	$amountReceived	The amount the invoice is for, minus amounts being given to other recipients
	 * @return	array( 'rule' => \IPS\nexus\CommissionRule, 'referrer' => \IPS\nexus\Customer, 'amount' => \IPS\nexus\Money )
	 */
	public function commission( \IPS\Math\Number $amountReceived )
	{
		if ( !isset( $this->status_extra['commissionrule'] ) )
		{		
			try
			{
				/* Get the referrer */
				$referrer = \IPS\Member::load( \IPS\Db::i()->select( 'referred_by', 'nexus_referrals', array( 'member_id=?', $this->member->member_id ) )->first() );
				if ( !$referrer->member_id or $referrer->inGroup( explode( ',', \IPS\Settings::i()->nexus_no_commission ) ) )
				{
					throw new \UnderflowException;
				}
				
				/* Get pertinent values */
				$byNumber  = \IPS\Db::i()->select( 'COUNT( i_id )', 'nexus_invoices', array( "i_member=? AND i_status=?", $referrer->member_id, static::STATUS_PAID ) )->first();
				$byAmounts  = iterator_to_array( \IPS\Db::i()->select( 'i_currency, SUM( i_total ) as value', 'nexus_invoices', array( "i_member=? AND i_status=?", $referrer->member_id, static::STATUS_PAID ), NULL, NULL, 'i_currency' )->setKeyField( 'i_currency' )->setValueField( 'value' ) );
				$forNumber  = \IPS\Db::i()->select( 'COUNT( i_id )', 'nexus_invoices', array( "i_member=? AND i_status=?", $this->member->member_id, static::STATUS_PAID ) )->first();
				$forAmounts  = iterator_to_array( \IPS\Db::i()->select( 'i_currency, SUM( i_total ) as value', 'nexus_invoices', array( "i_member=? AND i_status=?", $this->member->member_id, static::STATUS_PAID ), NULL, NULL, 'i_currency' )->setKeyField( 'i_currency' )->setValueField( 'value' ) );
	
				/* Loop the rules to find out the best one */
				$maxAmount = new \IPS\Math\Number('0');
				$actingRule = NULL;
				foreach ( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_referral_rules', array( array( "rrule_by_group='*' OR " . \IPS\Db::i()->findInSet( 'rrule_by_group', $referrer->groups ) ), array( "rrule_for_group='*' OR " . \IPS\Db::i()->findInSet( 'rrule_for_group', $this->member->groups ) ) ) ), 'IPS\nexus\CommissionRule' ) as $commissionRule )
				{
					/* Before we go nuts, don't bother if it's not going to give us more commission */
					if ( $amountReceived->percentage( $commissionRule->commission )->compare( $maxAmount ) === -1 )
					{
						continue;
					}
					if ( $commissionRule->commission_limit )
					{
						$commisionLimit = json_decode( $commissionRule->commission_limit, TRUE );
						if ( isset( $commisionLimit[ $this->currency ] ) and ( new \IPS\Math\Number( number_format( $commisionLimit[ $this->currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $this->currency ), '.', '' ) ) )->compare( $maxAmount ) === -1 )
						{
							continue;
						}
					}
					
					/* Is the invoice amount right? */
					if ( $commissionRule->purchase_amount_op )
					{
						$amount = json_decode( $commissionRule->purchase_amount_unit, TRUE );
						if ( !$this->_checkCondition( $this->total->amount, $commissionRule->purchase_amount_op, $amount[ $this->currency ] ) )
						{
							continue;
						}
					}
				
					/* Does the referring user have the right amount of purchases? */
					if ( $commissionRule->by_purchases_op )
					{
						$val = $commissionRule->by_purchases_unit;
						if ( $commissionRule->by_purchases_type == 'v' )
						{
							$val = json_decode( $val, TRUE );
							$val = $val[ $this->currency ];
						}
						
						if ( !$this->_checkCondition( ( $commissionRule->by_purchases_type == 'n' ? $byNumber : $byAmounts[ $this->currency ] ), $commissionRule->by_purchases_op, $val ) )
						{
							continue;
						}
					}
					
					
					/* Does the purchasing user have the right amount of purchases? */
					if ( $commissionRule->for_purchases_op )
					{
						$val = $commissionRule->for_purchases_unit;
						if ( $commissionRule->for_purchases_type == 'v' )
						{
							$val = json_decode( $val, TRUE );
							$val = $val[ $this->currency ];
						}
						
						if ( !$this->_checkCondition( ( $commissionRule->for_purchases_type == 'n' ? $forNumber : $forAmounts[ $this->currency ] ), $commissionRule->for_purchases_op, $val ) )
						{
							continue;
						}
					}
									
					/* Work out what packages apply */
					$amountToApplyCommissionOn = $amountReceived;
					if ( $commissionRule->purchase_packages )
					{
						if ( $commissionRule->purchase_package_limit )
						{
							$amountToApplyCommissionOn = new \IPS\Math\Number('0');
						}
												
						$itemsToCount = array();
						foreach ( $this->items as $k => $item )
						{
							/* What's the package ID? */
							if ( $item instanceof \IPS\nexus\extensions\nexus\Item\Package )
							{
								$packageId = $item->id;
							}
							elseif ( $commissionRule->purchase_renewal and $item instanceof \IPS\nexus\Invoice\Item\Renewal and $item::$application == 'nexus' and $item::$type == 'package' )
							{
								try
								{
									$packageId = \IPS\nexus\Purchase::load( $item->id )->item_id;
								}
								catch ( \OutOfRangeException $e ) { }
							}
													
							/* Is it in our list? */
							if ( !$packageId or !in_array( $packageId, explode( ',', $commissionRule->purchase_packages ) ) )
							{
								continue;
							}
							
							/* Add it to the list */
							if ( $packageId )
							{
								$itemsToCount[ $k ] = $packageId;
							}
							if ( $commissionRule->purchase_package_limit )
							{
								$amountToApplyCommissionOn = $amountToApplyCommissionOn->add( $item->price->amount->multiply( new \IPS\Math\Number( "{$item->quantity}" ) ) );
							}
						}
						
						/* If we need ALL of the packages, do we have them all? */
						if ( $commissionRule->purchase_any )
						{
							if ( empty( $itemsToCount ) )
							{
								continue;
							}
						}
						else
						{
							foreach ( explode( ',', $commissionRule->purchase_packages ) as $pid )
							{
								if ( !in_array( $pid, $itemsToCount ) )
								{
									continue 2;
								}
							}
						}
					}
					
					/* If we're still here, we're applying commission */
					$amountToGive = $amountToApplyCommissionOn->percentage( $commissionRule->commission );
					if ( $commissionRule->commission_limit and $commissionRule->commission_limit != '*' )
					{
						$limit = json_decode( $commissionRule->commission_limit, TRUE );
						if ( isset( $limit[ $this->currency ] ) and $amountToGive->compare( new \IPS\Math\Number( number_format( $limit[ $this->currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $this->currency ), '.', '' ) ) ) === 1 )
						{
							$amountToGive = new \IPS\Math\Number( number_format( $limit[ $this->currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $this->currency ), '.', '' ) );
						}
					}
					
					/* Is this more than what we have? */
					if ( ( $amountToGive->compare( $maxAmount ) === 1 ) or ( $amountToGive->isGreaterThanZero() and !$maxAmount->isGreaterThanZero() ) )
					{
						$maxAmount = $amountToGive;
						$actingRule = $commissionRule;
					}
				}
				
				/* Return */
				$extra = $this->status_extra;
				$extra['commissionrule'] = $actingRule ? $actingRule->id : 0;
				$extra['commissionref'] = $referrer->member_id;
				$extra['commissionamount'] = $maxAmount;
				$this->status_extra = $extra;
			}
			catch ( \UnderflowException $e )
			{
				$extra = $this->status_extra;
				$extra['commissionrule'] = 0;
				$extra['commissionref'] = 0;
				$extra['commissionamount'] = 0;
				$this->status_extra = $extra;
			}
			
			$this->save();
		}
		
		try
		{
			return array(
				'rule'		=> $this->status_extra['commissionrule'] ? \IPS\nexus\CommissionRule::load( $this->status_extra['commissionrule'] ) : NULL,
				'referrer'	=> $this->status_extra['commissionref'] ? \IPS\nexus\Customer::load( $this->status_extra['commissionref'] ) : NULL,
				'amount'	=> $this->status_extra['commissionamount'] ? new \IPS\nexus\Money( $this->status_extra['commissionamount'], $this->currency ) : NULL
			);
		}
		catch( \OutOfRangeException $e )
		{
			/* If either the commission rule, or the commission ref, are no longer valid, then let's try again and see if it matches up with a different one. */
			$extra = $this->status_extra;
			unset( $extra['commissionrule'], $extra['commissionref'], $extra['commissionamount'] );
			$this->status_extra = $extra;
			
			$this->save();
			
			return $this->commission( $amountReceived );
		}
	}
	
	/**
	 * Check condition
	 *
	 * @param	float	$a			First parameter
	 * @param	string	$operator	Operator (g = greater than, e = equal to, l = less than)
	 * @param	float	$b			Second parameter
	 * @return	bool
	 */
	protected function _checkCondition( $a, $operator, $b )
	{
		if ( $a instanceof \IPS\Math\Number and !( $b instanceof \IPS\Math\Number ) )
		{
			$b = new \IPS\Math\Number( "{$b}" );
		}
		if ( !( $a instanceof \IPS\Math\Number ) and $b instanceof \IPS\Math\Number )
		{
			$a = new \IPS\Math\Number( "{$a}" );
		}
		
		switch ( $operator )
		{
			case 'g':
				if ( $a instanceof \IPS\Math\Number )
				{
					return $a->compare( $b ) === 1;
				}
				else
				{
					return $a > $b;
				}
			case 'e':
				if ( $a instanceof \IPS\Math\Number )
				{
					return $a->compare( $b ) === 0;
				}
				else
				{
					return $a == $b;
				}
			case 'l':
				if ( $a instanceof \IPS\Math\Number )
				{
					return $a->compare( $b ) === 11;
				}
				else
				{
					return $a < $b;
				}
		}
		return FALSE;
	}
	
	/**
	 * Get amount left to pay
	 *
	 * @return	\IPS\nexus\Money
	 */
	public function amountToPay( $deductHeldTransactions=FALSE )
	{
		$amountToPay = $this->total;
		
		$statuses = array( \IPS\nexus\Transaction::STATUS_PAID, \IPS\nexus\Transaction::STATUS_PART_REFUNDED );
		if ( $deductHeldTransactions )
		{
			$statuses[] = \IPS\nexus\Transaction::STATUS_HELD;
			$statuses[] = \IPS\nexus\Transaction::STATUS_REVIEW;
			$statuses[] = \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING;
		}
		
		foreach ( $this->transactions( $statuses ) as $transaction )
		{
			if ( $transaction->currency === $this->currency )
			{
				$amountToSubtract = $transaction->amount->amount;
				if ( $transaction->status === \IPS\nexus\Transaction::STATUS_PART_REFUNDED )
				{
					$amountToSubtract = $amountToSubtract->subtract( $transaction->partial_refund->amount );
				}
				
				$amountToPay->amount = $amountToPay->amount->subtract( $amountToSubtract );
			}
		}
				
		return $amountToPay;
	}
	
	/**
	 * ACP Buttons
	 *
	 * @param	string	$ref	Referer
	 * @return	array
	 */
	public function buttons( $ref='v' )
	{
		$url = $this->acpUrl()->setQueryString( 'r', $ref );
		$return = array();
		
		if ( $this->status !== \IPS\nexus\Invoice::STATUS_PAID and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_add' ) )
		{
			$return['edit'] = array(
				'title'		=> 'edit',
				'icon'		=> 'pencil',
				'link'		=> $url->setQueryString( array( 'do' => 'generate', '_new' => 1 ) )
			);
		}
		
		if ( $this->status === \IPS\nexus\Invoice::STATUS_PENDING )
		{
			if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_edit' ) )
			{
				$return['paid'] = array(
					'title'		=> 'invoice_mark_paid',
					'icon'		=> 'check',
					'link'		=> $url->setQueryString( 'do', 'paid' ),
					'data'		=> count( $this->transactions( array( \IPS\nexus\Transaction::STATUS_PENDING, \IPS\nexus\Transaction::STATUS_WAITING, \IPS\nexus\Transaction::STATUS_HELD, \IPS\nexus\Transaction::STATUS_REVIEW, \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING ) ) ) ? array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('invoice_mark_paid') ) : array( 'confirm' => '' )
				);
			}
			if ( count( \IPS\nexus\Gateway::manualChargeGateways() ) and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'chargetocard' ) )
			{
				$return['card'] = array(
					'title'		=> 'invoice_charge_to_card',
					'icon'		=> 'credit-card',
					'link'		=> $url->setQueryString( array( 'do' => 'card', '_new' => 1 ) ),
				);
			}
			if ( isset( $this->member->cm_credits[ $this->currency ] ) and $this->member->cm_credits[ $this->currency ]->amount->isGreaterThanZero() and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_edit' ) )
			{
				$return['credit'] = array(
					'title'		=> 'invoice_charge_to_credit',
					'icon'		=> 'money',
					'link'		=> $url->setQueryString( 'do', 'credit' ),
					'data'		=> array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('invoice_charge_to_credit') )
				);
			}
		}
		if ( $this->status !== \IPS\nexus\Invoice::STATUS_PAID and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_resend' ) )
		{
			$return['resend'] = array(
				'title'		=> 'invoice_reissue',
				'icon'		=> 'refresh',
				'link'		=> $url->setQueryString( 'do', 'resend' ),
				'data'		=> array(
					'confirm'			=> '',
					'confirmSubMessage'	=> \IPS\Member::loggedIn()->language()->addToStack('invoice_reissue_confirm'),
					'confirmType'		=> 'verify',
					'confirmIcon'		=> 'question',
					'confirmButtons'	=> json_encode( array(
						'yes'				=>	\IPS\Member::loggedIn()->language()->addToStack('invoice_reissue_yes'),
						'no'				=>	\IPS\Member::loggedIn()->language()->addToStack('invoice_reissue_no'),
						'cancel'			=>	\IPS\Member::loggedIn()->language()->addToStack('cancel'),
					) )
				)
			);
		}
		$return['print'] = array(
			'title'		=> 'print',
			'icon'		=> 'print',
			'target'	=> '_blank',
			'link'		=> $url->setQueryString( 'do', 'printout' ),
		);
		if ( $this->status !== \IPS\nexus\Invoice::STATUS_CANCELED and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_edit' ) )
		{
			$return['unpaid'] = array(
				'title'		=> $this->status === \IPS\nexus\Invoice::STATUS_PAID ? 'invoice_mark_unpaid' : 'cancel',
				'icon'		=> 'times',
				'link'		=> $url->setQueryString( 'do', 'unpaid' ),
				'data'		=> array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('cancel') )
			);
		}
		if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_delete' ) )
		{
			$return['delete'] = array(
				'title'		=> 'delete',
				'icon'		=> 'times-circle',
				'link'		=> $url->setQueryString( 'do', 'delete' ),
				'data'		=> array( 'confirm' => '', 'confirmSubMessage' => \IPS\Member::loggedIn()->language()->addToStack('invoice_delete_warning') )
			);
		}
		
		return $return;
	}
	
	/**
	 * Can payment be split? Returns the minnimum split amount
	 *
	 * @return	FALSE|float
	 */
	public function canSplitPayment()
	{
		if ( \IPS\Settings::i()->nexus_split_payments == -1 )
		{
			return FALSE;
		}
				
		if ( \IPS\Settings::i()->nexus_split_payments )
		{
			$decoded = json_decode( \IPS\Settings::i()->nexus_split_payments, TRUE );
			if ( isset( $decoded[ $this->currency ] ) )
			{
				if ( $decoded[ $this->currency ]['amount'] <= $this->amountToPay()->amount )
				{
					return $decoded[ $this->currency ]['amount'];
				}
			}
			return FALSE;
		}
		else
		{
			return 0.01;
		}
	}
		
	/**
	 * Get output for API
	 *
	 * @return	array
	 * @apiresponse	int									id				ID number
	 * @apiresponse	string								title			Title
	 * @apiresponse	string								status			'paid' = Paid; 'pend' = Pending; 'expd' = Expired; 'canc' = Cancelled
	 * @apiresponse	datetime							issueDate		The date the invoice was issued
	 * @apiresponse	datetime							paidDate		The date the invoice was paid	
	 * @apiresponse	\IPS\nexus\Customer					customer		Customer
	 * @apiresponse	[\IPS\nexus\Invoice\Item]			items			Items
	 * @apiresponse	\IPS\nexus\Money					subTotal		Subtotal of item costs		
	 * @apiresponse	[\IPS\nexus\Invoice\Item]			shippingCharges	Shipping charges
	 * @apiresponse	\IPS\nexus\Money					shippingTotal	Total cost of shipping charges
	 * @apiresponse	[\IPS\nexus\Invoice\Item\TaxItem]	tax				Tax charges
	 * @apiresponse	\IPS\nexus\Money					taxTotal		Total cost of tax charges
	 * @apiresponse	\IPS\nexus\Money					total			Total
	 * @apiresponse	\IPS\nexus\Money					outstanding		The outstanding amount to be paid
	 * @apiresponse	\IPS\GeoLocation					billingAddress	Billing address
	 * @apiresponse	\IPS\GeoLocation					shippingAddress	Shipping Address
	 * @apiresponse	string								poNumber		PO Number (set by customer)
	 * @apiresponse	string								notes			Notes (set by customer)
	 * @apiresponse	[\IPS\nexus\Transaction]			transactions	Transactions against this invoice
	 * @apiresponse	[\IPS\nexus\Shipping\Order]			shipments		Shipments generated from this invoice (if paid)
	 * @apiresponse	[\IPS\nexus\Purchase]				purchases		Purchases generated from this invoice (if paid)
	 * @apiresponse	string								viewUrl			The URL to where the customer can view this invoice
	 * @apiresponse	string								checkoutUrl		The URL to where the customer can pay this invoice
	 */
	public function apiOutput()
	{
		$summary = $this->summary();
		
		$tax = array();
		foreach ( $summary['tax'] as $classId => $data )
		{
			$tax[] = ( new \IPS\nexus\Invoice\Item\TaxItem( $classId, $data ) )->apiOutput();
		}
		
		return array(
			'id'				=> $this->id,
			'title'				=> $this->title,
			'status'			=> $this->status,
			'issueDate'			=> $this->date->rfc3339(),
			'paidDate'			=> $this->paid ? $this->paid->rfc3339() : null,
			'customer'			=> $this->member->apiOutput(),
			'items'				=> array_map( function( $item ){
				return $item->apiOutput();
			}, $summary['items'] ),
			'subTotal'			=> $summary['subtotal']->apiOutput(),
			'shippingCharges'	=> array_map( function( $item ){
				return $item->apiOutput();
			}, $summary['shipping'] ),
			'shippingTotal'		=> $summary['shippingTotal']->apiOutput(),
			'tax'				=> $tax,
			'taxTotal'			=> $summary['taxTotal']->apiOutput(),
			'total'				=> $summary['total']->apiOutput(),
			'outstanding'		=> $this->amountToPay()->apiOutput(),
			'billingAddress'	=> $this->billaddress ? $this->billaddress->apiOutput() : null,
			'shippingAddress'	=> $this->shipaddress ? $this->shipaddress->apiOutput() : null,
			'poNumber'			=> $this->po,
			'notes'				=> $this->notes,
			'transactions'		=> array_map( function( $transaction ){
				return $transaction->apiOutput();
			}, iterator_to_array( $this->transactions() ) ),
			'shipments'			=> array_map( function( $shipment ){
				return $shipment->apiOutput();
			}, iterator_to_array( $this->shipments() ) ),
			'purchases'			=> array_map( function( $purchase ){
				return $purchase->apiOutput();
			}, iterator_to_array( $this->purchasesCreated() ) ),
			'viewUrl'			=> (string) $this->url(),
			'checkoutUrl'		=> (string) $this->checkoutUrl(),
		);
	}
	
	/* !Related Resources */
	
	/**
	 * Get transactions
	 *
	 * @param	array|null	$statuses	Statuses to get, or NULL for all
	 * @param	array		$where		Initial where clause
	 * @return	\IPS\Patterns\ActiveRecordIteator
	 */
	public function transactions( $statuses=NULL, $where = array() )
	{
		$where[] = array( 't_invoice=?', $this->id );
		if ( $statuses )
		{
			$where[] = \IPS\Db::i()->in( 't_status', $statuses );
		}
		
		return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_transactions', $where ), 'IPS\nexus\Transaction' );
	}
	
	/**
	 * Get shipments
	 *
	 * @param	array|null	$statuses	Statuses to get, or NULL for all
	 * @return	\IPS\Patterns\ActiveRecordIteator
	 */
	public function shipments( $statuses=NULL )
	{
		$where = array();
		$where[] = array( 'o_invoice=?', $this->id );
		if ( $statuses )
		{
			$where[] = \IPS\Db::i()->in( 'o_status', $statuses );
		}
		
		return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_ship_orders', $where ), 'IPS\nexus\Shipping\Order' );
	}
	
	/**
	 * Get purchases created by this invoice
	 *
	 * @return	\IPS\Patterns\ActiveRecordIteator
	 */
	public function purchasesCreated()
	{
		return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_purchases', array( 'ps_original_invoice=? AND ps_show=1', $this->id ) ), 'IPS\nexus\Purchase' );
	}
	
	/* !Actions */
	
	/**
	 * Create account from guest checkout
	 *
	 * @return	\IPS\nexus\Customer
	 */
	public function createAccountForGuest()
	{		
		$profileFields = $this->guest_data['profileFields'];
		$securityAnswers = isset( $this->guest_data['securityAnswers'] ) ? $this->guest_data['securityAnswers'] : array();
		
		/* If the member has already been created (the user made two separate purchases with the same data), use that, otherwise create one */
		try
		{
			$memberToSave = \IPS\nexus\Customer::load( $this->guest_data['member']['email'], 'email' );
		}
		catch ( \OutOfRangeException $e )
		{
			$memberToSave = new \IPS\nexus\Customer;
			foreach ( $this->guest_data['member'] as $k => $v )
			{
				$memberToSave->_data[ $k ] = $v;
				$memberToSave->changed[ $k ] = $v;
			}
			$memberToSave->save();
		}
		$this->member = $memberToSave;
		$this->guest_data = NULL;
		$this->save();
		
		/* If we provided answers to security questions in check out, save those */
		if ( \IPS\Settings::i()->security_questions_enabled and in_array( \IPS\Settings::i()->security_questions_prompt, array( 'register', 'optional' ) ) )
		{
			if ( $securityAnswers )
			{
				foreach ( $securityAnswers as $k => $v )
				{
					\IPS\Db::i()->insert( 'core_security_answers', array(
						'answer_question_id'	=> $k,
						'answer_member_id'		=> $this->member->member_id,
						'answer_answer'			=> $v
					) );
				}
				$this->member->members_bitoptions['has_security_answers'] = TRUE;

				/* Log MFA Enable */
				$this->member->logHistory( 'core', 'mfa', array( 'handler' => 'questions', 'enable' => TRUE ) );
			}
			else
			{
				$this->member->members_bitoptions['security_questions_opt_out'] = TRUE;

				/* Log MFA Optout */
				$this->member->logHistory( 'core', 'mfa', array( 'handler' => 'questions', 'enable' => FALSE, 'optout' => TRUE ) );
			}
			$this->member->save();
		}
						
		/* If we've entered an address during checkout, save it */
		if ( $this->billaddress !== NULL )
		{
			$billing					= new \IPS\nexus\Customer\Address;
			$billing->member			= $this->member;
			$billing->address			= $this->billaddress;
			$billing->primary_billing	= 1;
			$billing->save();
		}
		
		if ( $this->shipaddress !== NULL )
		{
			$shipping					= new \IPS\nexus\Customer\Address;
			$shipping->member			= $this->member;
			$shipping->address			= $this->shipaddress;
			$shipping->primary_shipping	= 1;
			$shipping->save();
		}
		
		$profileFields['member_id'] = $memberToSave->member_id;				
		\IPS\Db::i()->replace( 'core_pfields_content', $profileFields );
		
		/* Validation */
		if ( \IPS\Settings::i()->nexus_checkreg_validate or !$this->total->amount->isGreaterThanZero() )
		{
			$this->member->postRegistration( FALSE, TRUE );
		}
		else if( \IPS\Settings::i()->new_reg_notify )
		{
			\IPS\Email::buildFromTemplate( 'core', 'registration_notify', array( $memberToSave, $profileFields ), \IPS\Email::TYPE_LIST )->send( \IPS\Settings::i()->email_in );
		}
		
		/* Update associated transactions */				
		\IPS\Db::i()->update( 'nexus_billing_agreements', array( 'ba_member' => $this->member->member_id ), array( 'ba_id IN(?)', \IPS\Db::i()->select( 't_billing_agreement', 'nexus_transactions', array( 't_billing_agreement>0 AND t_invoice=? AND t_member=0', $this->id ) ) ) );
		\IPS\Db::i()->update( 'nexus_transactions', array( 't_member' => $this->member->member_id ), array( 't_invoice=? AND t_member=0', $this->id ) );
		
		/* Did we use any coupons? */
		foreach ( $this->items as $item )
		{
			if ( $item instanceof \IPS\nexus\extensions\nexus\Item\CouponDiscount )
			{
				$coupon = \IPS\nexus\Coupon::load( $item->id );
				$uses = $coupon->used_by ? json_decode( $coupon->used_by, TRUE ) : array();
				if ( isset( $uses[ $this->member->email ] ) )
				{
					$uses[ $this->member->member_id ] = $uses[ $this->member->email ];
					unset( $uses[ $this->member->email ] );
				}
				else
				{
					$uses[ $this->member->member_id ] = 1;
				}
				$coupon->used_by = json_encode( $uses );
				$coupon->save();
			}
		}
		
		return $memberToSave;
	}
		
	/**
	 * Mark as paid
	 *
	 * @param	\IPS\Member|NULL	$member	The member changing the status. Only set if the status is being manually changed by an admin.
	 * @return	\IPS\Member|NULL	If the invoice belonged to a guest, a member will be created by approving and returned here
	 * @note	It is the responsibility of the code calling this to log the action on the customer's account *before* calling this method
	 */
	public function markPaid( \IPS\Member $member = NULL )
	{
		$return = NULL;
		
		if ( $this->status !== static::STATUS_PAID )
		{
			/* Create the member account if this was a guest */
			if ( !$this->member->member_id and $this->guest_data and $this->requiresLogin() )
			{
				$return = $this->createAccountForGuest();
			}
			
			/* Init */
			$extra = $this->status_extra;
			$shipOrders = array();			
			
			/* Find any billing agreements */
			$billingAgreement = NULL;
			foreach ( $this->transactions( array( \IPS\nexus\Transaction::STATUS_PAID, \IPS\nexus\Transaction::STATUS_PART_REFUNDED ), array( array( 't_billing_agreement IS NOT NULL' ) ) ) as $transaction )
			{
				$billingAgreement = $transaction->billing_agreement;
			}
			
			/* Loop items */
			$purchases = array();
			$setParentIds = array();
			$groupParentIds = array();
			$amountReceived = $this->total->amount;
			foreach ( $this->items as $k => $item )
			{
				/* onPaid */
				$item->onPaid( $this );
				
				/* Create a purchase record */
				$purchase = NULL;
				if( $item instanceof \IPS\nexus\Invoice\Item\Purchase )
				{
					$showPurchaseRecord = $item->showPurchaseRecord();
					
					for ( $i = 0; $i < $item->quantity; $i++ )
					{
						/* Create a purchase record */
						$purchase = new \IPS\nexus\Purchase;
						$purchase->member = $this->member;
						$purchase->name = $item->name;
						if ( $item->renewalTerm )
						{
							$purchase->renewals = $item->renewalTerm;
							$purchase->expire = \IPS\DateTime::create()->add( $item->renewalTerm->interval );
							if ( $item->renewalTerm->gracePeriod )
							{
								$purchase->grace_period = \IPS\DateTime::create()->add( $item->renewalTerm->gracePeriod )->getTimestamp() - time();
							}
							if ( $billingAgreement )
							{
								$purchase->billing_agreement = $billingAgreement;
							}
						}
						if ( $item->expireDate )
						{
							$purchase->expire = $item->expireDate;
						}
						$purchase->app = $item::$application;
						$purchase->type = $item::$type;
						$purchase->item_id = intval( $item->id );
						$purchase->custom_fields = $item->purchaseDetails;
						$purchase->original_invoice = $this;
						$purchase->tax = $item->tax ? $item->tax->id : 0;
						if ( $item->payTo )
						{
							$purchase->pay_to = $item->payTo;
							$purchase->commission = $item->commission;
						}
						if ( $item->parent !== NULL )
						{
							if ( $item->parent instanceof \IPS\nexus\Purchase )
							{
								$purchase->parent = $item->parent;
							}
							else
							{
								$setParentIds[ $k ] = $item->parent;
								if ( $item->groupWithParent )
								{
									$groupParentIds[ $k ] = TRUE;
								}
							}
						}
						$purchase->extra = $item->extra;
						$purchase->show = (int) $showPurchaseRecord;
						$purchase->save();
						if( $purchase->parent and $item->groupWithParent )
						{
							$purchase->groupWithParent();
						}
						
						/* Make a note of the ID */
						if ( !isset( $purchases[ $k ] ) )
						{
							$purchases[ $k ] = array();
						}
						$purchases[ $k ][] = $purchase;
						
						/* Claim attachments */
						foreach ( \IPS\Db::i()->select( '*', 'core_attachments_map', array( 'location_key=? AND id1=? AND id3=?', 'nexus_Purchases', $k, "invoice-{$this->id}" ) ) as $attachMap )
						{
							$attachMap['id1'] = $purchase->id;
							$attachMap['id3'] = 'purchase';
							\IPS\Db::i()->insert( 'core_attachments_map', $attachMap );
						}
						
						/* Log */
						if ( $showPurchaseRecord )
						{
							$this->member->log( 'purchase', array(
								'type'			=> 'new',
								'id'			=> $purchase->id,
								'name'			=> $purchase->name,
								'invoice_id'	=> $this->id,
								'invoice_title' => $this->title
							), FALSE );
						}
					}
				}
				
				/* Does it need shipping? */
				if ( $item->physical )
				{
					if ( !isset( $shipOrders[ $item->chosenShippingMethodId ] ) )
					{
						$shipOrders[ $item->chosenShippingMethodId ] = array();
					}
					$shipOrders[ $item->chosenShippingMethodId ][ $k ] = $item;
				}
				
				/* Is the money going to another member? */
				if ( $item->payTo and !$item->payTo->inGroup( explode( ',', \IPS\Settings::i()->nexus_no_commission ) ) )
				{
					$recipientAmounts = $item->recipientAmounts();
										
					$credits = $item->payTo->cm_credits;
					$credits[ $this->currency ]->amount = $credits[ $this->currency ]->amount->add( $recipientAmounts['recipient_final']->amount );
					$item->payTo->cm_credits = $credits;
					$item->payTo->save();
					$amountReceived = $amountReceived->subtract( $recipientAmounts['recipient_final']->amount );
					
					if ( !isset( $extra['commission'][ $item->payTo->member_id ] ) )
					{
						$extra['commission'][ $item->payTo->member_id ] = array( 'amount' => new \IPS\Math\Number('0') );
					}
					$extra['commission'][ $item->payTo->member_id ]['amount'] = $extra['commission'][ $item->payTo->member_id ]['amount']->add( $recipientAmounts['recipient_final']->amount );	
					
					$item->payTo->log( 'comission', array(
						'type'			=> 'purchase',
						'currency'		=> $this->currency,
						'amount'		=> $recipientAmounts['recipient_final']->amount,
						'new_amount'	=> $credits[ $this->currency ]->amount,
						'member'		=> $this->member->member_id,
						'id'			=> $purchase ? $purchase->id : 0,
						'name'			=> $item->name
					), FALSE );
					
					\IPS\Email::buildFromTemplate( 'nexus', 'commissionEarned', array( $this, $item, $recipientAmounts['recipient_final'] ), \IPS\Email::TYPE_TRANSACTIONAL )
						->send(
							$item->payTo,
							array_map(
								function( $contact )
								{
									return $contact->alt_id->email;
								},
								iterator_to_array( $item->payTo->alternativeContacts( array( 'billing=1' ) ) )
							),
							( ( in_array( 'commission_earned', explode( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() )
						);
				}
			}
			
			/* Referral Commission */
			if ( \IPS\Settings::i()->cm_ref_on and $commission = $this->commission( $amountReceived ) and $commission['amount'] )
			{
				$credits = $commission['referrer']->cm_credits;
				$credits[ $this->currency ]->amount = $credits[ $this->currency ]->amount->add( $commission['amount']->amount );
				$commission['referrer']->cm_credits = $credits;
				$commission['referrer']->save();
				
				if ( !isset( $extra['commission'][ $commission['referrer']->member_id ] ) )
				{
					$extra['commission'][ $commission['referrer']->member_id ] = array( 'amount' => new \IPS\Math\Number('0') );
				}
				
				$extra['commission'][ $commission['referrer']->member_id ]['amount'] = $extra['commission'][ $commission['referrer']->member_id ]['amount']->add( $commission['amount']->amount );	
				
				$commission['referrer']->log( 'comission', array(
					'type'			=> 'invoice',
					'currency'		=> $this->currency,
					'amount'		=> $commission['amount']->amount,
					'new_amount'	=> $credits[ $this->currency ]->amount,
					'member'		=> $this->member->member_id,
					'invoice_id'	=> $this->id,
					'invoice_title'	=> $this->title
				), FALSE );
				
				$record = \IPS\Db::i()->select( '*', 'nexus_referrals', array( 'member_id=? AND referred_by=?', $this->member->member_id, $commission['referrer']->member_id ) )->first();
				$amounts = $record['amount'] ? json_decode( $record['amount'], TRUE ) : array();
				if ( !isset( $amounts[ $this->currency ] ) )
				{
					$amounts[ $this->currency ] = new \IPS\Math\Number('0');
				}
				elseif ( !( $amounts[ $this->currency ] instanceof \IPS\Math\Number ) )
				{
					$amounts[ $this->currency ] = new \IPS\Math\Number( number_format( $amounts[ $this->currency ], \IPS\nexus\Money::numberOfDecimalsForCurrency( $this->currency ), '.', '' ) );
				}
				$amounts[ $this->currency ] = $amounts[ $this->currency ]->add( $commission['amount']->amount );
				\IPS\Db::i()->update( 'nexus_referrals', array( 'amount' => json_encode( $amounts ) ), array( 'member_id=? AND referred_by=?', $this->member->member_id, $commission['referrer']->member_id ) );
				
				\IPS\Email::buildFromTemplate( 'nexus', 'commissionEarned', array( $this, NULL, $commission['amount'] ), \IPS\Email::TYPE_TRANSACTIONAL )
					->send(
						$commission['referrer'],
						array_map(
							function( $contact )
							{
								return $contact->alt_id->email;
							},
							iterator_to_array( $commission['referrer']->alternativeContacts( array( 'billing=1' ) ) )
						),
						( ( in_array( 'commission_earned', explode( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() )
					);
			}
			
			/* Change commission values back to numbers */
			if ( isset( $extra['commission'] ) )
			{
				foreach ( $extra['commission'] as $memberId => $data )
				{
					if ( $data['amount'] and $data['amount'] instanceof \IPS\Math\Number )
					{
						$extra['commission'][ $memberId ]['amount'] = (string) $data['amount'];
					}
				}
			}
			
			/* Set parent IDs */
			foreach ( $setParentIds as $k => $v )
			{
				foreach ( $purchases[ $k ] as $k2 => $_purchase )
				{					
					$_purchase->parent = isset( $purchases[ $v ][ $k2 ] ) ? $purchases[ $v ][ $k2 ] : $purchases[ $v ][0];					
					$_purchase->save();
					
					if( isset( $groupParentIds[ $k ] ) )
					{
						$_purchase->groupWithParent();
					}
				}
			}
			
			/* Run callbacks */
			foreach ( $purchases as $_purchases )
			{
				foreach ( $_purchases as $purchase )
				{
					$purchase->onPurchaseGenerated( $this );
				}
			}
			
			/* Create shipping orders */
			foreach ( $shipOrders as $method => $items )
			{
				$shipOrder = new \IPS\nexus\Shipping\Order;
				$shipOrder->invoice = $this;
				$shipOrder->data = array( 'cm_first_name' => $this->member->cm_first_name, 'cm_last_name' => $this->member->cm_last_name, 'address' => $this->shipaddress, 'cm_phone' => $this->member->cm_phone );
				if ( $method )
				{
					try
					{
						$shipOrder->method = \IPS\nexus\Shipping\FlatRate::load( $method );
					}
					catch ( \OutOfRangeException $e )
					{
						$shipOrder->api_service = $method;
					}
				}
				$shipOrder->items = $items;
				$shipOrder->save();
				
				$this->member->log( 'shipping', array(
					'type'			=> 'new',
					'id'			=> $shipOrder->id,
					'invoice_id'	=> $this->id,
					'invoice_title' => $this->title
				), FALSE );
			}
			
			/* Refuse any pending transactions */
			foreach ( $this->transactions( array( \IPS\nexus\Transaction::STATUS_PENDING, \IPS\nexus\Transaction::STATUS_WAITING, \IPS\nexus\Transaction::STATUS_HELD, \IPS\nexus\Transaction::STATUS_REVIEW, \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING ) ) as $transaction )
			{
				if ( $transaction->status === \IPS\nexus\Transaction::STATUS_PENDING )
				{
					$transaction->delete();
				}
				else
				{
					try
					{
						$transaction->method->void( $transaction );
					}
					catch ( \Exception $e ){}
					
					$textra = $transaction->extra;
					$textra['history'][] = array( 's' => \IPS\nexus\Transaction::STATUS_REFUSED, 'on' => time(), 'note' => 'invoicePaid' );
					$transaction->extra = $textra;
					$transaction->status = \IPS\nexus\Transaction::STATUS_REFUSED;
					$transaction->save();
				}
			}
			
			/* Update invoice */
			$this->status = static::STATUS_PAID;
			$this->paid = new \IPS\DateTime;
			
			/* Log */
			if ( $member )
			{
				$extra['type']		= 'manual';
				$extra['setTo']		= static::STATUS_PAID;
				$extra['setByID']	= $member->member_id;
				$extra['date']		= time();
			}
			
			/* Save */
			$this->status_extra = $extra;
			$this->save();
		}
		
		return $return;
	}
	
	/**
	 * Mark as unpaid
	 *
	 * @param	string|NULL			$status	The status to set to
	 * @param	\IPS\Member|NULL	$member	The member changing the status. Only set if the status is being manually changed by an admin.
	 * @return	void
	 */
	public function markUnpaid( $status, \IPS\Member $member = NULL )
	{		
		/* Do stuff */
		if ( $this->status === static::STATUS_PAID )
		{
			/* Loop items */
			foreach ( $this->items as $k => $item )
			{
				$item->onUnpaid( $this, $this->status );
				
				/* Is the money going to another member? */
				if ( $item->payTo and !$item->payTo->inGroup( explode( ',', \IPS\Settings::i()->nexus_no_commission ) ) )
				{
					$recipientAmounts = $item->recipientAmounts();
										
					$credits = $item->payTo->cm_credits;
					$credits[ $this->currency ]->amount = $credits[ $this->currency ]->amount->subtract( $recipientAmounts['recipient_final']->amount );
					$item->payTo->cm_credits = $credits;
					$item->payTo->save();
										
					$item->payTo->log( 'comission', array(
						'type'			=> 'invoice_refund',
						'currency'		=> $this->currency,
						'amount'		=> $recipientAmounts['recipient_final']->amount,
						'new_amount'	=> $credits[ $this->currency ]->amount,
						'member'		=> $this->member->member_id,
						'invoice_id'	=> $this->id,
						'invoice_title'	=> $this->title
					), FALSE );
					
					\IPS\Email::buildFromTemplate( 'nexus', 'commissionRevoked', array( $this, $item, $recipientAmounts['recipient_final'] ), \IPS\Email::TYPE_TRANSACTIONAL )
						->send(
							$item->payTo,
							array_map(
								function( $contact )
								{
									return $contact->alt_id->email;
								},
								iterator_to_array( $item->payTo->alternativeContacts( array( 'billing=1' ) ) )
							),
							( ( in_array( 'commission_earned', explode( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() )
						);
				}
			}
			
			/* Delete purchases */
			foreach ( $this->purchasesCreated() as $purchase )
			{
				$purchase->delete();
			}
			
			/* Delete shipping orders */
			foreach ( $this->shipments() as $shipment )
			{
				$shipment->delete();
			}
		}
		else
		{
			foreach ( $this->items as $k => $item )
			{
				$item->onInvoiceCancel( $this );
			}
		}
		
		/* Set the status */		
		$this->status = $status;
		
		/* Log */
		if ( $member )
		{
			$extra = $this->status_extra;
			$extra['type']		= 'manual';
			$extra['setTo']		= $status;
			$extra['setByID']	= $member->member_id;
			$extra['date']		= time();
			$this->status_extra = $extra;
		}

		/* Save */
		$this->save();
	}
	
	/**
	 * What will happen if we mark this unpaid?
	 *
	 * @return	array
	 */
	public function unpaidConsequences()
	{
		$return = array();
		
		$purchases = $this->purchasesCreated();
		if ( count( $purchases ) )
		{
			$return['purchases'] = array();
			foreach ( $purchases as $purchase )
			{
				$return['purchases'][] = \IPS\Theme::i()->getTemplate('purchases')->link( $purchase );
			}
		}
		
		$shipments = $this->shipments();
		if ( count( $shipments ) )
		{
			$return['shipments'] = array();
			foreach ( $shipments as $shipment )
			{
				if ( $shipment->status === \IPS\nexus\Shipping\Order::STATUS_SHIPPED )
				{
					$return['shipments'][] = \IPS\Theme::i()->getTemplate('shiporders')->link( $shipment );
				}
				else
				{
					$return['shipments'][] = array(
						'message' => \IPS\Theme::i()->getTemplate('shiporders')->link( $shipment ),
						'warning' => \IPS\Member::loggedIn()->language()->addToStack('invoice_unpaid_shipments_shipped'),
					);
				}
			}
		}
		
		foreach ( $this->items as $item )
		{
			if ( $item->payTo )
			{
				$amount = $item->amountForRecipient();
				$message = \IPS\Member::loggedIn()->language()->addToStack('account_credit_remove', FALSE, array( 'sprintf' => array( $amount, $item->payTo->cm_name ) ) );
				
				$credits = $item->payTo->cm_credits;
				if ( !$credits[ $amount->currency ]->amount->subtract( $amount->amount )->isPositive() )
				{
					$return[] = array( 'message' => $message, 'warning' => \IPS\Member::loggedIn()->language()->addToStack('account_credit_remove_neg') );
				}
				else
				{
					$return[] = $message;
				}
			}

            if( count( $item->onUnpaidDescription( $this ) ) )
            {
                foreach( $item->onUnpaidDescription( $this ) as $desc )
                {
                    $return[] = $desc;
                }
            }
		}
				
		return $return;
	}
	
	/**
	 * Send the notification to the member that this invoice has been generated
	 *
	 * @return	void
	 */
	public function sendNotification()
	{		
		\IPS\Email::buildFromTemplate( 'nexus', 'newInvoice', array( $this, $this->summary() ), \IPS\Email::TYPE_TRANSACTIONAL )
			->send(
				$this->member,
				array_map(
					function( $contact )
					{
						return $contact->alt_id->email;
					},
					iterator_to_array( $this->member->alternativeContacts( array( 'billing=1' ) ) )
				),
				( ( in_array( 'new_invoice', explode( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() )
			);
	}
	
	/* !URLS */
	
	/**
	 * Get checkout URL
	 *
	 * @return	void
	 */
	public function checkoutUrl()
	{
		if ( !$this->id )
		{
			$this->save();
		}
		
		return \IPS\Http\Url::internal( 'app=nexus&module=checkout&controller=checkout&id=' . $this->id, 'front', 'nexus_checkout', array(), \IPS\Settings::i()->nexus_https );
	}

	/**
	 * @brief	Cached URL
	 */
	protected $_url	= NULL;

	/**
	 * Get URL
	 *
	 * @return	\IPS\Http\Url
	 */
	public function url()
	{
		if( $this->_url === NULL )
		{
			$this->_url = \IPS\Http\Url::internal( "app=nexus&module=clients&controller=invoices&do=view&id={$this->id}", 'front', 'clientsinvoice' );
		}

		return $this->_url;
	}
	
	/**
	 * ACP "View Invoice" URL
	 *
	 * @return	\IPS\Http\Url
	 */
	public function acpUrl()
	{
		return \IPS\Http\Url::internal( "app=nexus&module=payments&controller=invoices&do=view&id={$this->id}", 'admin' );
	}
}