Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @subpackage Nexus * @since 17 Mar 2014 */ namespace IPS\nexus\Gateway; /* 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; } /** * Authorize.Net Gateway */ class _AuthorizeNet extends \IPS\nexus\Gateway { /* !Features */ const SUPPORTS_REFUNDS = TRUE; const SUPPORTS_PARTIAL_REFUNDS = TRUE; /** * Check the gateway can process this... * * @param $amount \IPS\nexus\Money The amount * @param $billingAddress \IPS\GeoLocation|NULL The billing address, which may be NULL if one if not provided * @param $customer \IPS\nexus\Customer The customer (Default NULL value is for backwards compatibility - it should always be provided.) * @param array $recurrings Details about recurring costs * @return bool */ public function checkValidity( \IPS\nexus\Money $amount, \IPS\GeoLocation $billingAddress = NULL, \IPS\nexus\Customer $customer = NULL, $recurrings = array() ) { $settings = json_decode( $this->settings, TRUE ); /* If we don't know the processor, we can only go off the default checks */ if ( !$settings['processor'] ) { return parent::checkValidity( $amount, $billingAddress, $customer, $recurrings ); } /* European processors require a full name and billing address */ if ( in_array( $settings['processor'], array( 8, 9, 10, 11, 12, 13 ) ) ) { if ( !$customer->cm_first_name or !$customer->cm_last_name or !$billingAddress ) { return FALSE; } } /* GPN Canada and WorldPay Streamline Processing Platform also require a billing address */ if ( in_array( $settings['processor'], array( 4, 13 ) ) ) { if ( !$billingAddress ) { return FALSE; } } /* For everything else, check the currencies they can handle... */ $currencies = array(); if ( in_array( $settings['processor'], array( 1, 2, 4, 5, 6, 7, 8, 13, 14 ) ) ) { $currencies[] = 'USD'; } if ( in_array( $settings['processor'], array( 1, 2, 4 ) ) ) { $currencies[] = 'CAD'; } if ( in_array( $settings['processor'], array( 8, 9, 10, 11, 12, 13 ) ) ) { $currencies[] = 'GBP'; } if ( in_array( $settings['processor'], array( 8, 9, 11, 13 ) ) ) { $currencies[] = 'EUR'; } if ( in_array( $settings['processor'], array( 14, 15 ) ) ) { $currencies[] = 'AUD'; } if ( in_array( $settings['processor'], array( 14 ) ) ) { $currencies[] = 'NZD'; } if ( !in_array( $amount->currency, $currencies ) ) { return FALSE; } /* Still here? We're good - pass to parent for general checks */ return parent::checkValidity( $amount, $billingAddress, $customer, $recurrings ); } /** * Can store cards? * * @return bool */ public function canStoreCards() { $settings = json_decode( $this->settings, TRUE ); return ( $settings['method'] === 'AIM' and $settings['cim'] ); } /** * Admin can manually charge using this gateway? * * @return bool */ public function canAdminCharge() { $settings = json_decode( $this->settings, TRUE ); return ( $settings['method'] === 'AIM' ); } /* !Payment Gateway */ /** * Payment Screen Fields * * @param \IPS\nexus\Invoice $invoice Invoice * @param \IPS\nexus\Money $amount The amount to pay now * @param \IPS\nexus\Customer $member The member the payment screen is for (if in the ACP charging to a member's card) or NULL for currently logged in member * @param array $recurrings Details about recurring costs * @return array */ public function paymentScreen( \IPS\nexus\Invoice $invoice, \IPS\nexus\Money $amount, \IPS\nexus\Customer $member = NULL, $recurrings = array() ) { \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_gateways.js', 'nexus', 'global' ) ); $return = array(); $settings = json_decode( $this->settings, TRUE ); /* Accepted types */ if( $settings['processor'] ) { $types = array(); if ( in_array( $settings['processor'], array( 1, 2, 3, 4, 5, 6, 7 ) ) ) { $types[] = \IPS\nexus\CreditCard::TYPE_AMERICAN_EXPRESS; $types[] = \IPS\nexus\CreditCard::TYPE_DINERS_CLUB; $types[] = \IPS\nexus\CreditCard::TYPE_DISCOVER; $types[] = \IPS\nexus\CreditCard::TYPE_JCB; } elseif ( in_array( $settings['processor'], array( 9, 13 ) ) ) { $types[] = \IPS\nexus\CreditCard::TYPE_JCB; } $types[] = \IPS\nexus\CreditCard::TYPE_MASTERCARD; $types[] = \IPS\nexus\CreditCard::TYPE_VISA; } else { $types = array( \IPS\nexus\CreditCard::TYPE_AMERICAN_EXPRESS, \IPS\nexus\CreditCard::TYPE_DINERS_CLUB, \IPS\nexus\CreditCard::TYPE_DISCOVER, \IPS\nexus\CreditCard::TYPE_JCB, \IPS\nexus\CreditCard::TYPE_MASTERCARD, \IPS\nexus\CreditCard::TYPE_VISA ); } $options = array( 'types' => $types, 'member' => $member ); /* If we're using DPM, we include all that data in here */ if ( $settings['method'] == 'DPM' ) { $timestamp = time(); $options['jsRequired'] = TRUE; $options['names'] = FALSE; $options['attr'] = array( 'data-controller' => 'nexus.global.gateways.authorizenet', 'data-id' => $this->id, 'class' => 'ipsHide', 'data-url' => \IPS\NEXUS_TEST_GATEWAYS ? 'https://test.authorize.net/gateway/transact.dll' : 'https://secure.authorize.net/gateway/transact.dll', 'data-fields' => json_encode( array( 'x_login' => $settings['login'], 'x_version' => '3.1', 'x_type' => 'AUTH_ONLY', 'x_method' => 'CC', 'x_amount' => (string) $amount->amount, 'x_currency_code' => $amount->currency, 'x_fp_hash' => hash_hmac( 'md5', "{$settings['login']}^{$invoice->id}^{$timestamp}^{$amount->amount}^{$amount->currency}", $settings['tran_key'] ), 'x_fp_sequence' => $invoice->id, 'x_fp_timestamp' => $timestamp, 'x_invoice_num' => $invoice->id, 'x_first_name' => $invoice->member->cm_first_name, 'x_last_name' => $invoice->member->cm_last_name, 'x_address' => $invoice->billaddress ? implode( ', ', $invoice->billaddress->addressLines ) : '', 'x_city' => $invoice->billaddress ? $invoice->billaddress->city : '', 'x_state' => $invoice->billaddress ? $invoice->billaddress->region : '', 'x_zip' => $invoice->billaddress ? $invoice->billaddress->postalCode : '', 'x_country' => $invoice->billaddress ? $invoice->billaddress->country : '', 'x_phone' => $invoice->member->cm_phone, 'x_email' => $invoice->member->email, 'x_email_customer' => 'FALSE', 'x_cust_id' => $invoice->member->member_id, 'x_customer_ip' => \IPS\Request::i()->ipAddress(), 'x_relay_response' => 'TRUE', 'x_relay_url' => \IPS\Settings::i()->base_url . 'applications/nexus/interface/gateways/authorize.php', ) ) ); } /* If we're using AIM, we might be able to save */ elseif ( $this->canStoreCards() ) { $options['save'] = $this; $options['member'] = $member; } /* And then return the card field */ $return['card'] = new \IPS\nexus\Form\CreditCard( $this->id . '_card', NULL, FALSE, $options ); return $return; } /** * Authorize (AIM only) * * @param \IPS\nexus\Transaction $transaction Transaction * @param array|\IPS\nexus\Customer\CreditCard $values Values from form OR a stored card object if this gateway supports them * @param \IPS\nexus\Fraud\MaxMind\Request|NULL $maxMind *If* MaxMind is enabled, the request object will be passed here so gateway can additional data before request is made * @param array $recurrings Details about recurring costs * @return \IPS\DateTime|NULL Auth is valid until or NULL to indicate auth is good forever * @throws \LogicException Message will be displayed to user */ public function auth( \IPS\nexus\Transaction $transaction, $values, \IPS\nexus\Fraud\MaxMind\Request $maxMind = NULL, $recurrings = array() ) { $card = is_array( $values ) ? $values[ $this->id . '_card' ] : $values; /* Stored Card */ if ( $card instanceof \IPS\nexus\Gateway\AuthorizeNet\CreditCard ) { /* MaxMind */ if ( $maxMind ) { $maxMind->setTransactionType( 'creditcard' ); } /* Authorize */ $profiles = $card->member->cm_profiles; $xml = $card->createAuthenticationXml( 'createCustomerProfileTransactionRequest' ); $xml->addChild( 'transaction', array( 'profileTransAuthOnly' => array( 'amount' => (string) $transaction->amount->amount, 'customerProfileId' => $profiles[ $card->method->id ], 'customerPaymentProfileId' => $card->data, ), ) ); $response = $this->_handleResponse( explode( ',', $card->api( $xml )->directResponse ) ); /* Change $card to the actual \IPS\nexus\CreditCard object so we get the last 4 digits correctly */ $card = $card->card; } /* New Card */ else { /* MaxMind */ if ( $maxMind ) { $maxMind->setCard( $card ); } /* Authorize */ $response = $this->api( array( 'x_type' => 'AUTH_ONLY', 'x_method' => 'CC', 'x_amount' => (string) $transaction->amount->amount, 'x_currency_code' => $transaction->amount->currency, 'x_card_num' => $card->number, 'x_exp_date' => str_pad( $card->expMonth, 2, '0', STR_PAD_LEFT ) . '-' . $card->expYear, 'x_card_code' => $card->ccv, 'x_first_name' => $transaction->member->cm_first_name, 'x_last_name' => $transaction->member->cm_last_name, 'x_address' => $transaction->invoice->billaddress ? implode( ', ', $transaction->invoice->billaddress->addressLines ) : '', 'x_city' => $transaction->invoice->billaddress ? $transaction->invoice->billaddress->city : '', 'x_state' => $transaction->invoice->billaddress ? $transaction->invoice->billaddress->region : '', 'x_zip' => $transaction->invoice->billaddress ? $transaction->invoice->billaddress->postalCode : '', 'x_country' => $transaction->invoice->billaddress ? $transaction->invoice->billaddress->country : '', 'x_phone' => $transaction->member->cm_phone, 'x_email' => $transaction->member->email, 'x_email_customer' => 'FALSE', 'x_cust_id' => $transaction->member->member_id, 'x_customer_ip' => \IPS\Request::i()->ipAddress(), ) ); /* Save? */ if ( $card->save ) { if ( !$transaction->member->member_id ) { $transaction->member = $transaction->invoice->createAccountForGuest(); \IPS\Session::i()->setMember( $transaction->member ); \IPS\Member\Device::loadOrCreate( $transaction->member, FALSE )->updateAfterAuthentication( NULL ); } try { $storedCard = new \IPS\nexus\Gateway\AuthorizeNet\CreditCard; $storedCard->member = $transaction->member; $storedCard->method = $this; $storedCard->card = $card; $storedCard->save(); } catch ( \Exception $e ) { } } } /* Set AVS code for MaxMind */ if ( $maxMind and $response['avs'] ) { $maxMind->setAVS( $response['avs'] ); } /* Remember the last 4 digits of the card number as we'll need them to refund */ $extra = $transaction->extra; $extra['lastFour'] = $card->lastFour; $transaction->extra = $extra; /* Return */ $transaction->gw_id = $response['id']; return \IPS\DateTime::create()->add( new \DateInterval( 'P30D' ) ); } /** * Void (AIM and DPM) * * @param \IPS\nexus\Transaction $transaction Transaction * @return void * @throws \Exception */ public function void( \IPS\nexus\Transaction $transaction ) { $this->api( array( 'x_type' => 'VOID', 'x_trans_id' => $transaction->gw_id, ) ); } /** * Capture (AIM and DPM) * * @param \IPS\nexus\Transaction $transaction Transaction * @return void * @throws \LogicException */ public function capture( \IPS\nexus\Transaction $transaction ) { $this->api( array( 'x_type' => 'PRIOR_AUTH_CAPTURE', 'x_trans_id' => $transaction->gw_id, ) ); } /** * Refund (AIM and DPM) * * @param \IPS\nexus\Transaction $transaction Transaction to be refunded * @param float|NULL $amount Amount to refund (NULL for full amount - always in same currency as transaction) * @return mixed Gateway reference ID for refund, if applicable * @throws \Exception */ public function refund( \IPS\nexus\Transaction $transaction, $amount = NULL ) { try { $this->api( array( 'x_type' => 'CREDIT', 'x_trans_id' => $transaction->gw_id, 'x_amount' => (string) ( $amount ?: $transaction->amount->amount ), 'x_card_num' => $transaction->extra['lastFour'] ) ); } catch( \IPS\nexus\Gateway\AuthorizeNet\Exception $e ) { /* If the transaction is unsettled (error code 54 from the API), then we need to void it instead */ if ( $e->getCode() == 54 ) { $this->void( $transaction ); } } } /* !ACP Configuration */ /** * Settings * * @param \IPS\Helpers\Form $form The form * @return void */ public function settings( &$form ) { $settings = json_decode( $this->settings, TRUE ); $form->add( new \IPS\Helpers\Form\Text( 'authorizenet_login', $settings['login'], TRUE ) ); $form->add( new \IPS\Helpers\Form\Text( 'authorizenet_tran_key', $settings['tran_key'], TRUE ) ); $form->add( new \IPS\Helpers\Form\Radio( 'authorizenet_method', $settings['method'], TRUE, array( 'options' => array( 'AIM' => 'authorizenet_AIM', 'DPM' => 'authorizenet_DPM' ), 'toggles' => array( 'AIM' => array( 'authorizenet_cim' ), 'DPM' => array( 'authorizenet_hash' ) ) ) ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'authorizenet_cim', $settings['cim'], FALSE, array(), NULL, NULL, NULL, 'authorizenet_cim' ) ); $form->add( new \IPS\Helpers\Form\Text( 'authorizenet_hash', $settings['hash'], FALSE, array(), NULL, NULL, NULL, 'authorizenet_hash' ) ); $form->add( new \IPS\Helpers\Form\Select( 'authorizenet_processor', isset( $settings['processor'] ) ? $settings['processor'] : '0', FALSE, array( 'options' => array( 0 => 'dont_know', 'North American Payment Processors' => array( 1 => 'Chase Paymentech Tampa Processing Platform', 2 => 'Elavon', 3 => 'First Data Merchant Services (FDMS) Omaha, Nashville, and EFSNet Processing Platforms', 4 => 'Global Payments', 5 => 'Heartland Payment Systems', 6 => 'TSYS Acquiring Solutions', 7 => 'WorldPay Atlanta Processing Platform', ), 'European Payment Processors' => array( 8 => 'AIB Merchant Services', 9 => 'Barclaycard', 10 => 'First Data Merchant Solutions (MSIP platform)', 11 => 'HSBC Merchant Services', 12 => 'Lloyds Bank Cardnet', 13 => 'Streamline', ), 'Asia-Pacific Processors' => array( 14 => 'FDI Australia', 15 => 'Westpac', ) ) ) ) ); } /** * Test Settings * * @param array $settings Settings * @return array * @throws \InvalidArgumentException */ public function testSettings( $settings ) { return $settings; } /* !Utility Methods */ /** * Send API Request * * @param array $data The data to send * @return array * @throws \IPS\Http|Exception * @throws \IPS\nexus\Gateway\AuthorizeNet\Exception */ public function api( $data=array() ) { $settings = json_decode( $this->settings, TRUE ); $response = \IPS\Http\Url::external( \IPS\NEXUS_TEST_GATEWAYS ? 'https://test.authorize.net/gateway/transact.dll' : 'https://secure.authorize.net/gateway/transact.dll' )->request()->post( array_merge( array( 'x_login' => $settings['login'], 'x_tran_key' => $settings['tran_key'], 'x_version' => '3.1', 'x_delim_data' => 'TRUE', 'x_delim_char' => '~', 'x_encap_char' => '', ), $data ) ); return $this->_handleResponse( explode( '~', $response ) ); } /** * Handle response * * @param array $data * @return array * @throws \IPS\nexus\Gateway\AuthorizeNet\Exception */ protected function _handleResponse( $response ) { if ( $response[0] != 1 ) { throw new \IPS\nexus\Gateway\AuthorizeNet\Exception( $response[2] ); } return array( 'id' => $response[6], 'avs' => $response[5], ); } }