<?php
/**
* @brief Package Node
* @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 29 Apr 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;
}
/**
* Package
*/
class _Package extends \IPS\Node\Model
{
/**
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons;
/**
* @brief [ActiveRecord] Database Table
*/
public static $databaseTable = 'nexus_packages';
/**
* @brief [ActiveRecord] Database Prefix
*/
public static $databasePrefix = 'p_';
/**
* @brief [Node] Order Database Column
*/
public static $databaseColumnOrder = 'position';
/**
* @brief [Node] Parent Node ID Database Column
*/
public static $parentNodeColumnId = 'group';
/**
* @brief [Node] Parent Node Class
*/
public static $parentNodeClass = 'IPS\nexus\Package\Group';
/**
* @brief [Node] Node Title
*/
public static $nodeTitle = 'menu__nexus_store_packages';
/**
* @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key
*/
public static $titleLangPrefix = 'nexus_package_';
/**
* @brief [Node] Description suffix. If specified, will look for a language key with "{$titleLangPrefix}_{$id}_{$descriptionLangSuffix}" as the key
*/
public static $descriptionLangSuffix = '_desc';
/* !ActiveRecord */
/**
* Construct ActiveRecord from database row
*
* @param array $data Row from database table
* @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
* @return static
*/
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
{
$classname = 'IPS\nexus\Package\\' . mb_ucfirst( $data['p_type'] );
if ( isset( $classname::$packageDatabaseTable ) )
{
$data = array_merge( $data, \IPS\Db::i()->select( '*', $classname::$packageDatabaseTable, array( 'p_id=?', $data['p_id'] ) )->first() );
}
/* Initiate an object */
$obj = new $classname;
$obj->_new = FALSE;
/* Import data */
$databasePrefixLength = \strlen( static::$databasePrefix );
foreach ( $data as $k => $v )
{
if( static::$databasePrefix AND mb_strpos( $k, static::$databasePrefix ) === 0 )
{
$k = \substr( $k, $databasePrefixLength );
}
$obj->_data[ $k ] = $v;
}
$obj->changed = array();
/* Init */
if ( method_exists( $obj, 'init' ) )
{
$obj->init();
}
/* Return */
return $obj;
}
/**
* @brief [Node] ACP Restrictions
* @code
array(
'app' => 'core', // The application key which holds the restrictrions
'module' => 'foo', // The module key which holds the restrictions
'map' => array( // [Optional] The key for each restriction - can alternatively use "prefix"
'add' => 'foo_add',
'edit' => 'foo_edit',
'permissions' => 'foo_perms',
'delete' => 'foo_delete'
),
'all' => 'foo_manage', // [Optional] The key to use for any restriction not provided in the map (only needed if not providing all 4)
'prefix' => 'foo_', // [Optional] Rather than specifying each key in the map, you can specify a prefix, and it will automatically look for restrictions with the key "[prefix]_add/edit/permissions/delete"
* @endcode
*/
protected static $restrictions = array(
'app' => 'nexus',
'module' => 'store',
'prefix' => 'packages_',
);
/**
* Set Default Values
*
* @return void
*/
public function setDefaultValues()
{
$this->member_groups = '*';
$this->date_added = time();
$this->store = TRUE;
}
/**
* Save Changed Columns
*
* @return void
*/
public function save()
{
$data = $this->changed;
$secondaryTable = array();
foreach ( $this->changed as $k => $v )
{
if ( isset( static::$packageDatabaseColumns ) and in_array( "p_{$k}", static::$packageDatabaseColumns ) )
{
$secondaryTable[ "p_{$k}" ] = $v;
unset( $this->changed[ $k ] );
}
elseif ( !in_array( "p_{$k}", array( 'p_id', 'p_name', 'p_seo_name', 'p_desc', 'p_group', 'p_stock', 'p_reg', 'p_store', 'p_member_groups', 'p_allow_upgrading', 'p_upgrade_charge', 'p_allow_downgrading', 'p_downgrade_refund', 'p_base_price', 'p_tax', 'p_renewal_days', 'p_primary_group', 'p_secondary_group', 'p_return_primary', 'p_return_secondary', 'p_position', 'p_associable', 'p_force_assoc', 'p_assoc_error', 'p_discounts', 'p_page', 'p_support', 'p_support_department', 'p_support_severity', 'p_featured', 'p_upsell', 'p_notify', 'p_type', 'p_custom', 'p_reviewable', 'p_review_moderate', 'p_image', 'p_methods', 'p_renew_options', 'p_group_renewals', 'p_rebuild_thumb', 'p_renewal_days_advance', 'p_date_added', 'p_reviews', 'p_rating', 'p_grace_period', 'p_email_purchase_type', 'p_email_purchase', 'p_email_expire_soon_type', 'p_email_expire_soon', 'p_email_expire_type', 'p_email_expire' ) ) )
{
unset( $this->changed[ $k ] );
}
}
if ( isset( $data['base_price'] ) )
{
$decoded = json_decode( $data['base_price'], TRUE );
if ( $decoded and is_array( $decoded ) )
{
$prices = array( 'id' => $this->id );
foreach ( $decoded as $currency => $value )
{
if ( !\IPS\Db::i()->checkForColumn( 'nexus_package_base_prices', $currency ) )
{
\IPS\Db::i()->addColumn( 'nexus_package_base_prices', array(
'name' => $currency,
'type' => 'FLOAT'
) );
}
$prices[ $currency ] = $value['amount'];
}
\IPS\Db::i()->replace( 'nexus_package_base_prices', $prices );
}
}
$this->date_updated = time();
parent::save();
$this->changed = $data;
if ( !empty( $secondaryTable ) AND isset( static::$packageDatabaseTable ) )
{
\IPS\Db::i()->update( static::$packageDatabaseTable, $secondaryTable, array( 'p_id=?', $this->id ) );
}
}
/**
* [ActiveRecord] Delete Record
*
* @return void
*/
public function delete()
{
parent::delete();
\IPS\Db::i()->delete( static::$packageDatabaseTable, array( 'p_id=?', $this->id ) );
\IPS\Content\Search\Index::i()->removeFromSearchIndex( $this->item() );
}
/**
* [ActiveRecord] Duplicate
*
* @return void
*/
public function __clone()
{
if( $this->skipCloneDuplication === TRUE )
{
return;
}
$primaryTable = array();
$secondaryTable = array();
foreach ( $this->_data as $k => $v )
{
if ( !in_array( $k, array( 'id', 'reviews', 'unapproved_reviews', 'hidden_reviews' ) ) )
{
if ( in_array( "p_{$k}", static::$packageDatabaseColumns ) )
{
$secondaryTable[ "p_{$k}" ] = $v;
}
else
{
$primaryTable[ "p_{$k}" ] = $v;
}
}
}
/* We need to update the date_added field so cloned products appear in the new products block */
$primaryTable['p_date_added'] = time();
$oldId = $this->_id;
$id = \IPS\Db::i()->insert( 'nexus_packages', $primaryTable );
$secondaryTable['p_id'] = $id;
\IPS\Db::i()->insert( static::$packageDatabaseTable, $secondaryTable );
\IPS\Lang::saveCustom( 'nexus', "nexus_package_{$id}", iterator_to_array( \IPS\Db::i()->select( 'CONCAT(word_custom, \' ' . \IPS\Member::loggedIn()->language()->get('copy_noun') . '\') as word_custom, lang_id', 'core_sys_lang_words', array( 'word_key=?', static::$titleLangPrefix . $this->_id ) )->setKeyField( 'lang_id' )->setValueField('word_custom') ) );
\IPS\Lang::saveCustom( 'nexus', static::$titleLangPrefix . $id . static::$descriptionLangSuffix, iterator_to_array( \IPS\Db::i()->select( 'word_custom, lang_id', 'core_sys_lang_words', array( 'word_key=?', static::$titleLangPrefix . $oldId . static::$descriptionLangSuffix ) )->setKeyField( 'lang_id' )->setValueField('word_custom') ) );
\IPS\Db::i()->insert( 'nexus_package_base_prices', \IPS\Db::i()->select( "{$id} AS id, " . implode( ',', array_map( function( $val )
{
return "`{$val}`";
}, \IPS\nexus\Money::currencies() ) ), 'nexus_package_base_prices', array( 'id=?', $this->_id ) ) );
foreach ( \IPS\Db::i()->select( '*', 'nexus_package_images', array( 'image_product=?', $this->_id ) ) as $image )
{
$file = \IPS\File::get( 'nexus_Products', $image['image_location'] );
\IPS\Db::i()->insert( 'nexus_package_images', array(
'image_product' => $id,
'image_location' => (string) \IPS\File::create( 'nexus_Products', $file->originalFilename, $file->contents(), $file->container ),
'image_primary' => $image['image_primary']
) );
}
\IPS\Db::i()->insert( 'nexus_product_options', \IPS\Db::i()->select( "NULL as opt_id, {$id} AS opt_package, opt_values, opt_stock, opt_base_price, opt_renew_price", 'nexus_product_options', array( 'opt_package=?', $this->_id ) ) );
\IPS\Db::i()->update( 'nexus_package_fields', "cf_packages=CONCAT( cf_packages, ',{$id}' )", \IPS\Db::i()->findInSet( 'cf_packages', array( $this->_id ) ) );
\IPS\Db::i()->update( 'nexus_referral_rules', "rrule_purchase_packages=CONCAT( rrule_purchase_packages, ',{$id}' )", \IPS\Db::i()->findInSet( 'rrule_purchase_packages', array( $this->_id ) ) );
\IPS\Db::i()->update( 'nexus_support_departments', "dpt_packages=CONCAT( dpt_packages, ',{$id}' )", \IPS\Db::i()->findInSet( 'dpt_packages', array( $this->_id ) ) );
$primaryKey = static::$databaseColumnId;
$this->$primaryKey = $id;
}
/* !Properties */
/**
* Get the lowest price for multiple packages
* May return price ($x) or "From $x" or a price struck and replaced with a discount price
*
* @param \IPS\Patterns\ActiveRecordIterator $iterator Iterator of packages
* @param \IPS\nexus\Customer|NULL $customer The customer (NULL for currently logged in member)
* @return string
*/
public static function lowestPriceToDisplay( \IPS\Patterns\ActiveRecordIterator $iterator, \IPS\nexus\Customer $customer = NULL )
{
$customer = $customer ?: \IPS\nexus\Customer::loggedIn();
$currency = ( $customer->member_id === \IPS\nexus\Customer::loggedIn()->member_id ) ? ( ( isset( $_SESSION['currency'] ) and in_array( $_SESSION['currency'], \IPS\nexus\Money::currencies() ) ) ? $_SESSION['currency'] : $customer->defaultCurrency() ) : $customer->defaultCurrency();
$lowest = NULL;
$_priceMayChange = FALSE;
$priceMayChange = FALSE;
foreach ( $iterator as $package )
{
$price = $package->_lowestPrice( $customer, $currency, $priceMayChange );
if ( $lowest !== NULL and $price != $lowest )
{
$_priceMayChange = TRUE;
}
if ( $lowest === NULL or $price < $lowest )
{
$lowest = $price;
}
}
return \IPS\Theme::i()->getTemplate( 'store', 'nexus' )->price( new \IPS\nexus\Money( $lowest, $currency ), $priceMayChange or $_priceMayChange );
}
/**
* @brief Icon
*/
public static $icon = 'archive';
/**
* @brief Title
*/
public static $title = 'product';
/**
* Basic purchase price
* May be further adjusted by renewal terms which add
*
* @param \IPS\nexus\Customer|NULL $customer The customer (NULL for currently logged in member)
* @param bool $usergroupDiscounts Account for usergroup discounts?
* @param bool $loyaltyDiscounts Account for loyalty discounts?
* @param bool $bulkDiscounts Account for bulk discounts?
* @param int $initialCount Will assume that number of additional purchases for this package
* @return \IPS\nexus\Money
* @throws \OutOfBoundsException
*/
public function price( \IPS\nexus\Customer $customer = NULL, $usergroupDiscounts = TRUE, $loyaltyDiscounts = TRUE, $bulkDiscounts = TRUE, $initialCount = 0 )
{
return static::priceFromData(
$this->id,
json_decode( $this->base_price, TRUE ),
json_decode( $this->discounts, TRUE ),
$customer,
NULL,
$usergroupDiscounts,
$loyaltyDiscounts,
$bulkDiscounts,
$initialCount
);
}
/**
* Basic purchase price
* May be further adjusted by renewal terms which add
*
* @param int $packageId Package ID
* @param array $prices Base Price data
* @param array $discounts Discount data
* @param \IPS\nexus\Customer $customer The customer (NULL for currently logged in member)
* @param string $currency The currency (NULL to autodetect based on $customer)
* @param bool $usergroupDiscounts Account for usergroup discounts?
* @param bool $loyaltyDiscounts Account for loyalty discounts?
* @param bool $bulkDiscounts Account for bulk discounts?
* @param int $initialCount Will assume that number of additional purchases for this package
* @return \IPS\nexus\Money
* @throws \OutOfBoundsException
*/
public static function priceFromData( $packageId, $prices, $discounts, \IPS\nexus\Customer $customer = NULL, $currency = NULL, $usergroupDiscounts = TRUE, $loyaltyDiscounts = TRUE, $bulkDiscounts = TRUE, $initialCount = 0 )
{
/* Base */
$customer = $customer ?: \IPS\nexus\Customer::loggedIn();
if ( !$currency )
{
$currency = ( $customer->member_id === \IPS\nexus\Customer::loggedIn()->member_id ) ? ( ( isset( $_SESSION['currency'] ) and in_array( $_SESSION['currency'], \IPS\nexus\Money::currencies() ) ) ? $_SESSION['currency'] : $customer->defaultCurrency() ) : $customer->defaultCurrency();
}
if ( !isset( $prices[ $currency ] ) )
{
throw new \OutOfBoundsException;
}
$price = $prices[ $currency ]['amount'];
/* Usergroup discounts */
if ( $usergroupDiscounts and isset( $discounts['usergroup'] ) )
{
foreach ( $discounts['usergroup'] as $discount )
{
if ( $discount['price'][ $currency ] < $price )
{
if ( $discount['secondary'] )
{
$inGroup = $customer->inGroup( $discount['group'] );
}
else
{
$inGroup = $customer->member_group_id == $discount['group'];
}
if ( $inGroup )
{
$price = $discount['price'][ $currency ];
}
}
}
}
/* Loyalty discounts */
if ( $loyaltyDiscounts and isset( $discounts['loyalty'] ) )
{
foreach ( $discounts['loyalty'] as $discount )
{
$comparingPackageId = ( $discount['package'] ?: $packageId );
$count = ( $comparingPackageId == $packageId ) ? $initialCount : 0;
if ( $discount['price'][ $currency ] < $price )
{
$count += $customer->member_id ? $customer->previousPurchasesCount( $discount['package'] ?: $packageId, $discount['active'] ) : 0;
}
if ( $customer->member_id === \IPS\Member::loggedIn()->member_id and isset( $_SESSION['cart'] ) )
{
foreach ( $_SESSION['cart'] as $item )
{
if ( $item->id == $comparingPackageId )
{
$count += $item->quantity;
}
}
}
if ( $count >= $discount['owns'] )
{
$price = $discount['price'][ $currency ];
}
}
}
/* Bulk Discounts */
if ( $bulkDiscounts and isset( $discounts['bulk'] ) )
{
/* Make sure we're only applying the highest quantity discount */
rsort( $discounts['bulk'] );
$discount = array_shift( $discounts['bulk'] );
$discountPackageId = ( $discount['package'] ?: $packageId );
$count = ( $discountPackageId == $packageId ) ? $initialCount : 0;
if ( $customer->member_id === \IPS\Member::loggedIn()->member_id and isset( $_SESSION['cart'] ) )
{
foreach ( $_SESSION['cart'] as $item )
{
if ( $item->id == $discountPackageId )
{
$count += $item->quantity;
}
}
}
if ( $count and ( ( $count + 1 ) % ( $discount['buying'] + 1 ) == 0 ) )
{
$price = $discount['price'][ $currency ];
}
}
/* Return */
return new \IPS\nexus\Money( $price, $currency );
}
/**
* Get the lowest price
*
* @param \IPS\nexus\Customer|NULL $customer The customer
* @param string $currency Currency
* @param bool $priceMayChange [Reference] will be set to a value indicating if the price might change (i.e. should be displayed as "From $x")
* @param float $priceNotIncludingDiscounts [Reference] will be set to the lowest price not taking discounts into consideration
* @return float
*/
protected function _lowestPrice( \IPS\nexus\Customer $customer, $currency, &$priceMayChange = FALSE, &$priceNotIncludingDiscounts = NULL )
{
$renewOptions = $this->renew_options ? json_decode( $this->renew_options, TRUE ) : array();
$tax = NULL;
if ( \IPS\Settings::i()->nexus_show_tax and $this->tax )
{
try
{
$tax = \IPS\nexus\Tax::load( $this->tax );
}
catch ( \OutOfRangeException $e ) { }
}
return static::lowestPriceFromData( $customer, $currency, $this->id, json_decode( $this->base_price, TRUE ), json_decode( $this->discounts, TRUE ), $renewOptions, $this->stock, $tax, $priceMayChange, $priceNotIncludingDiscounts );
}
/**
* Get the lowest price
*
* @param \IPS\nexus\Customer $customer The customer
* @param int $packageId Package ID
* @param array $prices Base Price data
* @param array $discounts Discount data
* @param array $renewOptions Renewal options data
* @param int $stock Stock type (-2 for if it adjusts on fields)
* @param \IPS\nexus\Tax|NULL $tax Tax rate
* @param bool $priceMayChange [Reference] will be set to a value indicating if the price might change (i.e. should be displayed as "From $x")
* @param float $priceNotIncludingDiscounts [Reference] will be set to the lowest price not taking discounts into consideration
* @return float
*/
static function lowestPriceFromData( \IPS\nexus\Customer $customer, $currency, $packageId, $prices, $discounts, $renewOptions, $stock, $tax, &$priceMayChange = FALSE, &$priceNotIncludingDiscounts = NULL )
{
/* What's the base price with and without discounts? */
$baseNoDiscounts = static::priceFromData( $packageId, $prices, $discounts, $customer, $currency, FALSE, FALSE, FALSE )->amount;
$base = static::priceFromData( $packageId, $prices, $discounts, $customer, $currency )->amount;
/* Adjustments based on custom fields */
if ( $stock == -2 )
{
$mostReducedOption = NULL;
foreach ( \IPS\Db::i()->select( '*', 'nexus_product_options', array( 'opt_package=?', $packageId ) ) as $option )
{
$basePriceAdjustments = json_decode( $option['opt_base_price'], TRUE );
if ( isset( $basePriceAdjustments[ $currency ] ) and ( $basePriceAdjustments[ $currency ] or $basePriceAdjustments[ $currency ] === '0' ) )
{
if ( $mostReducedOption === NULL or $basePriceAdjustments[ $currency ] < $mostReducedOption )
{
$mostReducedOption = new \IPS\Math\Number( number_format( $basePriceAdjustments[ $currency ], \IPS\nexus\Money::numberOfDecimalsForCurrency( $currency ), '.', '' ) );
}
$priceMayChange = TRUE;
}
}
$mostReducedOption = $mostReducedOption ?: new \IPS\Math\Number('0');
$baseNoDiscounts = $baseNoDiscounts->add( $mostReducedOption );
$base = $base->add( $mostReducedOption );
}
/* Init */
$lowestNoDiscounts = $baseNoDiscounts;
$lowest = $base;
/* It's possible the base price is $0, but all the renewal options add, so let's look at that */
if ( !empty( $renewOptions ) )
{
$lowestNoDiscounts = NULL;
$lowest = NULL;
foreach ( $renewOptions as $term )
{
if ( $term['add'] )
{
if ( count( $renewOptions ) > 1 )
{
$priceMayChange = TRUE;
}
$_lowest = $base->add( new \IPS\Math\Number( number_format( $term['cost'][ $currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $currency ), '.', '' ) ) );
$_lowestNoDiscounts = $baseNoDiscounts->add( new \IPS\Math\Number( number_format( $term['cost'][ $currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $currency ), '.', '' ) ) );
}
else
{
$_lowest = $base;
$_lowestNoDiscounts = $baseNoDiscounts;
}
if ( $lowest === NULL or $_lowest->compare( $lowest ) === -1 )
{
$lowest = $_lowest;
}
if ( $lowestNoDiscounts === NULL or $_lowestNoDiscounts->compare( $lowestNoDiscounts ) === -1 )
{
$lowestNoDiscounts = $_lowestNoDiscounts;
}
}
}
/* Add tax? */
if ( \IPS\Settings::i()->nexus_show_tax and $tax )
{
/* What is the rate? */
$taxRate = new \IPS\Math\Number( $tax->rate( $customer->estimatedLocation() ) );
/* Add it on */
$lowest = $lowest->add( $lowest->multiply( $taxRate ) );
$lowestNoDiscounts = $lowestNoDiscounts->add( $lowestNoDiscounts->multiply( $taxRate ) );
}
/* Return */
$priceNotIncludingDiscounts = $lowestNoDiscounts;
return $lowest;
}
/**
* Price to display in store
* May return price ($x) or "From $x" or a price struck and replaced with a discount price or NULL if not available in desired currency
*
* @param \IPS\nexus\Customer|NULL $customer The customer (NULL for currently logged in member)
* @param bool $includePriceDescription If TRUE, will include the "Price Description" setting (normally used to indicate if price includes tax)
* @return string|NULL
*/
public function priceToDisplay( \IPS\nexus\Customer $customer = NULL, $includePriceDescription=TRUE )
{
/* Get customer */
$customer = $customer ?: \IPS\nexus\Customer::loggedIn();
$currency = ( $customer->member_id === \IPS\nexus\Customer::loggedIn()->member_id ) ? ( ( isset( $_SESSION['currency'] ) and in_array( $_SESSION['currency'], \IPS\nexus\Money::currencies() ) ) ? $_SESSION['currency'] : $customer->defaultCurrency() ) : $customer->defaultCurrency();
/* Get the price */
$priceMayChange = FALSE;
$priceNotIncludingDiscounts = NULL;
try
{
$price = $this->_lowestPrice( $customer, $currency, $priceMayChange, $priceNotIncludingDiscounts );
}
catch ( \OutOfBoundsException $e )
{
return NULL;
}
/* Display */
if ( $price < $priceNotIncludingDiscounts )
{
return \IPS\Theme::i()->getTemplate( 'store', 'nexus' )->priceDiscounted( new \IPS\nexus\Money( $priceNotIncludingDiscounts, $currency ), new \IPS\nexus\Money( $price, $currency ), $priceMayChange, $includePriceDescription );
}
else
{
return \IPS\Theme::i()->getTemplate( 'store', 'nexus' )->price( new \IPS\nexus\Money( $price, $currency ), $priceMayChange, $includePriceDescription );
}
}
/**
* Support Severity
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return \IPS\nexus\Support\Severity|NULL
*/
public function supportSeverity( \IPS\nexus\Purchase $purchase )
{
if ( $this->support_severity )
{
try
{
return \IPS\nexus\Support\Severity::load( $this->support_severity );
}
catch ( \OutOfRangeException $e )
{
return NULL;
}
}
return NULL;
}
/**
* @brief Cached URLs
*/
protected $_url = array();
/**
* Get URL
*
* @param string|NULL $action Action
* @return \IPS\Http\Url
*/
public function url( $action=NULL )
{
$_key = md5( $action );
if( !isset( $this->_url[ $_key ] ) )
{
$this->_url[ $_key ] = \IPS\Http\Url::internal( "app=nexus&module=store&controller=product&id={$this->id}", 'front', 'store_product', \IPS\Http\Url\Friendly::seoTitle( \IPS\Member::loggedIn()->language()->get( 'nexus_package_' . $this->id ) ) );
if ( $action )
{
$this->_url[ $_key ] = $this->_url[ $_key ]->setQueryString( 'do', $action );
}
}
return $this->_url[ $_key ];
}
/**
* Get full image URL
*
* @return string
*/
public function get_image()
{
return ( $this->_data['image'] ) ? (string) \IPS\File::get( 'nexus_Products', $this->_data['image'] )->url : NULL;
}
/**
* Get additional name info
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return array
*/
public function getPurchaseNameInfo( \IPS\nexus\Purchase $purchase )
{
$stickyFields = array();
if ( count( $purchase->custom_fields ) )
{
$customFields = \IPS\nexus\Package\CustomField::roots();
foreach ( $purchase->custom_fields as $k => $v )
{
if ( $v and isset( $customFields[ $k ] ) and $customFields[ $k ]->sticky )
{
$stickyFields[] = strip_tags( \IPS\nexus\Package\CustomField::load( $k )->displayValue( $v, TRUE ) ); // We need to call displayValue so things like dates and select boxes show the right value, but we *also* need to call strip_tags as Url fields will return full links and we need text only. Note that fields which would show something other than a simple text output are prevented from being used in this context
}
}
}
return $stickyFields;
}
/**
* @brief Associable packages
*/
protected $_associablePackages = NULL;
/**
* Associable Packages
*
* @return array
*/
public function associablePackages()
{
if ( $this->_associablePackages === NULL )
{
$this->_associablePackages = array();
foreach ( explode( ',', $this->associable ) as $id )
{
try
{
$this->_associablePackages[ $id ] = \IPS\nexus\Package::load( $id );
}
catch ( \OutOfRangeException $e ) { }
}
}
return $this->_associablePackages;
}
/**
* [Node] Get buttons to display in tree
* Example code explains return value
*
* @code
array(
array(
'icon' => 'plus-circle', // Name of FontAwesome icon to use
'title' => 'foo', // Language key to use for button's title parameter
'link' => \IPS\Http\Url::internal( 'app=foo...' ) // URI to link to
'class' => 'modalLink' // CSS Class to use on link (Optional)
),
... // Additional buttons
);
* @endcode
* @param string $url Base URL
* @param bool $subnode Is this a subnode?
* @return array
*/
public function getButtons( $url, $subnode=FALSE )
{
$buttons = array();
if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'customers', 'purchases_view' ) )
{
$buttons['view_purchases'] = array(
'icon' => 'search',
'title' => 'view_purchases',
'link' => $url->setQueryString( array( 'do' => 'viewPurchases', 'id' => $this->_id ) )
);
}
$buttons = array_merge( $buttons, parent::getButtons( $url, $subnode ) );
if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'subscriptions', 'subscriptions_manage' ) and \IPS\nexus\Subscription\Package::canConvert( $this ) )
{
$buttons['convert_subs'] = array(
'icon' => 'certificate',
'title' => 'nexus_subs_convert',
'link' => \IPS\Http\Url::internal( 'app=nexus&module=subscriptions&controller=subscriptions&do=convertToSubscription&id=' . $this->_id )
);
}
return $buttons;
}
/* !ACP Package Form */
/**
* [Node] Add/Edit Form
*
* @param \IPS\Helpers\Form $form The form
* @return void
*/
public function form( &$form )
{
$groups = array();
foreach ( \IPS\Member\Group::groups() as $group )
{
$groups[ $group->g_id ] = $group->name;
}
$groupsExcludingGuestsAndAdmins = array();
foreach ( \IPS\Member\Group::groups( FALSE, FALSE ) as $group )
{
$groupsExcludingGuestsAndAdmins[ $group->g_id ] = $group->name;
}
$types = array();
$typeFields = array();
$typeFieldToggles = array();
$formId = $this->id ? "form_{$this->id}" : 'form_new';
foreach ( new \DirectoryIterator( \IPS\ROOT_PATH . '/applications/nexus/sources/Package' ) as $file )
{
if ( !$file->isDot() and mb_substr( $file, 0, 1 ) !== '.' and $file != 'Dedi.php' and $file != 'index.html' )
{
require_once $file;
$key = mb_substr( $file, 0, -4 );
$class = 'IPS\nexus\Package\_' . $key;
$autoloadClass = "IPS\\nexus\\Package\\{$key}";
if ( class_exists( $class, FALSE ) and in_array( 'IPS\nexus\Package', class_parents( $class ) ) )
{
$forceShow = TRUE;
$types[ mb_strtolower( $key ) ] = 'p_type_' . $key;
if ( $fields = $autoloadClass::acpFormFields( $this ) and is_array( $fields ) )
{
foreach ( $fields as $group => $fields )
{
foreach ( $fields as $field )
{
if ( $field->name === 'p_show' )
{
$forceShow = FALSE;
}
if ( !$field->htmlId )
{
$field->htmlId = $field->name;
$typeFieldToggles[ mb_strtolower( $key ) ][] = $field->htmlId;
}
$typeFields[ $group ][] = $field;
}
}
}
if ( $forceShow )
{
$typeFieldToggles[ mb_strtolower( $key ) ] = array_merge( isset( $typeFieldToggles[ mb_strtolower( $key ) ] ) ? $typeFieldToggles[ mb_strtolower( $key ) ] : array(), array( "{$formId}_tab_package_client_area", "{$formId}_header_package_associations", "{$formId}_header_package_associations_desc", 'p_associate', "{$formId}_header_package_renewals", 'p_renews', 'p_support_severity', 'p_lkey' ) );
}
}
}
}
$renewOptions = array();
if ( $this->renew_options and $_renewOptions = json_decode( $this->renew_options, TRUE ) and is_array( $_renewOptions ) )
{
foreach ( $_renewOptions as $option )
{
$costs = array();
foreach ( $option['cost'] as $cost )
{
$costs[ $cost['currency'] ] = new \IPS\nexus\Money( $cost['amount'], $cost['currency'] );
}
/* Catch any invalid renewal terms, these can occasionally appear from legacy IP.Subscriptions */
try
{
$renewOptions[] = new \IPS\nexus\Purchase\RenewalTerm( $costs, new \DateInterval( "P{$option['term']}" . mb_strtoupper( $option['unit'] ) ), NULL, $option['add'] );
}
catch( \Exception $ex) {}
}
}
$form->addTab('package_settings');
$form->addHeader('package_settings');
$form->add( new \IPS\Helpers\Form\Radio( 'p_type', ( $this->id ? $this->type : 'product' ), TRUE, array( 'options' => $types, 'toggles' => $typeFieldToggles, 'disabled' => (bool) $this->id ) ) );
$form->add( new \IPS\Helpers\Form\Translatable( 'p_name', NULL, TRUE, array( 'app' => 'nexus', 'key' => $this->id ? "nexus_package_{$this->id}" : NULL ) ) );
$form->add( new \IPS\Helpers\Form\Node( 'p_group', $this->group, TRUE, array( 'class' => 'IPS\nexus\Package\Group', 'subnodes' => FALSE ) ) );
$form->add( new \IPS\Helpers\Form\Node( 'p_custom_fields', $this->id ? \IPS\nexus\Package\CustomField::roots( NULL, NULL, array( array( \IPS\Db::i()->findInSet( 'cf_packages', array( $this->id ) ) ) ) ) : array(), FALSE, array( 'class' => 'IPS\nexus\Package\CustomField', 'multiple' => TRUE ) ) );
foreach ( $typeFields['package_settings'] as $field )
{
$form->add( $field );
}
$form->addHeader('package_registration');
$form->add( new \IPS\Helpers\Form\YesNo( 'p_reg', $this->reg, FALSE, array(), function( $val )
{
if ( $val and ( !\IPS\Request::i()->p_store_checkbox or ( \IPS\Request::i()->p_member_groups and ( !in_array( \IPS\Settings::i()->guest_group, \IPS\Request::i()->p_member_groups ) and \IPS\Request::i()->p_member_groups_unlimited != "*" ) ) ) )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'p_reg_err', FALSE, array( 'sprintf' => array( \IPS\Member\Group::load( \IPS\Settings::i()->guest_group )->name ) ) ) );
}
} ) );
$form->addHeader('package_associations');
$form->addMessage('package_associations_desc');
$form->add( new \IPS\Helpers\Form\Radio( 'p_associate', ( $this->force_assoc and count( $this->associablePackages() ) ) ? 2 : ( count( $this->associablePackages() ) ? 1 : 0 ), FALSE, array(
'options' => array(
0 => 'p_associate_none',
1 => 'p_associate_optional',
2 => 'p_associate_required'
),
'toggles' => array(
1 => array( 'p_associable', 'p_group_renewals', 'p_upsell' ),
2 => array( 'p_associable', 'p_group_renewals', 'p_assoc_error_editor', 'p_upsell' )
)
), NULL, NULL, NULL, 'p_associate' ) );
$form->add( new \IPS\Helpers\Form\Node( 'p_associable', $this->associablePackages(), FALSE, array( 'class' => 'IPS\nexus\Package\Group', 'multiple' => TRUE, 'permissionCheck' => function( $node )
{
return !( $node instanceof \IPS\nexus\Package\Group );
} ), function( $val )
{
if ( $val)
{
foreach ( $val AS $item )
{
if ( $item->id == $this->id)
{
throw new \DomainException( 'p_associable_error' );
}
}
}
}, NULL, NULL, 'p_associable' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'p_group_renewals', $this->group_renewals ?: FALSE, FALSE, array(), NULL, NULL, NULL, 'p_group_renewals' ) );
$form->add( new \IPS\Helpers\Form\Translatable( 'p_assoc_error', NULL, FALSE, array(
'app' => 'nexus',
'key' => $this->id ? "nexus_package_{$this->id}_assoc" : NULL,
'editor' => array(
'app' => 'nexus',
'key' => 'Admin',
'autoSaveKey' => ( $this->id ? "nexus-pkg-{$this->id}-assoc" : "nexus-new-pkg-assoc" ),
'attachIds' => $this->id ? array( $this->id, NULL, 'pkg-assoc' ) : NULL, 'minimize' => 'p_assoc_error_placeholder'
)
), NULL, NULL, NULL, 'p_assoc_error_editor' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'p_upsell', $this->upsell ?: FALSE, FALSE, array(), NULL, NULL, NULL, 'p_upsell' ) );
$form->addTab( 'package_pricing' );
$form->addHeader( 'package_purchase_price' );
$form->add( new \IPS\nexus\Form\Money( 'p_base_price', $this->base_price, TRUE ) );
$form->add( new \IPS\Helpers\Form\Node( 'p_tax', (int) $this->tax, FALSE, array( 'class' => 'IPS\nexus\Tax', 'zeroVal' => 'do_not_tax' ) ) );
$form->addHeader('package_discounts');
$discounts = json_decode( $this->discounts, TRUE );
$form->add( new \IPS\Helpers\Form\YesNo( 'p_set_discounts', ( is_array( $discounts ) AND count( $discounts ) ), FALSE, array( 'togglesOn' => array( 'p_usergroup_discounts', 'p_loyalty_discounts', 'p_bulk_discounts' ) ), NULL, NULL, NULL, 'p_set_discounts' ) );
$form->add( new \IPS\Helpers\Form\Stack( 'p_usergroup_discounts', isset( $discounts['usergroup'] ) ? $discounts['usergroup'] : array(), FALSE, array( 'stackFieldType' => 'IPS\nexus\Form\DiscountUsergroup' ), NULL, NULL, NULL, 'p_usergroup_discounts' ) );
$form->add( new \IPS\Helpers\Form\Stack( 'p_loyalty_discounts', isset( $discounts['loyalty'] ) ? $discounts['loyalty'] : array(), FALSE, array( 'stackFieldType' => 'IPS\nexus\Form\DiscountLoyalty' ), NULL, NULL, NULL, 'p_loyalty_discounts' ) );
$form->add( new \IPS\Helpers\Form\Stack( 'p_bulk_discounts', isset( $discounts['bulk'] ) ? $discounts['bulk'] : array(), FALSE, array( 'stackFieldType' => 'IPS\nexus\Form\DiscountBulk' ), NULL, NULL, NULL, 'p_bulk_discounts' ) );
$form->addHeader( 'package_renewals' );
$form->add( new \IPS\Helpers\Form\YesNo( 'p_renews', !empty( $renewOptions ), FALSE, array( 'togglesOn' => array( 'p_renew_options', 'p_grace_period', 'p_renew', ( $this->id ? "form_{$this->id}" : 'form_new' ) . '_header_package_client_area_renewals', 'p_email_expire_soon_type', 'p_email_expire_type' ) ), NULL, NULL, NULL, 'p_renews' ) );
$form->add( new \IPS\Helpers\Form\Stack( 'p_renew_options', $renewOptions, FALSE, array(
'stackFieldType' => 'IPS\nexus\Form\RenewalTerm',
'allCurrencies' => TRUE,
'addToBase' => TRUE
), NULL, NULL, NULL, 'p_renew_options' ) );
$form->add( new \IPS\Helpers\Form\Number( 'p_grace_period', $this->grace_period, FALSE, array( 'max' => \IPS\Settings::i()->cm_invoice_expireafter ?: NULL ), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack('days'), 'p_grace_period' ) );
$form->addTab('package_stock_adjustments');
$fields = new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_package_fields', array( array( '( ( cf_type=? OR cf_type=? ) AND cf_multiple=0 )', 'Select', 'Radio' ), \IPS\Db::i()->findInSet( 'cf_packages', array( $this->id ) ) ) ), 'IPS\nexus\Package\CustomField' );
if ( count( $fields ) )
{
\IPS\Member::loggedIn()->language()->words['p_stock_price_dynamic_desc'] = '';
}
$fieldOptions = array();
foreach ( $fields as $field )
{
$fieldOptions[ $field->id ] = $field->_title;
}
$form->add( new \IPS\Helpers\Form\Radio( 'p_stock_price_type', $this->stock == -2 ? 'dynamic' : 'static', TRUE, array(
'options' => array(
'static' => 'p_stock_price_static',
'dynamic' => 'p_stock_price_dynamic',
),
'toggles' => array(
'static' => array( 'p_stock' ),
'dynamic' => array( 'p_dynamic_stock' )
),
'disabled' => count( $fields ) ? array() : array( 'dynamic' )
) ) );
$form->add( new \IPS\Helpers\Form\Number( 'p_stock', $this->id ? ( $this->stock == -2 ? -1 : $this->stock ) : -1, TRUE, array( 'unlimited' => -1 ), NULL, NULL, NULL, 'p_stock' ) );
$package = $this;
$form->add( new \IPS\Helpers\Form\Custom( 'custom_fields', $this->optionIdKeys(), FALSE, array(
'rowHtml' => function( $input ) use ( $fields, $package )
{
return \IPS\Theme::i()->getTemplate('store')->productOptions( $input, $fields, $package );
}
), NULL, NULL, NULL, 'p_dynamic_stock' ) );
$form->addTab('package_store');
$form->addHeader('package_store_permissions');
$form->add( new \IPS\Helpers\Form\YesNo( 'p_store', $this->store, FALSE, array( 'togglesOn' => array( 'p_member_groups', 'p_featured', 'p_desc_editor', 'p_images', 'p_reviewable', "{$form->id}_header_package_store_display", "{$form->id}_header_package_reviews" ) ) ) );
$form->add( new \IPS\Helpers\Form\Select( 'p_member_groups', $this->member_groups === '*' ? '*' : explode( ',', $this->member_groups ), FALSE, array( 'options' => $groups, 'multiple' => TRUE, 'unlimited' => '*', 'unlimitedLang' => 'all' ), NULL, NULL, NULL, 'p_member_groups' ) );
$form->add( new \IPS\Helpers\Form\Node( 'p_methods', ( !$this->methods or $this->methods === '*' ) ? 0 : explode( ',', $this->methods ), TRUE, array( 'class' => 'IPS\nexus\Gateway', 'multiple' => TRUE, 'zeroVal' => 'any' ) ) );
foreach ( $typeFields['store_permissions'] as $field )
{
$form->add( $field );
}
$form->addHeader('package_store_display');
$form->add( new \IPS\Helpers\Form\YesNo( 'p_featured', $this->featured, FALSE, array(), NULL, NULL, NULL, 'p_featured' ) );
$form->add( new \IPS\Helpers\Form\Translatable( 'p_desc', NULL, FALSE, array(
'app' => 'nexus',
'key' => $this->id ? "nexus_package_{$this->id}_desc" : NULL,
'editor' => array(
'app' => 'nexus',
'key' => 'Admin',
'autoSaveKey' => ( $this->id ? "nexus-pkg-{$this->id}" : "nexus-new-pkg" ),
'attachIds' => $this->id ? array( $this->id, NULL, 'pkg' ) : NULL, 'minimize' => 'p_desc_placeholder'
)
), NULL, NULL, NULL, 'p_desc_editor' ) );
$form->add( new \IPS\Helpers\Form\Upload( 'p_images', iterator_to_array( new \IPS\File\Iterator( \IPS\Db::i()->select( 'image_location', 'nexus_package_images', array( 'image_product=?', $this->id ), 'image_primary desc' ), 'nexus_Products' ) ), FALSE, array( 'storageExtension' => 'nexus_Products', 'multiple' => TRUE, 'image' => TRUE, 'template' => "nexus.store.images" ), NULL, NULL, NULL, 'p_images' ) );
$form->addHeader('package_reviews');
$form->add( new \IPS\Helpers\Form\YesNo( 'p_reviewable', $this->reviewable, FALSE, array( 'togglesOn' => array( 'p_review_moderate' ) ), NULL, NULL, NULL, 'p_reviewable' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'p_review_moderate', $this->review_moderate ?: FALSE, FALSE, array(), NULL, NULL, NULL, 'p_review_moderate' ) );
$form->addHeader( 'package_notify' );
$form->add( new \IPS\Helpers\Form\Stack( 'p_notify', $this->notify ? explode( ',', $this->notify ) : array(), FALSE, array( 'stackFieldType' => 'Email' ) ) );
$form->addTab( 'package_benefits' );
$form->add( new \IPS\Helpers\Form\Select( 'p_primary_group', $this->primary_group ?: '*', FALSE, array( 'options' => $groupsExcludingGuestsAndAdmins, 'unlimited' => '*', 'unlimitedLang' => 'do_not_change', 'unlimitedToggles' => array( 'p_return_primary' ), 'unlimitedToggleOn' => FALSE ) ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'p_return_primary', $this->return_primary, FALSE, array(), NULL, NULL, NULL, 'p_return_primary' ) );
$form->add( new \IPS\Helpers\Form\Select( 'p_secondary_group', $this->secondary_group ? explode( ',', $this->secondary_group ) : '*', FALSE, array( 'options' => $groupsExcludingGuestsAndAdmins, 'multiple' => TRUE, 'unlimited' => '*', 'unlimitedLang' => 'do_not_change', 'unlimitedToggles' => array( 'p_return_secondary' ), 'unlimitedToggleOn' => FALSE ) ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'p_return_secondary', $this->return_secondary, FALSE, array(), NULL, NULL, NULL, 'p_return_secondary' ) );
$form->add( new \IPS\Helpers\Form\Node( 'p_support_severity', $this->support_severity ?: 0, FALSE, array( 'class' => 'IPS\nexus\Support\Severity', 'zeroVal' => 'none' ), NULL, NULL, NULL, 'p_support_severity' ) );
foreach ( $typeFields['package_benefits'] as $field )
{
$form->add( $field );
}
$form->addTab('package_client_area');
$form->addHeader('package_client_area_display');
$form->add( new \IPS\Helpers\Form\Translatable( 'p_page', NULL, FALSE, array(
'app' => 'nexus',
'key' => $this->id ? "nexus_package_{$this->id}_page" : NULL,
'editor' => array(
'app' => 'nexus',
'key' => 'Admin',
'autoSaveKey' => ( $this->id ? "nexus-pkg-{$this->id}-pg" : "nexus-new-pkg-pg" ),
'attachIds' => $this->id ? array( $this->id, NULL, 'pkg-pg' ) : NULL, 'minimize' => 'p_page_placeholder'
)
), NULL, NULL, NULL, 'p_page' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'p_support', $this->support, FALSE, array( 'togglesOn' => array( 'p_support_department' ) ) ) );
$form->add( new \IPS\Helpers\Form\Node( 'p_support_department', $this->support ? $this->support_department : 0, FALSE, array( 'class' => 'IPS\nexus\Support\Department', 'zeroVal' => 'none' ), NULL, NULL, NULL, 'p_support_department' ) );
$form->addHeader('package_client_area_renewals');
$form->add( new \IPS\Helpers\Form\YesNo( 'p_renew', $this->renewal_days != 0, FALSE, array( 'togglesOn' => array( 'p_renewal_days', 'p_renewal_days_advance' ) ), NULL, NULL, NULL, 'p_renew' ) );
$form->add( new \IPS\Helpers\Form\Number( 'p_renewal_days', $this->id ? $this->renewal_days : -1, FALSE, array( 'unlimited' => -1, 'unlimitedLang' => 'any_time' ), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack('days_before_expiry'), 'p_renewal_days' ) );
$form->add( new \IPS\Helpers\Form\Number( 'p_renewal_days_advance', $this->id ? $this->renewal_days_advance : -1, FALSE, array( 'unlimited' => -1 ), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack('days'), 'p_renewal_days_advance' ) );
$form->addHeader('package_upgrade_downgrade');
$form->addMessage('package_upgrade_downgrade_desc');
$form->add( new \IPS\Helpers\Form\YesNo( 'p_allow_upgrading', $this->allow_upgrading, FALSE, array( 'togglesOn' => array( 'p_upgrade_charge' ) ) ) );
$form->add( new \IPS\Helpers\Form\Radio( 'p_upgrade_charge', $this->upgrade_charge, FALSE, array( 'options' => array(
0 => 'p_upgrade_charge_none',
1 => 'p_upgrade_charge_full',
2 => 'p_upgrade_charge_prorate'
) ), NULL, NULL, NULL, 'p_upgrade_charge' ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'p_allow_downgrading', $this->allow_downgrading, FALSE, array( 'togglesOn' => array( 'p_downgrade_refund' ) ) ) );
$form->add( new \IPS\Helpers\Form\Radio( 'p_downgrade_refund', $this->downgrade_refund, FALSE, array( 'options' => array(
0 => 'p_downgrade_refund_none',
1 => 'p_downgrade_refund_full',
2 => 'p_downgrade_refund_prorate'
)), NULL, NULL, NULL, 'p_downgrade_refund' ) );
$form->addTab('package_custom_emails');
$form->addMessage('package_custom_emails_blurb');
foreach ( \IPS\Settings::i()->cm_invoice_warning ? array( 'purchase', 'expire_soon', 'expire' ) : array( 'purchase', 'expire' ) as $k )
{
$typeKey = "email_{$k}_type";
$valueKey = "email_{$k}";
$form->add( new \IPS\Helpers\Form\Radio( "p_email_{$k}_type", $this->$typeKey, FALSE, array(
'options' => array(
'' => 'p_email_none',
'wysiwyg' => 'p_email_wysiwyg',
'html_template' => 'p_email_html_template',
'html_raw' => 'p_email_html_raw',
),
'toggles' => array(
'wysiwyg' => array( "p_email_{$k}_subject", "p_email_{$k}_wysiwyg" ),
'html_template' => array( "p_email_{$k}_subject", "p_email_{$k}_html" ),
'html_raw' => array( "p_email_{$k}_subject", "p_email_{$k}_html" )
)
), NULL, NULL, NULL, "p_email_{$k}_type" ) );
$form->add( new \IPS\Helpers\Form\Translatable( "p_email_{$k}_subject", NULL, FALSE, array( 'app' => 'nexus', 'key' => $this->id ? "nexus_package_{$this->id}_email_{$k}_subject" : NULL ), NULL, NULL, NULL, "p_email_{$k}_subject" ) );
$form->add( new \IPS\Helpers\Form\Codemirror( "p_email_{$k}_html", $this->$valueKey, FALSE, array(
'preview' => \IPS\Http\Url::internal('app=nexus&module=store&controller=packages&do=emailPreview'),
'tags' => array(
'{$purchase->name}' => \IPS\Member::loggedIn()->language()->addToStack('ps_name'),
'{$purchase->expires}' => \IPS\Member::loggedIn()->language()->addToStack('p_email_tag_expire'),
'{$purchase->renewals}' => \IPS\Member::loggedIn()->language()->addToStack('ps_renewals'),
'{$purchase->custom_fields[1]}' => \IPS\Member::loggedIn()->language()->addToStack('p_email_tag_field'),
'{$purchase->url()}' => \IPS\Member::loggedIn()->language()->addToStack('p_email_tag_url'),
'{$purchase->licenseKey()->key}'=> \IPS\Member::loggedIn()->language()->addToStack('license_key'),
)
), NULL, NULL, NULL, "p_email_{$k}_html" ) );
$form->add( new \IPS\Helpers\Form\Editor( "p_email_{$k}_wysiwyg", $this->$valueKey, FALSE, array( 'app' => 'nexus', 'key' => 'Admin', 'autoSaveKey' => $this->id ? "nexus-pkg-{$this->id}-{$k}-email" : "nexus-new-pkg-{$k}-email" ), NULL, NULL, NULL, "p_email_{$k}_wysiwyg" ) );
}
}
/**
* [Node] Save Add/Edit Form
*
* @param array $values Values from the form
* @return void
*/
public function saveForm( $values )
{
if ( !$this->id )
{
$this->type = ( isset( $values['p_type'] ) ) ? $values['p_type'] : '';
$this->save();
unset( static::$multitons[ $this->id ] );
\IPS\File::claimAttachments( 'nexus-new-pkg', $this->id, NULL, 'pkg', TRUE );
\IPS\File::claimAttachments( 'nexus-new-pkg-assoc', $this->id, NULL, 'pkg-assoc', TRUE );
\IPS\File::claimAttachments( 'nexus-new-pkg-pg', $this->id, NULL, 'pkg-pg', TRUE );
$class = 'IPS\nexus\Package\\' . mb_ucfirst( $values['p_type'] );
if ( isset( $class::$packageDatabaseTable ) )
{
\IPS\Db::i()->insert( $class::$packageDatabaseTable, array( 'p_id' => $this->id ) );
}
$obj = $class::load( $this->id );
return $obj->saveForm( $obj->formatFormValues( $values ) );
}
$return = parent::saveForm( $values );
if ( !$this->custom )
{
\IPS\Content\Search\Index::i()->index( $this->item() );
}
return $return;
}
/**
* [Node] Format form values from add/edit form for save
*
* @param array $values Values from the form
* @return array
*/
public function formatFormValues( $values )
{
if ( !$this->id )
{
return $values;
}
/* Email content */
foreach ( array( 'purchase', 'expire_soon', 'expire' ) as $k )
{
if ( isset( $values["p_email_{$k}_type"] ) )
{
if ( $values["p_email_{$k}_type"] == 'wysiwyg' )
{
$values["p_email_{$k}"] = $values["p_email_{$k}_wysiwyg"];
}
elseif ( in_array( $values["p_email_{$k}_type"], array( 'html_template', 'html_raw' ) ) )
{
$values["p_email_{$k}"] = $values["p_email_{$k}_html"];
}
else
{
$values["p_email_{$k}"] = NULL;
}
}
unset( $values["p_email_{$k}_wysiwyg"] );
unset( $values["p_email_{$k}_html"] );
}
/* Translatables */
foreach ( array( 'name' => '', 'desc' => '_desc', 'assoc_error' => '_assoc', 'page' => '_page', 'email_purchase_subject' => '_email_purchase_subject', 'email_expire_soon_subject' => '_email_expire_soon_subject', 'email_expire_subject' => '_email_expire_subject' ) as $key => $suffix )
{
if ( isset( $values[ 'p_' . $key ] ) )
{
\IPS\Lang::saveCustom( 'nexus', "nexus_package_{$this->id}{$suffix}", $values[ 'p_' . $key ] );
}
unset( $values[ 'p_' . $key ] );
}
/* Custom Fields */
if( isset( $values['p_custom_fields'] ) )
{
$old = array_keys( \IPS\nexus\Package\CustomField::roots( NULL, NULL, array( array( \IPS\Db::i()->findInSet( 'cf_packages', array( $this->id ) ) ) ) ) );
$new = ( isset( $values['p_custom_fields'] ) and is_array( $values['p_custom_fields'] ) ) ? array_keys( $values['p_custom_fields'] ) : array();
$added = array_diff( $new, $old );
$removed = array_diff( $old, $new );
if ( count( $added ) )
{
foreach ( $added as $id )
{
$field = \IPS\nexus\Package\CustomField::load( $id );
$packages = explode( ',', $field->packages );
$packages[] = $this->id;
$field->packages = implode( ',', $packages );
$field->save();
}
}
if ( count( $removed ) )
{
foreach ( $removed as $id )
{
$field = \IPS\nexus\Package\CustomField::load( $id );
$packages = explode( ',', $field->packages );
unset( $packages[ array_search( $this->id, $packages ) ] );
$field->packages = implode( ',', $packages );
$field->save();
}
}
unset( $values['p_custom_fields'] );
}
/* Normalise */
if( isset( $values['p_group'] ) )
{
$values['p_group'] = $values['p_group']->id;
}
if( isset( $values['p_base_price'] ) )
{
$values['p_base_price'] = json_encode( $values['p_base_price'] );
}
if( isset( $values['p_member_groups'] ) )
{
$values['p_member_groups'] = $values['p_member_groups'] === '*' ? '*' : implode( ',', $values['p_member_groups'] );
}
if( isset( $values['p_primary_group'] ) )
{
$values['p_primary_group'] = $values['p_primary_group'] == '*' ? 0 : $values['p_primary_group'];
}
if( isset( $values['p_secondary_group'] ) )
{
$values['p_secondary_group'] = $values['p_secondary_group'] == '*' ? '' : implode( ',', $values['p_secondary_group'] );
}
if( isset( $values['p_support_department'] ) )
{
$values['p_support_department'] = ( $values['p_support'] and $values['p_support_department'] ) ? $values['p_support_department']->id : 0;
}
if( isset( $values['p_support_severity'] ) )
{
$values['p_support_severity'] = $values['p_support_severity'] ? $values['p_support_severity']->id : 0;
}
if( isset( $values['p_notify'] ) and is_array( $values['p_notify'] ) )
{
$values['p_notify'] = implode( ',', $values['p_notify'] );
}
if( isset( $values['p_methods'] ) )
{
$values['p_methods'] = ( isset( $values['p_methods'] ) and is_array( $values['p_methods'] ) ) ? implode( ',', array_keys( $values['p_methods'] ) ) : '*';
}
if( isset( $values['p_tax'] ) )
{
$values['p_tax'] = $values['p_tax'] ? $values['p_tax']->id : 0;
}
if( isset( $values['p_renew'] ) AND isset( $values['p_renewal_days'] ) )
{
$values['p_renewal_days'] = $values['p_renew'] ? $values['p_renewal_days'] : 0;
}
/* Renewal options */
if( isset( $values['p_renews'] ) )
{
if ( $values['p_renews'] )
{
$renewOptions = array();
foreach ( $values['p_renew_options'] as $option )
{
$term = $option->getTerm();
$renewOptions[] = array(
'cost' => $option->cost,
'term' => $term['term'],
'unit' => $term['unit'],
'add' => $option->addToBase
);
}
$values['p_renew_options'] = json_encode( $renewOptions );
}
else
{
$values['p_renew_options'] = '';
}
}
/* Associate */
if ( isset( $values['p_associate'] ) )
{
switch ( $values['p_associate'] )
{
case 0:
$values['p_associable'] = '';
$values['p_force_assoc'] = 0;
break;
case 1:
$values['p_associable'] = implode( ',', array_map( function( $node ) { return $node->id; }, $values['p_associable'] ) );
$values['p_force_assoc'] = 0;
break;
case 2:
$values['p_associable'] = implode( ',', array_map( function( $node ) { return $node->id; }, $values['p_associable'] ) );
$values['p_force_assoc'] = 1;
break;
}
}
/* Product Options */
\IPS\Db::i()->delete( 'nexus_product_options', array( 'opt_package=?', $this->id ) );
if ( isset( $values['p_stock_price_type'], $values['custom_fields'] ) )
{
if ( $values['p_stock_price_type'] === 'dynamic' )
{
$values['p_stock'] = -2;
foreach ( $values['custom_fields'] as $k => $data )
{
\IPS\Db::i()->insert( 'nexus_product_options', array(
'opt_package' => $this->id,
'opt_values' => rawurldecode( $k ),
'opt_stock' => isset( $data['unlimitedStock'] ) ? -1 : intval( $data['stock'] ),
'opt_base_price' => json_encode( $data['bpa'] ),
'opt_renew_price' => isset( $data['rpa'] ) ? json_encode( $data['rpa'] ) : ''
) );
}
}
unset( $values['p_stock_price_type'] );
unset( $values['custom_fields'] );
}
/* Discounts */
if( isset( $values['p_usergroup_discounts'] ) OR isset( $values['p_loyalty_discounts'] ) OR isset( $values['p_bulk_discounts'] ) )
{
$discounts = array();
}
if( isset( $values['p_usergroup_discounts'] ) )
{
foreach ( $values['p_usergroup_discounts'] as $k => $data )
{
if ( isset( $data['group'] ) and $data['group'] and isset( $data['price'] ) )
{
foreach( $data['price'] as $price )
{
if ( !$price )
{
continue 2;
}
}
$discounts['usergroup'][] = array(
'group' => $data['group'],
'price' => $data['price'],
'secondary' => (int) isset( $data['secondary'] ),
);
}
}
unset( $values['p_usergroup_discounts'] );
}
if( isset( $values['p_loyalty_discounts'] ) )
{
foreach ( $values['p_loyalty_discounts'] as $k => $data )
{
if ( $data['owns'] )
{
foreach( $data['price'] as $price )
{
if ( !$price )
{
continue 2;
}
}
$packageId = $data['package'] ? mb_substr( $data['package'], 0, mb_strpos( $data['package'], '.' ) ) : 0;
$discounts['loyalty'][] = array(
'owns' => $data['owns'],
'package' => ( $data['this'] or ( $this->id and $this->id == $packageId ) ) ? '0' : $packageId,
'price' => $data['price'],
'active' => (int) isset( $data['active'] ),
);
}
}
unset( $values['p_loyalty_discounts'] );
}
if( isset( $values['p_bulk_discounts'] ) )
{
foreach ( $values['p_bulk_discounts'] as $k => $data )
{
if ( $data['buying'] )
{
foreach( $data['price'] as $price )
{
if ( !$price )
{
continue 2;
}
}
$packageId = $data['package'] ? mb_substr( $data['package'], 0, mb_strpos( $data['package'], '.' ) ) : 0;
$discounts['bulk'][] = array(
'buying' => $data['buying'],
'package' => ( $data['this'] or ( $this->id and $this->id == $packageId ) ) ? '0' : $packageId,
'price' => $data['price'],
);
}
}
unset( $values['p_bulk_discounts'] );
}
if( isset( $discounts ) )
{
$values['discounts'] = json_encode( $discounts );
}
/* Images */
if( isset( $values['p_images'] ) )
{
\IPS\Db::i()->delete( 'nexus_package_images', array( 'image_product=?', $this->id ) );
if( !empty( $values['p_images'] ) )
{
$primary = TRUE;
foreach ( $values['p_images'] as $_key => $image )
{
if ( \IPS\Request::i()->p_images_primary_image )
{
$primary = ( \IPS\Request::i()->p_images_primary_image == $_key ) ? 1 : 0;
}
\IPS\Db::i()->insert( 'nexus_package_images', array(
'image_product' => $this->id,
'image_location' => (string) $image,
'image_primary' => $primary,
) );
if ( $primary )
{
$this->image = $image;
}
$primary = FALSE;
}
}
else
{
$this->image = NULL;
}
}
/* Save */
return $values;
}
/**
* ACP Fields
*
* @param \IPS\nexus\Package $package The package
* @param bool $custom If TRUE, is for a custom package
* @param bool $customEdit If TRUE, is editing a custom package
* @return array
*/
public static function acpFormFields( \IPS\nexus\Package $package, $custom=FALSE, $customEdit=FALSE )
{
return array();
}
/**
* Updateable fields
*
* @return array
*/
public static function updateableFields()
{
return array(
'renew_options',
'grace_period',
'primary_group',
'secondary_group',
'group_renewals'
);
}
/**
* Update existing purchases
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param array $changes The old values
* @param bool $cancelBillingAgreementIfNecessary If making changes to renewal terms, TRUE will cancel associated billing agreements. FALSE will skip that change
* @return void
*/
public function updatePurchase( \IPS\nexus\Purchase $purchase, $changes, $cancelBillingAgreementIfNecessary=FALSE )
{
if ( array_key_exists( 'renew_options', $changes ) and !empty( $changes['renew_options'] ) )
{
$newRenewTerms = json_decode( $this->renew_options, TRUE );
if( !is_array( $newRenewTerms ) )
{
$newRenewTerms = array();
}
$key = 'x';
if ( $purchase->renewals )
{
foreach ( $changes['renew_options'] as $k => $data )
{
if ( $data['old'] != 'x' )
{
if ( $purchase->renewals->cost->amount->compare( new \IPS\Math\Number( number_format( $data['old']['cost'][ $purchase->renewals->cost->currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $purchase->renewals->cost->currency ), '.', '' ) ) ) === 0 )
{
$term = $purchase->renewals->getTerm();
if ( $data['old']['term'] == $term['term'] and $data['old']['unit'] == $term['unit'] )
{
$key = $k;
break;
}
}
}
}
}
switch ( $changes['renew_options'][ $key ]['new'] )
{
case 'x':
$purchase->renewals = NULL;
$purchase->save();
if ( $billingAgreement = $purchase->billing_agreement )
{
try
{
$billingAgreement->cancel();
$billingAgreement->save();
}
catch ( \Exception $e ) { }
}
break;
case 'y':
$purchase->renewals = NULL;
$purchase->active = TRUE;
$purchase->save();
if ( $billingAgreement = $purchase->billing_agreement )
{
try
{
$billingAgreement->cancel();
$billingAgreement->save();
}
catch ( \Exception $e ) { }
}
break;
case 'z':
$purchase->renewals = NULL;
$purchase->active = FALSE;
$purchase->save();
if ( $billingAgreement = $purchase->billing_agreement )
{
try
{
$billingAgreement->cancel();
$billingAgreement->save();
}
catch ( \Exception $e ) { }
}
break;
case '-':
// do nothing
break;
default:
if ( mb_substr( $changes['renew_options'][ $key ]['new'], 0, 1 ) === 'o' )
{
if ( !$purchase->billing_agreement or $cancelBillingAgreementIfNecessary )
{
if ( $billingAgreement = $purchase->billing_agreement )
{
try
{
$billingAgreement->cancel();
$billingAgreement->save();
}
catch ( \Exception $e ) { }
}
$key = mb_substr( $changes['renew_options'][ $key ]['new'], 1 );
if ( isset( $newRenewTerms[ $key ] ) )
{
$tax = NULL;
if ( $purchase->tax )
{
try
{
$tax = \IPS\nexus\Tax::load( $purchase->tax );
}
catch ( \OutOfRangeException $e ) { }
}
$currency = $purchase->renewal_currency ?: $purchase->member->defaultCurrency( );
$purchase->renewals = new \IPS\nexus\Purchase\RenewalTerm(
new \IPS\nexus\Money( $newRenewTerms[ $key ]['cost'][ $currency ]['amount'], $currency ),
new \DateInterval( 'P' . $newRenewTerms[ $key ]['term'] . mb_strtoupper( $newRenewTerms[ $key ]['unit'] ) ),
$tax
);
$purchase->save();
}
}
}
break;
}
}
if ( array_key_exists( 'primary_group', $changes ) or array_key_exists( 'secondary_group', $changes ) AND ( !$purchase->expire OR $purchase->expire->getTimestamp() > time() ) )
{
$this->_removeUsergroups( $purchase, $changes );
$this->_addUsergroups( $purchase );
}
if ( array_key_exists( 'group_renewals', $changes ) )
{
if ( $this->group_renewals )
{
try
{
$purchase->groupWithParent();
}
catch ( \LogicException $e ) { }
}
else
{
try
{
$purchase->ungroupFromParent();
}
catch ( \LogicException $e ) { }
}
}
if ( array_key_exists( 'grace_period', $changes ) )
{
$purchase->grace_period = $this->grace_period * 86400;
$purchase->save();
}
}
/* !Store */
/**
* Store Form
*
* @param \IPS\Helpers\Form $form The form
* @param string $memberCurrency The currency being used
* @return void
*/
public function storeForm( \IPS\Helpers\Form $form, $memberCurrency )
{
if( $this->subscription )
{
$form->hiddenValues['quantity'] = 1;
}
else
{
$form->add( new \IPS\Helpers\Form\Number('quantity', 1, TRUE, array( 'min' => 1 ) ) );
}
}
/**
* Additional Secondary Page to show on checkout
*
* @param array $values Values from store form
* @return string|NULL
*/
public function storeAdditionalPage( array $values )
{
return NULL;
}
/**
* Add To Cart
*
* @param \IPS\nexus\extensions\nexus\Item\Package $item The item
* @param array $values Values from form
* @param string $memberCurrency The currency being used
* @return array Additional items to add
*/
public function addToCart( \IPS\nexus\extensions\nexus\Item\Package $item, array $values, $memberCurrency )
{
}
/**
* Get item
*
* @return \IPS\nexus\Package\Item
*/
public function item()
{
$data = array();
foreach ( $this->_data as $k => $v )
{
$data[ 'p_' . $k ] = $v;
}
return \IPS\nexus\Package\Item::constructFromData( $data );
}
/**
* Get option values for stock/price adjustments
*
* @param array $details Custom field values
* @return array
*/
public function optionValues( $details )
{
$optionValues = array();
if ( $this->stock == -2 )
{
foreach( $this->optionIdKeys() as $k )
{
$optionValues[ $k ] = ( isset( $details[ $k ] ) ) ? $details[ $k ] : NULL;
}
}
return $optionValues;
}
/**
* Get stock and price for option values
*
* @param array $optionValues Option values
* @param bool $includeDiscounts Get discounted price?
* @return array( 'price' => \IPS\nexus\Money, 'stock' => 5, 'renewalAdjustment' => 0 ) 'stock' will be -1 for unlimited, 0 for none in stock
*/
public function optionValuesStockAndPrice( $optionValues, $includeDiscounts=TRUE )
{
/* Dynamic */
if ( $this->stock == -2 )
{
$chosenOption = \IPS\Db::i()->select( '*', 'nexus_product_options', array( 'opt_package=? AND opt_values=?', $this->id, json_encode( $optionValues ) ) )->first();
/* Price */
$basePriceAdjustments = json_decode( $chosenOption['opt_base_price'], TRUE );
$defaultPrice = $this->price( NULL, $includeDiscounts, $includeDiscounts, $includeDiscounts );
if ( isset( $basePriceAdjustments[ $defaultPrice->currency ] ) )
{
$price = new \IPS\nexus\Money( $defaultPrice->amount->add( new \IPS\Math\Number( number_format( $basePriceAdjustments[ $defaultPrice->currency ], \IPS\nexus\Money::numberOfDecimalsForCurrency( $defaultPrice->currency ), '.', '' ) ) ), $defaultPrice->currency );
}
else
{
$price = $defaultPrice;
}
$renewalAdjustment = 0;
if ( $chosenOption['opt_renew_price'] )
{
$renewPriceAdjustments = json_decode( $chosenOption['opt_renew_price'], TRUE );
if ( isset( $renewPriceAdjustments[ $price->currency ] ) )
{
$renewalAdjustment = $renewPriceAdjustments[ $price->currency ];
}
}
/* Return */
return array( 'price' => $price, 'stock' => $chosenOption['opt_stock'], 'renewalAdjustment' => $renewalAdjustment );
}
/* Static */
else
{
return array( 'price' => $this->price(), 'stock' => $this->stock, 'renewalAdjustment' => 0 );
}
}
/**
* Create Item Object
*
* @param \IPS\nexus\Money $price Price
* @return \IPS\nexus\extensions\nexus\Item\Package
*/
public function createItemForCart( \IPS\nexus\Money $price )
{
$item = new \IPS\nexus\extensions\nexus\Item\Package( \IPS\Member::loggedIn()->language()->get( 'nexus_package_' . $this->id ), $price );
$item->id = $this->id;
$item->tax = $this->tax ? \IPS\nexus\Tax::load( $this->tax ) : NULL;
if ( $this instanceof \IPS\nexus\Package\Product and $this->physical )
{
$item->physical = TRUE;
$item->weight = new \IPS\nexus\Shipping\Weight( $this->weight );
$item->length = new \IPS\nexus\Shipping\Length( $this->length );
$item->width = new \IPS\nexus\Shipping\Length( $this->width );
$item->height = new \IPS\nexus\Shipping\Length( $this->height );
if ( $this->shipping !== '*' )
{
$item->shippingMethodIds = explode( ',', $this->shipping );
}
}
if ( $this->methods and $this->methods != '*' )
{
$item->paymentMethodIds = explode( ',', $this->methods );
}
if ( $this->group_renewals )
{
$item->groupWithParent = TRUE;
}
return $item;
}
/**
* Add items into cart
*
* @param array $details Custom field values
* @param int $quantity Quantity to add
* @param \IPS\nexus\Purchase\RenewalTerm|NULL $renewalTerm Chosen renewal term, if applicable
* @param \IPS\nexus\Purchase|int $parent Parent purchase, if applicable
* @param array|NULL $values If adding from the main purchase field, any additional values which may be used for extra items
* @return int
* @note Does not verify stock. Stock check first
*/
public function addItemsToCartData( $details=array(), $quantity=1, \IPS\nexus\Purchase\RenewalTerm $renewalTerm = NULL, $parent=NULL, $values=NULL )
{
$optionValues = $this->optionValues( $details );
$return = array();
/* Work out how we're splitting these */
$items = array();
$previousPrice = NULL;
$inThisBatch = 0;
for ( $i=0; $i<$quantity;$i++ )
{
$price = $this->price( NULL, TRUE, TRUE, TRUE, $i );
$memberCurrency = $price->currency;
$price = $price->amount;
if ( $previousPrice !== NULL and $previousPrice != $price )
{
$items[] = $inThisBatch;
$inThisBatch = 0;
}
$inThisBatch++;
$previousPrice = $price;
}
if ( $inThisBatch )
{
$items[] = $inThisBatch;
}
/* And do it */
foreach ( $items as $quantity )
{
/* Get base price */
$price = $this->price()->amount;
/* Renewal term */
if ( $renewalTerm and $renewalTerm->addToBase )
{
$price = $price->add( $renewalTerm->cost->amount );
}
/* Adjustments based on custom fields */
if ( $this->stock == -2 )
{
try
{
$chosenOption = \IPS\Db::i()->select( '*', 'nexus_product_options', array( 'opt_package=? AND opt_values=?', $this->id, json_encode( $optionValues ) ) )->first();
$basePriceAdjustments = json_decode( $chosenOption['opt_base_price'], TRUE );
if ( isset( $basePriceAdjustments[ $memberCurrency ] ) )
{
$price = $price->add( new \IPS\Math\Number( number_format( $basePriceAdjustments[ $memberCurrency ], \IPS\nexus\Money::numberOfDecimalsForCurrency( $memberCurrency ), '.', '' ) ) );
}
}
catch ( \UnderflowException $e ) {}
}
/* Create the item */
$item = $this->createItemForCart( new \IPS\nexus\Money( $price, $memberCurrency ) );
$item->renewalTerm = $renewalTerm;
$item->quantity = $quantity;
$item->details = $details;
/* Associations */
if ( $parent !== NULL )
{
$item->parent = $parent;
}
/* Do any package-sepcific modifications */
$extraItems = ( $values === NULL ) ? array() : $this->addToCart( $item, $values, $memberCurrency );
/* Hang on, is that the same as anything else in the cart? */
$added = FALSE;
foreach ( $_SESSION['cart'] as $_id => $_item )
{
$cloned = clone $_item;
$cloned->quantity = $quantity;
if ( $cloned == $item )
{
$_item->quantity += $quantity;
$added = TRUE;
$cartId = $_id;
break;
}
}
/* Nope, go ahead and add it */
if ( !$added )
{
$_SESSION['cart'][] = $item;
$keys = array_keys( $_SESSION['cart'] );
$cartId = array_pop( $keys );
}
/* Associate extras */
if ( count( $extraItems ) )
{
foreach ( $extraItems as $item )
{
$item->parent = $cartId;
$_SESSION['cart'][] = $item;
}
}
}
/* Turn off guest page caching */
if ( \IPS\CACHE_PAGE_TIMEOUT and !\IPS\Member::loggedIn()->member_id )
{
\IPS\Request::i()->setCookie( 'noCache', 1 );
}
/* Return */
return $cartId;
}
/* !Client Area */
/**
* Show Purchase Record?
*
* @return bool
*/
public function showPurchaseRecord()
{
return TRUE;
}
/**
* Get Client Area Page HTML
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return array( 'packageInfo' => '...', 'purchaseInfo' => '...' )
*/
public function clientAreaPage( \IPS\nexus\Purchase $purchase )
{
/* Custom Fields */
$editable = FALSE;
$customFieldsForm = new \IPS\Helpers\Form;
$customFieldsForm->class = 'ipsForm_vertical';
$customFields = \IPS\nexus\Package\CustomField::roots( NULL, NULL, array( array( \IPS\Db::i()->findInSet( 'cf_packages', array( $this->id ) ) ) ) );
foreach ( $customFields as $field )
{
$value = isset( $purchase->custom_fields[ $field->id ] ) ? $purchase->custom_fields[ $field->id ] : NULL;
if ( $field->editable )
{
$editable = TRUE;
$customFieldsForm->add( $field->buildHelper( $value ) );
}
else
{
$customFieldsForm->addDummy( $field->_title, $field->displayValue( $value ) );
}
}
if ( !$editable )
{
$customFieldsForm->actionButtons = array();
}
if( $values = $customFieldsForm->values() )
{
$customFieldsSave = $purchase->custom_fields;
foreach ( $customFields as $field )
{
if( array_key_exists( $field->type, \IPS\nexus\Package\CustomField::$additionalFieldTypes ) and !class_exists( 'IPS\Helpers\Form\\' . $field->type ) )
{
$class = 'IPS\nexus\Form\\' . $field->type;
}
else
{
$class = 'IPS\Helpers\Form\\' . $field->type;
}
if ( isset( $values[ 'nexus_pfield_' . $field->id ] ) )
{
if ( $class instanceof \IPS\Helpers\Form\Upload )
{
$customFieldsSave[ $field->id ] = (string) $values[ 'nexus_pfield_' . $field->id ];
}
else
{
$customFieldsSave[ $field->id ] = $class::stringValue( $values[ 'nexus_pfield_' . $field->id ] );
}
}
}
$purchase->custom_fields = $customFieldsSave;
$purchase->save();
}
/* Support Link */
$supportUrl = NULL;
if ( $purchase->active and $this->support )
{
$supportUrl = \IPS\Http\Url::internal( 'app=nexus&module=support&controller=home&do=create&purchase=' . $purchase->id, 'front', 'support_create' );
if ( $this->support_department )
{
$supportUrl = $supportUrl->setQueryString( 'department', $this->support_department );
}
}
/* Stuff only the owner or "billing" alternate contact can do */
$reactivateUrl = NULL;
$upgradeDowngradeUrl = NULL;
$upgradeDowngradeLang = 'change_package';
if ( $purchase->member->member_id === \IPS\Member::loggedIn()->member_id or array_key_exists( \IPS\Member::loggedIn()->member_id, iterator_to_array( $purchase->member->alternativeContacts( array( \IPS\Db::i()->findInSet( 'purchases', array( $purchase->id ) ) . ' AND billing=1' ) ) ) ) )
{
/* Reactivate */
if ( !$purchase->renewals and count( json_decode( $this->renew_options, TRUE ) ) and $purchase->can_reactivate and ( !$purchase->billing_agreement or $purchase->billing_agreement->canceled ) )
{
$reactivateUrl = \IPS\Http\Url::internal( "app=nexus&module=clients&controller=purchases&id={$purchase->id}&do=extra&act=reactivate", 'front', 'clientspurchaseextra', \IPS\Http\Url::seoTitle( $purchase->name ) )->csrf();
}
/* Upgrade/Downgrade */
$includesUpgrades = FALSE;
$includesDowngrades = FALSE;
if( count( $this->upgradeDowngradeOptions( $purchase, FALSE, $includesUpgrades, $includesDowngrades ) ) )
{
$upgradeDowngradeUrl = \IPS\Http\Url::internal( "app=nexus&module=clients&controller=purchases&id={$purchase->id}&do=extra&act=change", 'front', 'clientspurchaseextra', \IPS\Http\Url::seoTitle( $purchase->name ) );
if ( $includesUpgrades and !$includesDowngrades )
{
$upgradeDowngradeLang = 'change_package_upgrade';
}
elseif ( !$includesUpgrades and $includesDowngrades )
{
$upgradeDowngradeLang = 'change_package_downgrade';
}
}
}
/* Associated Downloads Files */
$associatedFiles = array();
if ( \IPS\Application::appIsEnabled( 'downloads' ) and \IPS\Settings::i()->idm_nexus_on )
{
$associatedFiles = \IPS\downloads\File::getItemsWithPermission( array( array( \IPS\Db::i()->findInSet( 'file_nexus', array( $this->id ) ) ) ) );
}
/* Associated support requests */
$last5AssociatedSupportRequests = \IPS\nexus\Support\Request::getItemsWithPermission( array( array( 'r_purchase=?', $purchase->id ) ), 'r_last_reply DESC', 5 );
/* Display */
return array(
'packageInfo' => \IPS\Member::loggedIn()->language()->checkKeyExists("nexus_package_{$this->id}_page") ? \IPS\Member::loggedIn()->language()->addToStack("nexus_package_{$this->id}_page") : '',
'purchaseInfo' => \IPS\Theme::i()->getTemplate( 'purchases' )->package( $this, $customFieldsForm, $supportUrl, $reactivateUrl, $upgradeDowngradeUrl, $upgradeDowngradeLang, $associatedFiles, $last5AssociatedSupportRequests ),
);
}
/**
* Client Area Action
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return string
*/
public function clientAreaAction( \IPS\nexus\Purchase $purchase )
{
switch ( \IPS\Request::i()->act )
{
case 'change':
$form = $this->upgradeDowngradeForm( $purchase );
if ( $values = $form->values() )
{
$newPackage = \IPS\nexus\Package::load( $values['change_package_to'] );
$chosenRenewalOption = isset( $values["renew_option_{$newPackage->id}" ] ) ? $values[ "renew_option_{$newPackage->id}" ] : NULL;
/* This package may only have one renewal option, in which case it is not displayed in the form - we should auto select it here, if renewal options are available */
if ( $chosenRenewalOption === NULL )
{
$renewOptions = json_decode( $newPackage->renew_options, TRUE );
if ( count( $renewOptions ) === 1 )
{
$chosenRenewalOption = key( $renewOptions );
}
}
$invoice = $this->upgradeDowngrade( $purchase, $newPackage, $chosenRenewalOption );
if ( $invoice )
{
\IPS\Output::i()->redirect( $invoice->checkoutUrl() );
}
$purchase->member->log( 'purchase', array( 'type' => 'change', 'id' => $purchase->id, 'old' => $purchase->name, 'name' => $newPackage->titleForLog(), 'system' => FALSE ) );
\IPS\Output::i()->redirect( $purchase->url() );
}
return (string) $form;
break;
case 'reactivate':
$currency = $purchase->original_invoice->currency;
$renewalOptions = json_decode( $this->renew_options, TRUE );
$noOptions = count( $renewalOptions ) === 1;
if ( $noOptions )
{
\IPS\Session::i()->csrfCheck();
$term = array_pop( $renewalOptions );
$purchase->renewals = new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $term['cost'][$currency]['amount'], $currency ), new \DateInterval( 'P' . $term['term'] . mb_strtoupper( $term['unit'] ) ) );
$purchase->cancelled = FALSE;
$purchase->save();
foreach ( $purchase->children() as $child )
{
if ( $child->grouped_renewals )
{
$child->groupWithParent();
}
}
$purchase->member->log( 'purchase', array( 'type' => 'info', 'id' => $purchase->id, 'name' => $purchase->name, 'info' => 'change_renewals', 'to' => array( 'cost' => $purchase->renewals->cost->amount, 'currency' => $purchase->renewals->cost->currency, 'term' => $purchase->renewals->getTerm() ) ) );
if ( !$purchase->active and $purchase->canRenewUntil() !== FALSE )
{
\IPS\Output::i()->redirect( $purchase->url()->setQueryString( 'do', 'renew' )->csrf() );
}
else
{
\IPS\Output::i()->redirect( $purchase->url() );
}
}
$form = new \IPS\Helpers\Form( 'reactivate', 'reactivate' );
$form->class = 'ipsForm_vertical';
$options = array();
foreach ( $renewalOptions as $k => $term )
{
$options[ $k ] = new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $term['cost'][$currency]['amount'], $currency ), new \DateInterval( 'P' . $term['term'] . mb_strtoupper( $term['unit'] ) ) );
}
$form->add( new \IPS\Helpers\Form\Radio( 'ps_renewals', NULL, TRUE, array( 'options' => $options, 'parse' => 'normal' ) ) );
if ( $values = $form->values() )
{
$term = $renewalOptions[ $values['ps_renewals'] ];
$purchase->renewals = new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $term['cost'][$currency]['amount'], $currency ), new \DateInterval( 'P' . $term['term'] . mb_strtoupper( $term['unit'] ) ) );
$purchase->cancelled = FALSE;
$purchase->save();
foreach ( $purchase->children() as $child )
{
if ( $child->grouped_renewals )
{
$child->groupWithParent();
}
}
$purchase->member->log( 'purchase', array( 'type' => 'info', 'id' => $purchase->id, 'name' => $purchase->name, 'info' => 'change_renewals', 'to' => array( 'cost' => $purchase->renewals->cost->amount, 'currency' => $purchase->renewals->cost->currency, 'term' => $purchase->renewals->getTerm() ) ) );
if ( !$purchase->active and $purchase->canRenewUntil() !== FALSE )
{
\IPS\Output::i()->redirect( $purchase->url()->setQueryString( 'do', 'renew' )->csrf() );
}
else
{
\IPS\Output::i()->redirect( $purchase->url() );
}
}
return (string) $form;
break;
}
}
/* !ACP */
/**
* ACP Add to invoice
*
* @param \IPS\nexus\extensions\nexus\Item\Package $item The item
* @param array $values Values from form
* @param string $k The key to add to the field names
* @param \IPS\nexus\Invoice $invoice The invoice
* @return void
*/
public function acpAddToInvoice( \IPS\nexus\extensions\nexus\Item\Package $item, array $values, $k, \IPS\nexus\Invoice $invoice )
{
}
/**
* Get ACP Page Buttons
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param \IPS\Http\Url $url The page URL
* @return array
*/
public function acpButtons( \IPS\nexus\Purchase $purchase, $url )
{
$return = array();
if ( count( $this->upgradeDowngradeOptions( $purchase, TRUE ) ) )
{
$return['change'] = array(
'icon' => 'archive',
'title' => 'change_package',
'link' => $url->setQueryString( array( 'do' => 'extra', 'act' => 'change' ) ),
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('change_package') )
);
}
return $return;
}
/**
* Get ACP Page HTML
*
* @return string
*/
public function acpPage( \IPS\nexus\Purchase $purchase )
{
return '';
}
/**
* ACP Action
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return string|void
*/
public function acpAction( \IPS\nexus\Purchase $purchase )
{
switch ( \IPS\Request::i()->act )
{
case 'change':
$form = $this->upgradeDowngradeForm( $purchase, TRUE );
$form->class = 'ipsForm_horizontal';
$form->add( new \IPS\Helpers\Form\YesNo( 'change_package_ship_charges', FALSE, TRUE ) );
if ( $values = $form->values() )
{
$newPackage = \IPS\nexus\Package::load( $values['change_package_to'] );
$invoice = $this->upgradeDowngrade( $purchase, $newPackage, isset( $values[ "renew_option_{$newPackage->id}" ] ) ? $values[ "renew_option_{$newPackage->id}" ] : NULL, $values['change_package_ship_charges'] );
if ( $invoice )
{
\IPS\Output::i()->redirect( $invoice->acpUrl() );
}
$purchase->member->log( 'purchase', array( 'type' => 'change', 'id' => $purchase->id, 'old' => $purchase->name, 'name' => $newPackage->_title, 'system' => FALSE ) );
\IPS\Output::i()->redirect( $purchase->acpUrl() );
}
return (string) $form;
break;
}
}
/**
* ACP Edit Form
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param \IPS\Helpers\Form $form The form
* @param \IPS\nexus\Purchase\RenewalTerm $renewals The renewal term
* @return string
*/
public function acpEdit( \IPS\nexus\Purchase $purchase, \IPS\Helpers\Form $form, $renewals )
{
$form->addHeader('menu__nexus_store_fields');
foreach ( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_package_fields', \IPS\Db::i()->findInSet( 'cf_packages', array( $this->id ) ) ), 'IPS\nexus\Package\CustomField' ) as $field )
{
$form->add( $field->buildHelper( isset( $purchase->custom_fields[ $field->id ] ) ? $purchase->custom_fields[ $field->id ] : NULL ) );
}
}
/**
* ACP Edit Save
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param array $values Values from form
* @return string
*/
public function acpEditSave( \IPS\nexus\Purchase $purchase, array $values )
{
$customFields = $purchase->custom_fields;
foreach ( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'nexus_package_fields', \IPS\Db::i()->findInSet( 'cf_packages', array( $this->id ) ) ), 'IPS\nexus\Package\CustomField' ) as $field )
{
if( array_key_exists( $field->type, \IPS\nexus\Package\CustomField::$additionalFieldTypes ) and !class_exists( 'IPS\Helpers\Form\\' . $field->type ) )
{
$class = 'IPS\nexus\Form\\' . $field->type;
}
else
{
$class = 'IPS\Helpers\Form\\' . $field->type;
}
if ( $class instanceof \IPS\Helpers\Form\Upload )
{
$customFields[ $field->id ] = (string) $values[ 'nexus_pfield_' . $field->id ];
}
else
{
$customFields[ $field->id ] = $class::stringValue( $values[ 'nexus_pfield_' . $field->id ] );
}
}
$purchase->custom_fields = $customFields;
$purchase->save();
}
/**
* Get ACP Support View HTML
*
* @return string
*/
public function acpSupportView( \IPS\nexus\Purchase $purchase )
{
return '';
}
/* !Actions */
/**
* On Paid
*
* @param \IPS\nexus\Invoice $invoice The invoice
* @return void
*/
public function onPaid( \IPS\nexus\Invoice $invoice )
{
// Blank for hooking
}
/**
* On Unpaid description
*
* @param \IPS\nexus\Invoice $invoice The invoice
* @return array
*/
public function onUnpaidDescription( \IPS\nexus\Invoice $invoice )
{
// Blank for hooking
return array();
}
/**
* On Unpaid
*
* @param \IPS\nexus\Invoice $invoice The invoice
* @param string $status Status
* @return void
*/
public function onUnpaid( \IPS\nexus\Invoice $invoice, $status )
{
// Blank for hooking
}
/**
* On Invoice Cancel (when unpaid)
*
* @param \IPS\nexus\Invoice $invoice The invoice
* @return void
*/
public function onInvoiceCancel( \IPS\nexus\Invoice $invoice )
{
// Blank for hooking
}
/**
* Check for member
* If a user initially checks out as a guest and then logs in during checkout, this method
* is ran to check the items they are purchasing can be bought.
* Is expected to throw a DomainException with an error message to display to the user if not valid
*
* @param \IPS\Member $member The new member
* @return void
* @throws \DomainException
*/
public function memberCanPurchase( \IPS\Member $member )
{
// Handled by subclasses
}
/**
* Can Renew Until
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @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 means can renew until that date
*/
public function canRenewUntil( \IPS\nexus\Purchase $purchase, $admin )
{
if ( !$purchase->expire )
{
return FALSE;
}
if ( $admin )
{
return TRUE;
}
if ( !$this->renewal_days )
{
return FALSE;
}
elseif ( $this->renewal_days != -1 )
{
$diff = $purchase->expire->diff( \IPS\DateTime::create() );
if ( $diff->invert and $diff->days > $this->renewal_days )
{
return FALSE;
}
}
if ( $this->renewal_days_advance == -1 )
{
return TRUE;
}
else
{
$baseDate = ( $purchase->expire->getTimestamp() > time() ) ? $purchase->expire : \IPS\DateTime::create();
return $baseDate->add( new \DateInterval( 'P' . $this->renewal_days_advance . 'D' ) );
}
}
/**
* On Purchase Generated
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param \IPS\nexus\Invoice $invoice The invoice
* @return void
*/
public function onPurchaseGenerated( \IPS\nexus\Purchase $purchase, \IPS\nexus\Invoice $invoice )
{
$this->_changeStockLevel( -1, $this->_getOptionId( $purchase ) );
$this->_addUsergroups( $purchase );
if ( $this->notify )
{
$email = \IPS\Email::buildFromTemplate( 'nexus', 'purchaseNotify', array( $purchase, $invoice ), \IPS\Email::TYPE_LIST );
foreach ( explode( ',', $this->notify ) as $to )
{
$email->send( $to );
}
}
$this->sendCustomEmail( 'purchase', $purchase );
}
/**
* On Renew (Renewal invoice paid. Is not called if expiry data is manually changed)
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param int $cycles Cycles
* @return void
*/
public function onRenew( \IPS\nexus\Purchase $purchase, $cycles )
{
// Blank for hooking
}
/**
* On Expiration Date Change
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return void
*/
public function onExpirationDateChange( \IPS\nexus\Purchase $purchase )
{
}
/**
* On expire soon
* If returns TRUE, the normal expire warning email will not be sent
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return void
*/
public function onExpireWarning( \IPS\nexus\Purchase $purchase )
{
return $this->sendCustomEmail( 'expire_soon', $purchase );
}
/**
* On Purchase Expired
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return void
*/
public function onExpire( \IPS\nexus\Purchase $purchase )
{
$this->_removeUsergroups( $purchase );
$this->sendCustomEmail( 'expire', $purchase );
}
/**
* On Purchase Canceled
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return void
*/
public function onCancel( \IPS\nexus\Purchase $purchase )
{
$this->_changeStockLevel( 1, $this->_getOptionId( $purchase ) );
$this->_removeUsergroups( $purchase );
}
/**
* On Purchase Deleted
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return void
*/
public function onDelete( \IPS\nexus\Purchase $purchase )
{
$this->onCancel( $purchase );
}
/**
* On Purchase Reactivated (renewed after being expired or reactivated after being canceled)
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return void
*/
public function onReactivate( \IPS\nexus\Purchase $purchase )
{
$this->_changeStockLevel( -1, $this->_getOptionId( $purchase ) );
$this->_addUsergroups( $purchase );
}
/**
* On Transfer (is ran before transferring)
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param \IPS\Member $newCustomer New Customer
* @return void
*/
public function onTransfer( \IPS\nexus\Purchase $purchase, \IPS\Member $newCustomer )
{
$this->_removeUsergroups( $purchase );
$purchase->member = $newCustomer;
$this->_addUsergroups( $purchase );
}
/**
* On Upgrade/Downgrade
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param \IPS\nexus\Package $newPackage The package to upgrade to
* @param int|NULL|\IPS\nexus\Purchase\RenewalTerm $chosenRenewalOption The chosen renewal option
* @return void
*/
public function onChange( \IPS\nexus\Purchase $purchase, \IPS\nexus\Package $newPackage, $chosenRenewalOption = NULL )
{
}
/**
* Send a custom email
*
* @param string $type Which custom email
* @param \IPS\nexus\Purchase $purchase The purchase
* @return bool
*/
public function sendCustomEmail( $type, \IPS\nexus\Purchase $purchase )
{
$typeKey = "email_{$type}_type";
$valueKey = "email_{$type}";
if ( $this->$typeKey )
{
if ( $this->$typeKey == 'wysiwyg' )
{
$email = \IPS\Email::buildFromContent( $purchase->member->language()->get( "nexus_package_{$this->id}_email_{$type}_subject" ), $this->$valueKey, NULL, \IPS\Email::TYPE_TRANSACTIONAL );
}
else
{
$functionName = 'nexus_customPurchaseEmail_' . $this->id;
\IPS\Theme::makeProcessFunction( $this->$valueKey, $functionName, '$purchase' );
$email = \IPS\Email::buildFromContent( $purchase->member->language()->get( "nexus_package_{$this->id}_email_{$type}_subject" ), call_user_func( 'IPS\\Theme\\'.$functionName, $purchase ), NULL, \IPS\Email::TYPE_TRANSACTIONAL, $this->$typeKey == 'html_template' );
}
$email->send( $purchase->member );
return TRUE;
}
return FALSE;
}
/* !Upgrading/Downgrading */
/**
* Upgrade/Downgrade Options
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param bool $overrideRestrictions If true, will be able to choose any packages in the same group
* @param bool $includesUpgrades Will be set if there are upgrade options, useful for knowing what text to show
* @param bool $includesDowngrades Will be set if there are downgrade options, useful for knowing what text to show
* @return array
*/
protected function upgradeDowngradeOptions( \IPS\nexus\Purchase $purchase, $overrideRestrictions = FALSE, &$includesUpgrades=FALSE, &$includesDowngrades=FALSE )
{
/* Cannot upgrade/downgrade custom or cancelled packages */
if ( $this->custom or $purchase->cancelled )
{
return array();
}
/* Cannot upgrade/downgrade without permission unless $overrideRestrictions is set */
if ( ( !$this->allow_upgrading AND !$this->allow_downgrading ) AND !$overrideRestrictions )
{
return array();
}
/* Cannot upgrade/downgrade with billing agreement */
if ( ( $purchase->billing_agreement and !$purchase->billing_agreement->canceled ) or ( $parent = $purchase->parent() and $parent->billing_agreement and !$parent->billing_agreement->canceled ) )
{
return array();
}
/* Loop through our siblings */
$return = array();
try
{
$currency = $purchase->original_invoice->currency;
}
catch ( \Exception $e )
{
$currency = $purchase->member->defaultCurrency();
}
$prices = json_decode( $this->base_price, TRUE );
$thisPrice = $prices[ $currency ]['amount'];
foreach ( $this->parent()->children() as $package )
{
if ( $package->id === $this->id )
{
continue;
}
$prices = json_decode( $package->base_price, TRUE );
if ( !isset( $prices[ $currency ] ) )
{
continue;
}
$price = $prices[ $currency ]['amount'];
/* Does Renewal add to base price */
if ( $package->renew_options )
{
$renewalOptions = json_decode( $package->renew_options, TRUE );
if ( !empty( $renewalOptions ) )
{
$option = array_shift( $renewalOptions );
if ( $option['add'] )
{
$price += ( $option['cost'][ $currency ]['amount'] );
}
}
}
/* Upgrade */
if ( $price >= $thisPrice and ( $this->allow_upgrading or $overrideRestrictions ) )
{
$return[] = $package;
$includesUpgrades = TRUE;
}
/* Downgrade */
elseif ( $price < $thisPrice and ( $this->allow_downgrading or $overrideRestrictions ) )
{
$return[] = $package;
$includesDowngrades = TRUE;
}
}
return $return;
}
/**
* Upgrade/Downgrade Form
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param bool $overrideRestrictions If true, will be able to choose any packages in the same group
* @return \IPS\Helpers\Form
*/
protected function upgradeDowngradeForm( \IPS\nexus\Purchase $purchase, $overrideRestrictions = FALSE )
{
try
{
$currency = $purchase->original_invoice->currency;
}
catch ( \Exception $e )
{
$currency = $purchase->member->defaultCurrency();
}
$options = array();
$desciptions = array();
$toggles = array();
$renewFields = array();
foreach ( $this->upgradeDowngradeOptions( $purchase, $overrideRestrictions ) as $package )
{
try
{
$costToUpgrade = $package->costToUpgrade( $purchase );
}
catch ( \InvalidArgumentException $e )
{
continue;
}
$options[ $package->id ] = $package->_title;
$renewOptions = json_decode( $package->renew_options, TRUE ) ?: array();
$renewalOptions = array();
foreach ( $renewOptions as $k => $option )
{
$renewalOptions[ $k ] = (string) new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $option['cost'][$currency]['amount'], $currency ), new \DateInterval( 'P' . $option['term'] . mb_strtoupper( $option['unit'] ) ) );
}
if ( count( $renewalOptions ) === 1 )
{
foreach ( $renewalOptions as $k => $v )
{
if ( $costToUpgrade->amount->isZero() )
{
$desciptions[ $package->id ] = $v;
}
elseif( $costToUpgrade->amount->isPositive() )
{
$desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'upgrade_cost_and_renew', FALSE, array( 'sprintf' => array( $costToUpgrade, $v ) ) );
}
else
{
$costToUpgrade->amount = $costToUpgrade->amount->multiply( new \IPS\Math\Number( '-1' ) );
$desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'downgrade_refund_and_renew', FALSE, array( 'sprintf' => array( $costToUpgrade, $v ) ) );
}
}
}
elseif ( count( $renewalOptions ) === 0 )
{
if( $costToUpgrade->amount->isGreaterThanZero() )
{
$desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'upgrade_cost', FALSE, array( 'sprintf' => array( $costToUpgrade ) ) );
}
elseif( !$costToUpgrade->amount->isPositive() )
{
$costToUpgrade->amount = $costToUpgrade->amount->multiply( new \IPS\Math\Number( '-1' ) );
$desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'downgrade_refund', FALSE, array( 'sprintf' => array( $costToUpgrade ) ) );
}
}
else
{
$renewalOptionsString = \IPS\Member::loggedIn()->language()->formatList( $renewalOptions, \IPS\Member::loggedIn()->language()->get('or_list_format') );
$renewFieldsDescriptions = array();
if ( $costToUpgrade->amount->isZero() )
{
$desciptions[ $package->id ] = $renewalOptionsString;
}
elseif( $costToUpgrade->amount->isGreaterThanZero() )
{
$desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'upgrade_cost_and_renew', FALSE, array( 'sprintf' => array( $costToUpgrade, $renewalOptionsString ) ) );
}
else
{
$costToUpgrade->amount = $costToUpgrade->amount->multiply( new \IPS\Math\Number( '-1' ) );
$desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'downgrade_refund_and_renew', FALSE, array( 'sprintf' => array( $costToUpgrade, $renewalOptionsString ) ) );
}
$renewFields[] = new \IPS\Helpers\Form\Radio( 'renew_option_' . $package->id, NULL, NULL, array( 'options' => $renewalOptions, 'descriptions' => $renewFieldsDescriptions, 'parse' => 'normal' ), NULL, NULL, NULL, 'renew_option_' . $package->id );
\IPS\Member::loggedIn()->language()->words['renew_option_' . $package->id] = \IPS\Member::loggedIn()->language()->addToStack( 'renewal_term', FALSE );
$toggles[ $package->id ] = array( 'renew_option_' . $package->id );
}
}
$form = new \IPS\Helpers\Form;
$form->class = 'ipsForm_vertical';
$form->add( new \IPS\Helpers\Form\Radio( 'change_package_to', NULL, TRUE, array( 'options' => $options, 'descriptions' => $desciptions, 'toggles' => $toggles, 'parse' => 'normal' ) ) );
foreach ( $renewFields as $field )
{
$form->add( $field );
}
return $form;
}
/**
* Cost to upgrade to this package (may return negative value for refund)
*
* @param \IPS\nexus\Purchase $purchase The purchase to be upgraded
* @return \IPS\nexus\Money|NULL
* @throws \InvalidArgumentException
*/
protected function costToUpgrade( \IPS\nexus\Purchase $purchase )
{
$package = \IPS\nexus\Package::load( $purchase->item_id );
try
{
$currency = $purchase->original_invoice->currency;
}
catch ( \Exception $e )
{
$currency = $purchase->member->defaultCurrency();
}
$priceOfExistingPackage = json_decode( $package->base_price, TRUE );
$priceOfExistingPackage = $priceOfExistingPackage[ $currency ]['amount'];
$renewalOptionsOnOldPackage = json_decode( $package->renew_options, TRUE );
if ( $purchase->renewals )
{
foreach ( $renewalOptionsOnOldPackage as $renewalOption )
{
$term = $purchase->renewals->getTerm();
if ( $renewalOption['add'] and $term['term'] == $renewalOption['term'] and $term['unit'] == $renewalOption['unit'] )
{
$priceOfExistingPackage += $renewalOption['cost'][ $currency ]['amount'];
}
}
}
$priceOfThisPackage = json_decode( $this->base_price, TRUE );
$priceOfThisPackage = $priceOfThisPackage[ $currency ]['amount'];
$renewalOptionsOnNewPackage = json_decode( $this->renew_options, TRUE );
if ( $purchase->renewals )
{
if ( $renewalOptionsOnNewPackage )
{
foreach ( $renewalOptionsOnNewPackage as $renewalOption )
{
$term = $purchase->renewals->getTerm();
if ( $renewalOption['add'] and $term['term'] == $renewalOption['term'] and $term['unit'] == $renewalOption['unit'] )
{
$priceOfThisPackage += $renewalOption['cost'][ $currency ]['amount'];
}
}
}
}
if ( $priceOfThisPackage >= $priceOfExistingPackage )
{
$type = $package->upgrade_charge;
}
else
{
$type = $package->downgrade_refund;
}
switch ( $type )
{
case 0:
return new \IPS\nexus\Money( 0, $currency );
case 1:
return new \IPS\nexus\Money( $priceOfThisPackage - $priceOfExistingPackage, $currency );
case 2:
if ( !$purchase->renewals )
{
return new \IPS\nexus\Money( 0, $currency );
}
if ( !$renewalOptionsOnNewPackage )
{
throw new \InvalidArgumentException;
}
/* What is the closest renewal option on the new package? We'll use that one */
$renewalOptionsInDays = array();
foreach ( $renewalOptionsOnNewPackage as $optionId => $renewalTerm )
{
$term = ( new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $renewalTerm['cost'][ $currency ]['amount'], $currency ), new \DateInterval( 'P' . $renewalTerm['term'] . mb_strtoupper( $renewalTerm['unit'] ) ), $purchase->renewals->tax, $renewalTerm['add'] ) );
$renewalOptionsInDays[ $term->days() ] = $term;
}
$closestRenewalOption = null;
$numberOfDaysInCurrentRenewalTerm = $purchase->renewals->days();
foreach ( $renewalOptionsInDays as $days => $term )
{
if ( $closestRenewalOption === null or abs( $numberOfDaysInCurrentRenewalTerm - $closestRenewalOption ) > abs( $days - $numberOfDaysInCurrentRenewalTerm ) )
{
$closestRenewalOption = $days;
}
}
$renewalTermToUse = $renewalOptionsInDays[ $closestRenewalOption ];
/* What is the difference between our current renewal term and the renewal term we're moving to? */
$diff = $purchase->renewals->diff( $renewalTermToUse );
/* Multiply that by how many days are left */
$numberOfDaysInCurrentRenewalTerm = new \IPS\Math\Number( $numberOfDaysInCurrentRenewalTerm );
$daysLeftUntilExpiry = new \IPS\Math\Number( (string) ceil( ( $purchase->expire->getTimestamp() - time() ) / 86400 ) );
if ( $numberOfDaysInCurrentRenewalTerm->compare( $daysLeftUntilExpiry ) === 0 )
{
return $diff;
}
else
{
return new \IPS\nexus\Money( $diff->amount->divide( $numberOfDaysInCurrentRenewalTerm )->multiply( $daysLeftUntilExpiry ), $currency );
}
}
}
/**
* Actually Upgrade/Downgrade
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param \IPS\nexus\Package $newPackage The package to upgrade to
* @param int|NULL|\IPS\nexus\Purchase\RenewalTerm $chosenRenewalOption The chosen renewal option
* @param bool $skipCharge If TRUE, an upgrade charges and downgrade refunds will not be issued
* @return \IPS\nexus\Invoice|void An invoice if an upgrade charge has to be paid, or void if not
* @throws \InvalidArgumentException If $chosenRenewalOption is invalid
*/
public function upgradeDowngrade( \IPS\nexus\Purchase $purchase, \IPS\nexus\Package $newPackage, $chosenRenewalOption = NULL, $skipCharge = FALSE )
{
/* Charge / Refund */
if ( !$skipCharge )
{
$costToUpgrade = $newPackage->costToUpgrade( $purchase );
/* Upgrade Charge */
if ( $costToUpgrade->amount->isGreaterThanZero() )
{
$item = new \IPS\nexus\extensions\nexus\Item\UpgradeCharge( sprintf( $purchase->member->language()->get( 'upgrade_charge_item' ), $purchase->member->language()->get( "nexus_package_{$this->id}" ), $purchase->member->language()->get( "nexus_package_{$newPackage->id}" ) ), $costToUpgrade );
$item->tax = $newPackage->tax ? \IPS\nexus\Tax::load( $newPackage->tax ) : NULL;
$item->id = $purchase->id;
$item->extra = array( 'newPackage' => $newPackage->id, 'oldPackage' => $this->id, 'renewalOption' => $chosenRenewalOption, 'previousRenewalTerms' => $purchase->renewals ? array( 'cost' => $purchase->renewals->cost->amount, 'currency' => $purchase->renewals->cost->currency, 'term' => $purchase->renewals->getTerm() ) : NULL );
if ( $newPackage->methods and $newPackage->methods != '*' )
{
$item->paymentMethodIds = explode( ',', $newPackage->methods );
}
$invoice = new \IPS\nexus\Invoice;
$invoice->member = $purchase->member;
$invoice->currency = $costToUpgrade->currency;
$invoice->addItem( $item );
$invoice->return_uri = "app=nexus&module=clients&controller=purchases&do=view&id={$purchase->id}";
$invoice->renewal_ids = array( $purchase->id );
$invoice->save();
return $invoice;
}
elseif ( !$costToUpgrade->amount->isPositive() )
{
$credits = $purchase->member->cm_credits;
$credits[ $costToUpgrade->currency ]->amount = $credits[ $costToUpgrade->currency ]->amount->add( $costToUpgrade->amount->multiply( new \IPS\Math\Number( '-1' ) ) );
$purchase->member->cm_credits = $credits;
$purchase->member->save();
}
}
/* Work out the new renewal term */
if ( $chosenRenewalOption instanceof \IPS\nexus\Purchase\RenewalTerm )
{
$term = $chosenRenewalOption;
}
else
{
$term = NULL;
$renewalOptions = json_decode( $newPackage->renew_options, TRUE );
if ( $chosenRenewalOption === NULL )
{
if ( count( $renewalOptions ) === 1 )
{
$term = array_pop( $renewalOptions );
}
elseif ( count( $renewalOptions ) !== 0 )
{
throw new \InvalidArgumentException;
}
}
else
{
if ( isset( $renewalOptions[ $chosenRenewalOption ] ) )
{
$term = $renewalOptions[ $chosenRenewalOption ];
}
else
{
throw new \InvalidArgumentException;
}
}
if ( $term )
{
try
{
$currency = $purchase->original_invoice->currency;
}
catch ( \OutOfRangeException $e )
{
$currency = $purchase->member->defaultCurrency();
}
$term = new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $term['cost'][$currency]['amount'], $currency ), new \DateInterval( 'P' . $term['term'] . mb_strtoupper( $term['unit'] ) ) );
}
}
/* If grouped, ungroup */
$grouped = $purchase->grouped_renewals;
if ( $grouped )
{
$purchase->ungroupFromParent();
$purchase->save();
}
/* Do package-specific stuff */
$this->onChange( $purchase, $newPackage, $chosenRenewalOption );
/* Remove usergroups */
$this->_removeUsergroups( $purchase );
/* Ungroup any children */
$groupedChildren = array();
foreach ( $purchase->children( NULL ) as $child )
{
if ( $child->grouped_renewals )
{
$child->ungroupFromParent();
$groupedChildren[] = $child;
}
}
/* If we didn't have an expiry date before, but the new package has a renewal term, set an expiry date */
if ( !$purchase->expire and $term )
{
$purchase->expire = \IPS\DateTime::create()->add( $term->interval );
}
/* OR if we did have an expiry date, but the new package does not have a renewal term, remove it */
elseif ( !$term )
{
$purchase->expire = NULL;
}
/* Update Purchase */
$purchase->name = \IPS\Member::loggedIn()->language()->get( "nexus_package_{$newPackage->id}" );
$purchase->item_id = $newPackage->id;
$purchase->renewals = $term;
$purchase->save();
/* Regroup children */
foreach ( $groupedChildren as $child )
{
$child->groupWithParent();
}
/* Re-add usergroups */
$newPackage->_addUsergroups( $purchase );
/* If grouped, regroup */
if ( $grouped )
{
$purchase->groupWithParent();
$purchase->save();
}
/* Cancel any pending invoices */
if ( $pendingInvoice = $purchase->invoice_pending )
{
$pendingInvoice->status = \IPS\nexus\invoice::STATUS_CANCELED;
$pendingInvoice->save();
$purchase->invoice_pending = NULL;
$purchase->save();
}
}
/* !Stock */
/**
* Change Stock Level
*
* @param int $changeBy Amount to change by
* @param int|NULL $optionId Product option ID, if applicable
* @return void
*/
protected function _changeStockLevel( $changeBy, $optionId=NULL )
{
if ( $this->stock == -2 )
{
if ( $optionId !== NULL )
{
try
{
$stock = \IPS\Db::i()->select( 'opt_stock', 'nexus_product_options', array( 'opt_id=?', $optionId ) )->first();
if ( $stock != -1 )
{
$newValue = $stock + $changeBy;
if ( $newValue < 0 )
{
$newValue = 0;
}
\IPS\Db::i()->update( 'nexus_product_options', array( 'opt_stock' => $newValue ), array( 'opt_id=?', $optionId ) );
}
}
catch ( \UnderflowException $e ) { }
}
}
elseif ( $this->stock > 0 )
{
$newValue = $this->stock + $changeBy;
if ( $newValue < 0 )
{
$newValue = 0;
}
$this->stock = $newValue;
$this->save();
}
}
/**
* @brief Fields which affect the option ID
*/
protected $_optionIdKeys = NULL;
/**
* Get field IDs which affect the option ID
*
* @return array
*/
public function optionIdKeys()
{
if( $this->_optionIdKeys === NULL )
{
$this->_optionIdKeys = array();
try
{
$options = \IPS\Db::i()->select( 'opt_values', 'nexus_product_options', array( 'opt_package=?', $this->id ) )->first();
if ( $options = json_decode( $options, TRUE ) )
{
$this->_optionIdKeys = array_keys( $options );
}
}
catch ( \UnderflowException $e ) { }
}
return $this->_optionIdKeys;
}
/**
* Get option ID
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return int|NULL
*/
protected function _getOptionId( \IPS\nexus\Purchase $purchase )
{
$optionValues = array();
foreach( $this->optionIdKeys() as $id )
{
$optionValues[ $id ] = $purchase->custom_fields[$id];
}
try
{
return \IPS\Db::i()->select( 'opt_id', 'nexus_product_options', array( 'opt_package=? AND opt_values=?', $this->id, json_encode( $optionValues ) ) )->first();
}
catch ( \UnderflowException $e )
{
return NULL;
}
}
/* !Usergroups */
/**
* Add user groups
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @return void
*/
public function _addUsergroups( \IPS\nexus\Purchase $purchase )
{
$extra = $purchase->extra;
/* Primary Group */
if ( $this->primary_group and $this->primary_group != $purchase->member->member_group_id and !in_array( $purchase->member->member_group_id, explode( ',', \IPS\Settings::i()->cm_protected ) ) )
{
/* Hang on, are we about to boot someone out the ACP? */
if ( ! ( $purchase->member->isAdmin() and !in_array( $this->primary_group, array_keys( \IPS\Member::administrators()['g'] ) ) ) )
{
/* Save the current group */
$extra['old_primary_group'] = $purchase->member->member_group_id;
$purchase->extra = $extra;
$purchase->save();
if ( !$purchase->member->cm_return_group )
{
$purchase->member->cm_return_group = $purchase->member->member_group_id;
}
/* And update to the new group */
$purchase->member->member_group_id = $this->primary_group;
$purchase->member->members_bitoptions['ignore_promotions'] = true;
$purchase->member->save();
$purchase->member->logHistory( 'core', 'group', array( 'type' => 'primary', 'by' => 'purchase', 'action' => 'add', 'id' => $purchase->id, 'name' => $purchase->name, 'old' => $extra['old_primary_group'], 'new' => $purchase->member->member_group_id ) );
}
}
/* Secondary Groups */
$secondary = array_filter( explode( ',', $this->secondary_group ), function( $v ){ return (bool) $v; } );
$current_secondary = $purchase->member->mgroup_others ? explode( ',', $purchase->member->mgroup_others ) : array();
$new_secondary = $current_secondary;
if ( !empty( $secondary ) )
{
foreach ( $secondary as $gid )
{
if ( !in_array( $gid, $new_secondary ) )
{
$new_secondary[] = $gid;
}
}
}
if ( $current_secondary != $new_secondary )
{
$extra['old_secondary_groups'] = $purchase->member->mgroup_others;
$purchase->extra = $extra;
$purchase->save();
$purchase->member->mgroup_others = ',' . implode( ',', $new_secondary ) . ',';
$purchase->member->save();
$purchase->member->logHistory( 'core', 'group', array( 'type' => 'secondary', 'by' => 'purchase', 'action' => 'add', 'id' => $purchase->id, 'name' => $purchase->name, 'old' => $current_secondary, 'new' => $new_secondary ) );
}
}
/**
* Remove user groups
*
* @param \IPS\nexus\Purchase $purchase The purchase
* @param array $basis The current package data, if needed to override
* @return void
*/
public function _removeUsergroups( \IPS\nexus\Purchase $purchase, $basis = array() )
{
$basis = array_merge( $this->_data, $basis );
$extra = $purchase->extra;
/* Primary Group */
if ( $basis['return_primary'] )
{
/* We only want to move them back if they haven't been moved again since */
if ( $purchase->member->member_group_id == $basis['primary_group'] )
{
$oldGroup = $purchase->member->member_group_id;
/* Have we made other purchases that have changed their primary group? */
try
{
$next = \IPS\Db::i()->select( array( 'ps_id', 'ps_name', 'p_primary_group' ), 'nexus_purchases', array( "ps_member=? AND ps_app=? AND ps_type IN('package','subscription') AND ps_active=1 AND p_primary_group<>0 AND ps_id<>?", $purchase->member->member_id, 'nexus', $purchase->id ) )
->join( 'nexus_packages', 'p_id=ps_item_id' )
->first();
/* Make sure this group exists */
try
{
\IPS\Member\Group::load( $next['p_primary_group'] );
}
catch( \OutOfRangeException $e )
{
throw new \UnderflowException;
}
$purchase->member->member_group_id = $next['p_primary_group'];
$purchase->member->save();
$purchase->member->logHistory( 'core', 'group', array( 'type' => 'primary', 'by' => 'purchase', 'action' => 'change', 'remove_id' => $purchase->id, 'remove_name' => $purchase->name, 'id' => $next['ps_id'], 'name' => $next['ps_name'], 'old' => $oldGroup, 'new' => $purchase->member->member_group_id ) );
}
/* No, move them to their original group */
catch ( \UnderflowException $e )
{
/* Does this group exist? */
try
{
\IPS\Member\Group::load( $purchase->member->cm_return_group );
$purchase->member->member_group_id = $purchase->member->cm_return_group;
}
catch ( \OutOfRangeException $e )
{
$purchase->member->member_group_id = \IPS\Settings::i()->member_group;
}
/* Save */
$purchase->member->cm_return_group = 0;
$purchase->member->members_bitoptions['ignore_promotions'] = false;
$purchase->member->save();
$purchase->member->logHistory( 'core', 'group', array( 'type' => 'primary', 'by' => 'purchase', 'action' => 'remove', 'id' => $purchase->id, 'name' => $purchase->name, 'old' => $oldGroup, 'new' => $purchase->member->member_group_id ) );
}
}
}
// Secondary groups
if ( isset( $extra['old_secondary_groups'] ) and $extra['old_secondary_groups'] !== NULL and $basis['return_secondary'] )
{
/* Work some stuff out */
$membersSecondaryGroups = $purchase->member->mgroup_others ? array_unique( array_filter( explode( ',', $purchase->member->mgroup_others ) ) ) : array();
$currentSecondaryGroups = $membersSecondaryGroups;
$membersPreviousSecondaryGroupsBeforeThisPurchase = array_unique( array_filter( explode( ',', $extra['old_secondary_groups'] ) ) );
$secondaryGroupsAwardedByThisPurchase = array_unique( array_filter( explode( ',', $basis['secondary_group'] ) ) );
/* Have we made other purchases that have added secondary groups? */
$secondaryGroupsAwardedByOtherPurchases = array();
foreach ( \IPS\Db::i()->select( 'p_secondary_group', 'nexus_purchases', array( "ps_member=? AND ps_app=? AND ps_type IN('package','subscription') AND ps_active=1 AND p_secondary_group IS NOT NULL AND p_secondary_group<>? AND ps_id<>?", $purchase->member->member_id, 'nexus', '', $purchase->id ) )->join( 'nexus_packages', 'p_id=ps_item_id' ) as $secondaryGroups )
{
$secondaryGroupsAwardedByOtherPurchases = array_merge( $secondaryGroupsAwardedByOtherPurchases, array_filter( explode( ',', $secondaryGroups ) ) );
}
$secondaryGroupsAwardedByOtherPurchases = array_unique( $secondaryGroupsAwardedByOtherPurchases );
/* Loop through */
foreach ( $secondaryGroupsAwardedByThisPurchase as $groupId )
{
/* If we had this group before we made this purchase, we're going to keep it */
if ( in_array( $groupId, $membersPreviousSecondaryGroupsBeforeThisPurchase ) )
{
continue;
}
/* If we are being awarded this group by a different purchase, we're also going to keep it */
if ( in_array( $groupId, $secondaryGroupsAwardedByOtherPurchases ) )
{
continue;
}
/* If we're still here, remove it */
unset( $membersSecondaryGroups[ array_search( $groupId, $membersSecondaryGroups ) ] );
}
/* And make sure only valid groups are saved */
$membersSecondaryGroups = array_filter( $membersSecondaryGroups, function( $group ){
try
{
\IPS\Member\Group::load( $group );
return TRUE;
}
catch( \OutOfRangeException $e )
{
return FALSE;
}
});
/* Save */
if( $currentSecondaryGroups != $membersSecondaryGroups )
{
$purchase->member->mgroup_others = implode( ',', $membersSecondaryGroups );
$purchase->member->save();
$purchase->member->logHistory( 'core', 'group', array( 'type' => 'secondary', 'by' => 'purchase', 'action' => 'remove', 'id' => $purchase->id, 'name' => $purchase->name, 'old' => $currentSecondaryGroups, 'new' => $membersSecondaryGroups ) );
}
}
}
}