Version 4.7.0

This commit is contained in:
Neo committed 2025-12-19 16:00:35 -08:00
1 parent 517a5e1f70
commit 8ba7d43898
1933 files changed
+65613 -11332

No files matched your search

@@ -209,7 +209,7 @@ class _Braintree extends \IPS\nexus\Gateway
'line1' => isset( $invoice->shipaddress->addressLines[0] ) ? $invoice->shipaddress->addressLines[0] : NULL,
'line2' => isset( $invoice->shipaddress->addressLines[1] ) ? $invoice->shipaddress->addressLines[1] : NULL,
'city' => $invoice->shipaddress->city,
'state' => $invoice->shipaddress->region,
'state' => $settings['type'] === 'paypal' ? $this->_payPalState( $invoice->shipaddress ) : $invoice->shipaddress->region,
'postalCode' => $invoice->shipaddress->postalCode,
'countryCode' => $invoice->shipaddress->country,
'phone' => $invoice->member->cm_phone,
@@ -259,7 +259,7 @@ class _Braintree extends \IPS\nexus\Gateway
'lastName' => $invoice->member->cm_last_name,
'streetAddress' => $invoice->billaddress ? implode( ', ', $invoice->billaddress->addressLines ) : NULL,
'locality' => $invoice->billaddress ? $invoice->billaddress->city : NULL,
'region' => $invoice->billaddress ? $invoice->billaddress->region : NULL,
'region' => $invoice->billaddress ? ( $settings['type'] === 'paypal' ? $this->_payPalState( $invoice->billaddress ) : $invoice->billaddress->region ) : NULL,
'postalCode' => $invoice->billaddress ? $invoice->billaddress->postalCode : NULL,
'countryCodeAlpha2' => $invoice->billaddress ? $invoice->billaddress->country : NULL,
'phoneNumber' => preg_replace( '/[^0-9]/', '', $invoice->member->cm_phone ),
@@ -358,7 +358,7 @@ class _Braintree extends \IPS\nexus\Gateway
'lastName' => $transaction->invoice->member->cm_last_name,
'locality' => $transaction->invoice->billaddress->city,
'postalCode' => $transaction->invoice->billaddress->postalCode,
'region' => $transaction->invoice->billaddress->region,
'region' => $settings['type'] === 'paypal' ? $this->_payPalState( $transaction->invoice->billaddress ) : $transaction->invoice->billaddress->region,
'streetAddress' => implode( ', ', $transaction->invoice->billaddress->addressLines )
);
@@ -392,7 +392,7 @@ class _Braintree extends \IPS\nexus\Gateway
'lastName' => $transaction->invoice->member->cm_last_name,
'locality' => $transaction->invoice->shipaddress->city,
'postalCode' => $transaction->invoice->shipaddress->postalCode,
'region' => $transaction->invoice->shipaddress->region,
'region' => $settings['type'] === 'paypal' ? $this->_payPalState( $transaction->invoice->billaddress ) : $transaction->invoice->billaddress->region,
'streetAddress' => implode( ', ', $transaction->invoice->shipaddress->addressLines )
);
@@ -985,5 +985,28 @@ class _Braintree extends \IPS\nexus\Gateway
return $errorMessage;
}
/**
* Get short code for PayPal State
*
* @param \IPS\GeoLocation $address
* @return string
*/
protected function _payPalState( \IPS\GeoLocation $address ): string
{
/* PayPal requires short codes for states */
$state = $address->region;
if ( isset( \IPS\nexus\Customer\Address::$stateCodes[ $address->country ] ) )
{
if ( !array_key_exists( $state, \IPS\nexus\Customer\Address::$stateCodes[ $address->country ] ) )
{
$_state = array_search( $address->region, \IPS\nexus\Customer\Address::$stateCodes[ $address->country ] );
if ( $_state !== FALSE )
{
$state = $_state;
}
}
}
return $state;
}
}
+50 -3
View File
@@ -183,7 +183,7 @@ class _Gateway extends \IPS\Node\Model
*/
public function form( &$form )
{
$form->add( new \IPS\Helpers\Form\Translatable( 'paymethod_name', NULL, TRUE, array( 'app' => 'nexus', 'key' => "nexus_paymethod_{$this->id}" ) ) );
$form->add( new \IPS\Helpers\Form\Translatable( 'paymethod_name', NULL, TRUE, array( 'app' => 'nexus', 'key' => $this->id ? "nexus_paymethod_{$this->id}" : NULL ) ) );
$this->settings( $form );
$form->add( new \IPS\Helpers\Form\Select( 'paymethod_countries', ( $this->id and $this->countries !== '*' ) ? explode( ',', $this->countries ) : '*', FALSE, array( 'options' => array_map( function( $val )
{
@@ -210,6 +210,8 @@ class _Gateway extends \IPS\Node\Model
$values['countries'] = \is_array( $values['paymethod_countries'] ) ? implode( ',', $values['paymethod_countries'] ) : $values['paymethod_countries'];
}
$values['validationfile'] = (string) $values['m_validationfile'];
$settings = array();
foreach ( $values as $k => $v )
{
@@ -217,7 +219,7 @@ class _Gateway extends \IPS\Node\Model
{
$settings[ mb_substr( $k, mb_strlen( $this->gateway ) + 1 ) ] = $v;
}
if( $k != "countries" )
if( $k != "countries" AND $k != 'validationfile' )
{
unset( $values[$k] );
}
@@ -360,6 +362,21 @@ class _Gateway extends \IPS\Node\Model
}
}
/**
* [Node] Dissalow gateways with validation files to be copyable ( the file can't /shouldn't be copied and reusing the existing value will delete the source file when the new copy is deleted )
*
* @return bool
*/
public function canCopy()
{
if ( $this->deleteOrMoveQueued() === TRUE )
{
return FALSE;
}
return ( !$this->validationfile );
}
/**
* [ActiveRecord] Delete
*
@@ -370,7 +387,14 @@ class _Gateway extends \IPS\Node\Model
/* Delete cards and billing agreements */
\IPS\Db::i()->delete( 'nexus_customer_cards', array( 'card_method=?', $this->id ) );
\IPS\Db::i()->delete( 'nexus_billing_agreements', array( 'ba_method=?', $this->id ) );
try
{
\IPS\File::get( 'nexus_Gateways', $this->validationfile )->delete();
}
catch( \Exception $ex ) { }
/* Delete */
parent::delete();
@@ -635,4 +659,27 @@ class _Gateway extends \IPS\Node\Model
'name' => $this->_title
);
}
/**
* Returns the Apple Pay domain verification file if there's one.
*
* @return void|null
*/
public static function getStripeAppleVerificationFile()
{
foreach ( static::roots() as $gateway )
{
if ( $gateway->gateway == 'Stripe' AND $gateway->validationfile )
{
try
{
return \IPS\File::get( 'nexus_Gateways', $gateway->validationfile );
}
catch ( \Exception $e )
{
return NULL;
}
}
}
}
}
@@ -118,9 +118,38 @@ class _BillingAgreement extends \IPS\nexus\Customer\BillingAgreement
*/
public function latestUnclaimedTransaction()
{
$url = "payments/billing-agreements/{$this->gw_id}/transactions?start_date=" . date('Y-m-d', time() - (86400 * 31)) . '&end_date=' . date('Y-m-d');
try
$data = $this->_getData();
/* PayPal Subscriptions */
if ( isset( $data['plan_id'] ) )
{
$transactions = $this->method->api( "billing/subscriptions/{$this->gw_id}/transactions?start_time=" . \IPS\DateTime::create()->sub( new \DateInterval( 'P31D' ) )->rfc3339() . '&end_time=' . \IPS\DateTime::create()->rfc3339(), NULL, 'get' );
foreach ( array_reverse( $transactions['transactions'] ) as $t )
{
if ( $t['status'] == 'COMPLETED' )
{
try
{
\IPS\Db::i()->select( 't_id', 'nexus_transactions', array( 't_method=? AND t_gw_id=?', $this->method->id, $t['id'] ) )->first();
}
catch ( \UnderflowException $e )
{
$transaction = new \IPS\nexus\Transaction;
$transaction->member = $this->member;
$transaction->method = $this->method;
$transaction->amount = new \IPS\nexus\Money( $t['amount_with_breakdown']['gross_amount']['value'], $t['amount_with_breakdown']['gross_amount']['currency_code'] );
$transaction->date = new \IPS\DateTime( $t['time'] );
$transaction->extra = array( 'automatic' => TRUE );
$transaction->gw_id = $t['id'];
$transaction->billing_agreement = $this;
return $transaction;
}
}
}
}
/* Legacy Billing Agreement */
else
{
$url = "payments/billing-agreements/{$this->gw_id}/transactions?start_date=" . date( 'Y-m-d', time() - ( 86400 * 31 ) ) . '&end_date=' . date( 'Y-m-d' );
$transactions = $this->method->api( $url, NULL, 'get' );
foreach ( array_reverse( $transactions['agreement_transaction_list'] ) as $t )
{
@@ -128,7 +157,7 @@ class _BillingAgreement extends \IPS\nexus\Customer\BillingAgreement
{
try
{
$existingTransaction = \IPS\Db::i()->select( 't_id', 'nexus_transactions', array( 't_method=? AND t_gw_id=?', $this->method->id, $t['transaction_id'] ) )->first();
\IPS\Db::i()->select( 't_id', 'nexus_transactions', array( 't_method=? AND t_gw_id=?', $this->method->id, $t['transaction_id'] ) )->first();
}
catch ( \UnderflowException $e )
{
@@ -145,10 +174,6 @@ class _BillingAgreement extends \IPS\nexus\Customer\BillingAgreement
}
}
}
catch ( \IPS\Http\Url\Exception $e )
{
throw $e;
}
throw new \OutOfRangeException( "{$this->id} ({$this->gw_id})\n{$url}\n\n" . print_r( $transactions, TRUE ) );
}
@@ -47,12 +47,6 @@ class _PayPal extends \IPS\nexus\Gateway
return FALSE;
}
/* Billing agreements require a billing address */
elseif ( static::_canProcessRecurringsAsBillingAgreement( $recurrings ) and ( isset( $settings['billing_agreements'] ) and $settings['billing_agreements'] == 'required' ) and !$billingAddress )
{
return FALSE;
}
/* Check transaction limit */
switch ( $amount->currency )
{
@@ -538,7 +532,7 @@ class _PayPal extends \IPS\nexus\Gateway
'description' => $description,
'billing_cycles' => array(),
'payment_preferences' => array(
'auto_bill_outstanding' => TRUE,
'auto_bill_outstanding' => FALSE,
'payment_failure_threshold' => \intval( $settings['billing_agreement_allowed_fails'] )
),
'taxes' => array(
@@ -618,29 +612,36 @@ class _PayPal extends \IPS\nexus\Gateway
public function void( \IPS\nexus\Transaction $transaction )
{
/* If this is is the intitial transaction for a billing agreement which hasn't
been processed yet, cancel the billing agreement */
been processed yet, cancel the billing agreement. If it's a subscription it may still be the first payment */
if ( $transaction->status === $transaction::STATUS_GATEWAY_PENDING and $transaction->billing_agreement )
{
$transaction->billing_agreement->cancel();
return;
}
/* Try to find the authorization ID */
if ( mb_substr( $transaction->gw_id, 0, 4 ) === 'PAY-' )
{
$authId = NULL;
$payment = $this->api( "payments/payment/{$transaction->gw_id}", NULL, 'get' );
foreach ( $payment['transactions'][0]['related_resources'] as $rr )
try
{
if ( isset( $rr['authorization'] ) )
$payment = $this->api( "payments/payment/{$transaction->gw_id}", NULL, 'get' );
foreach ( $payment['transactions'][0]['related_resources'] as $rr )
{
$authId = $rr['authorization']['id'];
if ( isset( $rr['authorization'] ) )
{
$authId = $rr['authorization']['id'];
}
}
if ( !$authId )
{
throw new \RuntimeException;
}
}
if ( !$authId )
/* Let's try the gateway id as the auth id */
catch ( \IPS\nexus\Gateway\PayPal\Exception $e )
{
throw new \RuntimeException;
$authId = $transaction->gw_id;
}
}
else
@@ -683,24 +684,53 @@ class _PayPal extends \IPS\nexus\Gateway
if ( $billingAgreement = $transaction->billing_agreement )
{
$response = 'WAIT';
$transactions = $this->api( "payments/billing-agreements/{$transaction->billing_agreement->gw_id}/transactions?start_date=" . date( 'Y-m-d', $transaction->date->getTimestamp() - 86400 ) . '&end_date=' . date( 'Y-m-d' ), NULL, 'get' );
foreach ( $transactions['agreement_transaction_list'] as $t )
/* Subscriptions API */
if( $data = $billingAgreement->_getData() AND isset( $data['plan_id'] ) )
{
if ( ( !$transaction->gw_id or $transaction->gw_id == $t['transaction_id'] ) and $transaction->amount->currency == $t['amount']['currency'] and $transaction->amount->amountAsString() == $t['amount']['value'] )
{
if ( $t['status'] == 'Completed' )
$transactions = $this->api( "billing/subscriptions/{$transaction->billing_agreement->gw_id}/transactions?start_time=" . \IPS\DateTime::ts( $transaction->date->getTimestamp() - 86400 )->rfc3339() . '&end_time=' . \IPS\DateTime::ts( time() )->rfc3339(), NULL, 'get' );
foreach ( $transactions['transactions'] as $t )
{
if ( ( !$transaction->gw_id or $transaction->gw_id == $t['id'] ) and $transaction->amount->currency == $t['amount_with_breakdown']['gross_amount']['currency_code'] and $transaction->amount->amountAsString() == $t['amount_with_breakdown']['gross_amount']['value'] )
{
$transaction->gw_id = $t['transaction_id'];
$transaction->save();
return TRUE;
if ( $t['status'] == 'COMPLETED' )
{
$transaction->gw_id = $t['id'];
$transaction->save();
return TRUE;
}
elseif ( $t['status'] == 'REFUNDED' )
{
$response = 'RFND';
}
elseif ( $t['status'] == 'DECLINED' )
{
$response = 'FAIL';
}
}
elseif ( $t['status'] == 'Refunded' )
}
}
/* Legacy Billing Agreements */
else
{
$transactions = $this->api( "payments/billing-agreements/{$transaction->billing_agreement->gw_id}/transactions?start_date=" . date( 'Y-m-d', $transaction->date->getTimestamp() - 86400 ) . '&end_date=' . date( 'Y-m-d' ), NULL, 'get' );
foreach ( $transactions['agreement_transaction_list'] as $t )
{
if ( ( !$transaction->gw_id or $transaction->gw_id == $t['transaction_id'] ) and $transaction->amount->currency == $t['amount']['currency'] and $transaction->amount->amountAsString() == $t['amount']['value'] )
{
$response = 'RFND';
}
elseif ( $t['status'] == 'Failed' )
{
$response = 'FAIL';
if ( $t['status'] == 'Completed' )
{
$transaction->gw_id = $t['transaction_id'];
$transaction->save();
return TRUE;
}
elseif ( $t['status'] == 'Refunded' )
{
$response = 'RFND';
}
elseif ( $t['status'] == 'Failed' )
{
$response = 'FAIL';
}
}
}
}
@@ -781,9 +811,21 @@ class _PayPal extends \IPS\nexus\Gateway
{
/* The capture ID is *normally* the gateway transaction ID */
$captureId = $transaction->gw_id;
/* Refund Amount */
$amount = $amount ? new \IPS\nexus\Money( $amount, $transaction->currency ) : $transaction->amount;
/* If it's a billing agreement and the gateway ID isn't prefixed with an I, it's likely to be a subscription */
if( $transaction->billing_agreement AND $transaction->gw_id AND mb_substr( $transaction->gw_id, 0, 2 ) !== 'I-' )
{
$response = $this->api( "payments/sale/{$transaction->gw_id}/refund", array( 'amount' => array(
'currency' => $amount->currency,
'total' => $amount->amountAsString()
) ) );
return $response['id'];
}
/* But if it starts with I- (or is a blank but known to be a billing agreement payment) - that's a billing agreement */
if ( ( $transaction->billing_agreement and !$transaction->gw_id ) or ( mb_substr( $transaction->gw_id, 0, 2 ) === 'I-' ) )
elseif ( ( $transaction->billing_agreement and !$transaction->gw_id ) or ( mb_substr( $transaction->gw_id, 0, 2 ) === 'I-' ) )
{
$transactions = $this->api( "payments/billing-agreements/{$transaction->billing_agreement->gw_id}/transactions?start_date=" . $transaction->date->sub( new \DateInterval('P1D') )->format('Y-m-d') . '&end_date=' . $transaction->date->format('Y-m-d'), NULL, 'get' );
foreach ( $transactions['agreement_transaction_list'] as $t )
@@ -813,7 +855,6 @@ class _PayPal extends \IPS\nexus\Gateway
}
/* Process Refund */
$amount = $amount ? new \IPS\nexus\Money( $amount, $transaction->currency ) : $transaction->amount;
$response = $this->api( "payments/capture/{$captureId}/refund", array( 'amount' => array(
'currency' => $amount->currency,
'total' => $amount->amountAsString()
@@ -1157,8 +1198,11 @@ class _PayPal extends \IPS\nexus\Gateway
if ( \count( $recurrings ) == 1 )
{
$recurrance = array_pop( $recurrings );
if ( $recurrance['dueDate']->getTimestamp() === \IPS\DateTime::create()->add( $recurrance['term']->interval )->getTimestamp() )
/* We need to allow for some minor deviation than checking for the exact second, this can happen if the request is at just the exact time where
the clock ticks over to the next second */
$difference = \IPS\DateTime::create()->add( $recurrance['term']->interval )->getTimestamp() - $recurrance['dueDate']->getTimestamp();
if ( $difference >= 0 AND $difference < 10 )
{
return TRUE;
}
@@ -314,14 +314,6 @@ class _Stripe extends \IPS\nexus\Gateway
'member' => $member,
) ) );
}
elseif ( $settings['type'] === 'amex' )
{
return array( 'card' => new \IPS\Helpers\Form\Custom( $this->id . '_card', NULL, FALSE, array(
'rowHtml' => function( $field ) use ( $settings ) {
return \IPS\Theme::i()->getTemplate( 'forms', 'nexus', 'global' )->amexExpressCheckout( $field, $this, $settings['amex_client_id'] );
}
) ) );
}
elseif ( $settings['type'] === 'sofort' and \IPS\NEXUS_TEST_GATEWAYS )
{
return array(
@@ -608,12 +600,6 @@ class _Stripe extends \IPS\nexus\Gateway
'phone' => isset( $transaction->invoice->member->cm_phone ) ? $transaction->invoice->member->cm_phone : NULL
);
}
/* Amex Express Checkout */
if ( $settings['type'] === 'amex' )
{
$data['card'] = $values[ $this->id . '_card' ];
}
/* Source-based */
elseif ( \is_string( $card ) )
@@ -851,9 +837,9 @@ class _Stripe extends \IPS\nexus\Gateway
{
$response = $charge;
$response['source'] = $response['payment_method_details'];
foreach ( array( 'cvc_check', 'address_line1_check', 'address_zip_check' ) as $k )
foreach ( array( 'cvc_check', 'address_line1_check', 'address_postal_code_check' ) as $k )
{
$response['source']['card'][ $k ] = isset( $response['payment_method_details']['checks'][ $k ] ) ? $response['payment_method_details']['checks'][ $k ] : 'unavailable';
$response['source']['card'][ $k ] = isset( $response['payment_method_details']['card']['checks'][ $k ] ) ? $response['payment_method_details']['card']['checks'][ $k ] : 'unavailable';
}
break;
}
@@ -975,7 +961,7 @@ class _Stripe extends \IPS\nexus\Gateway
public function form( &$form )
{
$form->addHeader('stripe_basic_settings');
$form->add( new \IPS\Helpers\Form\Translatable( 'paymethod_name', NULL, TRUE, array( 'app' => 'nexus', 'key' => "nexus_paymethod_{$this->id}" ) ) );
$form->add( new \IPS\Helpers\Form\Translatable( 'paymethod_name', NULL, TRUE, array( 'app' => 'nexus', 'key' => $this->id ? "nexus_paymethod_{$this->id}" : NULL ) ) );
$form->add( new \IPS\Helpers\Form\Select( 'paymethod_countries', ( $this->id and $this->countries !== '*' ) ? explode( ',', $this->countries ) : '*', FALSE, array( 'options' => array_map( function( $val )
{
return "country-{$val}";
@@ -1002,7 +988,6 @@ class _Stripe extends \IPS\nexus\Gateway
'card' => 'stripe_type_card',
'native' => 'stripe_type_native',
'alipay' => 'stripe_type_alipay',
'amex' => 'stripe_type_amex',
'bancontact'=> 'stripe_type_bancontact',
'giropay' => 'stripe_type_giropay',
'ideal' => 'stripe_type_ideal',
@@ -1010,11 +995,17 @@ class _Stripe extends \IPS\nexus\Gateway
),
'toggles' => array(
'card' => array( 'stripe_cards' ),
'amex' => array( 'stripe_amex_client_id' )
'native' => array( 'stripe_apple_pay_file')
)
) ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'stripe_cards', $settings ? $settings['cards'] : TRUE, FALSE, array(), NULL, NULL, NULL, 'stripe_cards' ) );
$form->add( new \IPS\Helpers\Form\Text( 'stripe_amex_client_id', ( $settings and isset( $settings['amex_client_id'] ) ) ? $settings['amex_client_id'] : NULL, NULL, array(), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack('stripe_amex_client_id_gen'), 'stripe_amex_client_id' ) );
if ( \IPS\CIC )
{
$form->add( new \IPS\Helpers\Form\Upload( 'm_validationfile', $this->validationfile ? \IPS\File::get( 'nexus_Gateways', $this->validationfile ) : '', FALSE, array( 'storageExtension' => 'nexus_Gateways' ), NULL, NULL, NULL, 'stripe_apple_pay_file' ) );
\IPS\Member::loggedIn()->language()->words[ 'm_validationfile'] = \IPS\Member::loggedIn()->language()->addToStack('stripe_apple_verificationfile');
}
$form->addHeader('stripe_webhook');
$form->addMessage('stripe_webhook_blurb');
\IPS\Member::loggedIn()->language()->words["stripe_webhook_blurb"] = sprintf( \IPS\Member::loggedIn()->language()->get('stripe_webhook_blurb'), (string) \IPS\Http\Url::internal( 'applications/nexus/interface/gateways/stripe.php', 'interface' ) );
@@ -1058,7 +1049,7 @@ class _Stripe extends \IPS\nexus\Gateway
}
if ( !$webhookId )
{
throw new \DomainException( \IPS\Member::loggedIn()->language()->addToStack( 'stripe_webhook_invalid', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->formatList( $this->webhookEvents ) ) ) ) );
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack( 'stripe_webhook_invalid', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->formatList( $this->webhookEvents ) ) ) ) );
}
$settings['webhook_id'] = $webhookId;
+73 -37
View File
@@ -1597,14 +1597,12 @@ class _Invoice extends \IPS\Patterns\ActiveRecord
$memberToSave->$k = $v;
}
$memberToSave->save();
/* Was this a referral? */
if( $this->guest_data['referred_by'] )
{
$referredBy = \IPS\Member::load( $this->guest_data['referred_by'] )->addReferral( $memberToSave );
}
$memberToSave->logHistory( 'core', 'account', array( 'type' => 'register_checkout', 'spamCode' => $spamData['code'], 'spamAction' => $spamData['action'], 'invoice' => $this->id ), FALSE );
if( isset( $this->guest_data['agreed_terms'] ) and $this->guest_data['agreed_terms'] )
{
$memberToSave->logHistory( 'core', 'terms_acceptance', array( 'type' => 'terms' ) );
}
}
$this->member = $memberToSave;
$postBeforeRegister = isset( $this->guest_data['pbr'] ) ? $this->guest_data['pbr'] : NULL;
@@ -1905,8 +1903,10 @@ class _Invoice extends \IPS\Patterns\ActiveRecord
}
/* Referral Commission */
$this->status_extra = $extra;
if ( \IPS\Settings::i()->ref_on and $commission = $this->commission( $amountReceived ) and $commission['amount'] )
{
$extra = $this->status_extra; // Reload, the commission method may have changed this.
$credits = $commission['referrer']->cm_credits;
$credits[ $this->currency ]->amount = $credits[ $this->currency ]->amount->add( $commission['amount']->amount );
$commission['referrer']->cm_credits = $credits;
@@ -2093,6 +2093,7 @@ class _Invoice extends \IPS\Patterns\ActiveRecord
public function markUnpaid( $status, \IPS\Member $member = NULL )
{
/* Do stuff */
$extra = $this->status_extra;
if ( $this->status === static::STATUS_PAID )
{
/* Loop items */
@@ -2104,36 +2105,15 @@ class _Invoice extends \IPS\Patterns\ActiveRecord
if ( $item->payTo and !$item->payTo->inGroup( explode( ',', \IPS\Settings::i()->nexus_no_commission ) ) )
{
$recipientAmounts = $item->recipientAmounts();
$credits = $item->payTo->cm_credits;
$credits[ $this->currency ]->amount = $credits[ $this->currency ]->amount->subtract( $recipientAmounts['recipient_final']->amount );
$item->payTo->cm_credits = $credits;
$item->payTo->save();
$item->payTo->log( 'comission', array(
'type' => 'invoice_refund',
'currency' => $this->currency,
'amount' => $recipientAmounts['recipient_final']->amount,
'new_amount' => $credits[ $this->currency ]->amount,
'member' => $this->member->member_id,
'invoice_id' => $this->id,
'invoice_title' => $this->title
), FALSE );
\IPS\Email::buildFromTemplate( 'nexus', 'commissionRevoked', array( $this, $item, $recipientAmounts['recipient_final'] ), \IPS\Email::TYPE_TRANSACTIONAL )
->send(
$item->payTo,
array_map(
function( $contact )
{
return $contact->alt_id->email;
},
iterator_to_array( $item->payTo->alternativeContacts( array( 'billing=1' ) ) )
),
( ( \in_array( 'commission_earned', explode( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() )
);
$this->revokeCommission( $item->payTo, $recipientAmounts['recipient_final'], $item );
}
}
/* Revoke commissions based on the referral rules */
if( !empty( $extra['commissionref'] ) AND !empty( $extra['commissionamount'] ) )
{
$this->revokeCommission( \IPS\nexus\Customer::load( $extra['commissionref'] ), new \IPS\nexus\Money( $extra['commissionamount'], $this->currency ) );
}
/* Delete purchases */
foreach ( $this->purchasesCreated() as $purchase )
@@ -2161,7 +2141,6 @@ class _Invoice extends \IPS\Patterns\ActiveRecord
/* Log */
if ( $member )
{
$extra = $this->status_extra;
$extra['type'] = 'manual';
$extra['setTo'] = $status;
$extra['setByID'] = $member->member_id;
@@ -2172,6 +2151,44 @@ class _Invoice extends \IPS\Patterns\ActiveRecord
/* Save */
$this->save();
}
/**
* Revoke Commission
*
* @param \IPS\Member $paidTo Revoke the commission from this member
* @param \IPS\nexus\Money $commission Amount of commission to revoke
* @param \IPS\nexus\Invoice\Item|null $item Item commission was for (if applicable)
*/
protected function revokeCommission( \IPS\Member $paidTo, \IPS\nexus\Money $commission, \IPS\nexus\Invoice\Item $item=NULL )
{
$credits = $paidTo->cm_credits;
$credits[ $this->currency ]->amount = $credits[ $this->currency ]->amount->subtract( $commission->amount );
$paidTo->cm_credits = $credits;
$paidTo->save();
$paidTo->log( 'comission', array(
'type' => 'invoice_refund',
'currency' => $this->currency,
'amount' => $commission->amount,
'new_amount' => $credits[ $this->currency ]->amount,
'member' => $this->member->member_id,
'invoice_id' => $this->id,
'invoice_title' => $this->title
), FALSE );
\IPS\Email::buildFromTemplate( 'nexus', 'commissionRevoked', array( $this, $item, $commission ), \IPS\Email::TYPE_TRANSACTIONAL )
->send(
$paidTo,
array_map(
function( $contact )
{
return $contact->alt_id->email;
},
iterator_to_array( $paidTo->alternativeContacts( array( 'billing=1' ) ) )
),
( ( \in_array( 'commission_earned', explode( ',', \IPS\Settings::i()->nexus_notify_copy_types ) ) AND \IPS\Settings::i()->nexus_notify_copy_email ) ? explode( ',', \IPS\Settings::i()->nexus_notify_copy_email ) : array() )
);
}
/**
* What will happen if we mark this unpaid?
@@ -2243,7 +2260,26 @@ class _Invoice extends \IPS\Patterns\ActiveRecord
}
}
}
/* Referral Commission */
$extra = $this->status_extra;
if( !empty( $extra['commissionref'] ) AND !empty( $extra['commissionamount'] ) )
{
$customer = \IPS\nexus\Customer::load( $extra['commissionref'] );
$amount = new \IPS\nexus\Money( $extra['commissionamount'], $this->currency );
$message = \IPS\Member::loggedIn()->language()->addToStack( 'account_credit_remove_commission', FALSE, array( 'sprintf' => array( $amount, $customer->cm_name ) ) );
$credits = $customer->cm_credits;
if ( !$credits[ $amount->currency ]->amount->subtract( $amount->amount )->isPositive() )
{
$return[] = array( 'message' => $message, 'warning' => \IPS\Member::loggedIn()->language()->addToStack( 'account_credit_remove_neg' ) );
}
else
{
$return[] = $message;
}
}
return $return;
}
@@ -372,7 +372,7 @@ abstract class _Item
{
/* If one is set to be associated with something in the cart and the other is set to be associated
with an existing purchase, trying to compare those will throw an error */
if ( $item->parent or $this->parent )
if ( isset( $item->parent ) and isset( $this->parent ) )
{
if ( \gettype( $this->parent ) != \gettype( $item->parent ) )
{
@@ -120,7 +120,6 @@ class _CustomField extends \IPS\CustomField
'Radio' => array( 'cf_sticky' ),
'Text' => array( 'cf_sticky' ),
'Url' => array( 'cf_sticky' ),
'YesNo' => array( 'cf_sticky' ),
);
/**
@@ -189,6 +188,12 @@ class _CustomField extends \IPS\CustomField
$values['pf_multiple'] = 0;
}
/* Disable 'sticky' for fields that don't support it */
if( !\in_array( 'cf_sticky', static::$additionalFieldToggles[ $values['pf_type'] ] ?? [] ) )
{
$values['cf_sticky'] = false;
}
return parent::formatFormValues( $values );
}
@@ -56,7 +56,7 @@ class _Filter extends \IPS\Node\Model
* @brief [Node] Description suffix. If specified, will look for a language key with "{$titleLangPrefix}_{$id}_{$descriptionLangSuffix}" as the key
*/
public static $descriptionLangSuffix = '_public';
/**
* @brief [Node] ACP Restrictions
* @code
@@ -192,6 +192,8 @@ class _Filter extends \IPS\Node\Model
public function delete()
{
\IPS\Db::i()->delete( 'nexus_package_filters_values', array( 'pfv_filter=?', $this->id ) );
\IPS\Lang::deleteCustom( 'nexus', static::$titleLangPrefix . $this->_id );
\IPS\Lang::deleteCustom( 'nexus', static::$titleLangPrefix . $this->_id . static::$descriptionLangSuffix );
parent::delete();
}
+1 -1
View File
@@ -583,7 +583,7 @@ class _Item extends \IPS\Content\Item implements \IPS\Content\Featurable, \IPS\C
*/
public function metaDescription( $return = NULL )
{
return \IPS\Member::loggedIn()->language()->addToStack("nexus_package_{$this->id}_desc", FALSE, array( 'striptags' => TRUE ) );
return \IPS\Member::loggedIn()->language()->addToStack("nexus_package_{$this->id}_desc", FALSE, array( 'striptags' => TRUE, 'removeNewlines' => TRUE ) );
}
/**
+75 -12
View File
@@ -27,6 +27,12 @@ class _Package extends \IPS\Node\Model
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons;
/**
* @brief [ActiveRecord] Caches
* @note Defined cache keys will be cleared automatically as needed
*/
protected $caches = array( 'nexus_reg_product_count' );
/**
* @brief [ActiveRecord] Database Table
@@ -720,7 +726,16 @@ class _Package extends \IPS\Node\Model
$priceMayChange = FALSE;
$priceNotIncludingDiscounts = NULL;
$relevantRenewalTerm = NULL;
$price = static::lowestPriceFromData( $customer, $currency, $packageId, $prices, $discounts, $renewOptions, $stock, $tax, $priceMayChange, $priceNotIncludingDiscounts, $relevantRenewalTerm );
try
{
$price = static::lowestPriceFromData( $customer, $currency, $packageId, $prices, $discounts, $renewOptions, $stock, $tax, $priceMayChange, $priceNotIncludingDiscounts, $relevantRenewalTerm );
}
catch ( \OutOfBoundsException $e )
{
return NULL;
}
$primaryPrice = new \IPS\nexus\Money( $price, $currency );
/* Renewals */
@@ -1108,7 +1123,7 @@ class _Package extends \IPS\Node\Model
$form->addHeader('package_registration');
$form->add( new \IPS\Helpers\Form\YesNo( 'p_reg', $this->reg, FALSE, array(), function( $val )
{
if ( $val and ( !\IPS\Request::i()->p_store_checkbox or ( \IPS\Request::i()->p_member_groups and ( !\in_array( \IPS\Settings::i()->guest_group, \IPS\Request::i()->p_member_groups ) and \IPS\Request::i()->p_member_groups_unlimited != "*" ) ) ) )
if ( $val and ( !\IPS\Request::i()->p_store_checkbox or ( \IPS\Request::i()->p_member_groups and !\in_array( \IPS\Settings::i()->guest_group, array_keys( \IPS\Request::i()->p_member_groups ) ) ) ) )
{
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 ) ) ) );
}
@@ -2076,6 +2091,11 @@ class _Package extends \IPS\Node\Model
}
return $optionValues;
}
/**
* @brief Temporarily store stock data based on query.
*/
protected $_stockQueryCache = [];
/**
* Stock level
@@ -2089,14 +2109,19 @@ class _Package extends \IPS\Node\Model
case -1: // Unlimited
return NULL;
case -2: // Variable
if ( \IPS\Db::i()->select( 'COUNT(*)', 'nexus_product_options', array( 'opt_package=? AND opt_stock<>0', $this->id ) )->first() )
if( !array_key_exists( $this->id, $this->_stockQueryCache ) )
{
return NULL;
}
else
{
return 0;
if ( \IPS\Db::i()->select( 'COUNT(*)', 'nexus_product_options', array( 'opt_package=? AND opt_stock<>0', $this->id ) )->first() )
{
$this->_stockQueryCache[ $this->id ] = NULL;
}
else
{
$this->_stockQueryCache[ $this->id ] = 0;
}
}
return $this->_stockQueryCache[ $this->id ];
default:
return (int) $this->stock;
}
@@ -2995,7 +3020,7 @@ class _Package extends \IPS\Node\Model
{
if ( $this->$typeKey == 'wysiwyg' )
{
$email = \IPS\Email::buildFromContent( $purchase->member->language()->get( "nexus_package_{$this->id}_email_{$type}_subject" ), \IPS\Text\Parser::removeLazyLoad( $this->$valueKey ), NULL, \IPS\Email::TYPE_TRANSACTIONAL, TRUE, 'package_customemail' );
$email = \IPS\Email::buildFromContent( $purchase->member->language()->get( "nexus_package_{$this->id}_email_{$type}_subject" ), \IPS\Text\Parser::removeLazyLoad( $this->$valueKey ), NULL, \IPS\Email::TYPE_TRANSACTIONAL, \IPS\Email::WRAPPER_USE, 'package_customemail' );
}
else
{
@@ -3003,7 +3028,7 @@ class _Package extends \IPS\Node\Model
\IPS\Theme::makeProcessFunction( $this->$valueKey, $functionName, '$purchase' );
$themeFunction = 'IPS\\Theme\\'.$functionName;
$email = \IPS\Email::buildFromContent( $purchase->member->language()->get( "nexus_package_{$this->id}_email_{$type}_subject" ), $themeFunction( $purchase ), NULL, \IPS\Email::TYPE_TRANSACTIONAL, $this->$typeKey == 'html_template', 'package_customemail' );
$email = \IPS\Email::buildFromContent( $purchase->member->language()->get( "nexus_package_{$this->id}_email_{$type}_subject" ), $themeFunction( $purchase ), NULL, \IPS\Email::TYPE_TRANSACTIONAL, ( $this->$typeKey == 'html_template' ? \IPS\Email::WRAPPER_USE : \IPS\Email::WRAPPER_NONE ), 'package_customemail' );
}
$email->send( $purchase->member );
@@ -3101,6 +3126,12 @@ class _Package extends \IPS\Node\Model
/* Upgrade */
if ( $price >= $thisPrice and ( $this->allow_upgrading or $overrideRestrictions ) )
{
/* We want to pro-rata renewals for upgrades, but we do not have any renewals here, so no */
if ( $package->upgrade_charge == 2 and ! $purchase->renewals )
{
continue;
}
$return[] = $package;
$includesUpgrades = TRUE;
}
@@ -3420,11 +3451,11 @@ class _Package extends \IPS\Node\Model
$renewalOptions = json_decode( $newPackage->renew_options, TRUE );
if ( $chosenRenewalOption === NULL )
{
if ( \count( $renewalOptions ) === 1 )
if ( $renewalOptions AND \count( $renewalOptions ) === 1 )
{
$term = array_pop( $renewalOptions );
}
elseif ( \count( $renewalOptions ) !== 0 )
elseif ( $renewalOptions AND \count( $renewalOptions ) !== 0 )
{
throw new \InvalidArgumentException;
}
@@ -3489,6 +3520,19 @@ class _Package extends \IPS\Node\Model
{
$purchase->expire = NULL;
}
/* Initial Term, apply the difference if the new package has a longer initial term */
if( $term and $newPackage->initial_term )
{
$initial = new \DateInterval( "P{$newPackage->initial_term}" );
/* If we're still in the initial period of the new package */
$diff = \IPS\DateTime::create()->diff( $purchase->start->add( $initial ) );
if( $diff->days > 0 )
{
$purchase->expire = $purchase->start->add( $diff );
}
}
/* Update Purchase */
$purchase->name = \IPS\Member::loggedIn()->language()->get( "nexus_package_{$newPackage->id}" );
@@ -3813,4 +3857,23 @@ class _Package extends \IPS\Node\Model
{
return \IPS\Db::i()->select( 'COUNT(*)', 'nexus_packages', 'p_reg=1 AND p_store=1' )->first();
}
/**
* [Node] Does the currently logged in user have permission to delete this node?
*
* @return bool
*/
public function canDelete()
{
foreach ( \IPS\Db::i()->select( 'data', 'core_queue', array( 'app=? AND `key`=?', 'nexus', 'MassChangePurchases' ) ) as $row )
{
$data = json_decode( $row, TRUE );
if ( $data['id'] == $this->_id OR $data['mass_change_purchases_to'] == $this->_id )
{
return FALSE;
}
}
return parent::canDelete();
}
}
@@ -143,7 +143,7 @@ class _Purchase extends \IPS\Node\Model
$renewals = (string) $this->renewals;
if ( $this->renewals->tax )
{
$renewals .= \IPS\Member::loggedIn()->language()->addToStack( 'plus_tax_rate', NULL, array( 'sprintf' => array( $this->renewals->tax->_title ) ) );
$renewals .= \IPS\Member::loggedIn()->language()->addToStack( 'plus_tax_rate', FALSE, array( 'sprintf' => array( $this->renewals->tax->_title ) ) );
}
$description[] = $renewals;
}
@@ -1481,11 +1481,13 @@ class _Purchase extends \IPS\Node\Model
/* !ACP Display */
/**
* ACP URL
* Get the URL of the AdminCP page for this node
*
* @return \IPS\Http\Url
* @param string|NULL $do The "do" query parameter of the url (e.g. 'form', 'permissions', etc).
*
* @return \IPS\Http\Url | NULL
*/
public function acpUrl()
public function acpUrl( $do="form" )
{
return \IPS\Http\Url::internal( "app=nexus&module=customers&controller=purchases&do=view&id={$this->id}", 'admin' );
}
@@ -1512,6 +1514,7 @@ class _Purchase extends \IPS\Node\Model
);
}
$extension = NULL;
try
{
$extension = $this->extension();
@@ -1565,10 +1568,9 @@ class _Purchase extends \IPS\Node\Model
if ( ( !$this->billing_agreement or $this->billing_agreement->canceled ) and ( !$this->grouped_renewals or !$parent or !$parent->billing_agreement or $parent->billing_agreement->canceled ) and \IPS\Member::loggedIn()->hasAcpRestriction( 'nexus', 'customers', 'purchases_cancel' ) )
{
$extension = $this->extension();
if ( $this->cancelled )
{
if ( $extension::canAcpReactivate( $this ) )
if ( $extension AND $extension::canAcpReactivate( $this ) )
{
$return['reactivate'] = array(
'icon' => 'check',
@@ -118,24 +118,11 @@ class _RenewalTerm
/**
* Number of days
*
* @return int
* @return string
*/
public function days()
public function days(): string
{
$days = 0;
if ( $this->interval->y )
{
$days += ( 365 * $this->interval->y );
}
if ( $this->interval->m )
{
$days += ( ( 365 / 12 ) * $this->interval->m );
}
if ( $this->interval->d )
{
$days += $this->interval->d;
}
return number_format( $days, 2, '.', '' );
return number_format( \IPS\DateTime::intervalToDays( $this->interval ), 2, '.', '' );
}
/**
@@ -29,6 +29,12 @@ class _Package extends \IPS\Node\Model
* @brief [ActiveRecord] Multiton Store
*/
protected static $multitons;
/**
* @brief [ActiveRecord] Caches
* @note Defined cache keys will be cleared automatically as needed
*/
protected $caches = array( 'nexus_sub_count' );
/**
* @brief [ActiveRecord] Database Table
@@ -602,15 +608,15 @@ class _Package extends \IPS\Node\Model
{
/* Fetch purchase */
$purchase = NULL;
foreach ( \IPS\nexus\extensions\nexus\Item\Subscription::getPurchases( $member, $package->id ) as $row )
foreach ( \IPS\nexus\extensions\nexus\Item\Subscription::getPurchases( $member, $package->id, TRUE, TRUE ) as $row )
{
if ( $row->active and ! $row->cancelled )
if ( !$row->cancelled OR ( $row->cancelled AND $row->can_reactivate ) )
{
$purchase = $row;
break;
}
}
if ( $purchase === NULL )
{
return NULL;
@@ -624,14 +630,13 @@ class _Package extends \IPS\Node\Model
{
$currency = $purchase->member->defaultCurrency();
}
$priceOfExistingPackage = json_decode( $package->price, TRUE );
if ( isset( $priceOfExistingPackage['cost'] ) )
{
$priceOfExistingPackage = $priceOfExistingPackage['cost'];
}
$priceOfExistingPackage = $priceOfExistingPackage[ $currency ]['amount'];
$renewalOptionsOnOldPackage = json_decode( $package->renew_options, TRUE );
$priceOfThisPackage = json_decode( $this->price, TRUE );
if ( isset( $priceOfThisPackage['cost'] ) )
@@ -640,7 +645,6 @@ class _Package extends \IPS\Node\Model
}
$priceOfThisPackage = $priceOfThisPackage[ $currency ]['amount'];
$renewalOptionsOnNewPackage = json_decode( $this->renew_options, TRUE );
/* It's a non-recurring subscription */
if ( empty( $renewalOptionsOnNewPackage ) )
@@ -677,20 +681,37 @@ class _Package extends \IPS\Node\Model
return new \IPS\nexus\Money( 0, $currency );
case 1:
/* If the purchase is expired, charge the full amount */
if( !$purchase->active )
{
$newPrice = new \IPS\nexus\Money( $priceOfThisPackage, $currency );
/* If the package has a free trial, charge the renewal fee instead to stop endless free trials */
if( $newPrice->amount->isZero() )
{
return new \IPS\nexus\Money( $renewalOptionsOnNewPackage['cost'][ $currency ]['amount'], $currency );
}
return new \IPS\nexus\Money( $priceOfThisPackage, $currency );
}
return new \IPS\nexus\Money( $priceOfThisPackage - $priceOfExistingPackage, $currency );
case 2:
if ( !$purchase->renewals )
{
return new \IPS\nexus\Money( 0, $currency );
}
if ( !$renewalOptionsOnNewPackage )
{
throw new \InvalidArgumentException;
}
/* What is the closest renewal option on the new package? We'll use that one */
$renewalOptionsInDays = array();
$term = ( new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $renewalOptionsOnNewPackage['cost'][ $currency ]['amount'], $currency ), new \DateInterval( 'P' . $renewalOptionsOnNewPackage['term'] . mb_strtoupper( $renewalOptionsOnNewPackage['unit'] ) ), $purchase->renewals->tax ) );
$renewalOptionsInDays[ $term->days() ] = $term;
$closestRenewalOption = null;
$numberOfDaysInCurrentRenewalTerm = $purchase->renewals->days();
foreach ( $renewalOptionsInDays as $days => $term )
@@ -701,23 +722,64 @@ class _Package extends \IPS\Node\Model
}
}
$renewalTermToUse = $renewalOptionsInDays[ $closestRenewalOption ];
/* What is the difference between our current renewal term and the renewal term we're moving to? */
$diff = $purchase->renewals->diff( $renewalTermToUse );
/* Multiply that by how many days are left */
$numberOfDaysInCurrentRenewalTerm = new \IPS\Math\Number( $numberOfDaysInCurrentRenewalTerm );
$daysLeftUntilExpiry = new \IPS\Math\Number( (string) ceil( ( $purchase->expire->getTimestamp() - time() ) / 86400 ) );
if ( $numberOfDaysInCurrentRenewalTerm->compare( $daysLeftUntilExpiry ) === 0 )
/* If the purchase is not active, it is the full cost */
if( !$purchase->active )
{
return $diff;
return new \IPS\nexus\Money( $renewalTermToUse->cost->amount, $currency );
}
/* Current Period Start Date */
$startOfCurrentPeriod = $purchase->expire->sub( $purchase->renewals->interval );
/* DateInterval of time between subscription start and now */
$timeSinceStartOfCurrentPeriod = $startOfCurrentPeriod->diff( \IPS\DateTime::create() );
$numberOfDaysInCurrentRenewalTerm = new \IPS\Math\Number( (string) round( $numberOfDaysInCurrentRenewalTerm ) );
/* Count Days in Period */
$daysUsedInPeriod = \IPS\DateTime::intervalToDays( $timeSinceStartOfCurrentPeriod );
$daysRemainingInPeriod = new \IPS\Math\Number( (string) $numberOfDaysInCurrentRenewalTerm->subtract( new \IPS\Math\Number( (string) $daysUsedInPeriod ) ) );
/* Currency Worth of value used in current sub */
$valueUsedOfSubscription = $purchase->renewals->cost->amount->divide( $numberOfDaysInCurrentRenewalTerm )->multiply( $daysRemainingInPeriod );
if ( !$daysRemainingInPeriod->isGreaterThanZero() )
{
return new \IPS\nexus\Money( new \IPS\Math\Number('0'), $currency );
}
else
{
return new \IPS\nexus\Money( $diff->amount->divide( $numberOfDaysInCurrentRenewalTerm )->multiply( $daysLeftUntilExpiry ), $currency );
return new \IPS\nexus\Money( $renewalTermToUse->cost->amount->subtract( $valueUsedOfSubscription ), $currency );
}
}
}
/**
* Cost to upgrade to this package (may return negative value for refund)
*
* @param \IPS\nexus\Subscription\Package $package The currently subscribed package
* @param \IPS\Member $member The member.
* @return \IPS\nexus\Money|NULL
* @throws \InvalidArgumentException
*/
public function costToUpgradeIncludingTax( \IPS\nexus\Subscription\Package $package, \IPS\Member $member )
{
$upgradeCost = $this->costToUpgrade( $package, $member );
if( $upgradeCost !== NULL AND $upgradeCost->amount->isGreaterThanZero() AND \IPS\Settings::i()->nexus_show_tax and $this->tax )
{
try
{
$taxRate = new \IPS\Math\Number( \IPS\nexus\Tax::load( $this->tax )->rate( \IPS\nexus\Customer::loggedIn()->estimatedLocation() ) );
$upgradeCost->amount = $upgradeCost->amount->add( $upgradeCost->amount->multiply( $taxRate ) );
}
catch ( \OutOfRangeException $e ) { }
}
return $upgradeCost;
}
/**
* Create the upgrade/downgrade invoice or refund
@@ -787,7 +849,15 @@ class _Package extends \IPS\Node\Model
{
$currency = $purchase->member->defaultCurrency();
}
$term = new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $term['cost'][$currency]['amount'], $currency ), new \DateInterval( 'P' . $term['term'] . mb_strtoupper( $term['unit'] ) ), $newPackage->tax ? \IPS\nexus\Tax::load( $newPackage->tax ) : NULL );
/* Check Tax exists */
$tax = NULL;
try
{
$tax = $newPackage->tax ? \IPS\nexus\Tax::load( $newPackage->tax ) : NULL;
}
catch( \OutOfRangeException $e ) {}
$term = new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( $term['cost'][$currency]['amount'], $currency ), new \DateInterval( 'P' . $term['term'] . mb_strtoupper( $term['unit'] ) ), $tax );
}
/* Remove usergroups */
@@ -806,24 +876,39 @@ class _Package extends \IPS\Node\Model
/* We have a term, but the unit is different from the existing package */
elseif ( $purchase->expire and $term and $oldTerm and ( $rawTerm['unit'] != $oldTerm['unit'] ) )
{
$difference = $purchase->expire->getTimestamp() - time();
if ( $difference > 0 )
/* If there is an upgrade cost, the expire date should be from when the current billing period started. */
if( $costToUpgrade->amount->isGreaterThanZero() AND \IPS\Settings::i()->nexus_subs_upgrade > 0 )
{
$newExpire = \IPS\DateTime::ts( ( time() - $difference ) );
$newExpire = $newExpire->add( $term->interval );
if ( $newExpire->getTimestamp() < time() )
{
$newExpire = \IPS\DateTime::create()->add( $term->interval );
}
$purchase->expire = $newExpire;
$startOfCurrentPeriod = $purchase->expire->sub( new \DateInterval( 'P' . $oldTerm['term'] . mb_strtoupper( $oldTerm['unit'] ) ) );
$purchase->expire = $startOfCurrentPeriod < $purchase->start ? $startOfCurrentPeriod->add( $term->interval ) : $purchase->start->add( $term->interval );
}
else
{
$difference = $purchase->expire->getTimestamp() - time();
if ( $difference > 0 )
{
$newExpire = \IPS\DateTime::ts( ( time() - $difference ) );
$newExpire = $newExpire->add( $term->interval );
if ( $newExpire->getTimestamp() < time() )
{
$newExpire = \IPS\DateTime::create()->add( $term->interval );
}
$purchase->expire = $newExpire;
}
}
}
/* Purchase expired in the past, so it needs one renewal term adding */
elseif( $purchase->expire->getTimestamp() < time() AND $term )
{
$purchase->expire = \IPS\DateTime::create()->add( $term->interval );
}
/* Update Purchase */
$purchase->name = \IPS\Member::loggedIn()->language()->get( "nexus_subs_{$newPackage->id}" );
$purchase->cancelled = FALSE;
$purchase->item_id = $newPackage->id;
$purchase->renewals = $term;
$purchase->save();
@@ -843,7 +928,7 @@ class _Package extends \IPS\Node\Model
/* Change the subscription itself */
try
{
\IPS\nexus\Subscription::loadActiveByMember( $purchase->member )->changePackage( $newPackage, $purchase->expire );
\IPS\nexus\Subscription::load( $purchase->id, 'sub_purchase_id' )->changePackage( $newPackage, $purchase->expire );
}
catch( \Exception $e )
{
@@ -896,10 +981,12 @@ class _Package extends \IPS\Node\Model
/**
* Adds a member to the subscription package
*
* @param \IPS\nexus\Customer $member The cutomer innit
* @return \IPS\nexus\Subscription The new subscription object added
* @param \IPS\nexus\Customer $member The customer innit
* @param bool $generatePurchase Generate purchase when adding member (for manual subscriptions)
* @param bool $purchaseRenews Does the purchase renew, or is it forever
* @return \IPS\nexus\Subscription The new subscription object added
*/
public function addMember( $member )
public function addMember( \IPS\nexus\Customer $member, bool $generatePurchase=FALSE, bool $purchaseRenews=FALSE ): \IPS\nexus\Subscription
{
try
{
@@ -929,6 +1016,39 @@ class _Package extends \IPS\Node\Model
$sub->expire = $expires;
$sub->renews = $renews;
$sub->save();
/* Do we need to generate a purchase to track this sub? */
if( $generatePurchase )
{
$purchase = new \IPS\nexus\Purchase;
$purchase->member = $member;
$purchase->name = $member->language()->get( static::$titleLangPrefix . $this->id );
$purchase->app = 'nexus';
$purchase->type = 'subscription';
$purchase->item_id = $this->id;
$purchase->tax = $this->tax;
$purchase->show = 1;
if( $purchaseRenews )
{
if ( $renewalTerm = $this->renewalTerm() )
{
$purchase->renewals = $renewalTerm;
}
$purchase->expire = \IPS\DateTime::ts( $expires );
}
else
{
$sub->expire = 0;
$sub->renews = 0;
}
$purchase->save();
/* update sub with purchase id */
$sub->purchase_id = $purchase->id;
$sub->save();
}
$this->_addUsergroups( $member );
@@ -956,7 +1076,7 @@ class _Package extends \IPS\Node\Model
* @param \IPS\nexus\Customer $member The cutomer innit
* @return void
*/
public function cancelMember( $member)
public function cancelMember( $member )
{
/* Run before marking it inactive or it won't find the row in _removeUsergroups */
$this->_removeUsergroups( $member );
@@ -981,7 +1101,7 @@ class _Package extends \IPS\Node\Model
* @param \IPS\nexus\Customer $member The cutomer innit
* @return void
*/
public function removeMember( $member)
public function removeMember( $member )
{
/* Run before marking it inactive or it won't find the row in _removeUsergroups */
$this->_removeUsergroups( $member );
@@ -1013,14 +1133,23 @@ class _Package extends \IPS\Node\Model
/* Hang on, are we about to boot someone out the ACP? */
if ( ! ( $member->isAdmin() and !\in_array( $this->primary_group, array_keys( \IPS\Member::administrators()['g'] ) ) ) )
{
/* Save the current group */
$previousGroup = $member->member_group_id;
/* Only do this if the target group exists */
try
{
$group = \IPS\Member\Group::load( $this->primary_group);
/* Save the current group */
$previousGroup = $member->member_group_id;
/* And update to the new group */
$member->member_group_id = $this->primary_group;
$member->members_bitoptions['ignore_promotions'] = true;
$member->save();
$member->logHistory( 'core', 'group', array( 'type' => 'primary', 'by' => 'subscription', 'action' => 'add', 'id' => $this->id, 'old' => $previousGroup, 'new' => $member->member_group_id ) );
/* And update to the new group */
$member->member_group_id = $this->primary_group;
$member->members_bitoptions['ignore_promotions'] = true;
$member->save();
$member->logHistory( 'core', 'group', array( 'type' => 'primary', 'by' => 'subscription', 'action' => 'add', 'id' => $this->id, 'old' => $previousGroup, 'new' => $member->member_group_id ) );
}
catch( \OutOfRangeException $e )
{
}
}
}
@@ -1035,7 +1164,16 @@ class _Package extends \IPS\Node\Model
{
if ( !\in_array( $gid, $newSecondary ) )
{
$newSecondary[] = $gid;
/* Only do this if the target group exists */
try
{
$group = \IPS\Member\Group::load( $gid );
$newSecondary[] = $gid;
}
catch( \OutOfRangeException $e )
{
}
}
}
}
@@ -36,7 +36,12 @@ class _Subscription extends \IPS\Patterns\ActiveRecord
* @brief Multiton Store
*/
protected static $multitons;
/**
* @brief [ActiveRecord] Database ID Fields
*/
protected static $databaseIdFields = array( 'sub_purchase_id' );
/**
* Get the DateTime object for when this little subscription doth run out
*
@@ -67,16 +72,6 @@ class _Subscription extends \IPS\Patterns\ActiveRecord
return NULL;
}
/**
* Check if the subscription expired
*
* @return bool
*/
public function isExpired() : bool
{
return $this->expire AND $this->expire < \IPS\DateTime::create()->getTimestamp();
}
/**
* Get the package (ooh, very mysterious, sounds like something from Narcos)
*
@@ -133,10 +128,18 @@ class _Subscription extends \IPS\Patterns\ActiveRecord
*/
public function currentBlurb()
{
if ( !$this->active )
if ( !$this->active AND !$this->purchase->cancelled )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'nexus_subs_subscribed_expired' );
}
elseif( $this->purchase->cancelled AND $this->purchase->can_reactivate )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'nexus_subs_subscribed_cancelled' );
}
elseif( $this->purchase->cancelled )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'nexus_subs_subscribed_cancelled_no_reactivate' );
}
if ( $this->expire and $this->renews and !$this->manually_added )
{
return \IPS\Member::loggedIn()->language()->addToStack( 'nexus_subs_subscribed_with_expire' . ( ( $this->purchase and $this->purchase->renewals ) ? '' : '_no_renewal' ), NULL, array( 'sprintf' => array( $this->_expire->dayAndMonth() . ' ' . $this->_expire->format('Y') ) ) );
@@ -210,28 +213,6 @@ class _Subscription extends \IPS\Patterns\ActiveRecord
throw new \OutOfRangeException;
}
}
/**
* Load a subscription by member and purchase
*
* @param \IPS\Member $member Take a guess
* @param \IPS\nexus\Purchase $purchase I mean it's really writing itself
* @return \IPS\nexus\Subscription
* @throws \OutOfRangeException
*/
public static function loadByMemberAndPurchase( \IPS\Member $member, \IPS\nexus\Purchase $purchase )
{
try
{
$where = array( array( 'sub_purchase_id=? and sub_member_id=?', $purchase->id, $member->member_id ) );
return static::constructFromData( \IPS\Db::i()->select( '*', 'nexus_member_subscriptions', $where )->first() );
}
catch( \Exception $ex )
{
throw new \OutOfRangeException;
}
}
/**
* Mark all subscriptions by this member as inactive
@@ -243,15 +224,4 @@ class _Subscription extends \IPS\Patterns\ActiveRecord
{
\IPS\Db::i()->update( 'nexus_member_subscriptions', array( 'sub_active' => 0 ), array( 'sub_member_id=?', $member->member_id ) );
}
/**
* Can we cancel this subscription?
*
* @return bool
*/
public function canCancel() : bool
{
return !$this->expire or $this->isExpired();
}
}
+2 -1
View File
@@ -126,9 +126,10 @@ class _Reply extends \IPS\Content\Comment implements \IPS\Content\Hideable
* @param \IPS\DateTime|NULL $time The time
* @param string|NULL $ipAddress The IP address or NULL to detect automatically
* @param int|NULL $hiddenStatus NULL to set automatically or override: 0 = unhidden; 1 = hidden, pending moderator approval; -1 = hidden (as if hidden by a moderator)
* @param int|NULL $anonymous NULL for no value, 0 or 1 for a value (0=no, 1=yes)
* @return static
*/
public static function create( $item, $comment, $first=FALSE, $guestName=NULL, $incrementPostCount=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL )
public static function create( $item, $comment, $first=FALSE, $guestName=NULL, $incrementPostCount=NULL, $member=NULL, \IPS\DateTime $time=NULL, $ipAddress=NULL, $hiddenStatus=NULL, $anonymous=NULL )
{
$obj = parent::create( $item, $comment, $first, $guestName, $incrementPostCount, $member, $time, $ipAddress, $hiddenStatus );
@@ -872,6 +872,9 @@ class _Request extends \IPS\Content\Item implements \IPS\Content\ReadMarkers
*/
public function processAfterCreate( $comment, $values )
{
/* When firing a webhook immediately after creation, readers may not yet have the first reply content */
static::$useWriteServer = TRUE;
parent::processAfterCreate( $comment, $values );
$this->afterCreateLog( $comment );
@@ -1023,7 +1026,7 @@ class _Request extends \IPS\Content\Item implements \IPS\Content\ReadMarkers
/* Redirect */
if ( $goToList )
{
\IPS\Output::i()->redirect( \IPS\Http\Url::internal("app=nexus&module=support&controller=requests"), \IPS\Member::loggedIn()->language()->addToStack( 'note_added_sent_to', NULL, array( 'sprintf' => $newDepartment->_title ) ) );
\IPS\Output::i()->redirect( \IPS\Http\Url::internal("app=nexus&module=support&controller=requests"), \IPS\Member::loggedIn()->language()->addToStack( 'note_added_sent_to', FALSE, array( 'sprintf' => $newDepartment->_title ) ) );
}
else
{
@@ -182,7 +182,7 @@ class _StockAction extends \IPS\Node\Model
$form->add( new \IPS\Helpers\Form\Translatable( 'action_name', NULL, TRUE, array( 'app' => 'nexus', 'key' => ( $this->id ? "nexus_stockaction_{$this->id}" : NULL ) ) ) );
$form->add( new \IPS\Helpers\Form\Node( 'action_status', $this->status ?: 0, FALSE, array( 'class' => 'IPS\nexus\Support\Status', 'zeroVal' => 'do_not_change' ) ) );
$form->add( new \IPS\Helpers\Form\Node( 'action_department', $this->department ?: 0, FALSE, array( 'class' => 'IPS\nexus\Support\Department', 'zeroVal' => 'do_not_move' ) ) );
$form->add( new \IPS\Helpers\Form\Select( 'action_staff', $this->staff ? $this->staff->member_id : 0, FALSE, array( 'parse' => 'normal', 'options' => array_merge( array( 0 => \IPS\Member::loggedIn()->language()->addToStack( 'do_not_change' ) ), \IPS\nexus\Support\Request::staff() ) ) ) );
$form->add( new \IPS\Helpers\Form\Select( 'action_staff', $this->staff ? $this->staff->member_id : 0, FALSE, array( 'parse' => 'normal', 'options' => array( 0 => \IPS\Member::loggedIn()->language()->addToStack( 'do_not_change' ) ) + \IPS\nexus\Support\Request::staff() ) ) );
$form->add( new \IPS\Helpers\Form\YesNo( 'action_message_on', $this->message, FALSE, array( 'togglesOn' => array( 'action_message_editor' ) ) ) );
$form->add( new \IPS\Helpers\Form\Editor( 'action_message', $this->message, FALSE, array( 'app' => 'nexus', 'key' => 'Support', 'autoSaveKey' => md5( 'stockaction-' . ( $this->id ?: 'new' ) ) ), NULL, NULL, NULL, 'action_message_editor' ) );
$form->add( new \IPS\Helpers\Form\Node( 'action_show_in', ( !$this->show_in or $this->show_in === '*' ) ? 0 : explode( ',', $this->show_in ), FALSE, array( 'class' => 'IPS\nexus\Support\Department', 'zeroVal' => 'all', 'multiple' => TRUE ) ) );
@@ -204,6 +204,13 @@ class _Transaction extends \IPS\Patterns\ActiveRecord
public function set_member( \IPS\Member $member )
{
$this->_data['member'] = (int) $member->member_id;
/* If this is an incomplete member, flag them so they will not get deleted */
if( $member->member_id AND ( empty( $member->name ) OR empty( $member->email ) OR $member->members_bitoptions['validating'] ) )
{
$member->members_bitoptions['created_externally'] = TRUE;
$member->save();
}
}
/**