<?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 */

use Exception;
use IPS\DateTime;
use IPS\Db;
use IPS\Email;
use IPS\Events\Event;
use IPS\File;
use IPS\GeoLocation;
use IPS\Http\Url;
use IPS\Math\Number;
use IPS\Member;
use IPS\nexus\Customer\Address;
use IPS\nexus\extensions\nexus\Item\CouponDiscount;
use IPS\nexus\extensions\nexus\Item\Package;
use IPS\nexus\Invoice\Item;
use IPS\nexus\Invoice\Item\Purchase;
use IPS\nexus\Invoice\Item\Renewal;
use IPS\nexus\Invoice\Item\TaxItem;
use IPS\nexus\Invoice\ItemsIterator;
use IPS\Patterns\ActiveRecord;
use IPS\Patterns\ActiveRecordIterator;
use IPS\Settings;
use IPS\Theme;
use LengthException;
use OutOfRangeException;
use ReflectionClass;
use RuntimeException;
use UnderflowException;
use function count;
use function defined;
use function get_called_class;
use function in_array;
use function intval;
use function is_array;
use function is_int;

if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Invoice Model
 *
 * @property Customer $member
 * @property ItemsIterator $items
 */
class Invoice extends ActiveRecord
{
	const STATUS_PAID		= 'paid';
	const STATUS_PENDING	= 'pend';
	const STATUS_EXPIRED	= 'expd';
	const STATUS_CANCELED	= 'canc';
	
	/**
	 * @brief	Multiton Store
	 */
	protected static array $multitons;

	/**
	 * @brief	Database Table
	 */
	public static ?string $databaseTable = 'nexus_invoices';
	
	/**
	 * @brief	Database Prefix
	 */
	public static string $databasePrefix = 'i_';

	/**
	 * Load and check permissions
	 *
	 * @param int $id
	 * @return    static
	 * @throws OutOfRangeException
	 */
	public static function loadAndCheckPerms( int $id ) : static
	{
		$obj = static::load( $id );

		if ( !$obj->canView() )
		{
			throw new OutOfRangeException;
		}

		return $obj;
	}
	
