Files
IPB/applications/nexus/sources/Invoice/Invoice.php
T
2025-12-19 16:00:35 -08:00

2380 lines
71 KiB
PHP

<?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( ?array $where, \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', 'nexus' )->status( $val );
},
'i_member' => function ( $val, $row )
{
if ( $val )
{
return \IPS\Theme::i()->getTemplate( 'global', 'nexus' )->userLink( \IPS\Member::load( $val ) );
}
elseif ( $row['i_guest_data'] )
{
$data = json_decode( $row['i_guest_data'], TRUE );
return htmlspecialchars( \IPS\nexus\Customer::constructFromData( $data['member'] )->cm_name, ENT_DISALLOWED, 'UTF-8', FALSE );
}
else
{
return \IPS\Theme::i()->getTemplate( 'global', 'nexus' )->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;
/**
* @brief Tracked
*/
protected $_tracked = array();
/**
* 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 ( isset( $this->_data['paid'] ) and $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 = NULL )
{
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 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();
}
}
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();
}
/* 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' => '', '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 \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', 'core_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->appKey == 'nexus' and $item->typeKey == '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 );
$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( $ref='v' )
{
$url = $this->acpUrl()->setQueryString( 'r', $ref );
$return = array();
if ( $this->status !== \IPS\nexus\Invoice::STATUS_PAID and ( $this->member->member_id or $this->guest_data ) 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 and ( $this->member->member_id or $this->guest_data ) )
{
if ( \IPS\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( \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( $this->member ) ) 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' )->csrf(),
'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' ),
);
$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 !== \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' )->csrf(),
'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
*
* @param \IPS\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] 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( \IPS\Member $authorizedMember = NULL )
{
$summary = $this->summary();
$tax = array();
foreach ( $summary['tax'] as $classId => $data )
{
$tax[] = ( new \IPS\nexus\Invoice\Item\TaxItem( $classId, $data ) )->apiOutput( $authorizedMember );
}
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( $authorizedMember ),
'items' => array_map( function( $item ) use ( $authorizedMember ) {
return $item->apiOutput( $authorizedMember );
}, $summary['items'] ),
'subTotal' => $summary['subtotal']->apiOutput( $authorizedMember ),
'shippingCharges' => array_map( function( $item ) use ( $authorizedMember ) {
return $item->apiOutput( $authorizedMember );
}, $summary['shipping'] ),
'shippingTotal' => $summary['shippingTotal']->apiOutput( $authorizedMember ),
'tax' => $tax,
'taxTotal' => $summary['taxTotal']->apiOutput( $authorizedMember ),
'total' => $summary['total']->apiOutput( $authorizedMember ),
'outstanding' => $this->amountToPay()->apiOutput( $authorizedMember ),
'billingAddress' => $this->billaddress ? $this->billaddress->apiOutput( $authorizedMember ) : null,
'shippingAddress' => $this->shipaddress ? $this->shipaddress->apiOutput( $authorizedMember ) : null,
'poNumber' => $this->po,
'notes' => $this->notes,
'transactions' => array_map( function( $transaction ) use ( $authorizedMember ) {
return $transaction->apiOutput( $authorizedMember );
}, iterator_to_array( $this->transactions() ) ),
'shipments' => array_map( function( $shipment ) use ( $authorizedMember ) {
return $shipment->apiOutput( $authorizedMember );
}, iterator_to_array( $this->shipments() ) ),
'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 \IPS\Patterns\ActiveRecordIteator
*/
public function transactions( $statuses=NULL, $where = array() )
{
$where[] = array( 't_invoice=?', $this->id );
if ( $statuses )
{
$where[] = array( \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();
$spamData = $this->guest_data['spamData'];
$cards = isset( $this->guest_data['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 = \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->$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;
$postBeforeRegister = isset( $this->guest_data['pbr'] ) ? $this->guest_data['pbr'] : NULL;
$this->guest_data = isset( $this->guest_data['guestTransactionKey'] ) ? array( 'guestTransactionKey' => $this->guest_data['guestTransactionKey'] ) : 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 );
/* If we opted to save a card at checkout, save it */
foreach ( $cards as $card )
{
$card['card_member'] = $memberToSave->member_id;
\IPS\Db::i()->insert( 'nexus_customer_cards', $card );
}
/* Validation */
if ( \IPS\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 */
\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->requiresLogin() )
{
if ( $this->guest_data )
{
$return = $this->createAccountForGuest();
}
else
{
throw new \RuntimeException('NO_MEMBER_DATA');
}
}
/* 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->initialInterval ?: $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;
$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 ( \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();
/* 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 = \IPS\nexus\Customer::constructFromData( \IPS\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 \IPS\Math\Number('0') );
}
elseif ( !( $extra['commission'][ $item->payTo->member_id ]['amount'] instanceof \IPS\Math\Number ) )
{
$extra['commission'][ $item->payTo->member_id ]['amount'] = new \IPS\Math\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 );
\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 */
$this->status_extra = $extra;
if ( \IPS\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 \IPS\Math\Number('0') );
}
elseif ( !( $extra['commission'][ $commission['referrer']->member_id ]['amount'] instanceof \IPS\Math\Number ) )
{
$extra['commission'][ $commission['referrer']->member_id ]['amount'] = new \IPS\Math\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 = \IPS\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 \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( 'core_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 );
\IPS\core\AdminNotification::send( 'nexus', 'Shipment', NULL, TRUE, $shipOrder );
}
/* 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;
/* Set our billing address automatically if it's not already set */
if( !$this->billaddress )
{
$this->billaddress = $this->member->primaryBillingAddress();
}
/* Send tracked notifications */
foreach ( \IPS\Db::i()->select( 'member_id', 'nexus_invoice_tracker', array( 'invoice_id=?', $this->id ) ) as $trackingMember )
{
$trackingMember = \IPS\Member::load( $trackingMember );
\IPS\Email::buildFromTemplate( 'nexus', 'invoiceNotify', array( $this, $this->summary( $trackingMember->language() ) ), \IPS\Email::TYPE_LIST )
->send( $trackingMember );
}
/* 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 */
$extra = $this->status_extra;
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();
$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( \IPS\nexus\Customer::load( $extra['commissionref'] ), new \IPS\nexus\Money( $extra['commissionamount'], $this->currency ) );
}
/* Delete purchases */
foreach ( $this->purchasesCreated() as $purchase )
{
$purchase->delete( FALSE );
}
/* 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['type'] = 'manual';
$extra['setTo'] = $status;
$extra['setByID'] = $member->member_id;
$extra['date'] = time();
$this->status_extra = $extra;
}
/* Save */
$this->save();
}
/**
* Revoke Commission
*
* @param \IPS\Member $paidTo Revoke the commission from this member
* @param \IPS\nexus\Money $commission Amount of commission to revoke
* @param \IPS\nexus\Invoice\Item|null $item Item commission was for (if applicable)
*/
protected function revokeCommission( \IPS\Member $paidTo, \IPS\nexus\Money $commission, \IPS\nexus\Invoice\Item $item=NULL )
{
$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 );
\IPS\Email::buildFromTemplate( 'nexus', 'commissionRevoked', array( $this, $item, $commission ), \IPS\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( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() )
);
}
/**
* 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'][ $purchase->id ] = \IPS\Theme::i()->getTemplate( 'purchases', 'nexus' )->link( $purchase );
foreach ( $purchase->children( NULL ) as $child )
{
$return['unassociate'][ $purchase->id ][ $child->id ] = \IPS\Theme::i()->getTemplate( 'purchases', 'nexus' )->link( $child );
}
}
}
$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', 'nexus' )->link( $shipment );
}
else
{
$return['shipments'][] = array(
'message' => \IPS\Theme::i()->getTemplate( 'shiporders', 'nexus' )->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;
}
}
}
/* Referral Commission */
$extra = $this->status_extra;
if( !empty( $extra['commissionref'] ) AND !empty( $extra['commissionamount'] ) )
{
$customer = \IPS\nexus\Customer::load( $extra['commissionref'] );
$amount = new \IPS\nexus\Money( $extra['commissionamount'], $this->currency );
$message = \IPS\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' => \IPS\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()
{
\IPS\Email::buildFromTemplate( 'nexus', 'newInvoice', array( $this, $this->summary( $this->member->language() ) , ( $this->paid ) ? $this->member->language()->get('invoice_mailsub_paid') : $this->member->language()->get('invoice_mailsub_pend') ), \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' );
}
/**
* @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' );
}
/**
* Is the member tracking this invoice?
*
* @param \IPS\Member|NULL $member The member to check for, or NULL for currently logged in member
* @return bool
*/
public function tracked( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
if( !isset( $this->_tracked[ $member->member_id ] ) )
{
try
{
$tracked = \IPS\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 $e )
{
$this->_tracked[ $member->member_id ] = FALSE;
}
}
return $this->_tracked[ $member->member_id ];
}
}