<?php
/**
* @brief Purchase 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;
}
/**
* Purchase Model
*/
class _Purchase extends \IPS\Node\Model
{
/**
* Tree
*
* @param \IPS\Http\Url $url URL
* @param array $where Where clause
* @param string $ref Referer
* @param \IPS\nexus\Purchase|NULL $root Root (NULL for all root purchases)
* @param bool $includeRoot Show the root?
* @return \IPS\Helpers\Tree\Tree
*/
public static function tree( \IPS\Http\Url $url, array $where, $ref = 'c', \IPS\nexus\Purchase $root = NULL, $includeRoot = TRUE )
{
$where[] = array( 'ps_show=1' );
return new \IPS\Helpers\Tree\Tree(
$url,
'Purchases',
function( $limit ) use ( $url, $where, $ref, $root, $includeRoot )
{
if ( $root )
{
if ( $includeRoot )
{
$where[] = array( 'ps_id=?', $root->id );
}
else
{
$where[] = array( 'ps_parent=?', $root->id );
}
}
else
{
$where[] = array( 'ps_parent=0' );
}
$rows = array();
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_purchases', $where, 'ps_start DESC', $limit ), 'IPS\nexus\Purchase' ) as $purchase )
{
$rows[ $purchase->id ] = $purchase->treeRow( $url, $ref );
}
return $rows;
},
function( $id ) use ( $url, $ref )
{
return \IPS\nexus\Purchase::load( $id )->treeRow( $url, $ref );
},
function( $id )
{
return \IPS\nexus\Purchase::load( $id )->parent();
},
function( $id ) use ( $url, $ref, $where )
{
$rows = array();
foreach ( \IPS\nexus\Purchase::load( $id )->children( NULL, NULL, TRUE, NULL, $where ) as $child )
{
$rows[ $child->id ] = $child->treeRow( $url, $ref );
}
return $rows;
},
NULL,
FALSE,
FALSE,
FALSE,
NULL,
function()
{
$where[] = array( 'ps_parent=0' );
return \IPS\Db::i()->select( 'COUNT(*)', 'nexus_purchases', $where )->first();
}
);
}
/**
* Tree Row
*
* @param \IPS\Http\Url $url URL
* @param string $ref Referer
* @return string
*/
public function treeRow( $url, $ref )
{
$childCount = $this->childrenCount( NULL, NULL, TRUE, array( array( 'ps_show=1' ) ) );
$hasCustomFields = FALSE;
foreach ( $this->custom_fields as $k => $v )
{
try
{
if ( trim( \IPS\nexus\Package\CustomField::load( $k )->displayValue( $v, TRUE ) ) )
{
$hasCustomFields = TRUE;
break;
}
}
catch( \OutOfRangeException $e ) {}
}
try
{
$description = call_user_func( array( $this->extension(), 'getPurchaseNameInfo' ), $this );
if( empty( $description ) )
{
$description[] = \IPS\Member::loggedIn()->language()->addToStack( 'purchase_number', FALSE, array( 'sprintf' => array( $this->id ) ) );
}
}
catch ( \OutOfRangeException $e )
{
$description = array();
}
if ( $this->grouped_renewals )
{
$description[] = \IPS\Member::loggedIn()->language()->addToStack('purchase_grouped');
}
elseif ( $this->renewals )
{
$renewals = (string) $this->renewals;
if ( $this->renewals->tax )
{
$renewals .= \IPS\Member::loggedIn()->language()->addToStack( 'plus_tax_rate', NULL, array( 'sprintf' => array( $this->renewals->tax->_title ) ) );
}
$description[] = $renewals;
}
$description = implode( ' · ', $description );
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->row(
$url,
$this->id,
( $hasCustomFields and !$childCount ) ? \IPS\Theme::i()->getTemplate( 'purchases', 'nexus' )->link( $this, FALSE, FALSE, TRUE ) : $this->_name,
$childCount,
array_merge( array(
'view' => array(
'link' => $this->acpUrl()->setQueryString( 'popup', true ),
'title' => 'view',
'icon' => 'search',
)
), $this->buttons( $ref ) ),
$description,
$this->getIcon(),
NULL,
$this->id == \IPS\Request::i()->root,
NULL,
NULL,
!$this->active ? ( $this->cancelled ? array( 'style5', 'purchase_canceled' ) : array( 'style6', 'purchase_expired' ) ) : NULL,
$hasCustomFields
);
}
/* ActiveRecord */
/**
* @brief Multiton Store
*/
protected static $multitons;
/**
* @brief Database Table
*/
public static $databaseTable = 'nexus_purchases';
/**
* @brief Database Prefix
*/
public static $databasePrefix = 'ps_';
/* !Node */
/**
* @brief Node Title
*/
public static $nodeTitle = 'purchases';
/**
* @brief [Node] Parent ID Database Column
*/
public static $databaseColumnParent = 'parent';
/**
* @brief [Node] If the node can be "owned", the owner "type" (typically "member" or "group") and the associated database column
*/
public static $ownerTypes = array(
'member' => 'member'
);
/**
* Get title
*
* @return string
*/
public function get__title()
{
return $this->name;
}
/* !Columns */
/**
* Set Default Values
*
* @return void
*/
public function setDefaultValues()
{
$this->_data['active'] = TRUE; // do directly so it doesn't call onReactivate
$this->start = new \IPS\DateTime;
$this->renewal_price = 0;
$this->renewal_currency = '';
$this->invoice_pending = NULL;
}
/**
* @brief Name with sticky fields
*/
protected $nameWithStickyFields;
/**
* Get name
*
* @return string
*/
public function get_name()
{
if ( !$this->nameWithStickyFields )
{
$this->nameWithStickyFields = $this->_data['name'];
try
{
$info = call_user_func( array( $this->extension(), 'getPurchaseNameInfo' ), $this );
if ( count( $info ) )
{
$this->nameWithStickyFields .= ' (' . implode( ' · ', $info ) . ')';
}
}
catch ( \OutOfRangeException $e ) { }
}
return $this->nameWithStickyFields;
}
/**
* Get name without sticky fields
*
* @return string
*/
public function get__name()
{
return $this->_data['name'];
}
/**
* Get member
*
* @return \IPS\Member
*/
public function get_member()
{
return \IPS\nexus\Customer::load( $this->_data['member'] );
}
/**
* Set member
*
* @param \IPS\Member
* @return void
*/
public function set_member( \IPS\Member $member )
{
$this->_data['member'] = $member->member_id;
}
/**
* Set active
*
* @param bool $active Is active?
* @return void
*/
public function set_active( $active )
{
if ( $this->id )
{
if ( $this->_data['active'] and !$active )
{
$this->onExpire();
}
elseif ( !$this->_data['active'] and $active )
{
$this->onReactivate();
}
}
$this->_data['active'] = $active;
}
/**
* Set cancelled
*
* @param bool $cancelled Is cancelled?
* @return void
*/
public function set_cancelled( $cancelled )
{
if ( $cancelled )
{
$this->_data['active'] = FALSE; // We call directly so onExpire doesn't run (as onCancel will run)
$this->onCancel();
}
else
{
if ( !$this->expire or $this->expire->getTimestamp() > time() )
{
$this->active = TRUE;
if ( $this->_data['cancelled'] )
{
$this->onReactivate();
}
}
}
$this->_data['cancelled'] = $cancelled;
}
/**
* Get start date
*
* @return \IPS\DateTime
*/
public function get_start()
{
return \IPS\DateTime::ts( $this->_data['start'] );
}
/**
* Set start date
*
* @param \IPS\DateTime $date The invoice date
* @return void
*/
public function set_start( \IPS\DateTime $date )
{
$this->_data['start'] = $date->getTimestamp();
}
/**
* Get expire date
*
* @return \IPS\DateTime|NULL
*/
public function get_expire()
{
return ( isset( $this->_data['expire'] ) and $this->_data['expire'] ) ? \IPS\DateTime::ts( $this->_data['expire'] ) : NULL;
}
/**
* Set expire date
*
* @param \IPS\DateTime|NULL $date The invoice date
* @return void
*/
public function set_expire( \IPS\DateTime $date = NULL )
{
if ( $date === NULL )
{
$this->_data['expire'] = 0;
$this->active = !$this->cancelled;
}
else
{
$this->_data['expire'] = $date->getTimestamp();
$this->active = ( !$this->cancelled and $date->add( new \DateInterval( 'PT' . intval( $this->grace_period ) . 'S' ) )->getTimestamp() > time() );
}
if ( $this->id )
{
$this->onExpirationDateChange();
}
}
/**
* Get renewal term
*
* @return \IPS\nexus\Purchase\RenewalTerm|NULL
*/
public function get_renewals()
{
if( isset( $this->_data['renewals'] ) and $this->_data['renewals'] )
{
$tax = NULL;
if ( $this->_data['tax'] )
{
try
{
$tax = \IPS\nexus\Tax::load( $this->_data['tax'] );
}
catch ( \Exception $e ) { }
}
return new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $this->_data['renewal_price'], $this->_data['renewal_currency'] ), new \DateInterval( 'P' . $this->_data['renewals'] . mb_strtoupper( $this->_data['renewal_unit'] ) ), $tax );
}
return NULL;
}
/**
* Set renewal term
*
* @param \IPS\nexus\Purchase\RenewalTerm|NULL $term The renewal term
* @return void
*/
public function set_renewals( \IPS\nexus\Purchase\RenewalTerm $term = NULL )
{
if ( $term === NULL )
{
$this->_data['renewals'] = 0;
}
else
{
$data = $term->getTerm();
$this->_data['renewals'] = $data['term'];
$this->_data['renewal_unit'] = $data['unit'];
$this->_data['renewal_price'] = $term->cost->amount;
$this->_data['renewal_currency'] = $term->cost->currency;
$this->_data['tax'] = $term->tax ? $term->tax->id : 0;
}
}
/**
* Get custom fields
*
* @return mixed
*/
public function get_custom_fields()
{
return json_decode( $this->_data['custom_fields'], TRUE ) ?: array();
}
/**
* Set custom fields
*
* @param mixed $customFields The data
* @return void
*/
public function set_custom_fields( $customFields )
{
$this->_data['custom_fields'] = json_encode( $customFields );
}
/**
* Get extra information
*
* @return mixed
*/
public function get_extra()
{
return json_decode( $this->_data['extra'], TRUE );
}
/**
* Set extra information
*
* @param mixed $extra The data
* @return void
*/
public function set_extra( $extra )
{
$this->_data['extra'] = json_encode( $extra );
}
/**
* Set parent purchase
*
* @param \IPS\nexus\Purchase
* @return void
*/
public function set_parent( \IPS\nexus\Purchase $purchase = NULL )
{
$this->_data['parent'] = $purchase ? $purchase->id : 0;
}
/**
* Get pending invoice
*
* @return \IPS\nexus\Invoice
*/
public function get_invoice_pending()
{
try
{
return $this->_data['invoice_pending'] ? \IPS\nexus\Invoice::load( $this->_data['invoice_pending'] ) : NULL;
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
/**
* Set pending invoice
*
* @param \IPS\nexus\Invoice
* @return void
*/
public function set_invoice_pending( \IPS\nexus\Invoice $invoice = NULL )
{
$this->_data['invoice_pending'] = $invoice ? $invoice->id : 0;
if ( !$invoice )
{
$this->_data['invoice_warning_sent'] = FALSE;
}
}
/**
* Get member to receive payments
*
* @return \IPS\Member
*/
public function get_pay_to()
{
try
{
return $this->_data['pay_to'] ? \IPS\nexus\Customer::load( $this->_data['pay_to'] ) : NULL;
}
catch ( \Exception $e )
{
return NULL;
}
}
/**
* Set member to receive payments
*
* @param \IPS\Member
* @return void
*/
public function set_pay_to( \IPS\Member $member )
{
$this->_data['pay_to'] = $member->member_id;
}
/**
* Get fee to commission on renewal charges
*
* @return \IPS\nexus\Money|NULL
*/
public function get_fee()
{
if ( $this->_data['fee'] and $this->renewals )
{
try
{
return new \IPS\nexus\Money( $this->_data['fee'], $this->renewals->cost->currency );
}
catch ( \Exception $e ) { }
}
return NULL;
}
/**
* Set fee to commission on renewal charges
*
* @param \IPS\nexus\Money|NULL $fee The fee
* @return void
*/
public function set_fee( \IPS\nexus\Money $fee = NULL )
{
$this->_data['fee'] = $fee ? $fee->amount : NULL;
}
/**
* Get original invoice
*
* @return \IPS\nexus\Invoice
*/
public function get_original_invoice()
{
return \IPS\nexus\Invoice::load( $this->_data['original_invoice'] );
}
/**
* Set original invoice
*
* @param \IPS\nexus\Invoice
* @return void
*/
public function set_original_invoice( \IPS\nexus\Invoice $invoice )
{
$this->_data['original_invoice'] = $invoice->id;
}
/**
* Get grouped renewals information
*
* @return array
*/
public function get_grouped_renewals()
{
return $this->_data['grouped_renewals'] ? json_decode( $this->_data['grouped_renewals'], TRUE ) : NULL;
}
/**
* Set grouped renewals information
*
* @param array|NULL $data The data
* @return void
*/
public function set_grouped_renewals( $data )
{
$this->_data['grouped_renewals'] = !is_null( $data ) ? json_encode( $data ) : NULL;
}
/**
* Get billing agreement
*
* @return \IPS\nexus\Customer\BillingAgreement|NULL
*/
public function get_billing_agreement()
{
if ( $this->_data['billing_agreement'] )
{
try
{
return \IPS\nexus\Customer\BillingAgreement::load( $this->_data['billing_agreement'] );
}
catch ( \OutOfRangeException $e )
{
$this->_data['billing_agreement'] = NULL;
}
}
return NULL;
}
/**
* Set billing agreement
*
* @param \IPS\nexus\Customer\BillingAgreement|NULL $billingAgreement The billing agreement
* @return void
*/
public function set_billing_agreement( \IPS\nexus\Customer\BillingAgreement $billingAgreement = NULL )
{
$this->_data['billing_agreement'] = $billingAgreement === NULL ? NULL : $billingAgreement->id;
}
/* !Properties and syncing */
/**
* @brief Extension
*/
protected $extension;
/**
* Get extension
*
* @return \IPS\nexus\Invoice\Item\Purchase
* @throws \OutOfRangeException
*/
protected function extension()
{
if ( $this->extension === NULL )
{
/* Load the app. \IPS\Application::load() throws UnexpectedValueException
if the Application.php does not exist, which we'll standardize to
OutOfRangeException so areas calling this method only need to check
for that */
try
{
$app = \IPS\Application::load( $this->app );
}
catch ( \UnexpectedValueException $e )
{
throw new \OutOfRangeException;
}
/* Find the extension */
foreach ( $app->extensions( 'nexus', 'Item', FALSE ) as $ext )
{
if ( $ext::$type == $this->type )
{
$this->extension = $ext;
break;
}
}
/* Don't have it? */
if ( !$this->extension )
{
throw new \OutOfRangeException;
}
}
return $this->extension;
}
/**
* Get icon
*
* @return string
*/
public function getIcon()
{
try
{
return call_user_func( array( $this->extension(), 'getIcon' ), $this );
}
catch ( \OutOfRangeException $e )
{
return 'question';
}
}
/**
* Get icon
*
* @return string
*/
public function getTypeTitle()
{
try
{
return call_user_func( array( $this->extension(), 'getTypeTitle' ), $this );
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
/**
* Get image
*
* @return string
*/
public function image()
{
try
{
return call_user_func( array( $this->extension(), 'purchaseImage' ), $this );
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
/**
* Get ACP Page HTML
*
* @return string
*/
public function acpPage()
{
try
{
return call_user_func( array( $this->extension(), 'acpPage' ), $this );
}
catch ( \OutOfRangeException $e )
{
return '';
}
}
/**
* ACP Edit Form
*
* @param \IPS\Helpers\Form $form The form
* @param \IPS\nexus\Purchase\RenewalTerm $renewals The renewal term
* @return string
*/
public function acpEdit( \IPS\Helpers\Form $form, $renewals )
{
try
{
return call_user_func( array( $this->extension(), 'acpEdit' ), $this, $form, $renewals );
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
/**
* ACP Edit Save
*
* @param array $values Values from form
* @return string
*/
public function acpEditSave( array $values )
{
try
{
return call_user_func( array( $this->extension(), 'acpEditSave' ), $this, $values );
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
/**
* Get Client Area Page HTML
*
* @return array
*/
public function clientAreaPage()
{
try
{
return call_user_func( array( $this->extension(), 'clientAreaPage' ), $this );
}
catch ( \OutOfRangeException $e )
{
return array( 'packageInfo' => '', 'purchaseInfo' => '' );
}
}
/**
* Perform ACP Action
*
* @return void
*/
public function acpAction()
{
try
{
return call_user_func( array( $this->extension(), 'acpAction' ), $this );
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
/**
* Perform Client Area Action
*
* @return void
*/
public function clientAreaAction()
{
try
{
return call_user_func( array( $this->extension(), 'clientAreaAction' ), $this );
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
/**
* Get ACP Support View HTML
*
* @return string
*/
public function acpSupportView()
{
try
{
return call_user_func( array( $this->extension(), 'acpSupportView' ), $this );
}
catch ( \OutOfRangeException $e )
{
return '';
}
}
/**
* Support Severity
*
* @return \IPS\nexus\Support\Severity|NULL
*/
public function supportSeverity()
{
try
{
return call_user_func( array( $this->extension(), 'supportSeverity' ), $this );
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
/**
* Get renewal payment methods IDs
*
* @return array|NULL
*/
public function renewalPaymentMethodIds()
{
try
{
return call_user_func( array( $this->extension(), 'renewalPaymentMethodIds' ), $this );
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
/**
* Call on*
*
* @param string $method Method
* @param array $params Params
* @return mixed
*/
public function __call( $method, $params )
{
if ( mb_substr( $method, 0, 2 ) === 'on' )
{
try
{
return call_user_func_array( array( $this->extension(), $method ), array_merge( array( $this ), $params ) );
}
catch ( \OutOfRangeException $e ) { }
}
throw new \BadMethodCallException;
}
/**
* 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 name Name
* @apiresponse string itemApp Key for application. For example, 'nexus' for products and renewals; 'downloads' for Downloads files
* @apiresponse string itemType Key for item type. For example, 'package' for products; 'file' for Downloads files.
* @apiresponse int itemId The ID for the item. For example, the product ID or the file ID.
* @apiresponse \IPS\nexus\Customer customer Customer
* @apiresponse datetime purchased Purchased date
* @apiresponse datetime expires Expiration date
* @apiresponse bool active If purchase is currently active (not expired)
* @apiresponse bool canceled If purchase has been canceled
* @apiresponse \IPS\nexus\Purchase\RenewalTerm renewalTerm Renewal term
* @apiresponse object customFields Values for custom fields
* @apiresponse \IPS\nexus\Purchase parent Parent purchase
* @apiresponse bool show If this purchase shows in the client area and AdminCP
* @apiresponse string licenseKey License key
* @apiresponse string image If the item has a relevant image (for exmaple, product image, Downloads file screenshot), the URL to it
* @apiresponse string url The URL for the customer to view this purchase in the client area
*/
public function apiOutput( \IPS\Member $authorizedMember = NULL )
{
$parent = NULL;
if ( $this->parent )
{
try
{
$parent = static::load( $this->parent )->apiOutput( $authorizedMember );
}
catch ( \OutOfRangeException $e ) {}
}
return array(
'id' => $this->id,
'name' => $this->name,
'itemApp' => $this->app,
'itemType' => $this->type,
'itemId' => $this->item_id,
'customer' => $this->member->apiOutput( $authorizedMember ),
'purchased' => $this->start->rfc3339(),
'expires' => $this->expire ? $this->expire->rfc3339() : null,
'active' => (bool) $this->active,
'canceled' => (bool) $this->cancelled,
'renewalTerm' => $this->renewals ? $this->renewals->apiOutput( $authorizedMember ) : null,
'customFields' => $this->custom_fields,
'parent' => $parent,
'show' => (bool) $this->show,
'licenseKey' => $this->licenseKey() ? $this->licenseKey()->key : null,
'image' => $this->image() ? ( (string) $this->image()->url ) : null,
'url' => (string) $this->url(),
);
}
/* !License Keys */
/**
* @brief License key
*/
public $licenseKey;
/**
* Get license key
*
* @return \IPS\nexus\Purchase\LicenseKey|NULL
*/
public function licenseKey()
{
if ( $this->licenseKey === NULL )
{
try
{
$this->licenseKey = \IPS\nexus\Purchase\LicenseKey::load( $this->id, 'lkey_purchase' );
}
catch ( \OutOfRangeException $e )
{
$this->licenseKey = FALSE;
}
}
return $this->licenseKey ?: NULL;
}
/**
* onExpire
*
* @return void
*/
public function onExpire()
{
if ( $licenseKey = $this->licenseKey() )
{
$licenseKey->active = FALSE;
$licenseKey->save();
}
return $this->__call( 'onExpire', array() );
}
/**
* onCancel
*
* @return void
*/
public function onCancel()
{
if ( $licenseKey = $this->licenseKey() )
{
$licenseKey->active = FALSE;
$licenseKey->save();
}
return $this->__call( 'onCancel', array() );
}
/**
* onReactivate
*
* @return void
*/
public function onReactivate()
{
if ( $licenseKey = $this->licenseKey() )
{
$licenseKey->active = TRUE;
$licenseKey->save();
}
return $this->__call( 'onReactivate', array() );
}
/**
* onDelete
*
* @return void
*/
public function onDelete()
{
if ( $licenseKey = $this->licenseKey() )
{
$licenseKey->delete();
}
return $this->__call( 'onDelete', array() );
}
/* !Client Area */
/**
* Can view?
*
* @param \IPS\Member|NULL $member The member to check (NULL for currently logged in member)
* @return bool
*/
public function canView( $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
if ( $this->member->member_id === $member->member_id or array_key_exists( $member->member_id, iterator_to_array( $this->member->alternativeContacts( array( \IPS\Db::i()->findInSet( 'purchases', array( $this->id ) ) ) ) ) ) )
{
return TRUE;
}
return FALSE;
}
/**
* Admin can change expire date / renewal term?
*
* @return bool
*/
public function canChangeExpireDate()
{
if ( $this->billing_agreement and !$this->billing_agreement->canceled )
{
return FALSE;
}
try
{
return call_user_func( array( $this->extension(), 'canChangeExpireDate' ), $this );
}
catch ( \OutOfRangeException $e )
{
return TRUE;
}
}
/**
* Can Renew Until
*
* @param \IPS\Member|NULL $member The member to check (NULL for currently logged in member)
* @param bool $inCycles Get in cycles rather than a date?
* @param bool $admin If TRUE, is for ACP. If FALSE, is for front-end.
* @return \IPS\DateTime|bool TRUE means can renew as much as they like. FALSE means cannot renew at all. \IPS\DateTime (or int if $inCycles is TRUE) means can renew until that date
*/
public function canRenewUntil( \IPS\Member $member = NULL, $inCycles = FALSE, $admin = FALSE )
{
/* Can this purchase be renewed at all? */
if ( !$this->canBeRenewed() )
{
return FALSE;
}
$member = $member ?: \IPS\Member::loggedIn();
if ( !$admin and $this->member->member_id !== $member->member_id and !array_key_exists( $member->member_id, iterator_to_array( $this->member->alternativeContacts( array( \IPS\Db::i()->findInSet( 'purchases', array( $this->id ) ) . ' AND billing=1' ) ) ) ) )
{
return FALSE;
}
if ( $this->cancelled or !$this->renewals or ( $this->billing_agreement and !$this->billing_agreement->canceled ) )
{
return FALSE;
}
if ( !$admin and $pendingInvoice = $this->invoice_pending and $pendingInvoice->status === $pendingInvoice::STATUS_PENDING )
{
return FALSE;
}
try
{
$date = call_user_func( array( $this->extension(), 'canRenewUntil' ), $this, $admin );
if ( $inCycles and $date instanceof \IPS\DateTime )
{
$now = \IPS\DateTime::create();
$cycles = 0;
while ( $now->add( $this->renewals->interval )->getTimestamp() < $date->getTimestamp() )
{
$cycles++;
}
return $cycles;
}
else
{
return $date;
}
}
catch ( \OutOfRangeException $e )
{
return FALSE;
}
}
/**
* Purchase can be renewed?
*
* @return boolean|NULL
*/
public function canBeRenewed()
{
try
{
return call_user_func( array( $this->extension(), 'canBeRenewed' ), $this );
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
/**
* Can Cancel
*
* @param \IPS\Member|NULL $member The member to check (NULL for currently logged in member)
* @return bool
*/
public function canCancel( \IPS\Member $member = NULL )
{
$member = $member ?: \IPS\Member::loggedIn();
if ( $this->member->member_id !== $member->member_id and !array_key_exists( $member->member_id, iterator_to_array( $this->member->alternativeContacts( array( \IPS\Db::i()->findInSet( 'purchases', array( $this->id ) ) . ' AND billing=1' ) ) ) ) )
{
return FALSE;
}
return !$this->grouped_renewals and ( !$this->billing_agreement or $this->billing_agreement->canceled ) and $this->renewals and $this->active;
}
/**
* @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=purchases&do=view&id={$this->id}", 'front', 'clientspurchase', array( \IPS\Http\Url\Friendly::seoTitle( $this->name ) ) );
}
return $this->_url;
}
/* !Actions */
/**
* Transfer
*
* @param \IPS\Member $newCustomer
* @return void
*/
public function transfer( \IPS\Member $newCustomer )
{
$this->onTransfer( $newCustomer );
$this->member = $newCustomer;
$this->save();
foreach ( $this->children() as $child )
{
$child->transfer( $newCustomer );
}
}
/**
* Delete
*
* @return void
*/
public function delete()
{
\IPS\File::unclaimAttachments( 'nexus_Purchases', $this->id, NULL, 'purchase' );
foreach ( $this->children() as $child )
{
$child->delete();
}
try
{
$this->onDelete();
}
catch ( \BadMethodCallException $e ) {}
parent::delete();
}
/* !Grouping */
/**
* Group with parent
*
* @return void
* @throws \LogicException
*/
public function groupWithParent()
{
/* Get the parent */
$parent = $this->parent();
if ( !$parent )
{
throw new \BadMethodCallException('no_parent');
}
/* If we have a renewal term, we need to merge it with the parent... */
if ( $this->renewals )
{
/* Remember what our term is */
$term = $this->renewals->getTerm();
$this->grouped_renewals = array( 'term' => $term['term'], 'unit' => $term['unit'], 'price' => $this->renewals->cost->amount, 'currency' => $this->renewals->cost->currency, 'tax' => $this->renewals->tax ? $this->renewals->tax->id : 0 );
$this->save();
/* Add our term to the parent... */
if ( !$this->cancelled )
{
/* If the parent also has a renewal term, merge them */
if ( $parent->renewals )
{
$parent->renewals = new \IPS\nexus\Purchase\RenewalTerm( $parent->renewals->add( $this->renewals ), $parent->renewals->interval, $parent->renewals->tax );
$parent->save();
}
/* Otherwise just set the parent to this */
else
{
$parent->renewals = $this->renewals;
if ( !$parent->expire )
{
$parent->expire = $this->expire;
}
$parent->save();
}
}
/* Cancel any pending invoices as they're no longer valid */
if ( $invoice = $parent->invoice_pending )
{
$invoice->status = $invoice::STATUS_CANCELED;
$invoice->save();
}
if ( $invoice = $this->invoice_pending )
{
$invoice->status = $invoice::STATUS_CANCELED;
$invoice->save();
}
}
else
{
$this->grouped_renewals = array();
}
/* Update this purchase */
$this->expire = NULL;
$this->save();
}
/**
* Ungroup from parent
*
* @return void
* @throws \LogicException
*/
public function ungroupFromParent()
{
/* Get the parent */
$parent = $this->parent();
if ( !$parent )
{
throw new \BadMethodCallException('no_parent');
}
/* If we have a renewal term, we need to remove it from the parent... */
if ( $this->renewals )
{
/* Restore parent renewal term */
if ( $parent->renewals and !$this->cancelled )
{
$newRenewalPrice = $parent->renewals->subtract( $this->renewals );
if ( $newRenewalPrice->amount->isZero() )
{
$parent->renewals = NULL;
}
else
{
$parent->renewals = new \IPS\nexus\Purchase\RenewalTerm( $newRenewalPrice, $parent->renewals->interval, $parent->renewals->tax );
}
$parent->save();
}
/* Restore this purchase renewal term */
$groupedRenewals = $this->grouped_renewals;
if ( $groupedRenewals['term'] and $groupedRenewals['unit'] )
{
try
{
$tax = ( isset( $groupedRenewals['tax'] ) and $groupedRenewals['tax'] ) ? \IPS\nexus\Tax::load( $groupedRenewals['tax'] ) : NULL;
}
catch ( \OutOfRangeException $e )
{
$tax = NULL;
}
$this->renewals = new \IPS\nexus\Purchase\RenewalTerm(
new \IPS\nexus\Money( $groupedRenewals['price'], isset( $groupedRenewals['currency'] ) ? $groupedRenewals['currency'] : $this->member->defaultCurrency() ),
new \DateInterval( 'P' . $groupedRenewals['term'] . mb_strtoupper( $groupedRenewals['unit'] ) ),
$tax
);
}
$this->expire = $parent->expire;
}
/* Ungroup */
$this->grouped_renewals = NULL;
$this->save();
/* Cancel any pending invoices as they're no longer valid */
if ( $invoice = $parent->invoice_pending )
{
$invoice->status = $invoice::STATUS_CANCELED;
$invoice->save();
}
if ( $invoice = $this->invoice_pending )
{
$invoice->status = $invoice::STATUS_CANCELED;
$invoice->save();
}
}
/* !ACP Display */
/**
* ACP URL
*
* @return \IPS\Http\Url
*/
public function acpUrl()
{
return \IPS\Http\Url::internal( "app=nexus&module=customers&controller=purchases&do=view&id={$this->id}", 'admin' );
}
/**
* ACP Buttons
*
* @param string $ref Referer
* @return array
*/
public function buttons( $ref='v' )
{
$return = array();
$url = $this->acpUrl()->setQueryString( 'r', $ref );
if( \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'customers', 'purchases_edit' ) )
{
$return['edit'] = array(
'icon' => 'pencil',
'title' => 'edit',
'link' => $url->setQueryString( 'do', 'edit' ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('edit') )
);
}
try
{
$return = array_merge( $return, call_user_func( array( $this->extension(), 'acpButtons' ), $this, $url ) );
}
catch ( \OutOfRangeException $e ) { }
$parent = $this->parent();
if ( !$this->grouped_renewals and ( !$this->billing_agreement or $this->billing_agreement->canceled ) and $this->renewals and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'payments', 'invoices_add' ) )
{
$return['renew'] = array(
'icon' => 'refresh',
'title' => 'generate_renewal_invoice',
'link' => $url->setQueryString( 'do', 'renew' ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('generate_renewal_invoice') )
);
}
if ( ( !$this->billing_agreement or $this->billing_agreement->canceled ) and $this->parent() and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'customers', 'purchases_edit' ) )
{
if ( !$this->grouped_renewals )
{
$return['group'] = array(
'icon' => 'compress',
'title' => 'group_with_parent',
'link' => $url->setQueryString( 'do', 'group' ),
'data' => array( 'confirm' => '', 'confirmSubMessage' => \IPS\Member::loggedIn()->language()->addToStack('group_with_parent_info') )
);
}
else
{
$return['group'] = array(
'icon' => 'expand',
'title' => 'ungroup_from_parent',
'link' => $url->setQueryString( 'do', 'ungroup' ),
'data' => array( 'confirm' => '', 'confirmSubMessage' => \IPS\Member::loggedIn()->language()->addToStack('ungroup_from_parent_info') )
);
}
}
if( ( !$this->billing_agreement or $this->billing_agreement->canceled ) and !$this->grouped_renewals and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'customers', 'purchases_transfer' ) )
{
$return['transfer'] = array(
'icon' => 'user',
'title' => 'transfer',
'link' => $url->setQueryString( 'do', 'transfer' ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('transfer') )
);
}
if ( ( !$this->billing_agreement or $this->billing_agreement->canceled ) and ( !$this->grouped_renewals or !$parent or !$parent->billing_agreement or $parent->billing_agreement->canceled ) and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'customers', 'purchases_cancel' ) )
{
if ( $this->cancelled )
{
$return['reactivate'] = array(
'icon' => 'check',
'title' => 'reactivate',
'link' => $url->setQueryString( 'do', 'reactivate' ),
'data' => array( 'confirm' => '' )
);
}
else
{
$return['cancel'] = array(
'icon' => 'times',
'title' => 'cancel',
'link' => $url->setQueryString( 'do', 'cancel' ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('cancel') )
);
}
}
if ( ( !$this->billing_agreement or $this->billing_agreement->canceled ) and ( !$this->grouped_renewals or !$parent or !$parent->billing_agreement or $parent->billing_agreement->canceled ) and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'customers', 'purchases_delete' ) )
{
$return['delete'] = array(
'icon' => 'times-circle',
'title' => 'delete',
'link' => $url->setQueryString( 'do', 'delete' ),
'data' => array( 'delete' => '' )
);
}
return $return;
}
}