Invision Power Services, Inc. * @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 */ 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 = count( $this->custom_fields ); try { $description = call_user_func( array( $this->extension(), 'getPurchaseNameInfo' ), $this ); } 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 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 * * @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() { $parent = NULL; if ( $this->parent ) { try { $parent = static::load( $this->parent )->apiOutput(); } 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(), '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() : 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 ) { $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; } } /** * 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 ) ), \IPS\Settings::i()->nexus_https ); } 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; } }