Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @subpackage Nexus * @since 10 Feb 2014 */ namespace IPS\nexus\modules\front\checkout; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * Checkout */ class _checkout extends \IPS\Dispatcher\Controller { /** * @brief Invoice */ protected $invoice; /** * @brief Does the user need to log in? */ protected $needsToLogin = FALSE; /** * Execute * * @return void */ public function execute() { \IPS\Output::i()->sidebar['enabled'] = FALSE; \IPS\Output::i()->bodyClasses[] = 'ipsLayout_minimal'; \IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'checkout.css', 'nexus' ) ); \IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'store.css', 'nexus' ) ); if ( \IPS\Theme::i()->settings['responsive'] ) { \IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'store_responsive.css', 'nexus', 'front' ) ); } \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_checkout.js', 'nexus', 'front' ) ); \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_gateways.js', 'nexus', 'global' ) ); \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'module__nexus_checkout' ); parent::execute(); } /** * Checkout * * @return void */ protected function manage() { /* Load invoice */ try { $this->invoice = \IPS\nexus\Invoice::load( \IPS\Request::i()->id ); if ( !$this->invoice->canView() ) { throw new \OutOfRangeException; } } catch ( \OutOfRangeException $e ) { $msg = 'no_invoice_view'; if ( !\IPS\Member::loggedIn()->member_id ) { $msg = 'no_invoice_view_guest'; } \IPS\Output::i()->error( $msg, '2X196/1', 403, '' ); } $checkoutUrl = $this->invoice->checkoutUrl(); /* Is it paid? */ if ( $this->invoice->status === \IPS\nexus\Invoice::STATUS_PAID ) { if ( $this->invoice->return_uri ) { \IPS\Output::i()->redirect( $this->invoice->return_uri ); } else { \IPS\Output::i()->redirect( $this->invoice->url() ); } } /* Or cancelled or expired? */ if ( $this->invoice->status !== \IPS\nexus\Invoice::STATUS_PENDING ) { \IPS\Output::i()->redirect( $this->invoice->url() ); } /* Do we need to *show* the first step */ $canSkipFirstStepIfNameAndBillingAddressIsKnown = TRUE; if ( \IPS\Member::loggedIn()->member_id ) { $showFirstStep = FALSE; if ( $this->invoice->hasItemsRequiringBillingAddress() or $this->invoice->hasPhysicalItems() ) { $showFirstStep = TRUE; } if ( !$showFirstStep ) { foreach ( \IPS\nexus\Customer\CustomField::roots() as $field ) { $column = $field->column; if ( $field->purchase_show and $field->purchase_require and !$this->invoice->member->$column ) { $showFirstStep = TRUE; $canSkipFirstStepIfNameAndBillingAddressIsKnown = FALSE; break; } } } } else { $showFirstStep = TRUE; } /* What are the steps? */ $steps = array(); if ( $showFirstStep ) { $steps['checkout_customer'] = array( $this, '_customer' ); } if ( $this->invoice->hasPhysicalItems() ) { $steps['checkout_shipping'] = array( $this, '_shipping' ); } $steps['checkout_pay'] = array( $this, '_pay' ); /* Even if we have to show the first step, can we skip it because we already have their name and a primary billing address? */ if ( $showFirstStep and $canSkipFirstStepIfNameAndBillingAddressIsKnown and \IPS\Member::loggedIn()->member_id and $this->invoice->member->cm_first_name and $this->invoice->member->cm_last_name and !isset( $_SESSION[ 'wizard-' . md5( $checkoutUrl ) . '-step' ] ) ) { if ( $primaryBillingAddress = \IPS\nexus\Customer::loggedIn()->primaryBillingAddress() ) { $this->invoice->billaddress = $primaryBillingAddress; $this->invoice->recalculateTotal(); $this->invoice->save(); $_SESSION[ 'wizard-' . md5( $checkoutUrl ) . '-step' ] = isset( $steps['checkout_shipping'] ) ? 'checkout_shipping' : 'checkout_pay'; } } /* Do we need to log in? */ $this->needsToLogin = ( !\IPS\Member::loggedIn()->member_id and ( $this->invoice->requiresLogin() or \IPS\Settings::i()->nexus_donate_loggedin ) ); /* Facebook Pixel */ \IPS\core\Facebook\Pixel::i()->InitiateCheckout = true; /* Do the wizard */ \IPS\Output::i()->sidebar['enabled'] = FALSE; if ( isset( \IPS\Output::i()->breadcrumb['module'][0] ) ) { \IPS\Output::i()->breadcrumb['module'][0] = NULL; } \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate('checkout')->checkoutWrapper( (string) new \IPS\Helpers\Wizard( $steps, $checkoutUrl, ( !isset( $steps['checkout_login'] ) and isset( $steps['checkout_customer'] ) ) ) ); } /** * Step: Customer Details * * @return string */ public function _customer() { /* Init */ $buttonLang = 'continue_to_review'; $needBillingInfo = ( $this->invoice->hasItemsRequiringBillingAddress() or $this->invoice->hasPhysicalItems() ); if ( $this->invoice->hasPhysicalItems() ) { $buttonLang = 'continue_to_shipping'; } $form = new \IPS\Helpers\Form( 'customer', $buttonLang, $this->invoice->checkoutUrl()->setQueryString( '_step', 'checkout_customer' ) ); /* Account Information */ if ( $this->needsToLogin and !\in_array( \IPS\Login::registrationType(), array( 'disabled', 'redirect' ) ) ) { $guestData = $this->invoice->guest_data; $postBeforeRegister = NULL; if ( isset( \IPS\Request::i()->cookie['post_before_register'] ) ) { try { $postBeforeRegister = \IPS\Db::i()->select( '*', 'core_post_before_registering', array( 'secret=?', \IPS\Request::i()->cookie['post_before_register'] ) )->first(); } catch ( \UnderflowException $e ) { } } /* Add the registration related js stuff to the checkout form for email field validation */ $form->attributes['data-controller'] = 'core.front.system.register'; \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_system.js', 'core', 'front' ), \IPS\Output::i()->js( 'front_templates.js', 'core', 'front' ) ); if( isset( $_SESSION['coppa_user'] ) ) { if ( \IPS\Settings::i()->minimum_age > 0 ) { $message = \IPS\Member::loggedIn()->language()->addToStack( 'register_denied_age', FALSE, array( 'sprintf' => array( \IPS\Settings::i()->minimum_age ) ) ); \IPS\Output::i()->error( $message, '2X196/D', 403, '' ); } else { \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('reg_awaiting_validation'); return \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'system', 'core' )->notCoppaValidated(); } } if ( \IPS\Settings::i()->minimum_age > 0 OR \IPS\Settings::i()->use_coppa ) { $form->addHeader( 'coppa_title' ); /* We dynamically replace this as we need to show this message, however we do not want to create a "bday_desc" language string which may not be appropriate on other forms */ \IPS\Member::loggedIn()->language()->words['bday_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'coppa_verification_only' ); $form->add( new \IPS\Helpers\Form\Date( 'bday', NULL, TRUE, array( 'max' => \IPS\DateTime::create() ) ) ); } $form->addHeader('account_information'); if ( \IPS\Settings::i()->nexus_checkreg_usernames ) { $form->add( new \IPS\Helpers\Form\Text( 'username', isset( $guestData['member']['name'] ) ? $guestData['name'] : NULL, TRUE, array( 'accountUsername' => TRUE, 'htmlAutocomplete' => "username" ) ) ); } $form->add( new \IPS\Helpers\Form\Email( 'email_address', $guestData ? $guestData['member']['email'] : ( $postBeforeRegister ? $postBeforeRegister['email'] : NULL ), TRUE, array( 'accountEmail' => TRUE, 'maxLength' => 150, 'htmlAutocomplete' => "email" ) ) ); $form->add( new \IPS\Helpers\Form\Password( 'password', NULL, TRUE, array( 'protect' => TRUE, 'showMeter' => \IPS\Settings::i()->password_strength_meter, 'checkStrength' => TRUE, 'htmlAutocomplete' => "new-password" ) ) ); $form->add( new \IPS\Helpers\Form\Password( 'password_confirm', NULL, TRUE, array( 'protect' => TRUE, 'confirm' => 'password', 'htmlAutocomplete' => "new-password" ) ) ); if ( $needBillingInfo ) { $form->addHeader('billing_information'); } } /* Billing Information */ if ( $needBillingInfo or ( $this->needsToLogin and !\in_array( \IPS\Login::registrationType(), array( 'disabled', 'redirect' ) ) and !\IPS\Settings::i()->nexus_checkreg_usernames ) ) { $form->add( new \IPS\Helpers\Form\Text( 'cm_first_name', $this->invoice->member->cm_first_name, TRUE, array( 'htmlAutocomplete' => "given-name" ) ) ); $form->add( new \IPS\Helpers\Form\Text( 'cm_last_name', $this->invoice->member->cm_last_name, TRUE, array( 'htmlAutocomplete' => "family-name" ) ) ); } if ( $needBillingInfo ) { $addresses = \IPS\Db::i()->select( '*', 'nexus_customer_addresses', array( '`member`=?', \IPS\Member::loggedIn()->member_id ) ); if ( \count( $addresses ) ) { $billing = NULL; $options = array(); foreach ( new \IPS\Patterns\ActiveRecordIterator( $addresses, 'IPS\nexus\Customer\Address' ) as $address ) { $options[ $address->id ] = $address->address->toString('
'); if ( ( !$this->invoice->billaddress and $address->primary_billing ) or $this->invoice->billaddress == $address->address ) { $billing = $address->id; } } $options[0] = \IPS\Member::loggedIn()->language()->addToStack( 'other' ); $form->add( new \IPS\Helpers\Form\Radio( 'billing_address', $billing, TRUE, array( 'options' => $options, 'toggles' => array( 0 => array( 'new_billing_address' ) ), 'parse' => 'raw' ) ) ); $newAddress = new \IPS\Helpers\Form\Address( 'new_billing_address', !$billing ? $this->invoice->billaddress : NULL, FALSE, array(), NULL, NULL, NULL, 'new_billing_address' ); $newAddress->label = ' '; $form->add( $newAddress ); } else { $form->add( new \IPS\Helpers\Form\Address( 'new_billing_address', $this->invoice->billaddress, TRUE ) ); } } /* Customer Fields */ $customer = \IPS\nexus\Customer::loggedIn(); foreach ( \IPS\nexus\Customer\CustomField::roots() as $field ) { if ( $field->purchase_show ) { $column = $field->column; $field->not_null = $field->purchase_require; $input = $field->buildHelper( $customer->$column ); $input->appearRequired = $field->purchase_require; $form->add( $input ); } } /* Additional Information */ if ( $this->needsToLogin and !\in_array( \IPS\Login::registrationType(), array( 'disabled', 'redirect' ) ) ) { $form->addHeader('additional_information'); /* Custom fields */ if( \IPS\Login::registrationType() == 'full' ) { $customFields = \IPS\core\ProfileFields\Field::fields( $guestData ? $guestData['profileFields'] : NULL, \IPS\core\ProfileFields\Field::REG ); if ( \count( $customFields ) ) { foreach ( $customFields as $group => $fields ) { foreach ( $fields as $field ) { $form->add( $field ); } } $form->addSeparator(); } } /* Security Questions */ if ( \IPS\Settings::i()->security_questions_enabled and \in_array( \IPS\Settings::i()->security_questions_prompt, array( 'register', 'optional' ) ) ) { $numberOfQuestions = \IPS\Settings::i()->security_questions_number ?: 3; $securityQuestions = array(); foreach ( \IPS\MFA\SecurityQuestions\Question::roots() as $securityQuestion ) { $securityQuestions[ $securityQuestion->id ] = $securityQuestion->_title; } $form->addMessage( \IPS\Member::loggedIn()->language()->addToStack('security_questions_setup_blurb', FALSE, array( 'pluralize' => array( $numberOfQuestions ) ) ) ); if ( \IPS\Settings::i()->security_questions_prompt === 'optional' ) { $securityOptoutToggles = array(); foreach ( range( 1, min( $numberOfQuestions, \count( $securityQuestions ) ) ) as $i ) { $securityOptoutToggles[] = 'security_question_q_' . $i; $securityOptoutToggles[] = 'security_question_a_' . $i; } $optOutCheckbox = new \IPS\Helpers\Form\Checkbox( 'security_questions_optout_title', FALSE, FALSE, array( 'togglesOff' => $securityOptoutToggles ) ); if ( \IPS\Member::loggedIn()->language()->checkKeyExists('security_questions_opt_out_warning_value') ) { $optOutCheckbox->description = \IPS\Member::loggedIn()->language()->addToStack('security_questions_opt_out_warning_value'); } $form->add( $optOutCheckbox ); } foreach ( range( 1, min( $numberOfQuestions, \count( $securityQuestions ) ) ) as $i ) { $securityValidation = function( $val ) { if ( !$val and ( \IPS\Settings::i()->security_questions_prompt === 'register' or !isset( \IPS\Request::i()->security_questions_optout_title_checkbox ) ) ) { throw new \DomainException('form_required'); } }; $questionField = new \IPS\Helpers\Form\Select( 'security_question_q_' . $i, NULL, FALSE, array( 'options' => $securityQuestions ), $securityValidation, NULL, NULL, 'security_question_q_' . $i ); $questionField->label = \IPS\Member::loggedIn()->language()->addToStack('security_question_q'); $answerField = new \IPS\Helpers\Form\Text( 'security_question_a_' . $i, NULL, NULL, array(), $securityValidation, NULL, NULL, 'security_question_a_' . $i ); $answerField->label = \IPS\Member::loggedIn()->language()->addToStack('security_question_a'); $form->add( $questionField ); $form->add( $answerField ); } $form->addSeparator(); } /* Q&A */ if ( \IPS\Settings::i()->nexus_checkreg_captcha ) { $question = FALSE; try { $question = \IPS\Db::i()->select( '*', 'core_question_and_answer', NULL, "RAND()" )->first(); } catch ( \UnderflowException $e ) {} if( $question ) { $form->hiddenValues['q_and_a_id'] = $question['qa_id']; $form->add( new \IPS\Helpers\Form\Text( 'q_and_a', NULL, TRUE, array(), function( $val ) { $qanda = \intval( \IPS\Request::i()->q_and_a_id ); $pass = true; if( $qanda ) { $question = \IPS\Db::i()->select( '*', 'core_question_and_answer', array( 'qa_id=?', $qanda ) )->first(); $answers = json_decode( $question['qa_answers'] ); if( \count( $answers ) ) { $pass = FALSE; foreach( $answers as $answer ) { $answer = trim( $answer ); if( mb_strlen( $answer ) AND mb_strtolower( $answer ) == mb_strtolower( $val ) ) { $pass = TRUE; } } } } else { $questions = \IPS\Db::i()->select( 'count(*)', 'core_question_and_answer', 'qa_id > 0' )->first(); if( $questions ) { $pass = FALSE; } } if( !$pass ) { throw new \DomainException( 'q_and_a_incorrect' ); } } ) ); \IPS\Member::loggedIn()->language()->words['q_and_a'] = \IPS\Member::loggedIn()->language()->addToStack( 'core_question_and_answer_' . $question['qa_id'], FALSE ); } } /* Captcha */ if ( !$guestData ) { $captcha = new \IPS\Helpers\Form\Captcha; if ( (string) $captcha !== '' ) { $form->add( $captcha ); } } /* Misc */ $form->add( new \IPS\Helpers\Form\Checkbox( 'reg_admin_mails', $guestData ? $guestData['member']['allow_admin_mails'] : ( \IPS\Settings::i()->updates_consent_default == 'enabled' ? TRUE : FALSE ), FALSE ) ); \IPS\core\modules\front\system\register::buildRegistrationTerm(); $form->add( new \IPS\Helpers\Form\Checkbox( 'reg_agreed_terms', (bool) $guestData, TRUE, array(), function( $val ) { if ( !$val ) { throw new \InvalidArgumentException('reg_not_agreed_terms'); } } ) ); } /* Handle submission */ if ( $values = $form->values() ) { /* Set guest transaction key cookie so only this guest can view transaction info */ if( !isset( \IPS\Request::i()->cookie['guestTransactionKey'] ) AND !\IPS\Member::loggedIn()->member_id ) { $guestTransactionKey = \IPS\Login::generateRandomString(); \IPS\Request::i()->setCookie( 'guestTransactionKey', $guestTransactionKey, \IPS\DateTime::ts( time() )->add( new \DateInterval( 'P30D' ) ) ); } /* If user is a guest create the member object but don't save it */ if ( $this->needsToLogin ) { /* It shouldn't be possible to get here */ if ( \in_array( \IPS\Login::registrationType(), array( 'disabled', 'redirect' ) ) ) { \IPS\Output::i()->error( 'reg_disabled', '3X196/A', 403, '' ); } /* Did we pass the minimum age requirement? */ if ( \IPS\Settings::i()->minimum_age > 0 OR \IPS\Settings::i()->use_coppa ) { if ( \IPS\Settings::i()->minimum_age > 0 AND $values['bday']->diff( \IPS\DateTime::create() )->y < \IPS\Settings::i()->minimum_age ) { $_SESSION['coppa_user'] = TRUE; $message = \IPS\Member::loggedIn()->language()->addToStack( 'register_denied_age', FALSE, array( 'sprintf' => array( \IPS\Settings::i()->minimum_age ) ) ); \IPS\Output::i()->error( $message, '2X196/E', 403, '' ); } /* We did, but we should check normal COPPA too */ else if( ( $values['bday']->diff( \IPS\DateTime::create() )->y < 13 ) ) { $_SESSION['coppa_user'] = TRUE; return \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'system', 'core' )->notCoppaValidated(); } } /* Security questions */ $securityQuestionAnswers = array(); if ( \IPS\Settings::i()->security_questions_enabled and ( \IPS\Settings::i()->security_questions_prompt === 'register' or ( \IPS\Settings::i()->security_questions_prompt === 'optional' and !$values['security_questions_optout_title'] ) ) ) { foreach ( $values as $k => $v ) { if ( preg_match( '/^security_question_q_(\d+)$/', $k, $matches ) ) { if ( isset( $securityQuestionAnswers[ $v ] ) ) { $form->error = \IPS\Member::loggedIn()->language()->addToStack( 'security_questions_unique', FALSE, array( 'pluralize' => array( \IPS\Settings::i()->security_questions_number ?: 3 ) ) ); break; } else { $securityQuestionAnswers[ $v ] = \IPS\Text\Encrypt::fromPlaintext( $values[ 'security_question_a_' . $matches[1] ] )->tag(); } } } } /* Continue... */ if ( !$form->error ) { /* Set basic details */ $member = new \IPS\nexus\Customer; if ( \IPS\Settings::i()->nexus_checkreg_usernames ) { if ( $needBillingInfo ) { $member->cm_first_name = $values['cm_first_name']; $member->cm_last_name = $values['cm_last_name']; } $member->name = $values['username']; } else { $member->cm_first_name = $values['cm_first_name']; $member->cm_last_name = $values['cm_last_name']; if( !\IPS\Login::usernameIsInUse( "{$values['cm_first_name']} {$values['cm_last_name']}" ) ) { $member->name = "{$values['cm_first_name']} {$values['cm_last_name']}"; } } $member->email = $values['email_address']; $member->setLocalPassword( $values['password'] ); $member->allow_admin_mails = $values['reg_admin_mails']; $member->member_group_id = \IPS\Settings::i()->member_group; /* Customer Fields */ foreach ( \IPS\nexus\Customer\CustomField::roots() as $field ) { if ( $field->purchase_show ) { $column = $field->column; $helper = $field->buildHelper(); $member->$column = $helper::stringValue( $values["nexus_ccfield_{$field->id}"] ); } if ( $field->type === 'Editor' ) { $field->claimAttachments( $member->member_id ); } } /* Custom Fields */ $profileFields = array(); foreach ( \IPS\core\ProfileFields\Field::fields( array(), \IPS\core\ProfileFields\Field::REG ) as $group => $fields ) { foreach ( $fields as $id => $field ) { if ( $field instanceof \IPS\Helpers\Form\Upload ) { $profileFields[ "field_{$id}" ] = (string) $values[ $field->name ]; } else { $profileFields[ "field_{$id}" ] = $field::stringValue( !empty( $values[ $field->name ] ) ? $values[ $field->name ] : NULL ); } if ( $fields instanceof \IPS\Helpers\Form\Editor ) { $field->claimAttachments( $member->member_id ); } } } /* Run it through the spam service */ $spamCode = NULL; $spamAction = NULL; if( \IPS\Settings::i()->spam_service_enabled ) { $spamAction = $member->spamService( 'register', NULL, $spamCode ); if( $spamAction == 4 ) { \IPS\Output::i()->error( 'spam_denied_account', '2S129/1', 403, '' ); } } /* Save on invoice */ $this->invoice->guest_data = array( 'member' => $member->changed, 'profileFields' => $profileFields, 'securityAnswers' => $securityQuestionAnswers, 'spamData' => array( 'code' => $spamCode, 'action' => $spamAction ), 'guestTransactionKey' => \IPS\Request::i()->cookie['guestTransactionKey'], 'pbr' => $postBeforeRegister ? $postBeforeRegister['secret'] : NULL, 'referred_by' => isset( \IPS\Request::i()->cookie['referred_by'] ) ? \IPS\Request::i()->cookie['referred_by'] : NULL ); } } /* Otherwise just update the name and details */ else { $changes = array(); if ( $needBillingInfo ) { foreach ( array( 'cm_first_name', 'cm_last_name' ) as $k ) { if ( $values[ $k ] != \IPS\nexus\Customer::loggedIn()->$k ) { $changes['name'] = \IPS\nexus\Customer::loggedIn()->cm_name; \IPS\nexus\Customer::loggedIn()->$k = $values[ $k ]; } } } foreach ( \IPS\nexus\Customer\CustomField::roots() as $field ) { if ( $field->purchase_show ) { $column = $field->column; $helper = $field->buildHelper(); $valueToSave = $helper::stringValue( $values["nexus_ccfield_{$field->id}"] ); if ( \IPS\nexus\Customer::loggedIn()->$column != $valueToSave ) { $changes['other'][] = array( 'name' => 'nexus_ccfield_' . $field->id, 'value' => $field->displayValue( $valueToSave ), 'old' => $field->displayValue( \IPS\nexus\Customer::loggedIn()->$column ) ); } \IPS\nexus\Customer::loggedIn()->$column = $valueToSave; } } if ( !empty( $changes ) ) { \IPS\nexus\Customer::loggedIn()->log( 'info', $changes ); } /* We only want to do this if it's an actual account */ if ( \IPS\nexus\Customer::loggedIn()->member_id ) { \IPS\nexus\Customer::loggedIn()->save(); } else { /* Otherwise, we need to store this as guest data */ $this->invoice->guest_data = array( 'member' => \IPS\nexus\Customer::loggedIn()->changed, 'profileFields' => array(), 'securityAnswers' => array(), 'guestTransactionKey' => \IPS\Request::i()->cookie['guestTransactionKey'] ); } } if ( !$form->error ) { /* Save the billing address */ if ( $needBillingInfo ) { if ( \count( $addresses ) and $values['billing_address'] ) { $this->invoice->billaddress = \IPS\nexus\Customer\Address::load( $values['billing_address'] )->address; } else { if( empty( $values['new_billing_address']->addressLines ) or !$values['new_billing_address']->city or !$values['new_billing_address']->country or ( !$values['new_billing_address']->region and array_key_exists( $values['new_billing_address']->country, \IPS\GeoLocation::$states ) ) or !$values['new_billing_address']->postalCode ) { $form->error = \IPS\Member::loggedIn()->language()->addToStack('billing_address_required'); return $form; } if ( \IPS\Member::loggedIn()->member_id ) { $address = new \IPS\nexus\Customer\Address; $address->member = \IPS\Member::loggedIn(); $address->address = $values['new_billing_address']; $address->primary_billing = !\count( $addresses ); $address->primary_shipping = ( !\count( $addresses ) and !$this->invoice->hasPhysicalItems() ); $address->save(); \IPS\nexus\Customer::loggedIn()->log( 'address', array( 'type' => 'add', 'details' => json_encode( $values['new_billing_address'] ) ) ); } $this->invoice->billaddress = $values['new_billing_address']; } } /* Save */ $this->invoice->recalculateTotal(); $this->invoice->save(); return array(); } } /* If we're not logged in, and we need an account for this purchase, show the login form */ $login = NULL; $loginError = NULL; $mfaOutput = NULL; if ( $this->needsToLogin ) { /* Two-Factor Authentication */ if ( isset( \IPS\Request::i()->mfa ) and isset( $_SESSION['processing2FACheckout'] ) and $_SESSION['processing2FACheckout']['invoice'] === $this->invoice->id ) { $member = \IPS\Member::load( $_SESSION['processing2FACheckout']['memberId'] ); if ( !$member->member_id ) { unset( $_SESSION['processing2FACheckout'] ); \IPS\Output::i()->redirect( $this->invoice->checkoutUrl() ); } $device = \IPS\Member\Device::loadOrCreate( $member ); $mfaOutput = \IPS\MFA\MFAHandler::accessToArea( 'core', $device->known ? 'AuthenticateFrontKnown' : 'AuthenticateFront', $this->invoice->checkoutUrl()->setQueryString( 'mfa', 1 ), $member ); if ( !$mfaOutput ) { /* Set the invoice owner */ $this->invoice->member = $member; $this->invoice->save(); /* Process the login */ ( new \IPS\Login\Success( $member, \IPS\Login\Handler::load( $_SESSION['processing2FACheckout']['handler'] ), $_SESSION['processing2FACheckout']['remember'], $_SESSION['processing2FACheckout']['anonymous'] ) )->process(); /* Redirect */ \IPS\Output::i()->redirect( $this->invoice->checkoutUrl() ); } } /* Login */ $login = new \IPS\Login( $this->invoice->checkoutUrl() ); try { if ( $success = $login->authenticate() ) { /* Verify it's okay for this member to be buying those items */ try { foreach ( $this->invoice->items as $item ) { $item->memberCanPurchase( $success->member ); } } catch ( \DomainException $e ) { \IPS\Output::i()->error( $e->getMessage(), '1X196/B', 403, '' ); } /* Process the login */ if ( $success->mfa() ) { $_SESSION['processing2FACheckout'] = array( 'memberId' => $success->member->member_id, 'invoice' => $this->invoice->id, 'anonymous' => $success->anonymous, 'remember' => $success->rememberMe, 'handler' => $success->handler->id ); \IPS\Output::i()->redirect( $this->invoice->checkoutUrl()->setQueryString( 'mfa', 1 ) ); } else { /* Set the invoice owner */ $this->invoice->member = $success->member; $this->invoice->save(); /* Process the login */ $success->process(); /* Redirect */ \IPS\Output::i()->redirect( $this->invoice->checkoutUrl() ); } } } catch ( \IPS\Login\Exception $e ) { $loginError = $e->getMessage(); } } /* Display */ return \IPS\Theme::i()->getTemplate('checkout')->customerInformation( $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'checkout', 'nexus' ), 'customerInformationForm' ) ), $login, $loginError, $this->invoice ); } /** * Step: Select Shipping * * @return \IPS\Helpers\Form */ public function _shipping() { /* Init */ $form = new \IPS\Helpers\Form( 'shipping', 'continue_to_review', $this->invoice->checkoutUrl()->setQueryString( '_step', 'checkout_shipping' ) ); $form->attributes['data-controller'] = 'nexus.front.checkout.billingForm'; $form->attributes['data-new-billing-address-url'] = $this->invoice->checkoutUrl()->setQueryString( 'do', 'addShippingAddress' ); /* Shipping Address field */ $primaryShipping = NULL; $billingAddress = NULL; if ( \IPS\Member::loggedIn()->member_id ) { $addresses = \IPS\Db::i()->select( '*', 'nexus_customer_addresses', array( '`member`=?', \IPS\Member::loggedIn()->member_id ) ); foreach ( new \IPS\Patterns\ActiveRecordIterator( $addresses, 'IPS\nexus\Customer\Address' ) as $address ) { if ( $address->primary_shipping ) { $primaryShipping = $address->id; } if ( $this->invoice->billaddress == $address->address ) { $billingAddress = $address->id; } } $form->hiddenValues['shipping_address'] = \IPS\nexus\Customer\Address::load( isset( \IPS\Request::i()->shipping_address ) ? \IPS\Request::i()->shipping_address : ( $primaryShipping ?: $billingAddress ) )->id; $this->invoice->shipaddress = \IPS\nexus\Customer\Address::load( isset( \IPS\Request::i()->shipping_address ) ? \IPS\Request::i()->shipping_address : ( $primaryShipping ?: $billingAddress ) )->address; } elseif ( !$this->invoice->shipaddress ) { $this->invoice->shipaddress = $this->invoice->billaddress; } /* Shipping method field */ if ( isset( \IPS\Request::i()->shipping_address ) ) { /* Save selected address */ $this->invoice->shipaddress = \IPS\nexus\Customer\Address::load( \IPS\Request::i()->shipping_address )->address; $this->invoice->save(); } /* Get shipping methods */ $shipMethods = array(); foreach ( \IPS\nexus\Shipping\FlatRate::roots() as $rate ) { if ( $rate->isAvailable( $this->invoice->shipaddress, iterator_to_array( $this->invoice->items ), $this->invoice->currency, $this->invoice ) ) { $shipMethods[ $rate->id ] = $rate; } } /* Shipping method field */ $shippingGroups = array(); $selected = array(); $shippingAddressErrors = array(); foreach ( $this->invoice->items as $k => $item ) { if ( $item->physical ) { if ( $item->shippingMethodIds ) { $availableMethods = ( \IPS\Settings::i()->easypost_api_key and \IPS\Settings::i()->easypost_show_rates ) ? array_intersect( $item->shippingMethodIds, array_merge( array_keys( $shipMethods ), array( 'easypost' ) ) ) : array_intersect( $item->shippingMethodIds, array_keys( $shipMethods ) ); } else { $availableMethods = ( \IPS\Settings::i()->easypost_api_key and \IPS\Settings::i()->easypost_show_rates ) ? array_merge( array_keys( $shipMethods ), array( 'easypost' ) ) : array_keys( $shipMethods ); } if ( empty( $availableMethods ) ) { $shippingAddressErrors[] = \IPS\Member::loggedIn()->language()->addToStack( 'checkout_no_ship', FALSE, array( 'sprintf' => array( $item->name ) ) ); } sort( $availableMethods ); $key = md5( json_encode( $availableMethods ) ); if ( !isset( $shippingGroups[ $key ] ) ) { $shippingGroups[ $key ] = array( 'items' => array(), 'methods' => array() ); foreach ( $availableMethods as $v ) { $shippingGroups[ $key ]['methods'][ $v ] = ( $v === 'easypost' ? NULL : $shipMethods[ $v ] ); } } $shippingGroups[ $key ]['items'][ $k ] = $item; if ( isset( $item->chosenShippingMethodId ) and $item->chosenShippingMethodId ) { $selected[ $key ] = $item->chosenShippingMethodId; } } } if ( \IPS\Settings::i()->easypost_api_key and \IPS\Settings::i()->easypost_show_rates ) { foreach ( $shippingGroups as $key => $data ) { if ( array_key_exists( 'easypost', $data['methods'] ) ) { unset( $shippingGroups[ $key ]['methods']['easypost'] ); $lengthInInches = 0; $widthInInches = 0; $heightInInches = 0; $weightInOz = 0; foreach ( $data['items'] as $item ) { $weightInOz += ( $item->weight->float('oz') * $item->quantity ); $heightInInches += ( $item->height->float('in') * $item->quantity ); foreach ( array( 'length', 'width' ) as $k ) { $v = "{$k}InInches"; if ( $item->$k->float('in') > $$v ) { $$v = $item->$k->float('in'); } } } try { $easyPost = \IPS\nexus\Shipping\EasyPostRate::getRates( $lengthInInches, $widthInInches, $heightInInches, $weightInOz, $this->invoice->member, $this->invoice->shipaddress, $this->invoice->currency ); if ( isset( $easyPost['rates'] ) ) { foreach ( $easyPost['rates'] as $rate ) { if ( $rate['currency'] === $this->invoice->currency ) { $shippingGroups[ $key ]['methods'][ $rate['service'] ] = new \IPS\nexus\Shipping\EasyPostRate( $rate ); } } } } catch ( \IPS\Http\Request\Exception $e ) { } if ( !\count( $shippingGroups[ $key ]['methods'] ) ) { \IPS\Output::i()->error( 'err_no_shipping_methods', '4X196/6', 403, 'err_no_shipping_methods_admin' ); } } } } $defaults = array(); foreach ( $shippingGroups as $key => $data ) { foreach ( $data['methods'] as $_methodId => $_methodData ) { $defaults[ $key ] = $_methodId; break; } } $form->add( new \IPS\nexus\Form\Shipping( 'shipping_method', \count( $selected ) ? $selected : $defaults, TRUE, array( 'options' => $shippingGroups, 'currency' => $this->invoice->currency, 'invoice' => $this->invoice ) ) ); /* Submissions */ if ( $values = $form->values() ) { /* Save new shipping address */ if ( !$this->invoice->shipaddress and !$form->hiddenValues['shipping_address'] ) { $this->addShippingAddress(); return \IPS\Output::i()->output; } elseif ( !isset( $values['shipping_method'] ) ) { \IPS\Output::i()->redirect( $this->invoice->checkoutUrl()->setQueryString( 'shipping_address', $form->hiddenValues['shipping_address'] ) ); } /* Remove any existing shipping charges on the invoice */ foreach ( $this->invoice->items as $k => $v ) { if ( $v instanceof \IPS\nexus\extensions\nexus\Item\ ShippingCharge ) { $this->invoice->removeItem( $k ); } } /* Loop chosen methods */ foreach ( $values['shipping_method'] as $key => $method ) { /* Set that we've chosen that method for those items */ foreach ( $shippingGroups[ $key ]['items'] as $k => $item ) { $this->invoice->changeItem( $k, array( 'chosen_shipping' => $method ) ); } /* Add the charge to the invoice */ $_method = $shippingGroups[ $key ]['methods'][ $method ]; $charge = new \IPS\nexus\extensions\nexus\Item\ShippingCharge( $_method->getName(), $_method->getPrice( $shippingGroups[ $key ]['items'], $this->invoice->currency, $this->invoice ) ); $charge->id = $method; $charge->tax = $_method->getTax(); $shippingItems[] = $charge; } /* Save */ foreach ( $shippingItems as $s ) { $this->invoice->addItem( $s ); } $this->invoice->save(); /* Continue */ return array(); } /* Display */ return \IPS\Theme::i()->getTemplate( 'checkout', 'nexus' )->checkoutShipping( $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'checkout', 'nexus' ), 'checkoutShippingForm' ), $this->invoice->shipaddress, $shippingAddressErrors ), $this->invoice ); } /** * Add shipping address * * @return \IPS\Helpers\Form */ protected function addShippingAddress() { if ( !$this->invoice ) { try { $this->invoice = \IPS\nexus\Invoice::loadAndCheckPerms( \IPS\Request::i()->id ); } catch ( \OutOfRangeException $e ) { \IPS\Output::i()->error( 'no_module_permission', '2X196/7', 403, '' ); } } $form = new \IPS\Helpers\Form( 'new_shipping_address', 'continue', $this->invoice->checkoutUrl()->setQueryString( 'do', 'addShippingAddress' ) ); /* Shipping Address field */ if ( \IPS\Member::loggedIn()->member_id ) { $addresses = \IPS\Db::i()->select( '*', 'nexus_customer_addresses', array( '`member`=?', \IPS\Member::loggedIn()->member_id ) ); $options = array(); $primaryShipping = NULL; $billingAddress = NULL; foreach ( new \IPS\Patterns\ActiveRecordIterator( $addresses, 'IPS\nexus\Customer\Address' ) as $address ) { $options[ $address->id ] = $address->address->toString('
'); if ( $address->primary_shipping ) { $primaryShipping = $address->id; } if ( $this->invoice->billaddress == $address->address ) { $billingAddress = $address->id; } } $options[0] = 'other'; $form->add( new \IPS\Helpers\Form\Radio( 'shipping_address', $primaryShipping ?: $billingAddress, TRUE, array( 'options' => $options, 'disabled' => ( isset( \IPS\Request::i()->shipping_address ) AND !isset( \IPS\Request::i()->new_shipping_address_submitted ) ) ), function( $val ) { if ( $val ) { return static::_shippingAddressValidation( \IPS\nexus\Customer\Address::load( $val )->address ); } } ) ); } $form->add( new \IPS\Helpers\Form\Address( 'new_shipping_address', NULL, FALSE, array(), function( $val ) { if ( $val ) { return static::_shippingAddressValidation( $val ); } }, NULL, NULL, 'new_shipping_address' ) ); if ( $values = $form->values() ) { if ( \IPS\Member::loggedIn()->member_id ) { $addressId = $values['shipping_address']; if ( \intval( $values['shipping_address'] ) === 0 ) { $address = new \IPS\nexus\Customer\Address; $address->member = \IPS\Member::loggedIn(); $address->address = $values['new_shipping_address']; $address->save(); $addressId = $address->id; \IPS\nexus\Customer::loggedIn()->log( 'address', array( 'type' => 'add', 'details' => json_encode( $values['shipping_address'] ) ) ); } \IPS\Output::i()->redirect( $this->invoice->checkoutUrl()->setQueryString( 'shipping_address', $addressId )->setQueryString( '_step', 'checkout_shipping' ) ); } else { $this->invoice->shipaddress = $values['new_shipping_address']; $this->invoice->save(); \IPS\Output::i()->redirect( $this->invoice->checkoutUrl() ); } } \IPS\Output::i()->output = \IPS\Member::loggedIn()->member_id ? $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'checkout', 'nexus' ), 'changeShippingAddressForm' ) ) : $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) ); } /** * Shipping Address Validation * * @param \IPS\Geolocation $address The address * @return void * @throws \DomainException */ protected static function _shippingAddressValidation( \IPS\GeoLocation $address ) { if ( \IPS\Settings::i()->easypost_api_key and $address->country === 'US' ) { $phone = NULL; foreach ( \IPS\nexus\Customer\CustomField::roots() as $field ) { if ( $field->type === 'Tel' ) { $fieldId = 'nexus_ccfield_' . $field->id; $phone = \IPS\Request::i()->$fieldId; if ( $field->column === 'cm_phone' ) { break; } } } $addressLines = $address->addressLines; $response = \IPS\Http\Url::external( 'https://api.easypost.com/v2/addresses' )->request()->login( \IPS\Settings::i()->easypost_api_key, '' )->post( array( 'address' => array( 'street1' => array_shift( $addressLines ), 'street2' => \count( $addressLines ) ? implode( ', ', $addressLines ) : NULL, 'city' => $address->city, 'state' => $address->region, 'zip' => $address->postalCode, 'country' => $address->country, 'name' => \IPS\Request::i()->cm_first_name . ' ' . \IPS\Request::i()->cm_last_name, 'phone' => $phone, 'email' => \IPS\Member::loggedIn()->email ) ) )->decodeJson(); $response = \IPS\Http\Url::external( "https://api.easypost.com/v2/addresses/{$response['id']}/verify" )->request()->login( \IPS\Settings::i()->easypost_api_key, '' )->get(); if ( $response->httpResponseCode != 200 ) { throw new \DomainException('address_couldnt_validate'); } } return NULL; } /** * Step: Select Payment Method * * @param array $data Wizard data * @return string */ public function _pay( $data ) { /* How much are we paying? */ $this->invoice->recalculateTotal(); $amountToPay = $this->invoice->amountToPay( TRUE ); if ( isset( \IPS\Request::i()->split ) ) { $split = new \IPS\Math\Number( \IPS\Request::i()->split ); if ( $amountToPay->amount->compare( $split ) === 1 ) { $amountToPay->amount = $split; } } if ( !$amountToPay->amount->isPositive() ) { \IPS\Output::i()->error( 'err_no_methods', '5X196/8', 500, '' ); } /* Work out recurring payments */ $recurrings = array(); $overriddenRenewalTerms = array(); foreach ( $this->invoice->items as $item ) { if ( $item->groupWithParent and \is_int( $item->parent ) and isset( $item->renewalTerm ) and $item->renewalTerm ) { $parent = $this->invoice->items[ $item->parent ]; if ( ( isset( $parent->renewalTerm ) and $parent->renewalTerm ) or isset( $overriddenRenewalTerms[ $item->parent ] ) ) { if ( isset( $overriddenRenewalTerms[ $item->parent ] ) ) { $oldTerm = $overriddenRenewalTerms[ $item->parent ]; } else { $oldTerm = $parent->renewalTerm; } for( $i=0, $j=$item->quantity; $i < $j; $i++ ) { $overriddenRenewalTerms[ $item->parent ] = new \IPS\nexus\Purchase\RenewalTerm( ( isset( $overriddenRenewalTerms[ $item->parent ] ) ) ? $overriddenRenewalTerms[ $item->parent ]->add( $item->renewalTerm ) : $oldTerm->add( $item->renewalTerm ), $oldTerm->interval, $oldTerm->tax ); } } else { $overriddenRenewalTerms[ $item->parent ] = $item->renewalTerm; } } } foreach ( $this->invoice->items as $k => $item ) { if ( !$item->groupWithParent ) { $term = NULL; $dueDate = NULL; if ( isset( $overriddenRenewalTerms[ $k ] ) ) { $term = $overriddenRenewalTerms[ $k ]; $dueDate = \IPS\DateTime::create()->add( $term->interval ); } elseif ( $item instanceof \IPS\nexus\Invoice\Item\Renewal ) { $term = \IPS\nexus\Purchase::load( $item->id )->renewals; if ( $expireDate = \IPS\nexus\Purchase::load( $item->id )->expire and $expireDate->getTimestamp() > time() ) { $dueDate = \IPS\nexus\Purchase::load( $item->id )->expire; } else { $dueDate = \IPS\DateTime::create(); } for ( $i = 0; $i < $item->quantity; $i++ ) { $dueDate = $dueDate->add( $term->interval ); } } elseif ( isset( $item->renewalTerm ) and $item->renewalTerm ) { $term = $item->renewalTerm; $dueDate = \IPS\DateTime::create()->add( $item->initialInterval ?: $term->interval ); } if ( $term ) { $format = $item->groupWithParent ? 'grouped' : $term->interval->format('%d/%m/%y') . '/' . $term->cost->currency . '/' . ( $term->tax ? $term->tax->id : '0' ); $clonedItem = clone $item; if ( $item instanceof \IPS\nexus\Invoice\Item\Renewal ) // If they are renewing for more than one cycle now, that's fine, but subsequently they will be moved back to the normal terms { $clonedItem->quantity = 1; } if ( isset( $recurrings[ $format ] ) ) { $recurrings[ $format ]['items'][] = $clonedItem; } else { $recurrings[ $format ] = array( 'items' => array( $clonedItem ), 'term' => new \IPS\nexus\Purchase\RenewalTerm( new \IPS\nexus\Money( 0, $term->cost->currency ), $term->interval, $term->tax ) ); } $recurrings[ $format ]['term']->cost->amount = $recurrings[ $format ]['term']->cost->amount->add( $term->cost->amount->multiply( new \IPS\Math\Number( "{$clonedItem->quantity}" ) ) ); $recurrings[ $format ]['dueDate'] = $dueDate; } } } /* If there's something to pay, we'll need to look at the gateways... */ $paymentType = NULL; $hiddenValues = array(); if ( $amountToPay->amount->isGreaterThanZero() ) { $paymentType = 'pay'; /* Get available payment methods */ $paymentMethods = array(); foreach ( \IPS\nexus\Gateway::roots() as $gateway ) { if ( $gateway->checkValidity( $amountToPay, $this->invoice->billaddress, \IPS\nexus\Customer::loggedIn()->member_id ? \IPS\nexus\Customer::loggedIn() : \IPS\nexus\Customer::constructFromData( $this->invoice->guest_data['member'] ), $recurrings ) ) { $paymentMethods[ $gateway->id ] = $gateway; } } /* Remove any not supported by items */ $canUseAccountCredit = TRUE; foreach ( $this->invoice->items as $item ) { if ( $item->paymentMethodIds ) { foreach ( $paymentMethods as $k => $v ) { if ( \in_array( '*', $item->paymentMethodIds ) ) // This looks odd but in older versions IPS\nexus\extensions\nexus\Item\Subscription::renewalPaymentMethodIds() was mistakenly returning array('*') and since the value is stored in the invoice we have to keep this for compatibility with invoices created at that time { continue; } if ( !\in_array( $k, $item->paymentMethodIds ) ) { unset( $paymentMethods[ $k ] ); } } } if ( !$item::$canUseAccountCredit ) { $canUseAccountCredit = FALSE; } } /* If we don't have any, show an error */ if ( \count( $paymentMethods ) === 0 and !$amountToPay->amount->isZero() ) { \IPS\Output::i()->error( 'err_no_methods', '4X196/3', 500, 'err_no_methods_admin' ); } /* Build form */ $elements = array(); $paymentMethodsToggles = array(); $showSubmitButton = FALSE; foreach ( $paymentMethods as $gateway ) { foreach ( $gateway->paymentScreen( $this->invoice, $amountToPay, NULL, $recurrings ) as $element ) { if ( !$element->htmlId ) { $element->htmlId = $gateway->id . '-' . $element->name; } $elements[] = $element; $paymentMethodsToggles[ $gateway->id ][] = $element->htmlId; } if ( $gateway->showSubmitButton() ) { $showSubmitButton = TRUE; $paymentMethodsToggles[ $gateway->id ][] = 'paymentMethodSubmit'; } } $paymentMethodOptions = array(); foreach ( $paymentMethods as $k => $v ) { $paymentMethodOptions[ $k ] = $v->_title; } if ( $canUseAccountCredit and isset( \IPS\nexus\Customer::loggedIn()->cm_credits[ $this->invoice->currency ] ) and \IPS\nexus\Customer::loggedIn()->cm_credits[ $this->invoice->currency ]->amount->isGreaterThanZero() ) { $paymentMethodOptions[0] = \IPS\Member::loggedIn()->language()->addToStack( 'account_credit_with_amount', FALSE, array( 'sprintf' => array( \IPS\nexus\Customer::loggedIn()->cm_credits[ $this->invoice->currency ] ) ) ); $paymentMethodsToggles[0][] = 'paymentMethodSubmit'; } if ( !$paymentMethodOptions ) { $showSubmitButton = TRUE; } } /* If there's nothing to pay now, but there will be something to pay later... if we DON'T have any stored payment methods, we may be able to prompt the user to create one */ elseif ( $recurrings and \IPS\nexus\Gateway::cardStorageGateways() and !\IPS\Db::i()->select( 'COUNT(*)', 'nexus_customer_cards', array( 'card_member=?', \IPS\nexus\Customer::loggedIn()->member_id ) )->first() and $elements = \IPS\nexus\Customer\CreditCard::createFormElements( \IPS\nexus\Customer::loggedIn(), FALSE, $showSubmitButton, $hiddenValues ) ) { $paymentType = 'card'; } /* Otherwise, just allow the user to confirm without paying anything */ else { /* But wait - if they have *already* submitted payment, and we are waiting for that to be approved, there's nothing to do right now */ foreach ( $this->invoice->transactions( [ \IPS\nexus\Transaction::STATUS_HELD, \IPS\nexus\Transaction::STATUS_REVIEW, \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING ] ) as $previousTransaction ) { \IPS\Output::i()->redirect( $previousTransaction->url() ); } /* Otherwise they just need to confirm */ $paymentType = 'none'; $showSubmitButton = TRUE; $elements = []; } /* Build the form */ $checkoutUrl = $this->invoice->checkoutUrl()->setQueryString( '_step', 'checkout_pay' ); if ( isset( \IPS\Request::i()->split ) ) { $checkoutUrl = $checkoutUrl->setQueryString( 'split', $amountToPay->amountAsString() ); } $form = new \IPS\Helpers\Form( 'select_method', 'checkout_pay', $checkoutUrl ); foreach ( $hiddenValues as $k => $v ) { $form->hiddenValues[ $k ] = $v; } $form->class = 'ipsForm_vertical'; if ( isset( \IPS\Request::i()->previousTransactions ) ) { $form->hiddenValues['previousTransactions'] = \IPS\Request::i()->previousTransactions; } else { if ( $previousTransactions = $this->invoice->transactions() and \count( $previousTransactions ) ) { $previousTransactionIds = array(); foreach ( $previousTransactions as $previousTransaction ) { $previousTransactionIds[] = $previousTransaction->id; } $form->hiddenValues['previousTransactions'] = implode( ',', $previousTransactionIds ); } } if ( $paymentType === 'pay' and \count( $paymentMethodOptions ) > 1 ) { $form->add( new \IPS\Helpers\Form\Radio( 'payment_method', NULL, TRUE, array( 'options' => $paymentMethodOptions, 'toggles' => $paymentMethodsToggles ) ) ); } foreach ( $elements as $element ) { $form->add( $element ); } if ( \IPS\Settings::i()->nexus_tac === 'checkbox' ) { $form->add( new \IPS\Helpers\Form\Checkbox( 'i_agree_to_tac', FALSE, TRUE, array( 'labelHtmlSprintf' => array( "" . \IPS\Member::loggedIn()->language()->addToStack( 'terms_and_conditions' ) . '' ) ), function( $val ) { if ( !$val ) { throw new \DomainException( 'you_must_agree_to_tac' ); } } ) ); } /* Error to show? */ if ( isset( \IPS\Request::i()->err ) ) { $form->error = \IPS\Request::i()->err; } /* Submitted? */ $values = $form->values(); if ( $values !== FALSE ) { /* Actually take a payment */ if ( $paymentType === 'pay' ) { /* Load gateway */ $gateway = NULL; if ( isset( $values['payment_method'] ) ) { if ( $values['payment_method'] != 0 ) { $gateway = \IPS\nexus\Gateway::load( $values['payment_method'] ); } } else { $gateway = array_pop( $paymentMethods ); } /* Do we already have a "waiting" transaction (which means a manual payment, such as by check or bank wire) we don't need to create a new one since it'll be exactly the same. We can just take them to the screen for the transaction we already have which shows the instructions they need */ try { $existingWaitingTransaction = \IPS\Db::i()->select( '*', 'nexus_transactions', array( 't_member=? AND t_invoice=? AND t_method=? AND t_status=? AND t_amount=? AND t_currency=?', \IPS\Member::loggedIn()->member_id, $this->invoice->id, ( $gateway === NULL ) ? 0 : $gateway->_id, \IPS\nexus\Transaction::STATUS_WAITING, (string) $amountToPay->amount, $amountToPay->currency ) )->first(); \IPS\Output::i()->redirect( \IPS\nexus\Transaction::constructFromData( $existingWaitingTransaction )->url() ); } catch ( \UnderflowException $e ) { } /* Create a transaction */ $transaction = new \IPS\nexus\Transaction; $transaction->member = \IPS\Member::loggedIn(); $transaction->invoice = $this->invoice; $transaction->amount = $amountToPay; $transaction->ip = \IPS\Request::i()->ipAddress(); /* Account Credit? */ if ( $gateway === NULL ) { $credits = \IPS\nexus\Customer::loggedIn()->cm_credits; $inWallet = $credits[ $this->invoice->currency ]->amount; if ( $transaction->amount->amount->compare( $inWallet ) === 1 ) { $transaction->amount = new \IPS\nexus\Money( $inWallet, $this->invoice->currency ); } $transaction->status = $transaction::STATUS_PAID; $transaction->save(); $credits[ $this->invoice->currency ]->amount = $credits[ $this->invoice->currency ]->amount->subtract( $transaction->amount->amount ); $this->invoice->member->cm_credits = $credits; $this->invoice->member->save(); $this->invoice->member->log( 'transaction', array( 'type' => 'paid', 'status' => \IPS\nexus\Transaction::STATUS_PAID, 'id' => $transaction->id, 'invoice_id' => $this->invoice->id, 'invoice_title' => $this->invoice->title, ) ); $transaction->sendNotification(); if ( !$this->invoice->amountToPay()->amount->isGreaterThanZero() ) { $this->invoice->markPaid(); } \IPS\Output::i()->redirect( $transaction->url() ); } /* Nope - gateway */ else { $transaction->method = $gateway; } /* Create a MaxMind request */ $maxMind = NULL; if ( \IPS\Settings::i()->maxmind_key and ( !\IPS\Settings::i()->maxmind_gateways or \IPS\Settings::i()->maxmind_gateways == '*' or \in_array( $transaction->method->id, explode( ',', \IPS\Settings::i()->maxmind_gateways ) ) ) ) { $maxMind = new \IPS\nexus\Fraud\MaxMind\Request; $maxMind->setTransaction( $transaction ); } /* Authorize */ try { $auth = $gateway->auth( $transaction, $values, $maxMind, $recurrings, 'checkout' ); if ( \is_array( $auth ) ) { return $this->_webhookRedirector( $auth ); } else { $transaction->auth = $auth; } } catch ( \LogicException $e ) { $form->error = $e->getMessage(); return $form; } catch ( \RuntimeException $e ) { \IPS\Log::log( $e, 'checkout' ); $form->error = \IPS\Member::loggedIn()->language()->addToStack('gateway_err'); return $form; } /* Check Fraud Rules and capture */ try { $memberJustCreated = $transaction->checkFraudRulesAndCapture( $maxMind ); } catch ( \LogicException $e ) { $form->error = $e->getMessage(); return $form; } catch ( \RuntimeException $e ) { \IPS\Log::log( $e, 'checkout' ); $form->error = \IPS\Member::loggedIn()->language()->addToStack('gateway_err'); return $form; } /* Logged in? */ if ( $memberJustCreated ) { \IPS\Session::i()->setMember( $memberJustCreated ); \IPS\Member\Device::loadOrCreate( $memberJustCreated, FALSE )->updateAfterAuthentication( NULL ); } /* Send email receipt */ $transaction->sendNotification(); /* Show status screen */ \IPS\Output::i()->redirect( $transaction->url() ); } /* Otherwise, we can just go ahead */ else { /* If this is a guest checking our, go ahead and create their account */ $customer = \IPS\nexus\Customer::loggedIn(); $memberJustCreated = NULL; if ( !$customer->member_id ) { $customer = $this->invoice->createAccountForGuest(); $memberJustCreated = $customer; } /* Did we want to store a card? */ if ( $paymentType === 'card' ) { try { $card = \IPS\nexus\Customer\CreditCard::createFormSubmit( $values, $customer, FALSE ); } catch ( \DomainException $e ) { $form->error = $e->getMessage(); return $form; } } /* Check if there's anything still to pay */ if ( $this->invoice->amountToPay()->amount->isZero() ) { /* Mark the invoice paid */ $this->invoice->markPaid(); /* Redirect */ $destination = $this->invoice->return_uri ?: $this->invoice->url(); if ( \IPS\Member::loggedIn()->member_id ) { \IPS\Output::i()->redirect( $destination ); } else { if ( $memberJustCreated ) { \IPS\Session::i()->setMember( $memberJustCreated ); \IPS\Member\Device::loadOrCreate( $memberJustCreated, FALSE )->updateAfterAuthentication( NULL ); } \IPS\Output::i()->redirect( $destination ); } } /* They're waiting for approval - show them a screen to indicate this so they don't try to pay twice */ else { foreach ( $this->invoice->transactions( array( \IPS\nexus\Transaction::STATUS_HELD, \IPS\nexus\Transaction::STATUS_REVIEW, \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING ) ) as $transaction ) { \IPS\Output::i()->redirect( $transaction->url() ); } \IPS\Output::i()->error( 'err_no_methods', '5X196/C', 500, '' ); } } } /* Coupons */ $couponForm = NULL; if ( \IPS\Db::i()->select( 'COUNT(*)', 'nexus_coupons' )->first() ) { $canUseCoupons = TRUE; foreach ( $this->invoice->items as $item ) { if ( !$item::$canUseCoupons ) { $canUseCoupons = FALSE; break; } } if ( $canUseCoupons ) { $invoice = $this->invoice; $couponForm = new \IPS\Helpers\Form( 'coupon', 'save', $this->invoice->checkoutUrl()->setQueryString( '_step', 'checkout_pay' ) ); $couponForm->add( new \IPS\Helpers\Form\Custom( 'coupon_code', NULL, TRUE, array( 'getHtml' => function( $field ) { return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->text( $field->name, 'text', $field->value, $field->required, 25 ); }, 'formatValue' => function( $field ) use ( $invoice ) { if ( $field->value ) { try { return \IPS\nexus\Coupon::load( $field->value, 'c_code' )->useCoupon( $invoice, \IPS\nexus\Customer::loggedIn() ); } catch ( \OutOfRangeException $e ) { throw new \DomainException('coupon_code_invalid'); } } return ''; } ) ) ); if ( $values = $couponForm->values() ) { $invoice->addItem( $values['coupon_code'] ); $invoice->save(); \IPS\Output::i()->redirect( $invoice->checkoutUrl()->setQueryString( '_step', 'checkout_pay' ) ); } } } /* Display */ return \IPS\Theme::i()->getTemplate('checkout')->confirmAndPay( $this->invoice, $this->invoice->summary(), $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'checkout', 'nexus' ), 'paymentForm' ), $this->invoice, $amountToPay, $showSubmitButton ), $amountToPay, $couponForm ? $couponForm->customTemplate( array( \IPS\Theme::i()->getTemplate( 'checkout', 'nexus' ), 'couponForm' ) ) : NULL, $recurrings, $overriddenRenewalTerms ); } /** * Split Payment * * @return void */ public function split() { /* Load invoice */ try { $invoice = \IPS\nexus\Invoice::loadAndCheckPerms( \IPS\Request::i()->id ); $minSplitAmount = $invoice->canSplitPayment(); if ( $minSplitAmount === FALSE ) { throw new \OutOfRangeException; } } catch ( \OutOfRangeException $e ) { \IPS\Output::i()->error( 'node_error', '2X196/4', 404, '' ); } /* What is the max? */ $maxSplitAmount = \floatval( (string) ( $invoice->amountToPay()->amount->subtract( new \IPS\Math\Number( number_format( $minSplitAmount, \IPS\nexus\Money::numberOfDecimalsForCurrency( $invoice->currency ), '.', '' ) ) ) ) ); /* Build Form */ $form = new \IPS\Helpers\Form( 'split', 'continue', $invoice->checkoutUrl()->setQueryString( 'do', 'split' ) ); $form->add( new \IPS\Helpers\Form\Number( 'split_payment_amount', 0, TRUE, array( 'min' => $minSplitAmount, 'max' => $maxSplitAmount, 'decimals' => TRUE ), NULL, NULL, $invoice->currency ) ); /* Handle Submissions */ if ( $values = $form->values() ) { \IPS\Output::i()->redirect( $invoice->checkoutUrl()->setQueryString( 'split', $values['split_payment_amount'] ) ); } /* Display */ \IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) ); } /** * View Transaction Status * * @return void */ public function transaction() { try { $transaction = \IPS\nexus\Transaction::load( \IPS\Request::i()->t ); if ( !$transaction->member->member_id or $transaction->member->member_id !== \IPS\Member::loggedIn()->member_id ) { throw new \OutOfRangeException; } } catch ( \OutOfRangeException $e ) { /* If we're a guest, we may still be able to view for the checkout session */ if ( !\IPS\Member::loggedIn()->member_id and isset( \IPS\Request::i()->cookie['guestTransactionKey'] ) and isset( $transaction->invoice->guest_data['guestTransactionKey'] ) and \IPS\Login::compareHashes( \IPS\Request::i()->cookie['guestTransactionKey'], $transaction->invoice->guest_data['guestTransactionKey'] ) ) { /* Allowing it as a guest */ if ( $transaction->member->member_id ) { \IPS\Session::i()->setMember( $transaction->member ); \IPS\Member\Device::loadOrCreate( $transaction->member, FALSE )->updateAfterAuthentication( NULL ); } } else { \IPS\Output::i()->error( 'node_error', '2X196/5', 403, '' ); } } $output = ''; $checkoutStatus = ''; switch ( $transaction->status ) { case \IPS\nexus\Transaction::STATUS_PAID: $complete = ( $transaction->invoice->status === \IPS\nexus\Invoice::STATUS_PAID ); $purchases = array(); $checkoutStatus = 'complete'; if ( $complete ) { if ( $transaction->invoice->return_uri ) { \IPS\Output::i()->redirect( $transaction->invoice->return_uri ); } else { $purchases = $transaction->invoice->purchasesCreated(); } } else { $checkoutStatus = 'continue'; } $output = \IPS\Theme::i()->getTemplate('checkout')->transactionOkay( $transaction, $complete, $purchases ); break; case \IPS\nexus\Transaction::STATUS_WAITING: $checkoutStatus = 'waiting'; $output = \IPS\Theme::i()->getTemplate('checkout')->transactionWait( $transaction ); break; case \IPS\nexus\Transaction::STATUS_HELD: $checkoutStatus = 'hold'; $output = \IPS\Theme::i()->getTemplate('checkout')->transactionHold( $transaction ); break; case \IPS\nexus\Transaction::STATUS_REFUSED: $checkoutStatus = 'refused'; $output = \IPS\Theme::i()->getTemplate('checkout')->transactionFail( $transaction ); break; case \IPS\nexus\Transaction::STATUS_GATEWAY_PENDING: $checkoutStatus = 'pending'; $output = \IPS\Theme::i()->getTemplate('checkout')->transactionGatewayPending( $transaction ); break; case \IPS\nexus\Transaction::STATUS_PENDING: if ( isset( \IPS\Request::i()->pending ) ) { $checkoutStatus = 'pending'; $output = \IPS\Theme::i()->getTemplate('checkout')->transactionGatewayPending( $transaction ); break; } default: \IPS\Output::i()->redirect( $transaction->invoice->checkoutUrl() ); break; } /* Facebook Pixel */ \IPS\core\Facebook\Pixel::i()->Purchase = array( 'value' => $transaction->invoice->total->amount, 'currency' => $transaction->invoice->total->currency ); \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate('checkout')->checkoutWrapper( $output, $checkoutStatus ); } /** * Wait for the webhook for a transaction to come through before it has been created * * @return void */ public function webhook() { /* Load invoice */ try { $this->invoice = \IPS\nexus\Invoice::load( \IPS\Request::i()->id ); if ( !$this->invoice->canView() ) { throw new \OutOfRangeException; } } catch ( \OutOfRangeException $e ) { /* If we're a geust, we may still be able to view for the checkout session */ if ( !\IPS\Member::loggedIn()->member_id and isset( \IPS\Request::i()->cookie['guestTransactionKey'] ) and isset( $this->invoice->guest_data['guestTransactionKey'] ) and \IPS\Login::compareHashes( \IPS\Request::i()->cookie['guestTransactionKey'], $this->invoice->guest_data['guestTransactionKey'] ) ) { // Allowing it as a guest } else { \IPS\Output::i()->error( 'node_error', '2X196/9', 403, '' ); } } /* Have we decided to give up waiting and just show a pending screen? */ if ( isset( \IPS\Request::i()->pending ) ) { $checkoutStatus = 'pending'; $output = \IPS\Theme::i()->getTemplate('checkout')->transactionGatewayPending( NULL, $this->invoice ); \IPS\core\Facebook\Pixel::i()->Purchase = array( 'value' => $this->invoice->total->amount, 'currency' => $this->invoice->total->currency ); \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate('checkout')->checkoutWrapper( $output, $checkoutStatus ); return; } /* Nope - show a redirector */ \IPS\Output::i()->output = $this->_webhookRedirector( isset( \IPS\Request::i()->exclude ) ? explode( ',', \IPS\Request::i()->exclude ) : array() ); return; } /** * Get a redirector that points to do=webhook * * @param array $exclude Transaction IDs to exclude * @return \IPS\Helpers\MultipleRedirect */ protected function _webhookRedirector( $exclude ) { return new \IPS\Helpers\MultipleRedirect( $this->invoice->checkoutUrl()->setQueryString( array( 'do' => 'webhook', 'exclude' => implode( ',', $exclude ) ) ), function( $data ) use ( $exclude ) { if ( $data === NULL ) { return array( time(), \IPS\Member::loggedIn()->language()->addToStack('processing_your_payment') ); } else { /* Do we have any transactions yet? */ foreach ( $this->invoice->transactions( array( \IPS\nexus\Transaction::STATUS_PAID, \IPS\nexus\Transaction::STATUS_HELD, \IPS\nexus\Transaction::STATUS_REFUSED ), $exclude ? array( array( \IPS\Db::i()->in( 't_id', $exclude, TRUE ) ) ) : array() ) as $transaction ) { \IPS\Output::i()->redirect( $transaction->url() ); } $giveUpTime = ( $data + 60 ); if ( time() > $giveUpTime ) { return NULL; } else { sleep(5); return array( $data, \IPS\Member::loggedIn()->language()->addToStack('processing_your_payment') ); } } }, function() { \IPS\Output::i()->redirect( $this->invoice->checkoutUrl()->setQueryString( array( 'do' => 'webhook', 'pending' => 1 ) ) ); } ); } }