	/**
	 * Get statuses
	 *
	 * @return	array
	 */
	public static function statuses() : array
	{
		$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|null	$where	Where clause
	 * @param Url	$url
	 * @param	string	$ref	Referer
	 * @return	\IPS\Helpers\Table\Db
	 */
	public static function table( ?array $where, Url $url, string $ref = 't' ) : \IPS\Helpers\Table\Db
	{
		/* 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 Theme::i()->getTemplate( 'invoices', 'nexus' )->status( $val );
			},
			'i_member'	=> function ( $val, $row )
			{
				if ( $val )
				{
					return Theme::i()->getTemplate( 'global', 'nexus' )->userLink( Member::load( $val ) );
				}
				elseif ( $row['i_guest_data'] )
				{
					$data  = json_decode( $row['i_guest_data'], TRUE );
					return htmlspecialchars( Customer::constructFromData( $data['member'] )->cm_name, ENT_DISALLOWED, 'UTF-8', FALSE );
				}
				else
				{
					return Theme::i()->getTemplate( 'global', 'nexus' )->userLink( Member::load( 0 ) );
				}
			},
			'i_total'	=> function( $val, $row )
			{
				return (string) new Money( $val, $row['i_currency'] );
			},
			'i_date'	=> function( $val )
			{
				return DateTime::ts( $val );
			}
		);
				
		/* Buttons */
		$table->rowButtons = function( $row ) use( $ref )
		{
			return array_merge( array(
				'view'	=> array(
					'icon'	=> 'search',
					'title'	=> 'invoice_view',
					'link'	=> Url::internal( "app=nexus&module=payments&controller=invoices&do=view&id={$row['i_id']}" )
				),
			), Invoice::constructFromData( $row )->buttons( $ref ) );
		};
		
		return $table;
	}
	
	/**
	 * @brief	Being generated by system?
	 */
	public bool $system = FALSE;
	
	/**
	 * Set Default Values
	 *
	 * @return	void
	 */
	public function setDefaultValues() : void
	{
		$this->status = static::STATUS_PENDING;
		$this->items = json_encode( array() );
		$this->date = new 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	Member|NULL	$member	The member to check for, or NULL for currently logged in member
	 * @return	bool
	 */
	public function canView( Member $member = null ) : bool
	{
		$member = $member ?: 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 $title
	 * @return	void
	 * @throws	LengthException
	 */
	public function set_title( string $title ) : void
	{
		if ( mb_strlen( $title ) > 128 )
		{
			throw new LengthException;
		}
		
		$this->_data['title'] = $title;
	}
	
	/**
	 * @brief	Customer|null
	 */
	protected ?Customer $_member = null;
	
	/**
	 * Get member
	 *
	 * @return    Customer
	 */
	public function get_member() : Customer
	{
		if ( $this->_member === NULL )
		{
			try
			{
				$this->_member = 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 )
			{
				$this->_member = new Customer;
				
				if ( $this->_data['member'] )
				{
					$this->_member->_data['cm_first_name'] = 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	Customer $member
	 * @return	void
	 */
	public function set_member( Customer $member ) : void
	{
		$this->_data['member'] = (int) $member->member_id;
		$this->_member = NULL;
		
		if ( !$this->currency and $member instanceof Customer)
		{
			$this->currency = $member->defaultCurrency();
		}
	}
	
	/**
	 * @brief	Items iterator
	 */
	protected ?ItemsIterator $_itemsIterator = null;
	
	/**
	 * @brief	Tracked
	 */
	protected array $_tracked = array();
	
	/**
	 * Get items
	 *
	 * @return	ItemsIterator
	 */
	public function get_items() : ItemsIterator
	{
		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,
					'methods'		=> '*',
					'cfields'		=> array(),
					'extra'			=> NULL,
				) );
			}
						
			/* Return it */
			$this->_itemsIterator = new ItemsIterator( $itemsJson );
			$this->_itemsIterator->currency = $this->currency;
		}

		return $this->_itemsIterator;
	}
	
	/**
	 * Get total
	 *
	 * @return    Money
	 */
	public function get_total() : Money
	{
		return new Money( $this->_data['total'], $this->currency );
	}
	
	/**
	 * Set total
	 *
	 * @param Money $total	The total
	 * @return	void
	 */
	public function set_total(Money $total ) : void
	{
		$this->_data['total'] = $total->amount;
	}
	
	/**
	 * Get date
	 *
	 * @return	DateTime
	 */
	public function get_date() : DateTime
	{
		return DateTime::ts( $this->_data['date'] );
	}
	
	/**
	 * Set date
	 *
	 * @param	DateTime	$date	The invoice date
	 * @return	void
	 */
	public function set_date( DateTime $date ) : void
	{
		$this->_data['date'] = $date->getTimestamp();
	}
	
	/**
	 * Get paid date
	 *
	 * @return	DateTime|NULL
	 */
	public function get_paid() : DateTime|null
	{
		return ( isset( $this->_data['paid'] ) and $this->_data['paid'] ) ? DateTime::ts( $this->_data['paid'] ) : NULL;
	}
	
	/**
	 * Set paid date
	 *
	 * @param	DateTime	$date	The invoice date
	 * @return	void
	 */
	public function set_paid( DateTime $date ) : void
	{
		$this->_data['paid'] = $date->getTimestamp();
	}
	
	/**
	 * Get return URI
	 *
	 * @return	Url|NULL
	 */
	public function get_return_uri() : Url|null
	{
		return $this->_data['return_uri'] ? Url::internal( $this->_data['return_uri'] ) : NULL;
	}
	
	/**
	 * Get status information
	 *
	 * @return	array
	 */
	public function get_status_extra() : array
	{
		return json_decode( $this->_data['status_extra'], TRUE );
	}
	
	/**
	 * Set status information
	 *
	 * @param	array|null	$extra	The data
	 * @return	void
	 */
	public function set_status_extra( ?array $extra ) : void
	{
		$this->_data['status_extra'] = json_encode( $extra );
	}
	
	/**
	 * Get renewal IDs
	 *
	 * @return	array
	 */
	public function get_renewal_ids() : array
	{
		return explode( ',', $this->_data['renewal_ids'] );
	}
	
	/**
	 * Set renewal IDs
	 *
	 * @param	array	$ids	The IDs
	 * @return	void
	 */
	public function set_renewal_ids( array $ids ) : void
	{
		$ids = array_unique( $ids );
		$ids = array_filter( $ids );
		$this->_data['renewal_ids'] = implode( ',', $ids );
	}

	/**
	 * Get billing address
	 *
	 * @return	GeoLocation|NULL
	 */
	public function get_billaddress() : GeoLocation|null
	{
		if ( isset( $this->_data['billaddress'] ) and $this->_data['billaddress'] )
		{
			return GeoLocation::buildFromJson( $this->_data['billaddress'] );
		}
		else
		{
			return NULL;
		}		
	}
	
	/**
	 * Set billing address
	 *
	 * @param	GeoLocation|null	$billingAddress	The billing address
	 * @return	void
	 */
	public function set_billaddress( ?GeoLocation $billingAddress = NULL ) : void
	{
		if ( $billingAddress )
		{
			$this->_data['billaddress'] = json_encode( $billingAddress );
			$this->_data['billcountry'] = $billingAddress->country;
		}
		else
		{
			$this->_data['billaddress'] = NULL;
			$this->_data['billcountry'] = NULL;
		}
	}
	
	/**
	 * Get guest data
	 *
	 * @return	array|null
	 */
	public function get_guest_data() : array|null
	{
		return $this->_data['guest_data'] ? json_decode( $this->_data['guest_data'], TRUE ) : NULL;
	}
	
	/**
	 * Set guest data
	 *
	 * @param	array|null	$data	The data
	 * @return	void
	 */
	public function set_guest_data( ?array $data ) : void
	{
		$this->_data['guest_data'] = $data ? json_encode( $data ) : NULL;
        $this->_member = null;
	}
	
	/**
	 * Set a title automatically based on item names
	 *
	 * @return	void
	 */
	public function setDefaultTitle() : void
	{
		$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(): void
	{		
		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
		{
			/* Has the invoice status changed? */
			$statusChanged = isset( $this->changed['status'] );

			parent::save();

			/* If the invoice status was changed, fire an event */
			if( $statusChanged )
			{
				Event::fire( 'onStatusChange', $this, array( $this->status ) );
			}
		}						
	}
	
	/**
	 * Delete
	 *
	 * @return    void
	 */
	public function delete(): void
	{
		File::unclaimAttachments( 'nexus_Purchases', NULL, NULL, "invoice-{$this->id}" );
		
		parent::delete();
		
		foreach ($this->transactions( array( Transaction::STATUS_PENDING, Transaction::STATUS_WAITING, Transaction::STATUS_HELD, Transaction::STATUS_REVIEW, Transaction::STATUS_GATEWAY_PENDING ) ) as $transaction )
		{
			if ( $transaction->status === Transaction::STATUS_PENDING )
			{
				$transaction->delete();
			}
			else
			{
				try
				{
					$transaction->method->void( $transaction );
				}
				catch ( Exception ){}
				
				$extra = $transaction->extra;
				$extra['history'][] = array( 's' => Transaction::STATUS_REFUSED, 'on' => time(), 'note' => 'invoiceDeleted' );
				$transaction->extra = $extra;
				$transaction->status = Transaction::STATUS_REFUSED;
				$transaction->save();
			}
		}
	}
	
	/* !Items */
	
	/**
	 * Add item
	 *
	 * @param	Item	$item		The item
	 * @param	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( Item $item, ?int $cartKey = NULL ) : int
	{
		/* Set basic data */
		$data = array(
			'act'			=> $item::$act,
			'app'			=> $item::$application ?? $item->application,
			'type'			=> $item::$type ?? $item->type,
			'cost'			=> $item->price->amountAsString(),
			'tax'			=> $item->tax?->id,
			'quantity'		=> $item->quantity,
			'itemName'		=> $item->name,
			'itemID'		=> $item->id ?? 0,
			'methods'		=> $item->paymentMethodIds ?: '*',
			'cfields'		=> $item->details,
			'extra'			=> $item->extra,
		);
		
		/* Parent? */
		if ( isset( $item->parent ) )
		{			
			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'] = DateTime::create()->add( $item->renewalTerm->gracePeriod )->getTimestamp() - time();
			}
		}
		if ( isset( $item->initialInterval ) )
		{
			if( $item->initialInterval->y )
			{
				$data['initial_interval_term'] = $item->initialInterval->y;
				$data['initial_interval_unit'] = 'y';
			}
			elseif( $item->initialInterval->m )
			{
				$data['initial_interval_term'] = $item->initialInterval->m;
				$data['initial_interval_unit'] = 'm';
			}
			else
			{
				$data['initial_interval_term'] = $item->initialInterval->d;
				$data['initial_interval_unit'] = 'd';
			}
		}
		
		/* Expire date? */
		if ( isset( $item->expireDate ) )
		{
			$data['expires'] = $item->expireDate->getTimestamp();
		}

		/* 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 Renewal )
		{
			$renewalIds = $this->renewal_ids;
			$renewalIds[] = $item->id;
			$this->renewal_ids = $renewalIds;
		}

		/* Increase the total */
		$this->recalculateTotal();

		/* Fire an event */
		Event::fire( 'onAddToInvoice', $item, [ $this ] );
		
		/* Return the ID */
		return $id;
	}
	
	/**
	 * Change item
	 *
	 * @param	int		$index	The index
	 * @param	array	$data	Data to set
	 * @return	void
	 */
	public function changeItem( int $index, array $data ) : void
	{
		$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( int $index ) : void
	{
		$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() : void
	{		
		$summary = $this->summary();		
		$this->total = $summary['total'];
	}
	
	/**
	 * Get summary for invoice tables and so on
	 *
	 * @return	array
	 */
	public function summary() : array
	{
		$return = array();
		$return['items'] = array();
		$subtotal = new Number('0');
		$taxableAmounts = array();
		$taxClasses = Tax::roots();
				
		/* Reduce taxable amount from coupons */
		$couponTaxReductions = array();
		$itemsAsArray = iterator_to_array( $this->items );
		foreach ( $this->items as $itemId => $item )
		{
			if ( $item instanceof CouponDiscount )
			{				
				if ( isset( $item->extra['type'] ) )
				{
					if ( isset( $item->extra['items'] ) )
					{
						$itemCount = count( $item->extra['items'] );
					}
					else
					{
						$itemCount = 0;
						foreach ( $itemsAsArray 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 CouponDiscount ) )
							{
								$itemCount++;
							}
						}
					}
					
					foreach ( $itemsAsArray 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 CouponDiscount ) and ( !isset( $item->extra['items'] ) or in_array( $k, $item->extra['items'] ) ) )
						{
							if ( $item->extra['type'] == 'p' )
							{
								$reductionAmount = $_item->linePrice()->amount->multiply( new Number("{$item->extra['value']}") )->divide( new Number('100') );
							}
							else
							{
								$reductionAmount = ( new Number( "{$item->extra['value']}" ) )->divide( new Number( "{$itemCount}" ) )->multiply( new 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 )
		{
			$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 tax is added) */
		$return['subtotal'] = new Money( $subtotal, $this->currency );

		/* Now work out the tax */
		$return['tax'] = array();
		$tax = new Number('0');
		foreach ( $taxableAmounts as $taxId => $amount )
		{
			$rate = $taxClasses[ $taxId ]->rate( $this->billaddress );
			$amount = $amount->multiply( new Number( $rate ) );
			
			$amount = new Money( $amount, $this->currency );
			
			$return['tax'][ $taxId ] = array( 'name' => '', 'rate' => $rate, 'amount' => $amount, 'type' => $taxClasses[ $taxId ]->type ); // "name" is no longer used. It is definef here only to not break custom themes. Get the title from the ID instead.
			$tax = $tax->add( $amount->amount );
		}
		$return['taxTotal'] = new Money( $tax, $this->currency );

		/* Discounts applied */
		$discount = new Number( '0' );
		foreach( $couponTaxReductions as $itemId => $amount )
		{
			$discount = $discount->add( $amount );
		}
		$return['discount'] = new Money( $discount, $this->currency );
		
		/* Add it all together */		
		$return['total'] = new Money( $subtotal->add( $tax ), $this->currency );
		
		return $return;			
	}
	
	/* !Information */
	
	/**
	 * Does the invoice contain items that require a billing address?
	 *
	 * @return	bool
	 */
	public function hasItemsRequiringBillingAddress() : bool
	{
		foreach ( $this->items as $item )
		{
			if ( $item->requiresBillingAddress() )
			{
				return TRUE;
			}
		}
		
		return FALSE;
	}
	
	/**
	 * Requires login?
	 *
	 * @return	bool
	 */
	public function requiresLogin() : bool
	{
		foreach ( $this->items as $item )
		{
			if ( $item::$requiresAccount )
			{
				return TRUE;
			}
		}
		
		return FALSE;
	}
	
	/**
	 * Amounts that go to a member
	 *
	 * @return	array
	 */
	public function payToRecipients() : array
	{
		$recipients = array();
		
		foreach ( $this->items as $item )
		{
			if ( $item->payTo )
			{
				if ( !isset( $recipients[ $item->payTo->member_id ] ) )
				{
					$recipients[ $item->payTo->member_id ] = new 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	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( Number $amountReceived ) : array
	{
		if ( !isset( $this->status_extra['commissionrule'] ) )
		{		
			try
			{
				/* Get the referrer */
				/* @var Member $referrer */
				$referrer = Member::load( Db::i()->select( 'referred_by', 'core_referrals', array( 'member_id=?', $this->member->member_id ) )->first() );
				if ( !$referrer->member_id or $referrer->inGroup( explode( ',', Settings::i()->nexus_no_commission ) ) )
				{
					throw new UnderflowException;
				}
				
				/* Get pertinent values */
				$byNumber  = 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( 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  = 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( 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 Number('0');
				$actingRule = NULL;
				foreach ( new ActiveRecordIterator( Db::i()->select( '*', 'nexus_referral_rules', array( array( "rrule_by_group='*' OR " . Db::i()->findInSet( 'rrule_by_group', $referrer->groups ) ), array( "rrule_for_group='*' OR " . 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 and $commissionRule->commission_limit != '*' )
					{
						$commisionLimit = json_decode( $commissionRule->commission_limit, TRUE );
						if ( isset( $commisionLimit[ $this->currency ] ) and ( new Number( number_format( $commisionLimit[ $this->currency ]['amount'], 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 Number('0');
						}
												
						$itemsToCount = array();
						foreach ( $this->items as $k => $item )
						{
							$itemType = null;
							$packageId = null;

							/* What's the package ID? */
							if( $item->isValid( $commissionRule->purchase_packages, $this, $this->member ) )
							{
								$itemType = $item::$title;
								$packageId = $item->id;
							}
							elseif ( $commissionRule->purchase_renewal and $item instanceof Renewal and $item->isValid( $commissionRule->purchase_packages, $this, $this->member ) )
							{
								try
								{
									$packageId = $item->getPurchase()->item_id;
									$itemType = $item->getPurchaseExtension()::$title;
								}
								catch ( OutOfRangeException ) { }
							}

							if ( !$packageId )
							{
								continue;
							}
							
							/* Add it to the list */
							if( !isset( $itemsToCount[ $itemType ] ) )
							{
								$itemsToCount[ $itemType ] = [];
							}
							$itemsToCount[ $itemType ][ $k ] = $packageId;

							if ( $commissionRule->purchase_package_limit )
							{
								$amountToApplyCommissionOn = $amountToApplyCommissionOn->add( $item->price->amount->multiply( new 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( $commissionRule->purchase_packages as $itemType => $items )
							{
								$missing = array_diff( $items, ( $itemsToCount[ $itemType ] ?? [] ) );
								if( count( $missing ) )
								{
									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 Number( number_format( $limit[ $this->currency ]['amount'], Money::numberOfDecimalsForCurrency( $this->currency ), '.', '' ) ) ) === 1 )
						{
							$amountToGive = new Number( number_format( $limit[ $this->currency ]['amount'], 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 )
			{
				$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'] ? CommissionRule::load( $this->status_extra['commissionrule'] ) : NULL,
				'referrer'	=> $this->status_extra['commissionref'] ? Customer::load( $this->status_extra['commissionref'] ) : NULL,
				'amount'	=> $this->status_extra['commissionamount'] ? new Money( $this->status_extra['commissionamount'], $this->currency ) : NULL
			);
		}
		catch( OutOfRangeException )
		{
			/* 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	int|Number	$a			First parameter
	 * @param	string	$operator	Operator (g = greater than, e = equal to, l = less than)
	 * @param	int|Number	$b			Second parameter
	 * @return	bool
	 */
	protected function _checkCondition( int|Number $a, string $operator, int|Number $b ) : bool
	{
		if ( $a instanceof Number and !( $b instanceof Number ) )
		{
			$b = new Number( "{$b}" );
		}
		if ( !( $a instanceof Number ) and $b instanceof Number )
		{
			$a = new Number( "{$a}" );
		}
		
		switch ( $operator )
		{
			case 'g':
				if ( $a instanceof Number )
				{
					return $a->compare( $b ) === 1;
				}
				else
				{
					return $a > $b;
				}
			case 'e':
				if ( $a instanceof Number )
				{
					return $a->compare( $b ) === 0;
				}
				else
				{
					return $a == $b;
				}
			case 'l':
				if ( $a instanceof Number )
				{
					return $a->compare( $b ) === 11;
				}
				else
				{
					return $a < $b;
				}
		}
		return FALSE;
	}
	
	/**
	 * Get amount left to pay
	 *
	 * @param bool $deductHeldTransactions
	 * @return    Money
	 */
	public function amountToPay( bool $deductHeldTransactions=FALSE ) : Money
	{
		$amountToPay = $this->total;
		
		$statuses = array( Transaction::STATUS_PAID, Transaction::STATUS_PART_REFUNDED );
		if ( $deductHeldTransactions )
		{
			$statuses[] = Transaction::STATUS_HELD;
			$statuses[] = Transaction::STATUS_REVIEW;
			$statuses[] = Transaction::STATUS_GATEWAY_PENDING;
		}
		
		foreach ( $this->transactions( $statuses ) as $transaction )
		{
			if ( $transaction->currency === $this->currency )
			{
				$amountToSubtract = $transaction->amount->amount;
				if ( $transaction->status === Transaction::STATUS_PART_REFUNDED )
				{
					$amountToSubtract = $amountToSubtract->subtract( $transaction->partial_refund->amount );
					$amountToSubtract = $amountToSubtract->subtract( $transaction->credit->amount );
				}
				
				$amountToPay->amount = $amountToPay->amount->subtract( $amountToSubtract );
			}
		}
				
		return $amountToPay;
	}
	
	/**
	 * ACP Buttons
	 *
	 * @param	string	$ref	Referer
	 * @return	array
	 */
	public function buttons( string $ref='v' ) : array
	{
		$url = $this->acpUrl()->setQueryString( 'r', $ref );
		$return = array();
		
		if ( $this->status !== static::STATUS_PAID and ( $this->member->member_id or $this->guest_data ) and 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 === static::STATUS_PENDING and ( $this->member->member_id or $this->guest_data ) )
		{
			if ( Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_edit' ) )
			{
				$return['paid'] = array(
					'title'		=> 'invoice_mark_paid',
					'icon'		=> 'check',
					'link'		=> $url->setQueryString( 'do', 'paid' )->csrf(),
					'data'		=> count( $this->transactions( array( Transaction::STATUS_PENDING, Transaction::STATUS_WAITING, Transaction::STATUS_HELD, Transaction::STATUS_REVIEW, Transaction::STATUS_GATEWAY_PENDING ) ) ) ? array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('invoice_mark_paid') ) : array( 'confirm' => '' )
				);
			}
			if ( count( Gateway::manualChargeGateways( $this->member ) ) and 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 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' => Member::loggedIn()->language()->addToStack('invoice_charge_to_credit') )
				);
			}
		}
		if ( $this->status !== static::STATUS_PAID and Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_resend' ) )
		{
			$return['resend'] = array(
				'title'		=> 'invoice_reissue',
				'icon'		=> 'refresh',
				'link'		=> $url->setQueryString( 'do', 'resend' )->csrf(),
				'data'		=> array(
					'confirm'			=> '',
					'confirmSubMessage'	=> Member::loggedIn()->language()->addToStack('invoice_reissue_confirm'),
					'confirmType'		=> 'verify',
					'confirmIcon'		=> 'question',
					'confirmButtons'	=> json_encode( array(
						'yes'				=>	Member::loggedIn()->language()->addToStack('invoice_reissue_yes'),
						'no'				=>	Member::loggedIn()->language()->addToStack('invoice_reissue_no'),
						'cancel'			=>	Member::loggedIn()->language()->addToStack('cancel'),
					) )
				)
			);
		}
		$return['print'] = array(
			'title'		=> 'print',
			'icon'		=> 'print',
			'target'	=> '_blank',
			'link'		=> $url->setQueryString( 'do', 'printout' ),
		);
		
		$trackUrl = $url->setQueryString( array( 'do' => 'track', 'id' => $this->id ) )->csrf();
		$return['track'] = array(
			'title'		=> $this->tracked() ? 'invoice_untrack' : 'invoice_track',
			'icon'		=> 'bullhorn',
			'link'		=> $this->tracked() ? $trackUrl : $trackUrl->setQueryString( 'track', 1 ),
			'data'		=> array( 'confirm' => '' )
		);
		
		if ( $this->status !== static::STATUS_CANCELED and Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_edit' ) )
		{
			$return['unpaid'] = array(
				'title'		=> $this->status === static::STATUS_PAID ? 'invoice_mark_unpaid' : 'cancel',
				'icon'		=> 'times',
				'link'		=> $url->setQueryString( 'do', 'unpaid' ),
				'data'		=> array( 'ipsDialog' => '', 'ipsDialog-title' => Member::loggedIn()->language()->addToStack('cancel') )
			);
		}
		if ( Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_delete' ) )
		{
			$return['delete'] = array(
				'title'		=> 'delete',
				'icon'		=> 'times-circle',
				'link'		=> $url->setQueryString( 'do', 'delete' )->csrf()->getSafeUrlFromFilters(),
				'data'		=> array( 'confirm' => '', 'confirmSubMessage' => Member::loggedIn()->language()->addToStack('invoice_delete_warning') )
			);
		}
		
		return $return;
	}
	
	/**
	 * Can payment be split? Returns the minnimum split amount
	 *
	 * @return	mixed
	 */
	public function canSplitPayment() : mixed
	{
		if ( Settings::i()->nexus_split_payments == -1 )
		{
			return FALSE;
		}
				
		if ( Settings::i()->nexus_split_payments )
		{
			$decoded = json_decode( 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
	 *
	 * @param	Member|NULL	$authorizedMember	The member making the API request or NULL for API Key / client_credentials
	 * @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\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	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\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( Member $authorizedMember = NULL ): array
	{
		$summary = $this->summary();
		
		$tax = array();
		foreach ( $summary['tax'] as $classId => $data )
		{
			$tax[] = ( new TaxItem( $classId, $data ) )->apiOutput( $authorizedMember );
		}
		
		return array(
			'id'				=> $this->id,
			'title'				=> $this->title,
			'status'			=> $this->status,
			'issueDate'			=> $this->date->rfc3339(),
			'paidDate'			=> $this->paid?->rfc3339(),
			'customer'			=> $this->member->apiOutput( $authorizedMember ),
			'items'				=> array_map( function( $item ) use ( $authorizedMember ) {
				return $item->apiOutput( $authorizedMember );
			}, $summary['items'] ),
			'subTotal'			=> $summary['subtotal']->apiOutput( $authorizedMember ),
			'tax'				=> $tax,
			'taxTotal'			=> $summary['taxTotal']->apiOutput( $authorizedMember ),
			'total'				=> $summary['total']->apiOutput( $authorizedMember ),
			'outstanding'		=> $this->amountToPay()->apiOutput( $authorizedMember ),
			'billingAddress'	=> $this->billaddress?->apiOutput($authorizedMember),
			'poNumber'			=> $this->po,
			'notes'				=> $this->notes,
			'transactions'		=> array_map( function( $transaction ) use ( $authorizedMember ) {
				return $transaction->apiOutput( $authorizedMember );
			}, iterator_to_array( $this->transactions() ) ),
			'purchases'			=> array_map( function( $purchase ) use ( $authorizedMember ) {
				return $purchase->apiOutput( $authorizedMember );
			}, 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	ActiveRecordIterator
	 */
	public function transactions( ?array $statuses=NULL, array $where = array() ) : ActiveRecordIterator
	{
		$where[] = array( 't_invoice=?', $this->id );
		if ( $statuses )
		{
			$where[] = array( Db::i()->in( 't_status', $statuses ) );
		}
		
		return new ActiveRecordIterator( Db::i()->select( '*', 'nexus_transactions', $where ), 'IPS\nexus\Transaction' );
	}

	/**
	 * Get purchases created by this invoice
	 *
	 * @return	ActiveRecordIterator
	 */
	public function purchasesCreated() : ActiveRecordIterator
	{
		return new ActiveRecordIterator( 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    Customer
	 */
	public function createAccountForGuest() : Customer
	{
		$guestData = $this->guest_data;
		$profileFields = $this->guest_data['profileFields'];
		$securityAnswers = $this->guest_data['securityAnswers'] ?? array();
		$spamData = $this->guest_data['spamData'];
		$cards = $this->guest_data['cards'] ?? 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 = Customer::load( $this->guest_data['member']['email'], 'email' );
		}
		catch ( OutOfRangeException )
		{
			$memberToSave = new Customer;
			foreach ( $this->guest_data['member'] as $k => $v )
			{
				$memberToSave->$k = $v;
			}
			$memberToSave->save();
			$memberToSave->logHistory( 'core', 'account', array( 'type' => 'register_checkout', 'spamCode' => $spamData['code'], 'spamAction' => $spamData['action'], 'invoice' => $this->id ), FALSE );

			if( isset( $this->guest_data['agreed_terms'] ) and $this->guest_data['agreed_terms'] )
			{
				$memberToSave->logHistory( 'core', 'terms_acceptance', array( 'type' => 'terms' ) );
			}
		}
		$this->member = $memberToSave;

		/* Get the PBR data */
		$postBeforeRegister = NULL;
		if( isset( $this->guest_data['pbr'] ) )
		{
			try
			{
				$postBeforeRegister = Db::i()->select( '*', 'core_post_before_registering', array( 'secret=?', $this->guest_data['pbr'] ) )->first();
			}
			catch ( UnderflowException ){}
		}

		$referredBy = $this->guest_data['referred_by'] ?? NULL;

		$this->guest_data = isset( $this->guest_data['guestTransactionKey'] ) ? array( 'guestTransactionKey' => $this->guest_data['guestTransactionKey'] ) : NULL;
		$this->save();

		/* Referrals */
		if( $referredBy )
		{
			try
			{
				Member::load( $referredBy )->addReferral( $this->member );
			}
			catch ( OutOfRangeException ) {}
		}

		/* If we provided answers to security questions in check out, save those */
		if ( Settings::i()->security_questions_enabled and in_array( Settings::i()->security_questions_prompt, array( 'register', 'optional' ) ) )
		{
			if ( $securityAnswers )
			{
				foreach ( $securityAnswers as $k => $v )
				{
					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 Address;
			$billing->member			= $this->member;
			$billing->address			= $this->billaddress;
			$billing->primary_billing	= 1;
			$billing->save();
		}

		$profileFields['member_id'] = $memberToSave->member_id;				
		Db::i()->replace( 'core_pfields_content', $profileFields );
		
		/* If we opted to save a card at checkout, save it */
		foreach ( $cards as $card )
		{
			$card['card_member'] = $memberToSave->member_id;
			Db::i()->insert( 'nexus_customer_cards', $card );
		}
						
		/* Validation */
		if ( Settings::i()->nexus_checkreg_validate or !$this->total->amount->isGreaterThanZero() )
		{
			$this->member->postRegistration( FALSE, TRUE, $postBeforeRegister );
		}
		else
		{
			$this->member->validationComplete( FALSE, $postBeforeRegister );
		}
		
		/* Update associated transactions */				
		Db::i()->update( 'nexus_billing_agreements', array( 'ba_member' => $this->member->member_id ), array( 'ba_id IN(?)', Db::i()->select( 't_billing_agreement', 'nexus_transactions', array( 't_billing_agreement>0 AND t_invoice=? AND t_member=0', $this->id ) ) ) );
		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 CouponDiscount )
			{
				$coupon = 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();
			}
		}

		Event::fire( 'onCreateAccountForGuest', $this, [ $memberToSave, $guestData ] );
		
		return $memberToSave;
	}
		
	/**
	 * Mark as paid
	 *
	 * @param	Member|NULL	$member	The member changing the status. Only set if the status is being manually changed by an admin.
	 * @return	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
	 * @throws RuntimeException
	 */
	public function markPaid( Member $member = NULL ) : Member|null
	{
		$return = NULL;
		$memberCreated = FALSE;
		
		if ( $this->status !== static::STATUS_PAID )
		{
			/* Create the member account if this was a guest */
			if ( !$this->member->member_id and $this->requiresLogin() )
			{
				if ( $this->guest_data )
				{
					$memberCreated = TRUE;
					$return = $this->createAccountForGuest();
				}
				else
				{
					throw new RuntimeException('NO_MEMBER_DATA');
				}
			}
			
			/* Init */
			$extra = $this->status_extra;

			/* Find any billing agreements */
			$billingAgreement = NULL;

			/* Turn off read/write separation so that we make sure to retrieve the latest records */
			$currentValue = \IPS\Db::i()->readWriteSeparation;
			\IPS\Db::i()->readWriteSeparation = false;
			foreach ($this->transactions( array( Transaction::STATUS_PAID, Transaction::STATUS_PART_REFUNDED ), array( array( 't_billing_agreement IS NOT NULL' ) ) ) as $transaction )
			{
				$billingAgreement = $transaction->billing_agreement;
			}
			\IPS\Db::i()->readWriteSeparation = $currentValue;
			
			/* Loop items */
			$purchases = array();
			$setParentIds = array();
			$groupParentIds = array();
			$amountReceived = $this->total->amount;
			foreach ( $this->items as $k => $item )
			{
				/* @var Invoice\Item $item */
				/* onPaid */
				$item->onPaid( $this );

				/* Fire the event here; it doesn't get fired with the other Commerce events */
				Event::fire( 'onPaid', $item, array( $this ) );
				
				/* Create a purchase record */
				$purchase = NULL;
				if( $item instanceof 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 = DateTime::create()->add( $item->initialInterval ?: $item->renewalTerm->interval );
							if ( $item->renewalTerm->gracePeriod )
							{
								$purchase->grace_period = 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;
							$purchase->fee = $item->fee ?: NULL;
						}
						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 ( 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';
							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 );
						}
					}
				}

				/* Is the money going to another member? */
				if ( $item->payTo and !$item->payTo->inGroup( explode( ',', Settings::i()->nexus_no_commission ) ) )
				{
					$recipientAmounts = $item->recipientAmounts();

					/* We explicitly re-load $item->payTo in order to ensure the account credit amounts have not changed since the time this invoice request began and now. If an admin began a payout that processed after this page load began, the account credit amount may have been reduced. */
					$item->payTo = Customer::constructFromData( Db::i()->select( '*, core_members.member_id AS _member_id', 'core_members', array( 'core_members.member_id=?', $item->payTo->member_id) )->join( 'nexus_customers', 'nexus_customers.member_id=core_members.member_id' )->first() );
					$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 Number('0') );
					}
					elseif ( !( $extra['commission'][ $item->payTo->member_id ]['amount'] instanceof Number ) )
					{
						$extra['commission'][ $item->payTo->member_id ]['amount'] = new Number( "{$extra['commission'][ $item->payTo->member_id ]['amount']}" );
					}
					$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 );
					
					Email::buildFromTemplate( 'nexus', 'commissionEarned', array( $this, $item, $recipientAmounts['recipient_final'] ), 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( ',', Settings::i()->nexus_notify_copy_types ) ) AND Settings::i()->nexus_notify_copy_email ) ? explode( ',', Settings::i()->nexus_notify_copy_email ) : array() )
						);
				}
			}
			
			/* Referral Commission */
			$this->status_extra = $extra;
			if ( Settings::i()->ref_on and $commission = $this->commission( $amountReceived ) and $commission['amount'] )
			{
				$extra = $this->status_extra; // Reload, the commission method may have changed this.
				$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 Number('0') );
				}
				elseif ( !( $extra['commission'][ $commission['referrer']->member_id ]['amount'] instanceof Number ) )
				{
					$extra['commission'][ $commission['referrer']->member_id ]['amount'] = new Number( "{$extra['commission'][ $commission['referrer']->member_id ]['amount']}" );
				}
				$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 = Db::i()->select( '*', 'core_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 Number('0');
				}
				elseif ( !( $amounts[ $this->currency ] instanceof Number ) )
				{
					$amounts[ $this->currency ] = new Number( number_format( $amounts[ $this->currency ], Money::numberOfDecimalsForCurrency( $this->currency ), '.', '' ) );
				}
				$amounts[ $this->currency ] = $amounts[ $this->currency ]->add( $commission['amount']->amount );
				Db::i()->update( 'core_referrals', array( 'amount' => json_encode( $amounts ) ), array( 'member_id=? AND referred_by=?', $this->member->member_id, $commission['referrer']->member_id ) );
				
				Email::buildFromTemplate( 'nexus', 'commissionEarned', array( $this, NULL, $commission['amount'] ), 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( ',', Settings::i()->nexus_notify_copy_types ) ) AND Settings::i()->nexus_notify_copy_email ) ? explode( ',', 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 Number )
					{
						$extra['commission'][ $memberId ]['amount'] = (string) $data['amount'];
					}
				}
			}
			
			/* Set parent IDs */
			foreach ( $setParentIds as $k => $v )
			{
				foreach ( $purchases[ $k ] as $k2 => $_purchase )
				{					
					$_purchase->parent = $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 );
				}
			}

			/* Refuse any pending transactions */
			foreach ($this->transactions( array( Transaction::STATUS_PENDING, Transaction::STATUS_WAITING, Transaction::STATUS_HELD, Transaction::STATUS_REVIEW, Transaction::STATUS_GATEWAY_PENDING ) ) as $transaction )
			{
				if ( $transaction->status === Transaction::STATUS_PENDING )
				{
					$transaction->delete();
				}
				else
				{
					try
					{
						$transaction->method->void( $transaction );
					}
					catch ( Exception ){}
					
					$textra = $transaction->extra;
					$textra['history'][] = array( 's' => Transaction::STATUS_REFUSED, 'on' => time(), 'note' => 'invoicePaid' );
					$transaction->extra = $textra;
					$transaction->status = Transaction::STATUS_REFUSED;
					$transaction->save();
				}
			}
			
			/* Update invoice */
			$this->status = static::STATUS_PAID;
			$this->paid = new DateTime;

			/* Set our billing address automatically if it's not already set */
			if( !$this->billaddress )
			{
				$this->billaddress = $this->member->primaryBillingAddress();
			}
			
			/* Send tracked notifications */
			foreach ( Db::i()->select( 'member_id', 'nexus_invoice_tracker', array( 'invoice_id=?', $this->id ) ) as $trackingMember )
			{
				$trackingMember = Member::load( $trackingMember );
				Email::buildFromTemplate( 'nexus', 'invoiceNotify', array( $this, $this->summary() ), Email::TYPE_LIST )
							->send( $trackingMember );
			}
			
			/* Log */
			if ( $member )
			{
				$extra['type']		= 'manual';
				$extra['setTo']		= static::STATUS_PAID;
				$extra['setByID']	= $member->member_id;
				$extra['date']		= time();
			}

			/* Update member total spend */
			if ( $memberCreated )
			{
				$this->member->recountTotalSpend();
			}

			/* Save */
			$this->status_extra = $extra;
			$this->save();
		}
		
		return $return;
	}
	
	/**
	 * Mark as unpaid
	 *
	 * @param	string|NULL			$status	The status to set to
	 * @param	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( ?string $status, Member $member = NULL ) : void
	{		
		/* Do stuff */
		$extra = $this->status_extra;
		if ( $this->status === static::STATUS_PAID )
		{
			/* Loop items */
			foreach ( $this->items as $k => $item )
			{
				/* @var Invoice\Item $item */
				$item->onUnpaid( $this, $this->status );

				/* Fire the event */
				Event::fire( 'onUnpaid', $item, array( $this, $this->status ) );
				
				/* Is the money going to another member? */
				if ( $item->payTo and !$item->payTo->inGroup( explode( ',', Settings::i()->nexus_no_commission ) ) )
				{
					$recipientAmounts = $item->recipientAmounts();
					$this->revokeCommission( $item->payTo, $recipientAmounts['recipient_final'], $item );
				}
			}

			/* Revoke commissions based on the referral rules */
			if( !empty( $extra['commissionref'] ) AND !empty( $extra['commissionamount'] ) )
			{
				$this->revokeCommission( Customer::load( $extra['commissionref'] ), new Money( $extra['commissionamount'], $this->currency ) );
			}
			
			/* Delete purchases */
			foreach ( $this->purchasesCreated() as $purchase )
			{
				$purchase->delete( FALSE );
			}
		}
		else
		{
			foreach ( $this->items as $k => $item )
			{
				/* @var Invoice\Item $item */
				$item->onInvoiceCancel( $this );

				Event::fire( 'onInvoiceCancel', $item, array( $this ) );
			}
		}
		
		/* Set the status */		
		$this->status = $status;
		
		/* Log */
		if ( $member )
		{
			$extra['type']		= 'manual';
			$extra['setTo']		= $status;
			$extra['setByID']	= $member->member_id;
			$extra['date']		= time();
			$this->status_extra = $extra;
		}

		/* Save */
		$this->save();
	}

	/**
	 * Revoke Commission
	 *
	 * @param   Member                     $paidTo     Revoke the commission from this member
	 * @param Money $commission Amount of commission to revoke
	 * @param   Item|null    $item       Item commission was for (if applicable)
	 * @return void
	 */
	protected function revokeCommission( Member $paidTo, Money $commission, Item $item=NULL ) : void
	{
		$credits = $paidTo->cm_credits;
		$credits[ $this->currency ]->amount = $credits[ $this->currency ]->amount->subtract( $commission->amount );
		$paidTo->cm_credits = $credits;
		$paidTo->save();

		$paidTo->log( 'comission', array(
			'type'			=> 'invoice_refund',
			'currency'		=> $this->currency,
			'amount'		=> $commission->amount,
			'new_amount'	=> $credits[ $this->currency ]->amount,
			'member'		=> $this->member->member_id,
			'invoice_id'	=> $this->id,
			'invoice_title'	=> $this->title
		), FALSE );

		Email::buildFromTemplate( 'nexus', 'commissionRevoked', array( $this, $item, $commission ), Email::TYPE_TRANSACTIONAL )
			->send(
				$paidTo,
				array_map(
					function( $contact )
					{
						return $contact->alt_id->email;
					},
					iterator_to_array( $paidTo->alternativeContacts( array( 'billing=1' ) ) )
				),
				( ( in_array( 'commission_earned', explode( ',', Settings::i()->nexus_notify_copy_types ) ) AND Settings::i()->nexus_notify_copy_email ) ? explode( ',', Settings::i()->nexus_notify_copy_email ) : array() )
			);
	}
	
	/**
	 * What will happen if we mark this unpaid?
	 *
	 * @return	array
	 */
	public function unpaidConsequences() : array
	{
		$return = array();
		
		$purchases = $this->purchasesCreated();
		if ( count( $purchases ) )
		{
			$return['purchases'] = array();
			foreach ( $purchases as $purchase )
			{
				/* @var \IPS\nexus\Purchase $purchase */
				$return['purchases'][ $purchase->id ] = Theme::i()->getTemplate( 'purchases', 'nexus' )->link( $purchase );
				
				foreach ( $purchase->children( NULL ) as $child )
				{
					$return['unassociate'][ $purchase->id ][ $child->id ] = Theme::i()->getTemplate( 'purchases', 'nexus' )->link( $child );
				}
			}
		}

		foreach ( $this->items as $item )
		{
			/* @var Invoice\Item $item */
			if ( $item->payTo )
			{
				$amount = $item->amountForRecipient();
				$message = 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' => Member::loggedIn()->language()->addToStack('account_credit_remove_neg') );
				}
				else
				{
					$return[] = $message;
				}
			}

            if( count( $item->onUnpaidDescription( $this ) ) )
            {
                foreach( $item->onUnpaidDescription( $this ) as $desc )
                {
                    $return[] = $desc;
                }
            }
		}

		/* Referral Commission */
		$extra = $this->status_extra;
		if( !empty( $extra['commissionref'] ) AND !empty( $extra['commissionamount'] ) )
		{
			$customer = Customer::load( $extra['commissionref'] );
			$amount = new Money( $extra['commissionamount'], $this->currency );
			$message = Member::loggedIn()->language()->addToStack( 'account_credit_remove_commission', FALSE, array( 'sprintf' => array( $amount, $customer->cm_name ) ) );

			$credits = $customer->cm_credits;
			if ( !$credits[ $amount->currency ]->amount->subtract( $amount->amount )->isPositive() )
			{
				$return[] = array( 'message' => $message, 'warning' => Member::loggedIn()->language()->addToStack( 'account_credit_remove_neg' ) );
			}
			else
			{
				$return[] = $message;
			}
		}

		return $return;
	}
	
	/**
	 * Send the notification to the member that this invoice has been generated
	 *
	 * @return	void
	 */
	public function sendNotification() : void
	{		
		Email::buildFromTemplate( 'nexus', 'newInvoice', array( $this, $this->summary() , ( $this->paid ) ? $this->member->language()->get('invoice_mailsub_paid') : $this->member->language()->get('invoice_mailsub_pend') ), 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( ',', Settings::i()->nexus_notify_copy_types ) ) AND Settings::i()->nexus_notify_copy_email ) ? explode( ',', Settings::i()->nexus_notify_copy_email ) : array() )
			);
	}
	
	/* !URLS */
	
	/**
	 * Get checkout URL
	 *
	 * @return	Url
	 */
	public function checkoutUrl() : Url
	{
		if ( !$this->id )
		{
			$this->save();
		}
		
		return Url::internal( 'app=nexus&module=checkout&controller=checkout&id=' . $this->id, 'front', 'nexus_checkout' );
	}

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

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

		return $this->_url;
	}
	
	/**
	 * ACP "View Invoice" URL
	 *
	 * @return	Url
	 */
	public function acpUrl() : Url
	{
		return Url::internal( "app=nexus&module=payments&controller=invoices&do=view&id={$this->id}", 'admin' );
	}
	
	/**
	 * Is the member tracking this invoice?
	 *
	 * @param	Member|NULL	$member	The member to check for, or NULL for currently logged in member
	 * @return	bool
	 */
	public function tracked( Member $member = NULL ) : bool
	{
		$member = $member ?: Member::loggedIn();

		if( !isset( $this->_tracked[ $member->member_id ] ) )
		{
			try
			{
				Db::i()->select( array( 'invoice_id' ), 'nexus_invoice_tracker', array( 'member_id=? and invoice_id=?', $member->member_id, $this->id ) )->first();

				$this->_tracked[ $member->member_id ] = TRUE;
			} 
			catch ( UnderflowException )
			{
				$this->_tracked[ $member->member_id ] = FALSE;
			}
		}
		
		return $this->_tracked[ $member->member_id ];
	}
}