· 9 years ago · Jan 13, 2017, 08:14 AM
1<?php
2/**
3 * @brief Package Node
4 * @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
5 * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
6 * @license http://www.invisionpower.com/legal/standards/
7 * @package IPS Community Suite
8 * @subpackage Nexus
9 * @since 29 Apr 2014
10 * @version SVN_VERSION_NUMBER
11 */
12
13namespace IPS\nexus;
14
15/* To prevent PHP errors (extending class does not exist) revealing path */
16if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
17{
18 header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
19 exit;
20}
21
22/**
23 * Package
24 */
25class _Package extends \IPS\Node\Model
26{
27 /**
28 * @brief [ActiveRecord] Multiton Store
29 */
30 protected static $multitons;
31
32 /**
33 * @brief [ActiveRecord] Database Table
34 */
35 public static $databaseTable = 'nexus_packages';
36
37 /**
38 * @brief [ActiveRecord] Database Prefix
39 */
40 public static $databasePrefix = 'p_';
41
42 /**
43 * @brief [Node] Order Database Column
44 */
45 public static $databaseColumnOrder = 'position';
46
47 /**
48 * @brief [Node] Parent Node ID Database Column
49 */
50 public static $parentNodeColumnId = 'group';
51
52 /**
53 * @brief [Node] Parent Node Class
54 */
55 public static $parentNodeClass = 'IPS\nexus\Package\Group';
56
57 /**
58 * @brief [Node] Node Title
59 */
60 public static $nodeTitle = 'menu__nexus_store_packages';
61
62 /**
63 * @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key
64 */
65 public static $titleLangPrefix = 'nexus_package_';
66
67 /**
68 * @@rief [Node] Description suffix. If specified, will look for a language key with "{$titleLangPrefix}_{$id}_{$descriptionLangSuffix}" as the key
69 */
70 public static $descriptionLangSuffix = '_desc';
71
72 /* !ActiveRecord */
73
74 /**
75 * Construct ActiveRecord from database row
76 *
77 * @param array $data Row from database table
78 * @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there?
79 * @return static
80 */
81 public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
82 {
83 $classname = 'IPS\nexus\Package\\' . ucfirst( $data['p_type'] );
84 if ( isset( $classname::$packageDatabaseTable ) )
85 {
86 $data = array_merge( $data, \IPS\Db::i()->select( '*', $classname::$packageDatabaseTable, array( 'p_id=?', $data['p_id'] ) )->first() );
87 }
88
89 /* Initiate an object */
90 $obj = new $classname;
91 $obj->_new = FALSE;
92
93 /* Import data */
94 foreach ( $data as $k => $v )
95 {
96 if( static::$databasePrefix AND mb_strpos( $k, static::$databasePrefix ) === 0 )
97 {
98 $k = \substr( $k, \strlen( static::$databasePrefix ) );
99 }
100
101 $obj->_data[ $k ] = $v;
102 }
103 $obj->changed = array();
104
105 /* Init */
106 if ( method_exists( $obj, 'init' ) )
107 {
108 $obj->init();
109 }
110
111 /* Return */
112 return $obj;
113 }
114
115 /**
116 * @brief [Node] ACP Restrictions
117 * @code
118 array(
119 'app' => 'core', // The application key which holds the restrictrions
120 'module' => 'foo', // The module key which holds the restrictions
121 'map' => array( // [Optional] The key for each restriction - can alternatively use "prefix"
122 'add' => 'foo_add',
123 'edit' => 'foo_edit',
124 'permissions' => 'foo_perms',
125 'delete' => 'foo_delete'
126 ),
127 'all' => 'foo_manage', // [Optional] The key to use for any restriction not provided in the map (only needed if not providing all 4)
128 '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"
129 * @endcode
130 */
131 protected static $restrictions = array(
132 'app' => 'nexus',
133 'module' => 'store',
134 'prefix' => 'packages_',
135 );
136
137 /**
138 * Set Default Values
139 *
140 * @return void
141 */
142 public function setDefaultValues()
143 {
144 $this->member_groups = '*';
145 $this->date_added = time();
146 $this->store = TRUE;
147 }
148
149 /**
150 * Save Changed Columns
151 *
152 * @return void
153 */
154 public function save()
155 {
156 $data = $this->changed;
157
158 $secondaryTable = array();
159 foreach ( $this->changed as $k => $v )
160 {
161 if ( isset( static::$packageDatabaseColumns ) and in_array( "p_{$k}", static::$packageDatabaseColumns ) )
162 {
163 $secondaryTable[ "p_{$k}" ] = $v;
164 unset( $this->changed[ $k ] );
165 }
166 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' ) ) )
167 {
168 unset( $this->changed[ $k ] );
169 }
170 }
171
172 if ( isset( $data['base_price'] ) )
173 {
174 $decoded = json_decode( $data['base_price'], TRUE );
175 if ( $decoded and is_array( $decoded ) )
176 {
177 $prices = array( 'id' => $this->id );
178 foreach ( $decoded as $currency => $value )
179 {
180 if ( !\IPS\Db::i()->checkForColumn( 'nexus_package_base_prices', $currency ) )
181 {
182 \IPS\Db::i()->addColumn( 'nexus_package_base_prices', array(
183 'name' => $currency,
184 'type' => 'FLOAT'
185 ) );
186 }
187 $prices[ $currency ] = $value['amount'];
188 }
189 \IPS\Db::i()->replace( 'nexus_package_base_prices', $prices );
190 }
191 }
192
193 parent::save();
194 $this->changed = $data;
195
196 if ( !empty( $secondaryTable ) AND isset( static::$packageDatabaseTable ) )
197 {
198 \IPS\Db::i()->update( static::$packageDatabaseTable, $secondaryTable, array( 'p_id=?', $this->id ) );
199 }
200 }
201
202 /**
203 * [ActiveRecord] Delete Record
204 *
205 * @return void
206 */
207 public function delete()
208 {
209 parent::delete();
210 \IPS\Db::i()->delete( static::$packageDatabaseTable, array( 'p_id=?', $this->id ) );
211 }
212
213 /**
214 * [ActiveRecord] Duplicate
215 *
216 * @return void
217 */
218 public function __clone()
219 {
220 if( $this->skipCloneDuplication === TRUE )
221 {
222 return;
223 }
224
225 $primaryTable = array();
226 $secondaryTable = array();
227 foreach ( $this->_data as $k => $v )
228 {
229 if ( !in_array( $k, array( 'id', 'reviews', 'unapproved_reviews', 'hidden_reviews' ) ) )
230 {
231 if ( in_array( "p_{$k}", static::$packageDatabaseColumns ) )
232 {
233 $secondaryTable[ "p_{$k}" ] = $v;
234 }
235 else
236 {
237 $primaryTable[ "p_{$k}" ] = $v;
238 }
239 }
240 }
241
242 /* We need to update the date_added field so cloned products appear in the new products block */
243 $primaryTable['p_date_added'] = time();
244
245 $oldId = $this->_id;
246 $id = \IPS\Db::i()->insert( 'nexus_packages', $primaryTable );
247
248 $secondaryTable['p_id'] = $id;
249 \IPS\Db::i()->insert( static::$packageDatabaseTable, $secondaryTable );
250
251 \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') ) );
252 \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') ) );
253
254 \IPS\Db::i()->insert( 'nexus_package_base_prices', \IPS\Db::i()->select( "{$id} AS id, " . implode( ',', array_map( function( $val )
255 {
256 return "`{$val}`";
257 }, \IPS\nexus\Money::currencies() ) ), 'nexus_package_base_prices', array( 'id=?', $this->_id ) ) );
258
259 foreach ( \IPS\Db::i()->select( '*', 'nexus_package_images', array( 'image_product=?', $this->_id ) ) as $image )
260 {
261 $file = \IPS\File::get( 'nexus_Products', $image['image_location'] );
262
263 \IPS\Db::i()->insert( 'nexus_package_images', array(
264 'image_product' => $id,
265 'image_location' => (string) \IPS\File::create( 'nexus_Products', $file->originalFilename, $file->contents(), $file->container ),
266 'image_primary' => $image['image_primary']
267 ) );
268 }
269
270 \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 ) ) );
271
272 \IPS\Db::i()->update( 'nexus_package_fields', "cf_packages=CONCAT( cf_packages, ',{$id}' )", \IPS\Db::i()->findInSet( 'cf_packages', array( $this->_id ) ) );
273 \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 ) ) );
274 \IPS\Db::i()->update( 'nexus_support_departments', "dpt_packages=CONCAT( dpt_packages, ',{$id}' )", \IPS\Db::i()->findInSet( 'dpt_packages', array( $this->_id ) ) );
275
276 $primaryKey = static::$databaseColumnId;
277 $this->$primaryKey = $id;
278 }
279
280 /* !Properties */
281
282 /**
283 * Get the lowest price for multiple packages
284 * May return price ($x) or "From $x" or a price struck and replaced with a discount price
285 *
286 * @param \IPS\Patterns\ActiveRecordIterator $iterator Iterator of packages
287 * @param \IPS\nexus\Customer|NULL $customer The customer (NULL for currently logged in member)
288 * @return string
289 */
290 public static function lowestPriceToDisplay( \IPS\Patterns\ActiveRecordIterator $iterator, \IPS\nexus\Customer $customer = NULL )
291 {
292 $customer = $customer ?: \IPS\nexus\Customer::loggedIn();
293 $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();
294
295 $lowest = NULL;
296 $_priceMayChange = FALSE;
297 $priceMayChange = FALSE;
298 foreach ( $iterator as $package )
299 {
300 $price = $package->_lowestPrice( $customer, $currency, $priceMayChange );
301 if ( $lowest !== NULL and $price != $lowest )
302 {
303 $_priceMayChange = TRUE;
304 }
305 if ( $lowest === NULL or $price < $lowest )
306 {
307 $lowest = $price;
308 }
309 }
310
311 return \IPS\Theme::i()->getTemplate( 'store', 'nexus' )->price( new \IPS\nexus\Money( $lowest, $currency ), $priceMayChange or $_priceMayChange );
312 }
313
314
315 /**
316 * @brief Icon
317 */
318 public static $icon = 'archive';
319
320 /**
321 * @brief Title
322 */
323 public static $title = 'product';
324
325 /**
326 * Basic purchase price
327 * May be further adjusted by renewal terms which add
328 *
329 * @param \IPS\nexus\Customer|NULL $customer The customer (NULL for currently logged in member)
330 * @param bool $usergroupDiscounts Account for usergroup discounts?
331 * @param bool $loyaltyDiscounts Account for loyalty discounts?
332 * @param bool $bulkDiscounts Account for bulk discounts?
333 * @param int $initialCount Will assume that number of additional purchases for this package
334 * @return \IPS\nexus\Money
335 */
336 public function price( \IPS\nexus\Customer $customer = NULL, $usergroupDiscounts = TRUE, $loyaltyDiscounts = TRUE, $bulkDiscounts = TRUE, $initialCount = 0 )
337 {
338 /* Base */
339 $customer = $customer ?: \IPS\nexus\Customer::loggedIn();
340 $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();
341 $prices = json_decode( $this->base_price, TRUE );
342 if ( !isset( $prices[ $currency ] ) )
343 {
344 throw new \OutOfBoundsException;
345 }
346 $price = $prices[ $currency ]['amount'];
347 $discounts = json_decode( $this->discounts, TRUE );
348
349 /* Usergroup discounts */
350 if ( $usergroupDiscounts and isset( $discounts['usergroup'] ) )
351 {
352 foreach ( $discounts['usergroup'] as $discount )
353 {
354 if ( $discount['price'][ $currency ] < $price )
355 {
356 if ( $discount['secondary'] )
357 {
358 $inGroup = $customer->inGroup( $discount['group'] );
359 }
360 else
361 {
362 $inGroup = $customer->member_group_id == $discount['group'];
363 }
364
365 if ( $inGroup )
366 {
367 $price = $discount['price'][ $currency ];
368 }
369 }
370 }
371 }
372
373 /* Loyalty discounts */
374 if ( $loyaltyDiscounts and isset( $discounts['loyalty'] ) )
375 {
376 foreach ( $discounts['loyalty'] as $discount )
377 {
378 $count = ( ( $discount['package'] ?: $this->id ) == $this->id ) ? $initialCount : 0;
379 if ( $discount['price'][ $currency ] < $price )
380 {
381 $count += $customer->member_id ? $customer->previousPurchasesCount( $discount['package'] ?: $this->id, $discount['active'] ) : 0;
382 }
383 if ( $customer->member_id === \IPS\Member::loggedIn()->member_id and isset( $_SESSION['cart'] ) )
384 {
385 foreach ( $_SESSION['cart'] as $item )
386 {
387 if ( $item->id == $this->id )
388 {
389 $count += $item->quantity;
390 }
391 }
392 }
393
394 if ( $count >= $discount['owns'] )
395 {
396 $price = $discount['price'][ $currency ];
397 }
398 }
399 }
400
401 /* Bulk Discounts */
402 if ( $bulkDiscounts and isset( $discounts['bulk'] ) )
403 {
404 /* Make sure we're only applying the highest quantity discount */
405 rsort( $discounts['bulk'] );
406 $discount = array_shift( $discounts['bulk'] );
407
408 $discountPackageId = ( $discount['package'] ?: $this->id );
409 $count = ( $discountPackageId == $this->id ) ? $initialCount : 0;
410 if ( $customer->member_id === \IPS\Member::loggedIn()->member_id and isset( $_SESSION['cart'] ) )
411 {
412 foreach ( $_SESSION['cart'] as $item )
413 {
414 if ( $item->id == $discountPackageId )
415 {
416 $count += $item->quantity;
417 }
418 }
419 }
420
421 if ( $count and ( ( $count + 1 ) % ( $discount['buying'] + 1 ) == 0 ) )
422 {
423 $price = $discount['price'][ $currency ];
424 }
425 }
426
427 /* Return */
428 return new \IPS\nexus\Money( $price, $currency );
429 }
430
431 /**
432 * Get the lowest price
433 *
434 * @param \IPS\nexus\Customer|NULL $customer The customer
435 * @param string $currency Currency
436 * @param bool $priceMayChange [Reference] will be set to a value indicating if the price might change (i.e. should be displayed as "From $x")
437 * @param float $priceNotIncludingDiscounts [Reference] will be set to the lowest price not taking discounts into consideration
438 * @return float
439 */
440 protected function _lowestPrice( \IPS\nexus\Customer $customer, $currency, &$priceMayChange = FALSE, &$priceNotIncludingDiscounts = NULL )
441 {
442 /* What's the base price with and without discounts? */
443 $baseNoDiscounts = $this->price( $customer, FALSE, FALSE, FALSE )->amount;
444 $base = $this->price( $customer )->amount;
445
446 /* Adjustments based on custom fields */
447 if ( $this->stock == -2 )
448 {
449 $mostReducedOption = NULL;
450 foreach ( \IPS\Db::i()->select( '*', 'nexus_product_options', array( 'opt_package=?', $this->id ) ) as $option )
451 {
452 $basePriceAdjustments = json_decode( $option['opt_base_price'], TRUE );
453 if ( isset( $basePriceAdjustments[ $currency ] ) and ( $basePriceAdjustments[ $currency ] or $basePriceAdjustments[ $currency ] === '0' ) )
454 {
455 if ( $mostReducedOption === NULL or $basePriceAdjustments[ $currency ] < $mostReducedOption )
456 {
457 $mostReducedOption = new \IPS\Math\Number( number_format( $basePriceAdjustments[ $currency ], \IPS\nexus\Money::numberOfDecimalsForCurrency( $currency ), '.', '' ) );
458 }
459
460 $priceMayChange = TRUE;
461 }
462 }
463 $mostReducedOption = $mostReducedOption ?: new \IPS\Math\Number('0');
464
465 $baseNoDiscounts = $baseNoDiscounts->add( $mostReducedOption );
466 $base = $base->add( $mostReducedOption );
467 }
468
469 /* Init */
470 $lowestNoDiscounts = $baseNoDiscounts;
471 $lowest = $base;
472
473 /* It's possible the base price is $0, but all the renewal options add, so let's look at that */
474 $renewOptions = $this->renew_options ? json_decode( $this->renew_options, TRUE ) : array();
475 if ( !empty( $renewOptions ) )
476 {
477 $lowestNoDiscounts = NULL;
478 $lowest = NULL;
479 foreach ( $renewOptions as $term )
480 {
481 if ( $term['add'] )
482 {
483 if ( count( $renewOptions ) > 1 )
484 {
485 $priceMayChange = TRUE;
486 }
487 $_lowest = $base->add( new \IPS\Math\Number( number_format( $term['cost'][ $currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $currency ), '.', '' ) ) );
488 $_lowestNoDiscounts = $baseNoDiscounts->add( new \IPS\Math\Number( number_format( $term['cost'][ $currency ]['amount'], \IPS\nexus\Money::numberOfDecimalsForCurrency( $currency ), '.', '' ) ) );
489 }
490 else
491 {
492 $_lowest = $base;
493 $_lowestNoDiscounts = $baseNoDiscounts;
494 }
495
496 if ( $lowest === NULL or $_lowest->compare( $lowest ) === -1 )
497 {
498 $lowest = $_lowest;
499 }
500 if ( $lowestNoDiscounts === NULL or $_lowestNoDiscounts->compare( $lowestNoDiscounts ) === -1 )
501 {
502 $lowestNoDiscounts = $_lowestNoDiscounts;
503 }
504 }
505 }
506
507 /* Add tax? */
508 if ( \IPS\Settings::i()->nexus_show_tax and $this->tax )
509 {
510 try
511 {
512 /* Load tax rate */
513 $tax = \IPS\nexus\Tax::load( $this->tax );
514
515 /* What is the rate? */
516 $taxRate = new \IPS\Math\Number( number_format( $tax->rate( $customer->estimatedLocation() ), 2, '.', '' ) );
517
518 /* Add it on */
519 $lowest = $lowest->add( $lowest->multiply( $taxRate ) );
520 $lowestNoDiscounts = $lowestNoDiscounts->add( $lowestNoDiscounts->multiply( $taxRate ) );
521 }
522 catch ( \OutOfRangeException $e ) { }
523 }
524
525 /* Return */
526 $priceNotIncludingDiscounts = $lowestNoDiscounts;
527 return $lowest;
528 }
529
530 /**
531 * Price to display in store
532 * 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
533 *
534 * @param \IPS\nexus\Customer|NULL $customer The customer (NULL for currently logged in member)
535 * @return string|NULL
536 */
537 public function priceToDisplay( \IPS\nexus\Customer $customer = NULL )
538 {
539 /* Get customer */
540 $customer = $customer ?: \IPS\nexus\Customer::loggedIn();
541 $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();
542
543 /* Get the price */
544 $priceMayChange = FALSE;
545 $priceNotIncludingDiscounts = NULL;
546 try
547 {
548 $price = $this->_lowestPrice( $customer, $currency, $priceMayChange, $priceNotIncludingDiscounts );
549 }
550 catch ( \OutOfBoundsException $e )
551 {
552 return NULL;
553 }
554
555 /* Display */
556 if ( $price < $priceNotIncludingDiscounts )
557 {
558 return \IPS\Theme::i()->getTemplate( 'store', 'nexus' )->priceDiscounted( new \IPS\nexus\Money( $priceNotIncludingDiscounts, $currency ), new \IPS\nexus\Money( $price, $currency ), $priceMayChange );
559 }
560 else
561 {
562 return \IPS\Theme::i()->getTemplate( 'store', 'nexus' )->price( new \IPS\nexus\Money( $price, $currency ), $priceMayChange );
563 }
564 }
565
566 /**
567 * Support Severity
568 *
569 * @param \IPS\nexus\Purchase $purchase The purchase
570 * @return \IPS\nexus\Support\Severity|NULL
571 */
572 public function supportSeverity( \IPS\nexus\Purchase $purchase )
573 {
574 if ( $this->support_severity )
575 {
576 try
577 {
578 return \IPS\nexus\Support\Severity::load( $this->support_severity );
579 }
580 catch ( \OutOfRangeException $e )
581 {
582 return NULL;
583 }
584 }
585 return NULL;
586 }
587
588 /**
589 * @brief Cached URLs
590 */
591 protected $_url = array();
592
593 /**
594 * Get URL
595 *
596 * @param string|NULL $action Action
597 * @return \IPS\Http\Url
598 */
599 public function url( $action=NULL )
600 {
601 $_key = md5( $action );
602
603 if( !isset( $this->_url[ $_key ] ) )
604 {
605 $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 ) ) );
606
607 if ( $action )
608 {
609 $this->_url[ $_key ] = $this->_url[ $_key ]->setQueryString( 'do', $action );
610 }
611 }
612
613 return $this->_url[ $_key ];
614 }
615
616 /**
617 * Get full image URL
618 *
619 * @return string
620 */
621 public function get_image()
622 {
623 return ( $this->_data['image'] ) ? (string) \IPS\File::get( 'nexus_Products', $this->_data['image'] )->url : NULL;
624 }
625
626 /**
627 * Get additional name info
628 *
629 * @param \IPS\nexus\Purchase $purchase The purchase
630 * @return array
631 */
632 public function getPurchaseNameInfo( \IPS\nexus\Purchase $purchase )
633 {
634 $stickyFields = array();
635 if ( count( $purchase->custom_fields ) )
636 {
637 $customFields = \IPS\nexus\Package\CustomField::roots();
638 foreach ( $purchase->custom_fields as $k => $v )
639 {
640 if ( $v and isset( $customFields[ $k ] ) and $customFields[ $k ]->sticky )
641 {
642 $stickyFields[] = $v;
643 }
644 }
645 }
646 return $stickyFields;
647 }
648
649 /**
650 * @brief Associable packages
651 */
652 protected $_associablePackages = NULL;
653
654 /**
655 * Associable Packages
656 *
657 * @return array
658 */
659 public function associablePackages()
660 {
661 if ( $this->_associablePackages === NULL )
662 {
663 $this->_associablePackages = array();
664 foreach ( explode( ',', $this->associable ) as $id )
665 {
666 try
667 {
668 $this->_associablePackages[ $id ] = \IPS\nexus\Package::load( $id );
669 }
670 catch ( \OutOfRangeException $e ) { }
671 }
672 }
673 return $this->_associablePackages;
674 }
675
676 /**
677 * [Node] Get buttons to display in tree
678 * Example code explains return value
679 *
680 * @code
681 array(
682 array(
683 'icon' => 'plus-circle', // Name of FontAwesome icon to use
684 'title' => 'foo', // Language key to use for button's title parameter
685 'link' => \IPS\Http\Url::internal( 'app=foo...' ) // URI to link to
686 'class' => 'modalLink' // CSS Class to use on link (Optional)
687 ),
688 ... // Additional buttons
689 );
690 * @endcode
691 * @param string $url Base URL
692 * @param bool $subnode Is this a subnode?
693 * @return array
694 */
695 public function getButtons( $url, $subnode=FALSE )
696 {
697 $buttons = array();
698
699 if ( \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'customers', 'purchases_view' ) )
700 {
701 $buttons['view_purchases'] = array(
702 'icon' => 'search',
703 'title' => 'view_purchases',
704 'link' => $url->setQueryString( array( 'do' => 'viewPurchases', 'id' => $this->_id ) )
705 );
706 }
707
708 $buttons = array_merge( $buttons, parent::getButtons( $url, $subnode ) );
709
710 return $buttons;
711 }
712
713 /* !ACP Package Form */
714
715 /**
716 * [Node] Add/Edit Form
717 *
718 * @param \IPS\Helpers\Form $form The form
719 * @return void
720 */
721 public function form( &$form )
722 {
723 $groups = array();
724 foreach ( \IPS\Member\Group::groups() as $group )
725 {
726 $groups[ $group->g_id ] = $group->name;
727 }
728
729 $types = array();
730 $typeFields = array();
731 $typeFieldToggles = array();
732 $formId = $this->id ? "form_{$this->id}" : 'form_new';
733 foreach ( new \DirectoryIterator( \IPS\ROOT_PATH . '/applications/nexus/sources/Package' ) as $file )
734 {
735 if ( !$file->isDot() and mb_substr( $file, 0, 1 ) !== '.' and $file != 'Dedi.php' and $file != 'index.html' )
736 {
737 require_once $file;
738 $key = mb_substr( $file, 0, -4 );
739 $class = 'IPS\nexus\Package\_' . $key;
740 $autoloadClass = "IPS\\nexus\\Package\\{$key}";
741 if ( class_exists( $class, FALSE ) and in_array( 'IPS\nexus\Package', class_parents( $class ) ) )
742 {
743 $forceShow = TRUE;
744 $types[ mb_strtolower( $key ) ] = 'p_type_' . $key;
745
746 if ( $fields = $autoloadClass::acpFormFields( $this ) and is_array( $fields ) )
747 {
748 foreach ( $fields as $group => $fields )
749 {
750 foreach ( $fields as $field )
751 {
752 if ( $field->name === 'p_show' )
753 {
754 $forceShow = FALSE;
755 }
756 if ( !$field->htmlId )
757 {
758 $field->htmlId = $field->name;
759 $typeFieldToggles[ mb_strtolower( $key ) ][] = $field->htmlId;
760 }
761 $typeFields[ $group ][] = $field;
762 }
763 }
764 }
765
766 if ( $forceShow )
767 {
768 $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' ) );
769 }
770 }
771 }
772 }
773
774 $renewOptions = array();
775 if ( $this->renew_options and $_renewOptions = json_decode( $this->renew_options, TRUE ) and is_array( $_renewOptions ) )
776 {
777 foreach ( $_renewOptions as $option )
778 {
779 $costs = array();
780 foreach ( $option['cost'] as $cost )
781 {
782 $costs[ $cost['currency'] ] = new \IPS\nexus\Money( $cost['amount'], $cost['currency'] );
783 }
784
785 /* Catch any invalid renewal terms, these can occasionally appear from legacy IP.Subscriptions */
786 try
787 {
788 $renewOptions[] = new \IPS\nexus\Purchase\RenewalTerm( $costs, new \DateInterval( "P{$option['term']}" . mb_strtoupper( $option['unit'] ) ), NULL, $option['add'] );
789 }
790 catch( \Exception $ex) {}
791 }
792 }
793
794 $form->addTab('package_settings');
795 $form->addHeader('package_settings');
796 $form->add( new \IPS\Helpers\Form\Radio( 'p_type', ( $this->id ? $this->type : 'product' ), TRUE, array( 'options' => $types, 'toggles' => $typeFieldToggles, 'disabled' => (bool) $this->id ) ) );
797 $form->add( new \IPS\Helpers\Form\Translatable( 'p_name', NULL, TRUE, array( 'app' => 'nexus', 'key' => $this->id ? "nexus_package_{$this->id}" : NULL ) ) );
798 $form->add( new \IPS\Helpers\Form\Node( 'p_group', $this->group, TRUE, array( 'class' => 'IPS\nexus\Package\Group', 'subnodes' => FALSE ) ) );
799 $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 ) ) );
800 foreach ( $typeFields['package_settings'] as $field )
801 {
802 $form->add( $field );
803 }
804 $form->addHeader('package_registration');
805 $form->add( new \IPS\Helpers\Form\YesNo( 'p_reg', $this->reg, FALSE, array(), function( $val )
806 {
807 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 != "*" ) ) ) )
808 {
809 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 ) ) ) );
810 }
811 } ) );
812 $form->addHeader('package_associations');
813 $form->addMessage('package_associations_desc');
814 $form->add( new \IPS\Helpers\Form\Radio( 'p_associate', ( $this->force_assoc and count( $this->associablePackages() ) ) ? 2 : ( count( $this->associablePackages() ) ? 1 : 0 ), FALSE, array(
815 'options' => array(
816 0 => 'p_associate_none',
817 1 => 'p_associate_optional',
818 2 => 'p_associate_required'
819 ),
820 'toggles' => array(
821 1 => array( 'p_associable', 'p_group_renewals', 'p_upsell' ),
822 2 => array( 'p_associable', 'p_group_renewals', 'p_assoc_error_editor', 'p_upsell' )
823 )
824 ), NULL, NULL, NULL, 'p_associate' ) );
825 $form->add( new \IPS\Helpers\Form\Node( 'p_associable', $this->associablePackages(), FALSE, array( 'class' => 'IPS\nexus\Package\Group', 'multiple' => TRUE, 'permissionCheck' => function( $node )
826 {
827 return !( $node instanceof \IPS\nexus\Package\Group );
828 } ), NULL, NULL, NULL, 'p_associable' ) );
829 $form->add( new \IPS\Helpers\Form\YesNo( 'p_group_renewals', $this->group_renewals ?: FALSE, FALSE, array(), NULL, NULL, NULL, 'p_group_renewals' ) );
830 $form->add( new \IPS\Helpers\Form\Translatable( 'p_assoc_error', NULL, FALSE, array(
831 'app' => 'nexus',
832 'key' => $this->id ? "nexus_package_{$this->id}_assoc" : NULL,
833 'editor' => array(
834 'app' => 'nexus',
835 'key' => 'Admin',
836 'autoSaveKey' => ( $this->id ? "nexus-pkg-{$this->id}-assoc" : "nexus-new-pkg-assoc" ),
837 'attachIds' => $this->id ? array( $this->id, NULL, 'pkg-assoc' ) : NULL, 'minimize' => 'p_assoc_error_placeholder'
838 )
839 ), NULL, NULL, NULL, 'p_assoc_error_editor' ) );
840 $form->add( new \IPS\Helpers\Form\YesNo( 'p_upsell', $this->upsell ?: FALSE, FALSE, array(), NULL, NULL, NULL, 'p_upsell' ) );
841
842 $form->addTab( 'package_pricing' );
843 $form->addHeader( 'package_purchase_price' );
844 $form->add( new \IPS\nexus\Form\Money( 'p_base_price', $this->base_price, TRUE ) );
845 $form->add( new \IPS\Helpers\Form\Node( 'p_tax', (int) $this->tax, FALSE, array( 'class' => 'IPS\nexus\Tax', 'zeroVal' => 'do_not_tax' ) ) );
846 $form->addHeader('package_discounts');
847 $discounts = json_decode( $this->discounts, TRUE );
848 $form->add( new \IPS\Helpers\Form\Stack( 'p_usergroup_discounts', isset( $discounts['usergroup'] ) ? $discounts['usergroup'] : array(), FALSE, array( 'stackFieldType' => 'IPS\nexus\Form\DiscountUsergroup' ) ) );
849 $form->add( new \IPS\Helpers\Form\Stack( 'p_loyalty_discounts', isset( $discounts['loyalty'] ) ? $discounts['loyalty'] : array(), FALSE, array( 'stackFieldType' => 'IPS\nexus\Form\DiscountLoyalty' ) ) );
850 $form->add( new \IPS\Helpers\Form\Stack( 'p_bulk_discounts', isset( $discounts['bulk'] ) ? $discounts['bulk'] : array(), FALSE, array( 'stackFieldType' => 'IPS\nexus\Form\DiscountBulk' ) ) );
851 $form->addHeader( 'package_renewals' );
852 $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' ) ), NULL, NULL, NULL, 'p_renews' ) );
853 $form->add( new \IPS\Helpers\Form\Stack( 'p_renew_options', $renewOptions, FALSE, array(
854 'stackFieldType' => 'IPS\nexus\Form\RenewalTerm',
855 'allCurrencies' => TRUE,
856 'addToBase' => TRUE
857 ), NULL, NULL, NULL, 'p_renew_options' ) );
858 $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' ) );
859
860 $form->addTab('package_stock_adjustments');
861 $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' );
862 if ( count( $fields ) )
863 {
864 \IPS\Member::loggedIn()->language()->words['p_stock_price_dynamic_desc'] = '';
865 }
866 $fieldOptions = array();
867 foreach ( $fields as $field )
868 {
869 $fieldOptions[ $field->id ] = $field->_title;
870 }
871 $form->add( new \IPS\Helpers\Form\Radio( 'p_stock_price_type', $this->stock == -2 ? 'dynamic' : 'static', TRUE, array(
872 'options' => array(
873 'static' => 'p_stock_price_static',
874 'dynamic' => 'p_stock_price_dynamic',
875 ),
876 'toggles' => array(
877 'static' => array( 'p_stock' ),
878 'dynamic' => array( 'p_dynamic_stock' )
879 ),
880 'disabled' => count( $fields ) ? array() : array( 'dynamic' )
881 ) ) );
882 $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' ) );
883 $package = $this;
884 $form->add( new \IPS\Helpers\Form\Custom( 'custom_fields', $this->optionIdKeys(), FALSE, array(
885 'rowHtml' => function( $input ) use ( $fields, $package )
886 {
887 return \IPS\Theme::i()->getTemplate('store')->productOptions( $input, $fields, $package );
888 }
889 ), NULL, NULL, NULL, 'p_dynamic_stock' ) );
890
891 $form->addTab('package_store');
892 $form->addHeader('package_store_permissions');
893 $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" ) ) ) );
894 $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' ) );
895 $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' ) ) );
896 foreach ( $typeFields['store_permissions'] as $field )
897 {
898 $form->add( $field );
899 }
900 $form->addHeader('package_store_display');
901 $form->add( new \IPS\Helpers\Form\YesNo( 'p_featured', $this->featured, FALSE, array(), NULL, NULL, NULL, 'p_featured' ) );
902 $form->add( new \IPS\Helpers\Form\Translatable( 'p_desc', NULL, FALSE, array(
903 'app' => 'nexus',
904 'key' => $this->id ? "nexus_package_{$this->id}_desc" : NULL,
905 'editor' => array(
906 'app' => 'nexus',
907 'key' => 'Admin',
908 'autoSaveKey' => ( $this->id ? "nexus-pkg-{$this->id}" : "nexus-new-pkg" ),
909 'attachIds' => $this->id ? array( $this->id, NULL, 'pkg' ) : NULL, 'minimize' => 'p_desc_placeholder'
910 )
911 ), NULL, NULL, NULL, 'p_desc_editor' ) );
912 $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' ) );
913 $form->addHeader('package_reviews');
914 $form->add( new \IPS\Helpers\Form\YesNo( 'p_reviewable', $this->reviewable, FALSE, array( 'togglesOn' => array( 'p_review_moderate' ) ), NULL, NULL, NULL, 'p_reviewable' ) );
915 $form->add( new \IPS\Helpers\Form\YesNo( 'p_review_moderate', $this->review_moderate ?: FALSE, FALSE, array(), NULL, NULL, NULL, 'p_review_moderate' ) );
916
917 $form->addHeader( 'package_benefits' );
918 unset( $groups[ \IPS\Settings::i()->guest_group ] );
919 $form->add( new \IPS\Helpers\Form\Select( 'p_primary_group', $this->primary_group ?: '*', FALSE, array( 'options' => $groups, 'unlimited' => '*', 'unlimitedLang' => 'do_not_change', 'unlimitedToggles' => array( 'p_return_primary' ), 'unlimitedToggleOn' => FALSE ) ) );
920 $form->add( new \IPS\Helpers\Form\YesNo( 'p_return_primary', $this->return_primary, FALSE, array(), NULL, NULL, NULL, 'p_return_primary' ) );
921 $form->add( new \IPS\Helpers\Form\Select( 'p_secondary_group', $this->secondary_group ? explode( ',', $this->secondary_group ) : '*', FALSE, array( 'options' => $groups, 'multiple' => TRUE, 'unlimited' => '*', 'unlimitedLang' => 'do_not_change', 'unlimitedToggles' => array( 'p_return_secondary' ), 'unlimitedToggleOn' => FALSE ) ) );
922 $form->add( new \IPS\Helpers\Form\YesNo( 'p_return_secondary', $this->return_secondary, FALSE, array(), NULL, NULL, NULL, 'p_return_secondary' ) );
923 $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' ) );
924 foreach ( $typeFields['package_benefits'] as $field )
925 {
926 $form->add( $field );
927 }
928 $form->addHeader( 'package_notify' );
929 $form->add( new \IPS\Helpers\Form\Stack( 'p_notify', $this->notify ? explode( ',', $this->notify ) : array(), FALSE, array( 'stackFieldType' => 'Email' ) ) );
930
931 $form->addTab('package_client_area');
932 $form->addHeader('package_client_area_display');
933 $form->add( new \IPS\Helpers\Form\Translatable( 'p_page', NULL, FALSE, array(
934 'app' => 'nexus',
935 'key' => $this->id ? "nexus_package_{$this->id}_page" : NULL,
936 'editor' => array(
937 'app' => 'nexus',
938 'key' => 'Admin',
939 'autoSaveKey' => ( $this->id ? "nexus-pkg-{$this->id}-pg" : "nexus-new-pkg-pg" ),
940 'attachIds' => $this->id ? array( $this->id, NULL, 'pkg-pg' ) : NULL, 'minimize' => 'p_page_placeholder'
941 )
942 ), NULL, NULL, NULL, 'p_desc_editor' ) );
943 $form->add( new \IPS\Helpers\Form\YesNo( 'p_support', $this->support, FALSE, array( 'togglesOn' => array( 'p_support_department' ) ) ) );
944 $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' ) );
945 $form->addHeader('package_client_area_renewals');
946 $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' ) );
947 $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' ) );
948 $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' ) );
949 $form->addHeader('package_upgrade_downgrade');
950 $form->addMessage('package_upgrade_downgrade_desc');
951 $form->add( new \IPS\Helpers\Form\YesNo( 'p_allow_upgrading', $this->allow_upgrading, FALSE, array( 'togglesOn' => array( 'p_upgrade_charge' ) ) ) );
952 $form->add( new \IPS\Helpers\Form\Radio( 'p_upgrade_charge', $this->upgrade_charge, FALSE, array( 'options' => array(
953 0 => 'p_upgrade_charge_none',
954 1 => 'p_upgrade_charge_full',
955 2 => 'p_upgrade_charge_prorate'
956 ) ), NULL, NULL, NULL, 'p_upgrade_charge' ) );
957 $form->add( new \IPS\Helpers\Form\YesNo( 'p_allow_downgrading', $this->allow_downgrading, FALSE, array( 'togglesOn' => array( 'p_downgrade_refund' ) ) ) );
958 $form->add( new \IPS\Helpers\Form\Radio( 'p_downgrade_refund', $this->downgrade_refund, FALSE, array( 'options' => array(
959 0 => 'p_downgrade_refund_none',
960 1 => 'p_downgrade_refund_full',
961 2 => 'p_downgrade_refund_prorate'
962 )), NULL, NULL, NULL, 'p_downgrade_refund' ) );
963 }
964
965 /**
966 * [Node] Save Add/Edit Form
967 *
968 * @param array $values Values from the form
969 * @return void
970 */
971 public function saveForm( $values )
972 {
973 if ( !$this->id )
974 {
975 $this->type = ( isset( $values['p_type'] ) ) ? $values['p_type'] : '';
976 $this->save();
977 unset( static::$multitons[ $this->id ] );
978
979 \IPS\File::claimAttachments( 'nexus-new-pkg', $this->id, NULL, 'pkg', TRUE );
980 \IPS\File::claimAttachments( 'nexus-new-pkg-assoc', $this->id, NULL, 'pkg-assoc', TRUE );
981 \IPS\File::claimAttachments( 'nexus-new-pkg-pg', $this->id, NULL, 'pkg-pg', TRUE );
982
983 $class = 'IPS\nexus\Package\\' . ucfirst( $values['p_type'] );
984
985 if ( isset( $class::$packageDatabaseTable ) )
986 {
987 \IPS\Db::i()->insert( $class::$packageDatabaseTable, array( 'p_id' => $this->id ) );
988 }
989
990 $obj = $class::load( $this->id );
991 return $obj->saveForm( $obj->formatFormValues( $values ) );
992 }
993
994 return parent::saveForm( $values );
995 }
996
997 /**
998 * [Node] Format form values from add/edit form for save
999 *
1000 * @param array $values Values from the form
1001 * @return array
1002 */
1003 public function formatFormValues( $values )
1004 {
1005 if ( !$this->id )
1006 {
1007 return $values;
1008 }
1009
1010 /* Translatables */
1011 foreach ( array( 'name' => '', 'desc' => '_desc', 'assoc_error' => '_assoc', 'page' => '_page' ) as $key => $suffix )
1012 {
1013 if ( isset( $values[ 'p_' . $key ] ) )
1014 {
1015 \IPS\Lang::saveCustom( 'nexus', "nexus_package_{$this->id}{$suffix}", $values[ 'p_' . $key ] );
1016 }
1017 unset( $values[ 'p_' . $key ] );
1018 }
1019
1020 /* Custom Fields */
1021 if( isset( $values['p_custom_fields'] ) )
1022 {
1023 $old = array_keys( \IPS\nexus\Package\CustomField::roots( NULL, NULL, array( array( \IPS\Db::i()->findInSet( 'cf_packages', array( $this->id ) ) ) ) ) );
1024 $new = ( isset( $values['p_custom_fields'] ) and is_array( $values['p_custom_fields'] ) ) ? array_keys( $values['p_custom_fields'] ) : array();
1025 $added = array_diff( $new, $old );
1026 $removed = array_diff( $old, $new );
1027 if ( count( $added ) )
1028 {
1029 foreach ( $added as $id )
1030 {
1031 $field = \IPS\nexus\Package\CustomField::load( $id );
1032 $packages = explode( ',', $field->packages );
1033 $packages[] = $this->id;
1034 $field->packages = implode( ',', $packages );
1035 $field->save();
1036 }
1037 }
1038 if ( count( $removed ) )
1039 {
1040 foreach ( $removed as $id )
1041 {
1042 $field = \IPS\nexus\Package\CustomField::load( $id );
1043 $packages = explode( ',', $field->packages );
1044 unset( $packages[ array_search( $this->id, $packages ) ] );
1045 $field->packages = implode( ',', $packages );
1046 $field->save();
1047 }
1048 }
1049 unset( $values['p_custom_fields'] );
1050 }
1051
1052 /* Normalise */
1053 if( isset( $values['p_group'] ) )
1054 {
1055 $values['p_group'] = $values['p_group']->id;
1056 }
1057
1058 if( isset( $values['p_base_price'] ) )
1059 {
1060 $values['p_base_price'] = json_encode( $values['p_base_price'] );
1061 }
1062
1063 if( isset( $values['p_member_groups'] ) )
1064 {
1065 $values['p_member_groups'] = $values['p_member_groups'] === '*' ? '*' : implode( ',', $values['p_member_groups'] );
1066 }
1067
1068 if( isset( $values['p_primary_group'] ) )
1069 {
1070 $values['p_primary_group'] = $values['p_primary_group'] == '*' ? 0 : $values['p_primary_group'];
1071 }
1072
1073 if( isset( $values['p_secondary_group'] ) )
1074 {
1075 $values['p_secondary_group'] = $values['p_secondary_group'] == '*' ? '' : implode( ',', $values['p_secondary_group'] );
1076 }
1077
1078 if( isset( $values['p_support_department'] ) )
1079 {
1080 $values['p_support_department'] = ( $values['p_support'] and $values['p_support_department'] ) ? $values['p_support_department']->id : 0;
1081 }
1082
1083 if( isset( $values['p_support_severity'] ) )
1084 {
1085 $values['p_support_severity'] = $values['p_support_severity'] ? $values['p_support_severity']->id : 0;
1086 }
1087
1088 if( isset( $values['p_notify'] ) and is_array( $values['p_notify'] ) )
1089 {
1090 $values['p_notify'] = implode( ',', $values['p_notify'] );
1091 }
1092
1093 if( isset( $values['p_methods'] ) )
1094 {
1095 $values['p_methods'] = ( isset( $values['p_methods'] ) and is_array( $values['p_methods'] ) ) ? implode( ',', array_keys( $values['p_methods'] ) ) : '*';
1096 }
1097
1098 if( isset( $values['p_tax'] ) )
1099 {
1100 $values['p_tax'] = $values['p_tax'] ? $values['p_tax']->id : 0;
1101 }
1102
1103 if( isset( $values['p_renew'] ) AND isset( $values['p_renewal_days'] ) )
1104 {
1105 $values['p_renewal_days'] = $values['p_renew'] ? $values['p_renewal_days'] : 0;
1106 }
1107
1108 /* Renewal options */
1109 if( isset( $values['p_renews'] ) )
1110 {
1111 if ( $values['p_renews'] )
1112 {
1113 $renewOptions = array();
1114 foreach ( $values['p_renew_options'] as $option )
1115 {
1116 $term = $option->getTerm();
1117 $renewOptions[] = array(
1118 'cost' => $option->cost,
1119 'term' => $term['term'],
1120 'unit' => $term['unit'],
1121 'add' => $option->addToBase
1122 );
1123 }
1124 $values['p_renew_options'] = json_encode( $renewOptions );
1125 }
1126 else
1127 {
1128 $values['p_renew_options'] = '';
1129 }
1130 }
1131
1132 /* Associate */
1133 if ( isset( $values['p_associate'] ) )
1134 {
1135 switch ( $values['p_associate'] )
1136 {
1137 case 0:
1138 $values['p_associable'] = '';
1139 $values['p_force_assoc'] = 0;
1140 break;
1141
1142 case 1:
1143 $values['p_associable'] = implode( ',', array_map( function( $node ) { return $node->id; }, $values['p_associable'] ) );
1144 $values['p_force_assoc'] = 0;
1145 break;
1146
1147 case 2:
1148 $values['p_associable'] = implode( ',', array_map( function( $node ) { return $node->id; }, $values['p_associable'] ) );
1149 $values['p_force_assoc'] = 1;
1150 break;
1151 }
1152 }
1153
1154 /* Product Options */
1155 \IPS\Db::i()->delete( 'nexus_product_options', array( 'opt_package=?', $this->id ) );
1156 if ( isset( $values['p_stock_price_type'], $values['custom_fields'] ) )
1157 {
1158 if ( $values['p_stock_price_type'] === 'dynamic' )
1159 {
1160 $values['p_stock'] = -2;
1161 foreach ( $values['custom_fields'] as $k => $data )
1162 {
1163 \IPS\Db::i()->insert( 'nexus_product_options', array(
1164 'opt_package' => $this->id,
1165 'opt_values' => rawurldecode( $k ),
1166 'opt_stock' => isset( $data['unlimitedStock'] ) ? -1 : intval( $data['stock'] ),
1167 'opt_base_price' => json_encode( $data['bpa'] ),
1168 'opt_renew_price' => isset( $data['rpa'] ) ? json_encode( $data['rpa'] ) : ''
1169 ) );
1170 }
1171 }
1172 unset( $values['p_stock_price_type'] );
1173 unset( $values['custom_fields'] );
1174 }
1175
1176 /* Discounts */
1177 if( isset( $values['p_usergroup_discounts'] ) OR isset( $values['p_loyalty_discounts'] ) OR isset( $values['p_bulk_discounts'] ) )
1178 {
1179 $discounts = array();
1180 }
1181
1182 if( isset( $values['p_usergroup_discounts'] ) )
1183 {
1184 foreach ( $values['p_usergroup_discounts'] as $k => $data )
1185 {
1186 if ( isset( $data['group'] ) and $data['group'] and isset( $data['price'] ) )
1187 {
1188 foreach( $data['price'] as $price )
1189 {
1190 if ( !$price )
1191 {
1192 continue 2;
1193 }
1194 }
1195
1196 $discounts['usergroup'][] = array(
1197 'group' => $data['group'],
1198 'price' => $data['price'],
1199 'secondary' => (int) isset( $data['secondary'] ),
1200 );
1201 }
1202 }
1203 unset( $values['p_usergroup_discounts'] );
1204 }
1205
1206 if( isset( $values['p_loyalty_discounts'] ) )
1207 {
1208 foreach ( $values['p_loyalty_discounts'] as $k => $data )
1209 {
1210 if ( $data['owns'] )
1211 {
1212 foreach( $data['price'] as $price )
1213 {
1214 if ( !$price )
1215 {
1216 continue 2;
1217 }
1218 }
1219
1220 $packageId = $data['package'] ? mb_substr( $data['package'], 0, mb_strpos( $data['package'], '.' ) ) : 0;
1221
1222 $discounts['loyalty'][] = array(
1223 'owns' => $data['owns'],
1224 'package' => ( $data['this'] or ( $this->id and $this->id == $packageId ) ) ? '0' : $packageId,
1225 'price' => $data['price'],
1226 'active' => (int) isset( $data['active'] ),
1227 );
1228 }
1229 }
1230 unset( $values['p_loyalty_discounts'] );
1231 }
1232
1233 if( isset( $values['p_bulk_discounts'] ) )
1234 {
1235 foreach ( $values['p_bulk_discounts'] as $k => $data )
1236 {
1237 if ( $data['buying'] )
1238 {
1239 foreach( $data['price'] as $price )
1240 {
1241 if ( !$price )
1242 {
1243 continue 2;
1244 }
1245 }
1246
1247 $packageId = $data['package'] ? mb_substr( $data['package'], 0, mb_strpos( $data['package'], '.' ) ) : 0;
1248
1249 $discounts['bulk'][] = array(
1250 'buying' => $data['buying'],
1251 'package' => ( $data['this'] or ( $this->id and $this->id == $packageId ) ) ? '0' : $packageId,
1252 'price' => $data['price'],
1253 );
1254 }
1255 }
1256 unset( $values['p_bulk_discounts'] );
1257 }
1258
1259 if( isset( $discounts ) )
1260 {
1261 $values['discounts'] = json_encode( $discounts );
1262 }
1263
1264 /* Images */
1265 if( isset( $values['p_images'] ) )
1266 {
1267 \IPS\Db::i()->delete( 'nexus_package_images', array( 'image_product=?', $this->id ) );
1268
1269 if( !empty( $values['p_images'] ) )
1270 {
1271 $primary = TRUE;
1272 foreach ( $values['p_images'] as $_key => $image )
1273 {
1274 if ( \IPS\Request::i()->p_images_primary_image )
1275 {
1276 $primary = ( \IPS\Request::i()->p_images_primary_image == $_key ) ? 1 : 0;
1277 }
1278
1279 \IPS\Db::i()->insert( 'nexus_package_images', array(
1280 'image_product' => $this->id,
1281 'image_location' => (string) $image,
1282 'image_primary' => $primary,
1283 ) );
1284
1285 if ( $primary )
1286 {
1287 $this->image = $image;
1288 }
1289
1290 $primary = FALSE;
1291 }
1292 }
1293 else
1294 {
1295 $this->image = NULL;
1296 }
1297 }
1298
1299 /* Save */
1300 return $values;
1301 }
1302
1303 /**
1304 * ACP Fields
1305 *
1306 * @param \IPS\nexus\Package $package The package
1307 * @param bool $custom If TRUE, is for a custom package
1308 * @param bool $customEdit If TRUE, is editing a custom package
1309 * @return array
1310 */
1311 public static function acpFormFields( \IPS\nexus\Package $package, $custom=FALSE, $customEdit=FALSE )
1312 {
1313 return array();
1314 }
1315
1316 /**
1317 * Updateable fields
1318 *
1319 * @return array
1320 */
1321 public static function updateableFields()
1322 {
1323 return array(
1324 'renew_options',
1325 'primary_group',
1326 'secondary_group',
1327 'group_renewals'
1328 );
1329 }
1330
1331 /**
1332 * Update existing purchases
1333 *
1334 * @param \IPS\nexus\Purchase $purchase The purchase
1335 * @param array $changes The old values
1336 * @return void
1337 */
1338 public function updatePurchase( \IPS\nexus\Purchase $purchase, $changes )
1339 {
1340 if ( array_key_exists( 'renew_options', $changes ) and !empty( $changes['renew_options'] ) )
1341 {
1342 $newRenewTerms = json_decode( $this->renew_options, TRUE );
1343
1344 if( !is_array( $newRenewTerms ) )
1345 {
1346 $newRenewTerms = array();
1347 }
1348
1349 $key = 'x';
1350 if ( $purchase->renewals )
1351 {
1352 foreach ( $changes['renew_options'] as $k => $data )
1353 {
1354 if ( $data['old'] != 'x' )
1355 {
1356 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 )
1357 {
1358 $term = $purchase->renewals->getTerm();
1359 if ( $data['old']['term'] == $term['term'] and $data['old']['unit'] == $term['unit'] )
1360 {
1361 $key = $k;
1362 break;
1363 }
1364 }
1365 }
1366 }
1367 }
1368
1369 switch ( $changes['renew_options'][ $key ]['new'] )
1370 {
1371 case 'x':
1372 $purchase->renewals = NULL;
1373 $purchase->save();
1374 break;
1375 case 'y':
1376 $purchase->renewals = NULL;
1377 $purchase->active = TRUE;
1378 $purchase->save();
1379 break;
1380 case 'z':
1381 $purchase->renewals = NULL;
1382 $purchase->active = FALSE;
1383 $purchase->save();
1384 break;
1385 case '-':
1386 // do nothing
1387 break;
1388 default:
1389 if ( mb_substr( $changes['renew_options'][ $key ]['new'], 0, 1 ) === 'o' )
1390 {
1391 $key = mb_substr( $changes['renew_options'][ $key ]['new'], 1 );
1392 if ( isset( $newRenewTerms[ $key ] ) )
1393 {
1394 $tax = NULL;
1395 if ( $purchase->tax )
1396 {
1397 try
1398 {
1399 $tax = \IPS\nexus\Tax::load( $purchase->tax );
1400 }
1401 catch ( \OutOfRangeException $e ) { }
1402 }
1403
1404 $currency = $purchase->renewal_currency ?: $purchase->member->defaultCurrency( );
1405
1406 $purchase->renewals = new \IPS\nexus\Purchase\RenewalTerm(
1407 new \IPS\nexus\Money( $newRenewTerms[ $key ]['cost'][ $currency ]['amount'], $currency ),
1408 new \DateInterval( 'P' . $newRenewTerms[ $key ]['term'] . mb_strtoupper( $newRenewTerms[ $key ]['unit'] ) ),
1409 $tax
1410 );
1411 $purchase->save();
1412 }
1413 }
1414 break;
1415 }
1416 }
1417
1418 if ( array_key_exists( 'primary_group', $changes ) or array_key_exists( 'secondary_group', $changes ) )
1419 {
1420 $this->_removeUsergroups( $purchase, $changes );
1421 $this->_addUsergroups( $purchase );
1422 }
1423
1424 if ( array_key_exists( 'group_renewals', $changes ) )
1425 {
1426 if ( $this->group_renewals )
1427 {
1428 try
1429 {
1430 $purchase->groupWithParent();
1431 }
1432 catch ( \LogicException $e ) { }
1433 }
1434 else
1435 {
1436 try
1437 {
1438 $purchase->ungroupFromParent();
1439 }
1440 catch ( \LogicException $e ) { }
1441 }
1442 }
1443 }
1444
1445 /* !Store */
1446
1447 /**
1448 * Store Form
1449 *
1450 * @param \IPS\Helpers\Form $form The form
1451 * @param string $memberCurrency The currency being used
1452 * @return void
1453 */
1454 public function storeForm( \IPS\Helpers\Form $form, $memberCurrency )
1455 {
1456 if( $this->subscription )
1457 {
1458 $form->hiddenValues['quantity'] = 1;
1459 }
1460 else
1461 {
1462 $form->add( new \IPS\Helpers\Form\Number('quantity', 1, TRUE, array( 'min' => 1 ) ) );
1463 }
1464 }
1465
1466 /**
1467 * Add To Cart
1468 *
1469 * @param \IPS\nexus\extensions\nexus\Item\Package $item The item
1470 * @param array $values Values from form
1471 * @param string $memberCurrency The currency being used
1472 * @return array Additional items to add
1473 */
1474 public function addToCart( \IPS\nexus\extensions\nexus\Item\Package $item, array $values, $memberCurrency )
1475 {
1476
1477 }
1478
1479 /**
1480 * Get item
1481 *
1482 * @return \IPS\nexus\Package\Item
1483 */
1484 public function item()
1485 {
1486 $data = array();
1487 foreach ( $this->_data as $k => $v )
1488 {
1489 $data[ 'p_' . $k ] = $v;
1490 }
1491
1492 return \IPS\nexus\Package\Item::constructFromData( $data );
1493 }
1494
1495 /**
1496 * Get option values for stock/price adjustments
1497 *
1498 * @param array $details Custom field values
1499 * @return array
1500 */
1501 public function optionValues( $details )
1502 {
1503 $optionValues = array();
1504 if ( $this->stock == -2 )
1505 {
1506 foreach( $this->optionIdKeys() as $k )
1507 {
1508 $optionValues[ $k ] = $details[ $k ];
1509 }
1510 }
1511 return $optionValues;
1512 }
1513
1514 /**
1515 * Get stock and price for option values
1516 *
1517 * @param array $optionValues Option values
1518 * @param bool $includeDiscounts Get discounted price?
1519 * @return array( 'price' => \IPS\nexus\Money, 'stock' => 5, 'renewalAdjustment' => 0 ) 'stock' will be -1 for unlimited, 0 for none in stock
1520 */
1521 public function optionValuesStockAndPrice( $optionValues, $includeDiscounts=TRUE )
1522 {
1523 /* Dynamic */
1524 if ( $this->stock == -2 )
1525 {
1526 $chosenOption = \IPS\Db::i()->select( '*', 'nexus_product_options', array( 'opt_package=? AND opt_values=?', $this->id, json_encode( $optionValues ) ) )->first();
1527
1528 /* Price */
1529 $basePriceAdjustments = json_decode( $chosenOption['opt_base_price'], TRUE );
1530 $defaultPrice = $this->price( NULL, $includeDiscounts, $includeDiscounts, $includeDiscounts );
1531 if ( isset( $basePriceAdjustments[ $defaultPrice->currency ] ) )
1532 {
1533 $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 );
1534 }
1535 else
1536 {
1537 $price = $defaultPrice;
1538 }
1539
1540 $renewalAdjustment = 0;
1541 if ( $chosenOption['opt_renew_price'] )
1542 {
1543 $renewPriceAdjustments = json_decode( $chosenOption['opt_renew_price'], TRUE );
1544 if ( isset( $renewPriceAdjustments[ $price->currency ] ) )
1545 {
1546 $renewalAdjustment = $renewPriceAdjustments[ $price->currency ];
1547 }
1548 }
1549
1550 /* Return */
1551 return array( 'price' => $price, 'stock' => $chosenOption['opt_stock'], 'renewalAdjustment' => $renewalAdjustment );
1552 }
1553 /* Static */
1554 else
1555 {
1556 return array( 'price' => $this->price(), 'stock' => $this->stock, 'renewalAdjustment' => 0 );
1557 }
1558 }
1559
1560 /**
1561 * Create Item ibject
1562 *
1563 * @param \IPS\nexus\Package $package The package
1564 * @param \IPS\nexus\Money $price Price
1565 * @return \IPS\nexus\extensions\nexus\Item\Package
1566 */
1567 public function createItemForCart( \IPS\nexus\Money $price )
1568 {
1569 $item = new \IPS\nexus\extensions\nexus\Item\Package( \IPS\Member::loggedIn()->language()->get( 'nexus_package_' . $this->id ), $price );
1570 $item->id = $this->id;
1571 $item->tax = $this->tax ? \IPS\nexus\Tax::load( $this->tax ) : NULL;
1572 if ( $this instanceof \IPS\nexus\Package\Product and $this->physical )
1573 {
1574 $item->physical = TRUE;
1575 $item->weight = new \IPS\nexus\Shipping\Weight( $this->weight );
1576 $item->length = new \IPS\nexus\Shipping\Length( $this->length );
1577 $item->width = new \IPS\nexus\Shipping\Length( $this->width );
1578 $item->height = new \IPS\nexus\Shipping\Length( $this->height );
1579 if ( $this->shipping !== '*' )
1580 {
1581 $item->shippingMethodIds = explode( ',', $this->shipping );
1582 }
1583 }
1584 if ( $this->methods and $this->methods != '*' )
1585 {
1586 $item->paymentMethodIds = explode( ',', $this->methods );
1587 }
1588 if ( $this->group_renewals )
1589 {
1590 $item->groupWithParent = TRUE;
1591 }
1592
1593 return $item;
1594 }
1595
1596 /**
1597 * Add items into cart
1598 *
1599 * @param array $details Custom field values
1600 * @param int $quantity Quantity to add
1601 * @param \IPS\nexus\Purchase\RenewalTerm|NULL $renewalTerm Chosen renewal term, if applicable
1602 * @param \IPS\nexus\Purchase|int $parent Parent purchase, if applicable
1603 * @param array|NULL $values If adding from the main purchase field, any additional values which may be used for extra items
1604 * @return int
1605 * @note Does not verify stock. Stock check first
1606 */
1607 public function addItemsToCartData( $details=array(), $quantity=1, \IPS\nexus\Purchase\RenewalTerm $renewalTerm = NULL, $parent=NULL, $values=NULL )
1608 {
1609 $optionValues = $this->optionValues( $details );
1610 $return = array();
1611
1612 /* Work out how we're splitting these */
1613 $items = array();
1614 $previousPrice = NULL;
1615 $inThisBatch = 0;
1616 for ( $i=0; $i<$quantity;$i++ )
1617 {
1618 $price = $this->price( NULL, TRUE, TRUE, TRUE, $i );
1619 $memberCurrency = $price->currency;
1620 $price = $price->amount;
1621
1622 if ( $previousPrice !== NULL and $previousPrice != $price )
1623 {
1624 $items[] = $inThisBatch;
1625 $inThisBatch = 0;
1626 }
1627
1628 $inThisBatch++;
1629 $previousPrice = $price;
1630 }
1631
1632 if ( $inThisBatch )
1633 {
1634 $items[] = $inThisBatch;
1635 }
1636
1637 /* And do it */
1638 foreach ( $items as $quantity )
1639 {
1640 /* Get base price */
1641 $price = $this->price()->amount;
1642
1643 /* Renewal term */
1644 if ( $renewalTerm and $renewalTerm->addToBase )
1645 {
1646 $price = $price->add( $renewalTerm->cost->amount );
1647 }
1648
1649 /* Adjustments based on custom fields */
1650 if ( $this->stock == -2 )
1651 {
1652 try
1653 {
1654 $chosenOption = \IPS\Db::i()->select( '*', 'nexus_product_options', array( 'opt_package=? AND opt_values=?', $this->id, json_encode( $optionValues ) ) )->first();
1655 $basePriceAdjustments = json_decode( $chosenOption['opt_base_price'], TRUE );
1656 if ( isset( $basePriceAdjustments[ $memberCurrency ] ) )
1657 {
1658 $price = $price->add( new \IPS\Math\Number( number_format( $basePriceAdjustments[ $memberCurrency ], \IPS\nexus\Money::numberOfDecimalsForCurrency( $memberCurrency ), '.', '' ) ) );
1659 }
1660 }
1661 catch ( \UnderflowException $e ) {}
1662 }
1663
1664 /* Create the item */
1665 $item = $this->createItemForCart( new \IPS\nexus\Money( $price, $memberCurrency ) );
1666 $item->renewalTerm = $renewalTerm;
1667 $item->quantity = $quantity;
1668 $item->details = $details;
1669
1670 /* Associations */
1671 if ( $parent !== NULL )
1672 {
1673 $item->parent = $parent;
1674 }
1675
1676 /* Do any package-sepcific modifications */
1677 $extraItems = ( $values === NULL ) ? array() : $this->addToCart( $item, $values, $memberCurrency );
1678
1679 /* Hang on, is that the same as anything else in the cart? */
1680 $added = FALSE;
1681 foreach ( $_SESSION['cart'] as $_id => $_item )
1682 {
1683 $cloned = clone $_item;
1684 $cloned->quantity = $quantity;
1685
1686 if ( $cloned == $item )
1687 {
1688 $_item->quantity += $quantity;
1689 $added = TRUE;
1690 $cartId = $_id;
1691 break;
1692 }
1693 }
1694
1695 /* Nope, go ahead and add it */
1696 if ( !$added )
1697 {
1698 $_SESSION['cart'][] = $item;
1699 $keys = array_keys( $_SESSION['cart'] );
1700 $cartId = array_pop( $keys );
1701 }
1702
1703 /* Associate extras */
1704 if ( count( $extraItems ) )
1705 {
1706 foreach ( $extraItems as $item )
1707 {
1708 $item->parent = $cartId;
1709 $_SESSION['cart'][] = $item;
1710 }
1711 }
1712 }
1713
1714 /* Turn off guest page caching */
1715 if ( \IPS\CACHE_PAGE_TIMEOUT and !\IPS\Member::loggedIn()->member_id )
1716 {
1717 \IPS\Request::i()->setCookie( 'noCache', 1 );
1718 }
1719
1720 /* Return */
1721 return $cartId;
1722 }
1723
1724 /* !Client Area */
1725
1726 /**
1727 * Show Purchase Record?
1728 *
1729 * @return bool
1730 */
1731 public function showPurchaseRecord()
1732 {
1733 return TRUE;
1734 }
1735
1736 /**
1737 * Get Client Area Page HTML
1738 *
1739 * @param \IPS\nexus\Purchase $purchase The purchase
1740 * @return array( 'packageInfo' => '...', 'purchaseInfo' => '...' )
1741 */
1742 public function clientAreaPage( \IPS\nexus\Purchase $purchase )
1743 {
1744 /* Custom Fields */
1745 $editable = FALSE;
1746 $customFieldsForm = new \IPS\Helpers\Form;
1747 $customFieldsForm->class = 'ipsForm_vertical';
1748 $customFields = \IPS\nexus\Package\CustomField::roots( NULL, NULL, array( array( \IPS\Db::i()->findInSet( 'cf_packages', array( $this->id ) ) ) ) );
1749 foreach ( $customFields as $field )
1750 {
1751 $value = isset( $purchase->custom_fields[ $field->id ] ) ? $purchase->custom_fields[ $field->id ] : NULL;
1752 if ( $field->editable )
1753 {
1754 $editable = TRUE;
1755 $customFieldsForm->add( $field->buildHelper( $value ) );
1756 }
1757 else
1758 {
1759 $customFieldsForm->addDummy( $field->_title, htmlspecialchars( $value, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) );
1760 }
1761 }
1762 if ( !$editable )
1763 {
1764 $customFieldsForm->actionButtons = array();
1765 }
1766
1767 if( $values = $customFieldsForm->values() )
1768 {
1769 foreach ( $customFields as $field )
1770 {
1771 if( array_key_exists( $field->type, \IPS\nexus\Package\CustomField::$additionalFieldTypes ) )
1772 {
1773 $class = 'IPS\nexus\Form\\' . $field->type;
1774 }
1775 else
1776 {
1777 $class = 'IPS\Helpers\Form\\' . $field->type;
1778 }
1779
1780 $customFields[ $field->id ] = $class::stringValue( $values[ 'nexus_pfield_' . $field->id ] );
1781 }
1782
1783 $purchase->custom_fields = $customFields;
1784 $purchase->save();
1785 }
1786
1787 /* Support Link */
1788 $supportUrl = NULL;
1789 if ( $purchase->active and $this->support )
1790 {
1791 $supportUrl = \IPS\Http\Url::internal( 'app=nexus&module=support&controller=home&do=create&purchase=' . $purchase->id, 'front', 'support_create' );
1792 if ( $this->support_department )
1793 {
1794 $supportUrl = $supportUrl->setQueryString( 'department', $this->support_department );
1795 }
1796 }
1797
1798 /* Stuff only the owner or "billing" alternate contact can do */
1799 $reactivateUrl = NULL;
1800 $upgradeDowngradeUrl = NULL;
1801 $upgradeDowngradeLang = 'change_package';
1802 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' ) ) ) ) )
1803 {
1804 /* Reactivate */
1805 if ( !$purchase->renewals and count( json_decode( $this->renew_options, TRUE ) ) and $purchase->can_reactivate and ( !$purchase->billing_agreement or $purchase->billing_agreement->status() == \IPS\nexus\Customer\BillingAgreement::STATUS_CANCELED ) )
1806 {
1807 $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();
1808 }
1809
1810 /* Upgrade/Downgrade */
1811 $includesUpgrades = FALSE;
1812 $includesDowngrades = FALSE;
1813 if( count( $this->upgradeDowngradeOptions( $purchase, FALSE, $includesUpgrades, $includesDowngrades ) ) )
1814 {
1815 $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 ) );
1816 if ( $includesUpgrades and !$includesDowngrades )
1817 {
1818 $upgradeDowngradeLang = 'change_package_upgrade';
1819 }
1820 elseif ( !$includesUpgrades and $includesDowngrades )
1821 {
1822 $upgradeDowngradeLang = 'change_package_downgrade';
1823 }
1824 }
1825 }
1826
1827 /* Associated Downloads Files */
1828 $associatedFiles = array();
1829 if ( \IPS\Application::appIsEnabled( 'downloads' ) and \IPS\Settings::i()->idm_nexus_on )
1830 {
1831 $associatedFiles = \IPS\downloads\File::getItemsWithPermission( array( array( \IPS\Db::i()->findInSet( 'file_nexus', array( $this->id ) ) ) ) );
1832 }
1833
1834 /* Associated support requests */
1835 $last5AssociatedSupportRequests = \IPS\nexus\Support\Request::getItemsWithPermission( array( array( 'r_purchase=?', $purchase->id ) ), 'r_last_reply DESC', 5 );
1836
1837 /* Display */
1838 return array(
1839 'packageInfo' => \IPS\Member::loggedIn()->language()->checkKeyExists("nexus_package_{$this->id}_page") ? \IPS\Member::loggedIn()->language()->addToStack("nexus_package_{$this->id}_page") : '',
1840 'purchaseInfo' => \IPS\Theme::i()->getTemplate( 'purchases' )->package( $this, $customFieldsForm, $supportUrl, $reactivateUrl, $upgradeDowngradeUrl, $upgradeDowngradeLang, $associatedFiles, $last5AssociatedSupportRequests ),
1841 );
1842 }
1843
1844 /**
1845 * Client Area Action
1846 *
1847 * @param \IPS\nexus\Purchase $purchase The purchase
1848 * @return string
1849 */
1850 public function clientAreaAction( \IPS\nexus\Purchase $purchase )
1851 {
1852 switch ( \IPS\Request::i()->act )
1853 {
1854 case 'change':
1855 $form = $this->upgradeDowngradeForm( $purchase );
1856 if ( $values = $form->values() )
1857 {
1858 $newPackage = \IPS\nexus\Package::load( $values['change_package_to'] );
1859 $chosenRenewalOption = isset( $values["renew_option_{$newPackage->id}" ] ) ? $values[ "renew_option_{$newPackage->id}" ] : NULL;
1860
1861 /* 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 */
1862 if ( $chosenRenewalOption === NULL )
1863 {
1864 $renewOptions = json_decode( $newPackage->renew_options, TRUE );
1865 if ( count( $renewOptions ) === 1 )
1866 {
1867 $chosenRenewalOption = key( $renewOptions );
1868 }
1869 }
1870
1871 $invoice = $this->upgradeDowngrade( $purchase, $newPackage, $chosenRenewalOption );
1872 if ( $invoice )
1873 {
1874 \IPS\Output::i()->redirect( $invoice->checkoutUrl() );
1875 }
1876 $purchase->member->log( 'purchase', array( 'type' => 'change', 'id' => $purchase->id, 'old' => $purchase->name, 'name' => $newPackage->_title, 'system' => FALSE ) );
1877 \IPS\Output::i()->redirect( $purchase->url() );
1878 }
1879 return (string) $form;
1880 break;
1881
1882 case 'reactivate':
1883 $currency = $purchase->original_invoice->currency;
1884 $renewalOptions = json_decode( $this->renew_options, TRUE );
1885 $noOptions = count( $renewalOptions ) === 1;
1886
1887 if ( $noOptions )
1888 {
1889 \IPS\Session::i()->csrfCheck();
1890 $term = array_pop( $renewalOptions );
1891 $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'] ) ) );
1892 $purchase->cancelled = FALSE;
1893 $purchase->save();
1894
1895 foreach ( $purchase->children() as $child )
1896 {
1897 if ( $child->grouped_renewals )
1898 {
1899 $child->groupWithParent();
1900 }
1901 }
1902
1903 $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() ) ) );
1904
1905 if ( !$purchase->active and $purchase->canRenewUntil() !== FALSE )
1906 {
1907 \IPS\Output::i()->redirect( $purchase->url()->setQueryString( 'do', 'renew' )->csrf() );
1908 }
1909 else
1910 {
1911 \IPS\Output::i()->redirect( $purchase->url() );
1912 }
1913 }
1914
1915 $form = new \IPS\Helpers\Form( 'reactivate', 'reactivate' );
1916 $form->class = 'ipsForm_vertical';
1917 $options = array();
1918 foreach ( $renewalOptions as $k => $term )
1919 {
1920 $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'] ) ) );
1921 }
1922 $form->add( new \IPS\Helpers\Form\Radio( 'ps_renewals', NULL, TRUE, array( 'options' => $options, 'parse' => 'none' ) ) );
1923 if ( $values = $form->values() )
1924 {
1925 $term = $renewalOptions[ $values['ps_renewals'] ];
1926 $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'] ) ) );
1927 $purchase->cancelled = FALSE;
1928 $purchase->save();
1929
1930 foreach ( $purchase->children() as $child )
1931 {
1932 if ( $child->grouped_renewals )
1933 {
1934 $child->groupWithParent();
1935 }
1936 }
1937
1938 $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() ) ) );
1939
1940 if ( !$purchase->active and $purchase->canRenewUntil() !== FALSE )
1941 {
1942 \IPS\Output::i()->redirect( $purchase->url()->setQueryString( 'do', 'renew' )->csrf() );
1943 }
1944 else
1945 {
1946 \IPS\Output::i()->redirect( $purchase->url() );
1947 }
1948 }
1949 return (string) $form;
1950 break;
1951 }
1952 }
1953
1954 /* !ACP */
1955
1956 /**
1957 * ACP Add to invoice
1958 *
1959 * @param \IPS\nexus\extensions\nexus\Item\Package $item The item
1960 * @param array $values Values from form
1961 * @param string $k The key to add to the field names
1962 * @param \IPS\nexus\Invoice $invoice The invoice
1963 * @return void
1964 */
1965 public function acpAddToInvoice( \IPS\nexus\extensions\nexus\Item\Package $item, array $values, $k, \IPS\nexus\Invoice $invoice )
1966 {
1967
1968 }
1969
1970 /**
1971 * Get ACP Page Buttons
1972 *
1973 * @param \IPS\nexus\Purchase $purchase The purchase
1974 * @param \IPS\Http\Url $url The page URL
1975 * @return array
1976 */
1977 public function acpButtons( \IPS\nexus\Purchase $purchase, $url )
1978 {
1979 $return = array();
1980
1981 if ( count( $this->upgradeDowngradeOptions( $purchase, TRUE ) ) )
1982 {
1983 $return['change'] = array(
1984 'icon' => 'archive',
1985 'title' => 'change_package',
1986 'link' => $url->setQueryString( array( 'do' => 'extra', 'act' => 'change' ) ),
1987 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('change_package') )
1988 );
1989 }
1990
1991 return $return;
1992 }
1993
1994 /**
1995 * Get ACP Page HTML
1996 *
1997 * @return string
1998 */
1999 public function acpPage( \IPS\nexus\Purchase $purchase )
2000 {
2001 return '';
2002 }
2003
2004 /**
2005 * ACP Action
2006 *
2007 * @param \IPS\nexus\Purchase $purchase The purchase
2008 * @return string|void
2009 */
2010 public function acpAction( \IPS\nexus\Purchase $purchase )
2011 {
2012 switch ( \IPS\Request::i()->act )
2013 {
2014 case 'change':
2015 $form = $this->upgradeDowngradeForm( $purchase, TRUE );
2016 $form->class = 'ipsForm_horizontal';
2017 $form->add( new \IPS\Helpers\Form\YesNo( 'change_package_ship_charges', FALSE, TRUE ) );
2018 if ( $values = $form->values() )
2019 {
2020 $newPackage = \IPS\nexus\Package::load( $values['change_package_to'] );
2021 $invoice = $this->upgradeDowngrade( $purchase, $newPackage, isset( $values[ "renew_option_{$newPackage->id}" ] ) ? $values[ "renew_option_{$newPackage->id}" ] : NULL, $values['change_package_ship_charges'] );
2022 if ( $invoice )
2023 {
2024 \IPS\Output::i()->redirect( $invoice->acpUrl() );
2025 }
2026 $purchase->member->log( 'purchase', array( 'type' => 'change', 'id' => $purchase->id, 'old' => $purchase->name, 'name' => $newPackage->_title, 'system' => FALSE ) );
2027 \IPS\Output::i()->redirect( $purchase->acpUrl() );
2028 }
2029 return (string) $form;
2030 break;
2031 }
2032 }
2033
2034 /**
2035 * ACP Edit Form
2036 *
2037 * @param \IPS\nexus\Purchase $purchase The purchase
2038 * @param \IPS\Helpers\Form $form The form
2039 * @param \IPS\nexus\Purchase\RenewalTerm $renewals The renewal term
2040 * @return string
2041 */
2042 public function acpEdit( \IPS\nexus\Purchase $purchase, \IPS\Helpers\Form $form, $renewals )
2043 {
2044 $form->addHeader('menu__nexus_store_fields');
2045 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 )
2046 {
2047 $form->add( $field->buildHelper( isset( $purchase->custom_fields[ $field->id ] ) ? $purchase->custom_fields[ $field->id ] : NULL ) );
2048 }
2049 }
2050
2051 /**
2052 * ACP Edit Save
2053 *
2054 * @param \IPS\nexus\Purchase $purchase The purchase
2055 * @param array $values Values from form
2056 * @return string
2057 */
2058 public function acpEditSave( \IPS\nexus\Purchase $purchase, array $values )
2059 {
2060 $customFields = $purchase->custom_fields;
2061
2062 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 )
2063 {
2064 if( array_key_exists( $field->type, \IPS\nexus\Package\CustomField::$additionalFieldTypes ) and !class_exists( 'IPS\Helpers\Form\\' . $field->type ) )
2065 {
2066 $class = 'IPS\nexus\Form\\' . $field->type;
2067 }
2068 else
2069 {
2070 $class = 'IPS\Helpers\Form\\' . $field->type;
2071 }
2072
2073 $customFields[ $field->id ] = $class::stringValue( $values[ 'nexus_pfield_' . $field->id ] );
2074 }
2075
2076 $purchase->custom_fields = $customFields;
2077 $purchase->save();
2078 }
2079
2080 /**
2081 * Get ACP Support View HTML
2082 *
2083 * @return string
2084 */
2085 public function acpSupportView( \IPS\nexus\Purchase $purchase )
2086 {
2087 return '';
2088 }
2089
2090 /* !Actions */
2091
2092 /**
2093 * On Paid
2094 *
2095 * @param \IPS\nexus\Invoice $invoice The invoice
2096 * @return void
2097 */
2098 public function onPaid( \IPS\nexus\Invoice $invoice )
2099 {
2100 // Blank for hooking
2101 }
2102
2103 /**
2104 * On Unpaid description
2105 *
2106 * @param \IPS\nexus\Invoice $invoice The invoice
2107 * @return array
2108 */
2109 public function onUnpaidDescription( \IPS\nexus\Invoice $invoice )
2110 {
2111 // Blank for hooking
2112 return array();
2113 }
2114
2115 /**
2116 * On Unpaid
2117 *
2118 * @param \IPS\nexus\Invoice $invoice The invoice
2119 * @param string $status Status
2120 * @return void
2121 */
2122 public function onUnpaid( \IPS\nexus\Invoice $invoice, $status )
2123 {
2124 // Blank for hooking
2125 }
2126
2127 /**
2128 * On Invoice Cancel (when unpaid)
2129 *
2130 * @param \IPS\nexus\Invoice $invoice The invoice
2131 * @return void
2132 */
2133 public function onInvoiceCancel( \IPS\nexus\Invoice $invoice )
2134 {
2135 // Blank for hooking
2136 }
2137
2138 /**
2139 * Check for member
2140 * If a user initially checks out as a guest and then logs in during checkout, this method
2141 * is ran to check the items they are purchasing can be bought.
2142 * Is expected to throw a DomainException with an error message to display to the user if not valid
2143 *
2144 * @param \IPS\Member $member The new member
2145 * @return void
2146 * @throws \DomainException
2147 */
2148 public function memberCanPurchase( \IPS\Member $member )
2149 {
2150 // Handled by subclasses
2151 }
2152
2153 /**
2154 * Can Renew Until
2155 *
2156 * @param \IPS\nexus\Purchase $purchase The purchase
2157 * @param bool $admin If TRUE, is for ACP. If FALSE, is for front-end.
2158 * @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
2159 */
2160 public function canRenewUntil( \IPS\nexus\Purchase $purchase, $admin )
2161 {
2162 if ( $admin )
2163 {
2164 return TRUE;
2165 }
2166
2167 if ( !$this->renewal_days )
2168 {
2169 return FALSE;
2170 }
2171 elseif ( $this->renewal_days != -1 )
2172 {
2173 $diff = $purchase->expire->diff( \IPS\DateTime::create() );
2174 if ( $diff->invert and $diff->days > $this->renewal_days )
2175 {
2176 return FALSE;
2177 }
2178 }
2179
2180 if ( $this->renewal_days_advance == -1 )
2181 {
2182 return TRUE;
2183 }
2184 else
2185 {
2186 $baseDate = ( $purchase->expire->getTimestamp() > time() ) ? $purchase->expire : \IPS\DateTime::create();
2187 return $baseDate->add( new \DateInterval( 'P' . $this->renewal_days_advance . 'D' ) );
2188 }
2189 }
2190
2191 /**
2192 * On Purchase Generated
2193 *
2194 * @param \IPS\nexus\Purchase $purchase The purchase
2195 * @param \IPS\nexus\Invoice $invoice The invoice
2196 * @return void
2197 */
2198 public function onPurchaseGenerated( \IPS\nexus\Purchase $purchase, \IPS\nexus\Invoice $invoice )
2199 {
2200 $this->_changeStockLevel( -1, $this->_getOptionId( $purchase ) );
2201 $this->_addUsergroups( $purchase );
2202
2203 if ( $this->notify )
2204 {
2205 $email = \IPS\Email::buildFromTemplate( 'nexus', 'purchaseNotify', array( $purchase, $invoice ), \IPS\Email::TYPE_LIST );
2206 foreach ( explode( ',', $this->notify ) as $to )
2207 {
2208 $email->send( $to );
2209 }
2210 }
2211 }
2212
2213 /**
2214 * On Renew (Renewal invoice paid. Is not called if expiry data is manually changed)
2215 *
2216 * @param \IPS\nexus\Purchase $purchase The purchase
2217 * @param int $cycles Cycles
2218 * @return void
2219 */
2220 public function onRenew( \IPS\nexus\Purchase $purchase, $cycles )
2221 {
2222 // Blank for hooking
2223 }
2224
2225 /**
2226 * On Expiration Date Change
2227 *
2228 * @param \IPS\nexus\Purchase $purchase The purchase
2229 * @return void
2230 */
2231 public function onExpirationDateChange( \IPS\nexus\Purchase $purchase )
2232 {
2233
2234 }
2235
2236 /**
2237 * On Purchase Expired
2238 *
2239 * @param \IPS\nexus\Purchase $purchase The purchase
2240 * @return void
2241 */
2242 public function onExpire( \IPS\nexus\Purchase $purchase )
2243 {
2244 $this->_removeUsergroups( $purchase );
2245 }
2246
2247 /**
2248 * On Purchase Canceled
2249 *
2250 * @param \IPS\nexus\Purchase $purchase The purchase
2251 * @return void
2252 */
2253 public function onCancel( \IPS\nexus\Purchase $purchase )
2254 {
2255 $this->_changeStockLevel( 1, $this->_getOptionId( $purchase ) );
2256 $this->_removeUsergroups( $purchase );
2257 }
2258
2259 /**
2260 * On Purchase Deleted
2261 *
2262 * @param \IPS\nexus\Purchase $purchase The purchase
2263 * @return void
2264 */
2265 public function onDelete( \IPS\nexus\Purchase $purchase )
2266 {
2267 $this->onCancel( $purchase );
2268 }
2269
2270 /**
2271 * On Purchase Reactivated (renewed after being expired or reactivated after being canceled)
2272 *
2273 * @param \IPS\nexus\Purchase $purchase The purchase
2274 * @return void
2275 */
2276 public function onReactivate( \IPS\nexus\Purchase $purchase )
2277 {
2278 $this->_changeStockLevel( -1, $this->_getOptionId( $purchase ) );
2279 $this->_addUsergroups( $purchase );
2280 }
2281
2282 /**
2283 * On Transfer (is ran before transferring)
2284 *
2285 * @param \IPS\nexus\Purchase $purchase The purchase
2286 * @param \IPS\Member $newCustomer New Customer
2287 * @return void
2288 */
2289 public function onTransfer( \IPS\nexus\Purchase $purchase, \IPS\Member $newCustomer )
2290 {
2291 $this->_removeUsergroups( $purchase );
2292 $purchase->member = $newCustomer;
2293 $this->_addUsergroups( $purchase );
2294 }
2295
2296 /**
2297 * On Upgrade/Downgrade
2298 *
2299 * @param \IPS\nexus\Purchase $purchase The purchase
2300 * @param \IPS\nexus\Package $newPackage The package to upgrade to
2301 * @param int|NULL|\IPS\nexus\Purchase\RenewalTerm $chosenRenewalOption The chosen renewal option
2302 * @return void
2303 */
2304 public function onChange( \IPS\nexus\Purchase $purchase, \IPS\nexus\Package $newPackage, $chosenRenewalOption = NULL )
2305 {
2306
2307 }
2308
2309 /* !Upgrading/Downgrading */
2310
2311 /**
2312 * Upgrade/Downgrade Options
2313 *
2314 * @param \IPS\nexus\Purchase $purchase The purchase
2315 * @param bool $overrideRestrictions If true, will be able to choose any packages in the same group
2316 * @param bool $includesUpgrades Will be set if there are upgrade options, useful for knowing what text to show
2317 * @param bool $includesDowngrades Will be set if there are downgrade options, useful for knowing what text to show
2318 * @return array
2319 */
2320 protected function upgradeDowngradeOptions( \IPS\nexus\Purchase $purchase, $overrideRestrictions = FALSE, &$includesUpgrades=FALSE, &$includesDowngrades=FALSE )
2321 {
2322 if ( $this->custom or !$purchase->active or ( !$this->allow_upgrading and !$this->allow_upgrading and !$overrideRestrictions ) or $purchase->grouped_renewals or ( $purchase->billing_agreement and $purchase->billing_agreement->status() != \IPS\nexus\Customer\BillingAgreement::STATUS_CANCELED ) )
2323 {
2324 return array();
2325 }
2326 else
2327 {
2328 $return = array();
2329 $currency = $purchase->original_invoice->currency;
2330 $prices = json_decode( $this->base_price, TRUE );
2331 $thisPrice = $prices[ $currency ]['amount'];
2332 foreach ( $this->parent()->children() as $package )
2333 {
2334 if ( $package->id === $this->id )
2335 {
2336 continue;
2337 }
2338
2339 $prices = json_decode( $package->base_price, TRUE );
2340 if ( !isset( $prices[ $currency ] ) )
2341 {
2342 continue;
2343 }
2344 $price = $prices[ $currency ]['amount'];
2345
2346 /* Does Renewal add to base price */
2347 if ( $package->renew_options )
2348 {
2349 $renewalOptions = json_decode( $package->renew_options, TRUE );
2350 if ( !empty( $renewalOptions ) )
2351 {
2352 $option = array_shift( $renewalOptions );
2353 if ( $option['add'] )
2354 {
2355 $price += ( $option['cost'][ $currency ]['amount'] );
2356 }
2357 }
2358 }
2359
2360 /* Upgrade */
2361 if ( $price >= $thisPrice and ( $this->allow_upgrading or $overrideRestrictions ) )
2362 {
2363 $return[] = $package;
2364 $includesUpgrades = TRUE;
2365 }
2366
2367 /* Downgrade */
2368 elseif ( $price < $thisPrice and ( $this->allow_downgrading or $overrideRestrictions ) )
2369 {
2370 $return[] = $package;
2371 $includesDowngrades = TRUE;
2372 }
2373 }
2374
2375 return $return;
2376 }
2377 }
2378
2379 /**
2380 * Upgrade/Downgrade Form
2381 *
2382 * @param \IPS\nexus\Purchase $purchase The purchase
2383 * @param bool $overrideRestrictions If true, will be able to choose any packages in the same group
2384 * @return \IPS\Helpers\Form
2385 */
2386 protected function upgradeDowngradeForm( \IPS\nexus\Purchase $purchase, $overrideRestrictions = FALSE )
2387 {
2388 $currency = $purchase->original_invoice->currency;
2389 $options = array();
2390 $desciptions = array();
2391 $toggles = array();
2392 $renewFields = array();
2393 foreach ( $this->upgradeDowngradeOptions( $purchase, $overrideRestrictions ) as $package )
2394 {
2395 $options[ $package->id ] = $package->_title;
2396
2397 $renewOptions = json_decode( $package->renew_options, TRUE ) ?: array();
2398 $renewalOptions = array();
2399 foreach ( $renewOptions as $k => $option )
2400 {
2401 $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'] ) ) );
2402 }
2403
2404 if ( count( $renewalOptions ) === 1 )
2405 {
2406 foreach ( $renewalOptions as $k => $v )
2407 {
2408 try
2409 {
2410 $costToUpgrade = $package->costToUpgrade( $purchase, $k );
2411 }
2412 catch ( \InvalidArgumentException $e )
2413 {
2414 unset( $options[ $package->id ] );
2415 continue 2;
2416 }
2417 if ( $costToUpgrade->amount->isZero() )
2418 {
2419 $desciptions[ $package->id ] = $v;
2420 }
2421 elseif( $costToUpgrade->amount->isPositive() )
2422 {
2423 $desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'upgrade_cost_and_renew', FALSE, array( 'sprintf' => array( $costToUpgrade, $v ) ) );
2424 }
2425 else
2426 {
2427 $costToUpgrade->amount = $costToUpgrade->amount->multiply( new \IPS\Math\Number( '-1' ) );
2428 $desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'downgrade_refund_and_renew', FALSE, array( 'sprintf' => array( $costToUpgrade, $v ) ) );
2429 }
2430 }
2431 }
2432 elseif ( count( $renewalOptions ) === 0 )
2433 {
2434 try
2435 {
2436 $costToUpgrade = $package->costToUpgrade( $purchase );
2437 }
2438 catch ( \InvalidArgumentException $e )
2439 {
2440 unset( $options[ $package->id ] );
2441 continue;
2442 }
2443 if( $costToUpgrade->amount->isGreaterThanZero() )
2444 {
2445 $desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'upgrade_cost', FALSE, array( 'sprintf' => array( $costToUpgrade ) ) );
2446 }
2447 elseif( !$costToUpgrade->amount->isPositive() )
2448 {
2449 $costToUpgrade->amount = $costToUpgrade->amount->multiply( new \IPS\Math\Number( '-1' ) );
2450 $desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'downgrade_refund', FALSE, array( 'sprintf' => array( $costToUpgrade ) ) );
2451 }
2452 }
2453 else
2454 {
2455 $renewalOptionsString = \IPS\Member::loggedIn()->language()->formatList( $renewalOptions, \IPS\Member::loggedIn()->language()->get('or_list_format') );
2456 $renewFieldsDescriptions = array();
2457 try
2458 {
2459 $costToUpgrade = $package->costToUpgrade( $purchase );
2460 if ( $costToUpgrade->amount->isZero() )
2461 {
2462 $desciptions[ $package->id ] = $renewalOptionsString;
2463 }
2464 elseif( $costToUpgrade->amount->isGreaterThanZero() )
2465 {
2466 $desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'upgrade_cost_and_renew', FALSE, array( 'sprintf' => array( $costToUpgrade, $renewalOptionsString ) ) );
2467 }
2468 else
2469 {
2470 $costToUpgrade->amount = $costToUpgrade->amount->multiply( new \IPS\Math\Number( '-1' ) );
2471 $desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'downgrade_refund_and_renew', FALSE, array( 'sprintf' => array( $costToUpgrade, $renewalOptionsString ) ) );
2472 }
2473 }
2474 catch ( \InvalidArgumentException $e )
2475 {
2476 if ( intval( $e->getMessage() ) >= 0 )
2477 {
2478 $desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'unknown_upgrade_charge_and_renew', FALSE, array( 'sprintf' => array( $renewalOptionsString ) ) );
2479 }
2480 else
2481 {
2482 $desciptions[ $package->id ] = \IPS\Member::loggedIn()->language()->addToStack( 'unknown_downgrade_refund_and_renew', FALSE, array( 'sprintf' => array( $renewalOptionsString ) ) );
2483 }
2484
2485 foreach ( $renewalOptions as $k => $v )
2486 {
2487 $costToUpgrade = $package->costToUpgrade( $purchase, $k );
2488 if( $costToUpgrade->amount->isGreaterThanZero() )
2489 {
2490 $renewFieldsDescriptions[ $k ] = \IPS\Member::loggedIn()->language()->addToStack( 'upgrade_cost', FALSE, array( 'sprintf' => array( $costToUpgrade ) ) );
2491 }
2492 elseif( !$costToUpgrade->amount->isPositive() )
2493 {
2494 $costToUpgrade->amount = $costToUpgrade->amount->multiply( new \IPS\Math\Number( '-1' ) );
2495 $renewFieldsDescriptions[ $k ] = \IPS\Member::loggedIn()->language()->addToStack( 'downgrade_refund', FALSE, array( 'sprintf' => array( $costToUpgrade ) ) );
2496 }
2497 }
2498 }
2499
2500 $renewFields[] = new \IPS\Helpers\Form\Radio( 'renew_option_' . $package->id, NULL, NULL, array( 'options' => $renewalOptions, 'descriptions' => $renewFieldsDescriptions, 'parse' => 'none' ), NULL, NULL, NULL, 'renew_option_' . $package->id );
2501 \IPS\Member::loggedIn()->language()->words['renew_option_' . $package->id] = \IPS\Member::loggedIn()->language()->addToStack( 'renewal_term', FALSE );
2502 $toggles[ $package->id ] = array( 'renew_option_' . $package->id );
2503 }
2504 }
2505
2506 $form = new \IPS\Helpers\Form;
2507 $form->class = 'ipsForm_vertical';
2508 $form->add( new \IPS\Helpers\Form\Radio( 'change_package_to', NULL, TRUE, array( 'options' => $options, 'descriptions' => $desciptions, 'toggles' => $toggles, 'parse' => 'none' ) ) );
2509 foreach ( $renewFields as $field )
2510 {
2511 $form->add( $field );
2512 }
2513
2514 return $form;
2515 }
2516
2517 /**
2518 * Cost to upgrade to this package (may return negative value for refund)
2519 *
2520 * @param \IPS\nexus\Purchase $purchase The purchase to be upgraded
2521 * @param int $renewalOption The key for the chosen renewal option
2522 * @return \IPS\nexus\Money|NULL
2523 */
2524 protected function costToUpgrade( \IPS\nexus\Purchase $purchase, $renewalOption = NULL )
2525 {
2526 $package = \IPS\nexus\Package::load( $purchase->item_id );
2527 $currency = $purchase->original_invoice->currency;
2528
2529 $priceOfExistingPackage = json_decode( $package->base_price, TRUE );
2530 $priceOfExistingPackage = $priceOfExistingPackage[ $currency ]['amount'];
2531
2532 $priceOfThisPackage = json_decode( $this->base_price, TRUE );
2533 $priceOfThisPackage = $priceOfThisPackage[ $currency ]['amount'];
2534
2535 if ( $priceOfThisPackage >= $priceOfExistingPackage )
2536 {
2537 $type = $package->upgrade_charge;
2538 }
2539 else
2540 {
2541 $type = $package->downgrade_refund;
2542 }
2543
2544 switch ( $type )
2545 {
2546 case 0:
2547 return new \IPS\nexus\Money( 0, $currency );
2548
2549 case 1:
2550 return new \IPS\nexus\Money( $priceOfThisPackage - $priceOfExistingPackage, $currency );
2551
2552 case 2:
2553
2554 if ( !$purchase->expire )
2555 {
2556 return new \IPS\nexus\Money( 0, $currency );
2557 }
2558
2559 $renewalOptions = json_decode( $this->renew_options, TRUE );
2560
2561 if ( $renewalOption === NULL or !isset( $renewalOptions[ $renewalOption ] ) )
2562 {
2563 throw new \InvalidArgumentException( $priceOfThisPackage - $priceOfExistingPackage );
2564 }
2565
2566 $option = $renewalOptions[ $renewalOption ];
2567 $option = new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $option['cost'][ $currency ]['amount'], $currency ), new \DateInterval( 'P' . $option['term'] . mb_strtoupper( $option['unit'] ) ) );
2568 $daysLeftUntilExpiry = new \IPS\Math\Number( (string) \IPS\DateTime::create()->diff( $purchase->expire )->days );
2569 $newCostPerDay = $option->costPerDay()->amount;
2570 $oldCostPerDay = $purchase->renewals->costPerDay()->amount;
2571 return new \IPS\nexus\Money( $newCostPerDay->multiply( $daysLeftUntilExpiry )->subtract( $oldCostPerDay->multiply( $daysLeftUntilExpiry ) ), $currency );
2572 }
2573 }
2574
2575 /**
2576 * Actually Upgrade/Downgrade
2577 *
2578 * @param \IPS\nexus\Purchase $purchase The purchase
2579 * @param \IPS\nexus\Package $newPackage The package to upgrade to
2580 * @param int|NULL|\IPS\nexus\Purchase\RenewalTerm $chosenRenewalOption The chosen renewal option
2581 * @param bool $skipCharge If TRUE, an upgrade charges and downgrade refunds will not be issued
2582 * @return \IPS\nexus\Invoice|void An invoice if an upgrade charge has to be paid, or void if not
2583 * @throws \InvalidArgumentException If $chosenRenewalOption is invalid
2584 */
2585 public function upgradeDowngrade( \IPS\nexus\Purchase $purchase, \IPS\nexus\Package $newPackage, $chosenRenewalOption = NULL, $skipCharge = FALSE )
2586 {
2587 /* Charge / Refund */
2588 if ( !$skipCharge )
2589 {
2590 $costToUpgrade = $newPackage->costToUpgrade( $purchase, $chosenRenewalOption );
2591
2592 /* Upgrade Charge */
2593 if ( $costToUpgrade->amount->isGreaterThanZero() )
2594 {
2595 $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 );
2596 $item->tax = $newPackage->tax ? \IPS\nexus\Tax::load( $newPackage->tax ) : NULL;
2597 $item->id = $purchase->id;
2598 $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 );
2599
2600 $invoice = new \IPS\nexus\Invoice;
2601 $invoice->member = $purchase->member;
2602 $invoice->currency = $costToUpgrade->currency;
2603 $invoice->addItem( $item );
2604 $invoice->return_uri = "app=nexus&module=clients&controller=purchases&do=view&id={$purchase->id}";
2605 $invoice->renewal_ids = array( $purchase->id );
2606 $invoice->save();
2607 return $invoice;
2608 }
2609 elseif ( !$costToUpgrade->amount->isPositive() )
2610 {
2611 $credits = $purchase->member->cm_credits;
2612 $credits[ $costToUpgrade->currency ]->amount = $credits[ $costToUpgrade->currency ]->amount->add( $costToUpgrade->amount->multiply( new \IPS\Math\Number( '-1' ) ) );
2613 $purchase->member->cm_credits = $credits;
2614 $purchase->member->save();
2615 }
2616 }
2617
2618 /* Work out the new renewal term */
2619 if ( $chosenRenewalOption instanceof \IPS\nexus\Purchase\RenewalTerm )
2620 {
2621 $term = $chosenRenewalOption;
2622 }
2623 else
2624 {
2625 $term = NULL;
2626 $renewalOptions = json_decode( $newPackage->renew_options, TRUE );
2627 if ( $chosenRenewalOption === NULL )
2628 {
2629 if ( count( $renewalOptions ) === 1 )
2630 {
2631 $term = array_pop( $renewalOptions );
2632 }
2633 elseif ( count( $renewalOptions ) !== 0 )
2634 {
2635 throw new \InvalidArgumentException;
2636 }
2637 }
2638 else
2639 {
2640 if ( isset( $renewalOptions[ $chosenRenewalOption ] ) )
2641 {
2642 $term = $renewalOptions[ $chosenRenewalOption ];
2643 }
2644 else
2645 {
2646 throw new \InvalidArgumentException;
2647 }
2648 }
2649 if ( $term )
2650 {
2651 $currency = $purchase->original_invoice->currency;
2652 $term = new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $term['cost'][$currency]['amount'], $currency ), new \DateInterval( 'P' . $term['term'] . mb_strtoupper( $term['unit'] ) ) );
2653 }
2654 }
2655
2656 /* Do package-specific stuff */
2657 $this->onChange( $purchase, $newPackage, $chosenRenewalOption );
2658
2659 /* Remove usergroups */
2660 $this->_removeUsergroups( $purchase );
2661
2662 /* Ungroup any children */
2663 $groupedChildren = array();
2664 foreach ( $purchase->children() as $child )
2665 {
2666 if ( $child->grouped_renewals )
2667 {
2668 $child->ungroupFromParent();
2669 $groupedChildren[] = $child;
2670 }
2671 }
2672
2673 /* If we didn't have an expiry date before, but the new package has a renewal term, set an expiry date */
2674 if ( !$purchase->expire and $term )
2675 {
2676 $purchase->expire = \IPS\DateTime::create()->add( $term->interval );
2677 }
2678 /* OR if we did have an expiry date, but the new package does not have a renewal term, remove it */
2679 elseif ( !$term )
2680 {
2681 $purchase->expire = NULL;
2682 }
2683
2684 /* Update Purchase */
2685 $purchase->name = \IPS\Member::loggedIn()->language()->get( "nexus_package_{$newPackage->id}" );
2686 $purchase->item_id = $newPackage->id;
2687 $purchase->renewals = $term;
2688 $purchase->save();
2689
2690 /* Regroup children */
2691 foreach ( $groupedChildren as $child )
2692 {
2693 $child->groupWithParent();
2694 }
2695
2696 /* Re-add usergroups */
2697 $newPackage->_addUsergroups( $purchase );
2698
2699 /* Cancel any pending invoices */
2700 if ( $pendingInvoice = $purchase->invoice_pending )
2701 {
2702 $pendingInvoice->status = \IPS\nexus\invoice::STATUS_CANCELED;
2703 $pendingInvoice->save();
2704 $purchase->invoice_pending = NULL;
2705 $purchase->save();
2706 }
2707
2708 /* Regenerate license key */
2709 try
2710 {
2711 if ( $old = $purchase->licenseKey() )
2712 {
2713 $old->delete();
2714 }
2715 }
2716 catch ( \OutOfRangeException $e ) { }
2717 if ( $newPackage->lkey )
2718 {
2719 $class = 'IPS\nexus\Purchase\LicenseKey\\' . ucfirst( $newPackage->lkey );
2720 $licenseKey = new $class;
2721 $licenseKey->identifier = $newPackage->lkey_identifier;
2722 $licenseKey->purchase = $purchase;
2723 $licenseKey->max_uses = $newPackage->lkey_uses;
2724 $licenseKey->save();
2725 }
2726 }
2727
2728 /* !Stock */
2729
2730 /**
2731 * Change Stock Level
2732 *
2733 * @param int $changeBy Amount to change by
2734 * @param int|NULL $optionId Product option ID, if applicable
2735 * @return void
2736 */
2737 protected function _changeStockLevel( $changeBy, $optionId=NULL )
2738 {
2739 if ( $this->stock == -2 )
2740 {
2741 if ( $optionId !== NULL )
2742 {
2743 try
2744 {
2745 $stock = \IPS\Db::i()->select( 'opt_stock', 'nexus_product_options', array( 'opt_id=?', $optionId ) )->first();
2746 if ( $stock != -1 )
2747 {
2748 $newValue = $stock + $changeBy;
2749 if ( $newValue < 0 )
2750 {
2751 $newValue = 0;
2752 }
2753
2754 \IPS\Db::i()->update( 'nexus_product_options', array( 'opt_stock' => $newValue ), array( 'opt_id=?', $optionId ) );
2755 }
2756 }
2757 catch ( \UnderflowException $e ) { }
2758 }
2759 }
2760 elseif ( $this->stock > 0 )
2761 {
2762 $newValue = $this->stock + $changeBy;
2763 if ( $newValue < 0 )
2764 {
2765 $newValue = 0;
2766 }
2767 $this->stock = $newValue;
2768 $this->save();
2769 }
2770 }
2771
2772 /**
2773 * @brief Fields which affect the option ID
2774 */
2775 protected $_optionIdKeys = NULL;
2776
2777 /**
2778 * Get field IDs which affect the option ID
2779 *
2780 * @return array
2781 */
2782 public function optionIdKeys()
2783 {
2784 if( $this->_optionIdKeys === NULL )
2785 {
2786 $this->_optionIdKeys = array();
2787 try
2788 {
2789 $options = \IPS\Db::i()->select( 'opt_values', 'nexus_product_options', array( 'opt_package=?', $this->id ) )->first();
2790 if ( $options = json_decode( $options, TRUE ) )
2791 {
2792 $this->_optionIdKeys = array_keys( $options );
2793 }
2794
2795 }
2796 catch ( \UnderflowException $e ) { }
2797 }
2798 return $this->_optionIdKeys;
2799 }
2800
2801 /**
2802 * Get option ID
2803 *
2804 * @param \IPS\nexus\Purchase $purchase The purchase
2805 * @return int|NULL
2806 */
2807 protected function _getOptionId( \IPS\nexus\Purchase $purchase )
2808 {
2809 $optionValues = array();
2810 foreach( $this->optionIdKeys() as $id )
2811 {
2812 $optionValues[ $id ] = $purchase->custom_fields[$id];
2813 }
2814
2815 try
2816 {
2817 return \IPS\Db::i()->select( 'opt_id', 'nexus_product_options', array( 'opt_package=? AND opt_values=?', $this->id, json_encode( $optionValues ) ) )->first();
2818 }
2819 catch ( \UnderflowException $e )
2820 {
2821 return NULL;
2822 }
2823 }
2824
2825 /* !Usergroups */
2826
2827 /**
2828 * Add user groups
2829 *
2830 * @param \IPS\nexus\Purchase $purchase The purchase
2831 * @return void
2832 */
2833 public function _addUsergroups( \IPS\nexus\Purchase $purchase )
2834 {
2835 $extra = $purchase->extra;
2836
2837 /* Primary Group */
2838 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 ) ) )
2839 {
2840 /* Hang on, are we about to boot someone out the ACP? */
2841 if ( ! ( \IPS\Member\Group::load( $purchase->member->member_group_id )->g_access_cp and !\IPS\Member\Group::load( $this->primary_group )->g_access_cp ) )
2842 {
2843 /* Save the current group */
2844 $extra['old_primary_group'] = $purchase->member->member_group_id;
2845 $purchase->extra = $extra;
2846 $purchase->save();
2847 if ( !$purchase->member->cm_return_group )
2848 {
2849 $purchase->member->cm_return_group = $purchase->member->member_group_id;
2850 }
2851
2852 /* And update to the new group */
2853 $purchase->member->member_group_id = $this->primary_group;
2854 $purchase->member->save();
2855 }
2856 }
2857
2858 /* Secondary Groups */
2859 $secondary = array_filter( explode( ',', $this->secondary_group ), create_function( '$v', 'return (bool) $v;' ) );
2860 $current_secondary = $purchase->member->mgroup_others ? explode( ',', $purchase->member->mgroup_others ) : array();
2861 $new_secondary = $current_secondary;
2862 if ( !empty( $secondary ) )
2863 {
2864 foreach ( $secondary as $gid )
2865 {
2866 if ( !in_array( $gid, $new_secondary ) )
2867 {
2868 $new_secondary[] = $gid;
2869 }
2870 }
2871 }
2872 if ( $current_secondary != $new_secondary )
2873 {
2874 $extra['old_secondary_groups'] = $purchase->member->mgroup_others;
2875 $purchase->extra = $extra;
2876 $purchase->save();
2877
2878 $purchase->member->mgroup_others = ',' . implode( ',', $new_secondary ) . ',';
2879 $purchase->member->save();
2880 }
2881 }
2882
2883 /**
2884 * Remove user groups
2885 *
2886 * @param \IPS\nexus\Purchase $purchase The purchase
2887 * @param array $basis The current package data, if needed to override
2888 * @return void
2889 */
2890 public function _removeUsergroups( \IPS\nexus\Purchase $purchase, $basis = array() )
2891 {
2892 $basis = array_merge( $this->_data, $basis );
2893 $extra = $purchase->extra;
2894
2895 /* Primary Group */
2896 if ( $basis['return_primary'] )
2897 {
2898 /* We only want to move them back if they haven't been moved again since */
2899 if ( $purchase->member->member_group_id == $basis['primary_group'] )
2900 {
2901 /* Have we made other purchases that have changed their primary group? */
2902 try
2903 {
2904 $purchase->member->member_group_id = \IPS\Db::i()->select( 'p_primary_group', 'nexus_purchases', array( 'ps_member=? AND ps_app=? AND ps_type=? AND ps_active=1 AND p_primary_group<>0 AND ps_id<>?', $purchase->member->member_id, 'nexus', 'package', $purchase->id ) )
2905 ->join( 'nexus_packages', 'p_id=ps_item_id' )
2906 ->first();
2907 $purchase->member->save();
2908 }
2909 /* No, move them to their original group */
2910 catch ( \UnderflowException $e )
2911 {
2912 $purchase->member->member_group_id = $purchase->member->cm_return_group;
2913
2914 /* Does this group exist? */
2915 try
2916 {
2917 \IPS\Member\Group::load( $purchase->member->member_group_id );
2918 }
2919 catch ( \OutOfRangeException $e )
2920 {
2921 $purchase->member->member_group_id = \IPS\Settings::i()->member_group;
2922 }
2923
2924 /* Save */
2925 $purchase->member->cm_return_group = 0;
2926 $purchase->member->save();
2927 }
2928 }
2929 }
2930
2931 // Secondary groups
2932 if ( isset( $extra['old_secondary_groups'] ) and $extra['old_secondary_groups'] !== NULL and $basis['return_secondary'] )
2933 {
2934 /* Work some stuff out */
2935 $membersSecondaryGroups = $purchase->member->mgroup_others ? array_unique( array_filter( explode( ',', $purchase->member->mgroup_others ) ) ) : array();
2936 $membersPreviousSecondaryGroupsBeforeThisPurchase = array_unique( array_filter( explode( ',', $extra['old_secondary_groups'] ) ) );
2937 $secondaryGroupsAwardedByThisPurchase = array_unique( array_filter( explode( ',', $basis['secondary_group'] ) ) );
2938
2939 /* Have we made other purchases that have added secondary groups? */
2940 $secondaryGroupsAwardedByOtherPurchases = array();
2941 foreach ( \IPS\Db::i()->select( 'p_secondary_group', 'nexus_purchases', array( 'ps_member=? AND ps_app=? AND ps_type=? AND ps_active=1 AND p_secondary_group IS NOT NULL AND p_secondary_group<>? AND ps_id<>?', $purchase->member->member_id, 'nexus', 'package', '', $purchase->id ) )->join( 'nexus_packages', 'p_id=ps_item_id' ) as $secondaryGroups )
2942 {
2943 $secondaryGroupsAwardedByOtherPurchases = array_merge( $secondaryGroupsAwardedByOtherPurchases, array_filter( explode( ',', $secondaryGroups ) ) );
2944 }
2945 $secondaryGroupsAwardedByOtherPurchases = array_unique( $secondaryGroupsAwardedByOtherPurchases );
2946
2947 /* Loop through */
2948 foreach ( $secondaryGroupsAwardedByThisPurchase as $groupId )
2949 {
2950 /* If we had this group before we made this purchase, we're going to keep it */
2951 if ( in_array( $groupId, $membersPreviousSecondaryGroupsBeforeThisPurchase ) )
2952 {
2953 continue;
2954 }
2955
2956 /* If we are being awarded this group by a different purchase, we're also going to keep it */
2957 if ( in_array( $groupId, $secondaryGroupsAwardedByOtherPurchases ) )
2958 {
2959 continue;
2960 }
2961
2962 /* If we're still here, remove it */
2963 unset( $membersSecondaryGroups[ array_search( $groupId, $membersSecondaryGroups ) ] );
2964 }
2965
2966 /* Save */
2967 $purchase->member->mgroup_others = implode( ',', $membersSecondaryGroups );
2968 $purchase->member->save();
2969 }
2970 }
2971}