2380 lines
71 KiB
PHP
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 ];
|
|
}
|
|
} |