Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @subpackage Nexus * @since 5 Dec 2018 */ 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; } /** * Braintree Gateway * @todo Prevent people from vaulting duplicate cards/PayPal/Venmo accounts (both at checkout and in account) */ class _Braintree 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 ); /* Check we have a merchant account id for the desired currency */ if ( !array_key_exists( $amount->currency, $settings['merchant_accounts'] ) ) { return FALSE; } /* Method specific checks */ switch ( $settings['type'] ) { /* Credit cards */ case 'card': /* Check some methods are accepted */ if ( !$settings['merchant_accounts'][ $amount->currency ]['cardTypes'] ) { return FALSE; } break; /* Venmo */ case 'venmo': /* Requires USD */ if ( $amount->currency !== 'USD' ) { return FALSE; } /* Requires browser support (unless they already have vaulted accounts) */ if ( isset( \IPS\Request::i()->cookie['VenmoSupported'] ) and !\IPS\Request::i()->cookie['VenmoSupported'] and !\IPS\Db::i()->select( 'COUNT(*)', 'nexus_customer_cards', array( 'card_member=? AND card_method=?', $customer ? $customer->member_id : \IPS\nexus\Customer::loggedIn()->member_id, $this->id ) )->first() ) { return FALSE; } break; /* Apple Pay */ case 'applepay': /* Requires browser support */ if ( isset( \IPS\Request::i()->cookie['ApplePaySupported'] ) and !\IPS\Request::i()->cookie['ApplePaySupported'] ) { return FALSE; } break; } /* If we're still here, we're good */ return parent::checkValidity( $amount, $billingAddress, $customer, $recurrings ); } /** * Can store cards? * * @param bool $adminCreatableOnly If TRUE, will only return gateways where the admin (opposed to the user) can create a new option * @return bool */ public function canStoreCards( $adminCreatableOnly = FALSE ) { $settings = json_decode( $this->settings, TRUE ); switch ( $settings['type'] ) { case 'card': return (bool) $settings['cards']; case 'paypal': return $adminCreatableOnly ? FALSE : ( (bool) $settings['paypal_vault'] ); case 'venmo': return $adminCreatableOnly ? FALSE : ( (bool) $settings['venmo_vault'] ); } return FALSE; } /** * Admin can manually charge using this gateway? * * @param \IPS\nexus\Customer $customer The customer we're wanting to charge * @return bool */ public function canAdminCharge( \IPS\nexus\Customer $customer ) { $settings = json_decode( $this->settings, TRUE ); switch ( $settings['type'] ) { case 'paypal': case 'venmo': return (bool) \IPS\Db::i()->select( 'COUNT(*)', 'nexus_customer_cards', array( 'card_member=? AND card_method=?', $customer->member_id, $this->id ) )->first(); case 'card': return TRUE; } return FALSE; } /* !Payment Gateway */ /** * Should the submit button show when this payment method is shown? * * @return bool */ public function showSubmitButton() { $settings = json_decode( $this->settings, TRUE ); return !\in_array( $settings['type'], array( 'paypal', 'venmo', 'applepay', 'googlepay' ) ); } /** * 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 * @param bool $type 'checkout' means the cusotmer is doing this on the normal checkout screen, 'admin' means the admin is doing this in the ACP, 'card' means the user is just adding a card * @return array */ public function paymentScreen( \IPS\nexus\Invoice $invoice, \IPS\nexus\Money $amount, \IPS\nexus\Customer $member = NULL, $recurrings = array(), $type = 'checkout' ) { $settings = json_decode( $this->settings, TRUE ); $clientToken = $this->gateway()->clientToken()->generate( array( 'merchantAccountId' => $settings['merchant_accounts'][ $amount->currency ]['id'], 'version' => '3' ) ); $member = $member ?: \IPS\nexus\Customer::loggedIn(); if ( $settings['type'] === 'paypal' or $settings['type'] === 'venmo' or $settings['type'] === 'applepay' or $settings['type'] === 'googlepay' ) { $vaultAccounts = NULL; if ( ( ( $settings['type'] === 'paypal' and $settings['paypal_vault'] ) or ( $settings['type'] === 'venmo' and $settings['venmo_vault'] ) ) and $type != 'card' ) { $vaultAccounts = array(); if ( $member->member_id ) { foreach( \IPS\Db::i()->select( '*', 'nexus_customer_cards', array( 'card_member=? AND card_method=?', $member->member_id, $this->id ) ) as $vaultAccount ) { try { $vaultAccount = \IPS\nexus\Customer\CreditCard::constructFromData( $vaultAccount ); $vaultAccount->card; // This is just to make the API call now and cache the response so we can catch the exception if one is thrown $vaultAccounts[ $vaultAccount->id ] = $vaultAccount; } catch ( \Exception $e ) {} } } } if ( $type === 'admin' ) { $options = array(); foreach ( $vaultAccounts as $id => $account ) { $options[ $account->data ] = $account->card->number; } $element = new \IPS\Helpers\Form\Radio( $this->id . '_card', NULL, FALSE, array( 'options' => $options ), NULL, NULL, NULL, $this->id . '_card' ); $element->label = \IPS\Member::loggedIn()->language()->addToStack('braintree_account'); return array( 'braintree' => $element ); } else { return array( 'braintree' => new \IPS\Helpers\Form\Custom( $this->id . '_card', NULL, FALSE, array( 'rowHtml' => function( $field ) use ( $clientToken, $vaultAccounts, $invoice, $amount, $settings, $type ) { $shippingAddress = NULL; if ( $invoice->shipaddress ) { $shippingAddress = json_encode( array( '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' => $settings['type'] === 'paypal' ? $this->_payPalState( $invoice->shipaddress ) : $invoice->shipaddress->region, 'postalCode' => $invoice->shipaddress->postalCode, 'countryCode' => $invoice->shipaddress->country, 'phone' => $invoice->member->cm_phone, 'recipientName' => $invoice->member->cm_name, ) ); } return \IPS\Theme::i()->getTemplate( 'forms', 'nexus', 'global' )->braintree( $clientToken, $settings['type'], $field, $vaultAccounts, $invoice, $amount, $this, $shippingAddress, isset( $settings['venmo_profile'] ) ? $settings['venmo_profile'] : NULL, $type, ( $settings['advanced_fraud'] and $type != 'admin' ), ( $settings['type'] === 'paypal' and $settings['paypal_credit'] and \in_array( $amount->currency, array( 'USD', 'GBP' ) ) and $type === 'checkout' ), $settings['googlepay_merchant'] ); } ), NULL, NULL, NULL, $this->id . '_card' ) ); } } elseif ( $settings['type'] === 'card' ) { if ( isset( \IPS\Request::i()->convertTokenToNonce ) ) { try { $card = \IPS\nexus\Customer\CreditCard::load( \IPS\Request::i()->convertTokenToNonce ); if ( $card->member->member_id and $card->member->member_id == \IPS\Member::loggedIn()->member_id ) { \IPS\Output::i()->json( array( 'success' => true, 'nonce' => $this->gateway()->paymentMethodNonce()->create( $card->data )->paymentMethodNonce->nonce ) ); } else { \IPS\Output::i()->json( array( 'success' => false ) ); } } catch ( \Exception $e ) { \IPS\Output::i()->json( array( 'success' => false, 'message' => $e->getMessage() ) ); } } return array( 'card' => new \IPS\nexus\Form\CreditCard( $this->id . '_card', NULL, FALSE, array( 'types' => $settings['merchant_accounts'][ $amount->currency ]['cardTypes'], 'attr' => array( 'data-controller' => 'nexus.global.gateways.braintree', 'data-clientToken' => $clientToken, 'data-method' => 'card', 'data-id' => $this->id, 'data-amount' => $amount->amount, 'data-3dsecure' => ( $settings['3d_secure'] and $type === 'checkout' ) ? 'true' : 'false', 'data-email' => $member->email, 'data-billingAddress' => json_encode( array( 'firstName' => $invoice->member->cm_first_name, 'lastName' => $invoice->member->cm_last_name, 'streetAddress' => $invoice->billaddress ? implode( ', ', $invoice->billaddress->addressLines ) : NULL, 'locality' => $invoice->billaddress ? $invoice->billaddress->city : 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 ), ) ), 'data-advanced-fraud' => ( $settings['advanced_fraud'] and $type != 'admin' ) ? 'true' : 'false', ), 'jsRequired' => TRUE, 'names' => FALSE, 'dummy' => TRUE, 'save' => ( $settings['cards'] ) ? $this : NULL, 'member' => $member, 'loading' => TRUE ) ) ); } } /** * Authorize * * @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 * @param string|NULL $source 'checkout' if the customer is doing this at a normal checkout, 'renewal' is an automatically generated renewal invoice, 'manual' is admin manually charging. NULL is unknown * @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(), $source = NULL ) { $settings = json_decode( $this->settings, TRUE ); /* We need a transaction ID */ $transaction->save(); /* Do we have a customer profile in Braintree already? */ $customerId = \IPS\Settings::i()->site_secret_key . '-' . $transaction->member->member_id; if( \strlen( $customerId ) > 36 ) { $customerId = substr( $customerId, -36 ); } try { $braintreeCustomer = $this->gateway()->customer()->find( $customerId ); $customerUpdate = array(); foreach ( array( 'email' => $transaction->member->email, 'firstName' => $transaction->member->cm_first_name, 'id' => $customerId, 'lastName' => $transaction->member->cm_last_name, 'phone' => $transaction->member->cm_phone ) as $k => $v ) { if ( $braintreeCustomer->$k != $v ) { $customerUpdate[ $k ] = $v; } } if ( $customerUpdate ) { $this->gateway()->customer()->update( $customerId, $customerUpdate ); } } catch ( \Braintree\Exception\NotFound $e ) { $braintreeCustomer = NULL; } /* Init basic data */ $profiles = $transaction->member->cm_profiles; $data = array( 'merchantAccountId' => $settings['merchant_accounts'][ $transaction->amount->currency ]['id'], 'amount' => $transaction->amount->amount, 'options' => array( 'paypal' => array(), 'submitForSettlement' => FALSE, ), 'orderId' => \substr( \IPS\Settings::i()->site_secret_key, 0, 20 ) . '-' . $transaction->id, ); /* Venmo profile */ if ( $settings['type'] === 'venmo' and isset( $settings['venmo_profile'] ) and $settings['venmo_profile'] ) { $data['options']['venmo']['profileId'] = $settings['venmo_profile']; } /* Add addresses */ if ( $transaction->invoice->billaddress ) { $billingAddress = array( 'countryCodeAlpha2' => $transaction->invoice->billaddress->country, 'firstName' => $transaction->invoice->member->cm_first_name, 'lastName' => $transaction->invoice->member->cm_last_name, 'locality' => $transaction->invoice->billaddress->city, 'postalCode' => $transaction->invoice->billaddress->postalCode, 'region' => $settings['type'] === 'paypal' ? $this->_payPalState( $transaction->invoice->billaddress ) : $transaction->invoice->billaddress->region, 'streetAddress' => implode( ', ', $transaction->invoice->billaddress->addressLines ) ); if ( $braintreeCustomer ) { foreach ( $braintreeCustomer->addresses as $address ) { foreach ( $billingAddress as $k => $v ) { if ( $billingAddress[ $k ] != $address->$k ) { continue 2; } } $data['billingAddressId'] = $address->id; break; } } if ( !isset( $data['billingAddressId'] ) ) { $data['billing'] = $billingAddress; } } if ( $transaction->invoice->shipaddress ) { $shippingAddress = array( 'countryCodeAlpha2' => $transaction->invoice->shipaddress->country, 'firstName' => $transaction->invoice->member->cm_first_name, 'lastName' => $transaction->invoice->member->cm_last_name, 'locality' => $transaction->invoice->shipaddress->city, 'postalCode' => $transaction->invoice->shipaddress->postalCode, 'region' => $settings['type'] === 'paypal' ? $this->_payPalState( $transaction->invoice->billaddress ) : $transaction->invoice->billaddress->region, 'streetAddress' => implode( ', ', $transaction->invoice->shipaddress->addressLines ) ); if ( $braintreeCustomer ) { foreach ( $braintreeCustomer->addresses as $address ) { foreach ( $shippingAddress as $k => $v ) { if ( $shippingAddress[ $k ] != $address->$k ) { continue 2; } } $data['shippingAddressId'] = $address->id; break; } } if ( !isset( $data['shippingAddressId'] ) ) { $data['shipping'] = $shippingAddress; } } /* Add device data and transaction source */ if ( $source === 'checkout' ) { $data['transactionSource'] = $recurrings ? 'recurring_first' : 'moto'; if ( \is_array( $_POST[ $this->id . '_card' ] ) and isset( $_POST[ $this->id . '_card' ]['device'] ) ) { $data['deviceData'] = $_POST[ $this->id . '_card' ]['device']; } if ( isset( $_SERVER['HTTP_USER_AGENT'] ) and mb_strlen( $_SERVER['HTTP_USER_AGENT'] ) <= 255 ) { $data['riskData']['customerBrowser'] = $_SERVER['HTTP_USER_AGENT']; } if ( $transaction->ip ) { $data['riskData']['customerIp'] = $transaction->ip; } } elseif ( $source === 'renewal' ) { $data['transactionSource'] = 'recurring'; } elseif ( $source === 'manual' ) { $data['transactionSource'] = 'unscheduled'; } /* If we're paying the whole invoice, we can add item data... */ if ( $transaction->amount->amount->compare( $transaction->invoice->total->amount ) === 0 ) { $summary = $transaction->invoice->summary(); foreach ( $summary['items'] as $item ) { $unitTaxAmount = $item->tax ? $item->price->amount->multiply( new \IPS\Math\Number( number_format( "{$item->tax->rate( $transaction->invoice->billaddress )}", \IPS\nexus\Money::numberOfDecimalsForCurrency( $transaction->amount->currency ), '.', '' ) ) ) : new \IPS\Math\Number('0'); $data['lineItems'][] = array( 'description' => mb_strlen( $item->name ) > 127 ? mb_substr( $item->name, 0, 124 ) . '...' : $item->name, 'kind' => $item->price->amount->isGreaterThanZero() ? 'debit' : 'credit', 'name' => mb_strlen( $item->name ) > 35 ? mb_substr( $item->name, 0, 32 ) . '...' : $item->name, 'productCode' => $item->id, 'quantity' => $item->quantity, 'taxAmount' => ( new \IPS\nexus\Money( $unitTaxAmount->multiply( new \IPS\Math\Number("{$item->quantity}") ), $transaction->amount->currency ) )->amountAsString(), 'totalAmount' => $item->price->amount->multiply( new \IPS\Math\Number("{$item->quantity}") )->absolute(), 'unitAmount' => $item->price->amount->absolute(), 'unitTaxAmount' => ( new \IPS\nexus\Money( $unitTaxAmount, $transaction->amount->currency ) )->amountAsString() ); } $data['shippingAmount'] = $summary['shippingTotal']->amountAsString(); $data['taxAmount'] = $summary['taxTotal']->amountAsString(); $data['options']['paypal']['description'] = mb_strlen( $transaction->invoice->title ) > 127 ? mb_substr( $transaction->invoice->title, 0, 124 ) . '...' : $transaction->invoice->title; } else { $data['options']['paypal']['description'] = sprintf( $transaction->member->language()->get('partial_payment_desc'), $transaction->invoice->id ); } /* Figure out what we're dealing with */ $token = NULL; $nonce = NULL; $save = FALSE; if ( \is_string( $values[ $this->id . '_card' ] ) ) // Admin manually charging PayPal/Venmo account { $token = $values[ $this->id . '_card' ]; } elseif ( $values[ $this->id . '_card' ] instanceof \IPS\nexus\Gateway\Braintree\CreditCard ) // Stored card or PayPal/Venmo account initiated by automatic renewal { $token = $values[ $this->id . '_card' ]->data; } elseif ( $values[ $this->id . '_card' ] instanceof \IPS\nexus\CreditCard ) // New card { $nonce = $values[ $this->id . '_card' ]->token; $save = $values[ $this->id . '_card' ]->save; } elseif ( isset( $values[ $this->id . '_card' ]['token'] ) ) // Apple Pay and Google Pay { $nonce = $values[ $this->id . '_card' ]['token']; } elseif ( isset( $values[ $this->id . '_card' ]['stored'] ) ) // PayPal/Venmo (maybe stored or maybe new) initiated by normal checkout { if ( mb_substr( $values[ $this->id . '_card' ]['stored'], 0, 6 ) === 'NONCE:' ) // New { $exploded = explode( ':', $values[ $this->id . '_card' ]['stored'] ); $nonce = $exploded[2]; $save = (bool) \intval( $exploded[1] ); } else // Stored { $token = $values[ $this->id . '_card' ]['stored']; } } else { throw new \RuntimeException; } /* If we're supposed to be using 3DSecure, check it was verified */ if ( $source == 'checkout' and $settings['type'] === 'card' and $settings['3d_secure'] ) { try { $details = $this->gateway()->paymentMethodNonce()->find( $nonce ); if ( !$details->threeDSecureInfo or ( !$details->threeDSecureInfo->liabilityShifted and $details->threeDSecureInfo->liabilityShiftPossible ) ) { throw new \LogicException(); } } catch ( \Braintree\Exception\NotFound $e ) { throw new \LogicException(); } } /* Put that into the right places */ if ( $token ) { $data['paymentMethodToken'] = $token; } else { $data['paymentMethodNonce'] = $nonce; if ( $save ) { if ( $braintreeCustomer ) { $data['customerId'] = $customerId; } else { $data['customer'] = array( 'email' => $transaction->member->email, 'firstName' => $transaction->member->cm_first_name, 'id' => $customerId, 'lastName' => $transaction->member->cm_last_name, 'phone' => $transaction->member->cm_phone ); } $data['options']['storeInVaultOnSuccess'] = TRUE; } } if ( isset( $data['customer'] ) or isset( $data['customerId'] ) ) { $options['addBillingAddressToPaymentMethod'] = TRUE; $options['storeShippingAddressInVault'] = TRUE; } /* Do it! */ $result = $this->gateway()->transaction()->sale( $data ); if ( $result->success ) { /* Save the customer ID */ if ( isset( $result->transaction->customer ) and isset( $result->transaction->customer['id'] ) and !isset( $profiles[ $this->id ] ) ) { $profiles[ $this->id ] = $result->transaction->customer['id']; $transaction->member->cm_profiles = $profiles; $transaction->member->save(); } /* Save the payment method if we've chosen to do so */ if ( $nonce and $save ) { $storedCard = new \IPS\nexus\Gateway\Braintree\CreditCard; $storedCard->member = $transaction->member; $storedCard->method = $this; $storedCard->data = ( $settings['type'] === 'paypal' ) ? $result->transaction->paypal['token'] : $result->transaction->creditCard['token']; $storedCard->save(); } /* Set MaxMind data */ if ( $maxMind ) { if ( $result->transaction->paymentInstrumentType === 'credit_card' ) { if ( $result->transaction->creditCardDetails ) { $maxMind->setCard( $result->transaction->creditCardDetails->maskedNumber ); } else { $maxMind->setTransactionType('creditcard'); } } elseif ( $result->transaction->paymentInstrumentType === 'paypal_account' ) { $maxMind->setTransactionType('paypal'); } else { $maxMind->setTransactionType('other'); } if ( $result->transaction->avsStreetAddressResponseCode ) { $maxMind->setAVS( $result->transaction->avsStreetAddressResponseCode ); } elseif ( $result->transaction->avsPostalCodeResponseCode ) { $maxMind->setAVS( $result->transaction->avsPostalCodeResponseCode ); } if ( $result->transaction->cvvResponseCode and \in_array( $result->transaction->cvvResponseCode, array( 'M', 'N' ) ) ) { $maxMind->setCVV( $result->transaction->cvvResponseCode === 'M' ); } } /* Return */ $transaction->gw_id = $result->transaction->id; return \IPS\DateTime::ts( $result->transaction->authorizationExpiresAt->getTimestamp() ); } else { if ( isset( $result->transaction ) and isset( $result->transaction->id ) ) { $transaction->gw_id = $result->transaction->id; } $transaction->status = $transaction::STATUS_REFUSED; $extra = $transaction->extra; $extra['history'][] = array( 's' => \IPS\nexus\Transaction::STATUS_REFUSED, 'noteRaw' => $result->message ); $transaction->extra = $extra; $transaction->save(); throw new \DomainException( \IPS\Member::loggedIn()->language()->get( static::errorMessageFromCode( ( isset( $result->transaction ) and isset( $result->transaction->processorResponseCode ) ) ? $result->transaction->processorResponseCode : '' ) ) ); } } /** * Void * * @param \IPS\nexus\Transaction $transaction Transaction * @return void * @throws \Exception */ public function void( \IPS\nexus\Transaction $transaction ) { $result = $this->gateway()->transaction()->void( $transaction->gw_id ); if ( !$result->success ) { throw new \LogicException( $result->message ); } } /** * Capture * * @param \IPS\nexus\Transaction $transaction Transaction * @return void * @throws \LogicException */ public function capture( \IPS\nexus\Transaction $transaction ) { $result = $this->gateway()->transaction()->submitForSettlement( $transaction->gw_id ); if ( !$result->success ) { throw new \LogicException( $result->message ); } } /** * Refund * * @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) * @param string|NULL $reason Reason for refund, if applicable * @return mixed Gateway reference ID for refund, if applicable * @throws \Exception */ public function refund( \IPS\nexus\Transaction $transaction, $amount = NULL, $reason = NULL ) { $result = $this->gateway()->transaction()->refund( $transaction->gw_id, $amount ); if ( !$result->success ) { throw new \LogicException( $result->message ); } } /** * Extra data to show on the ACP transaction page * * @param \IPS\nexus\Transaction $transaction Transaction * @param string $type "short" or "full" * @return string */ public function extraData( \IPS\nexus\Transaction $transaction, $type = 'short' ) { if ( !$transaction->gw_id ) { return NULL; } try { $braintreeData = $this->gateway()->transaction()->find( $transaction->gw_id ); } catch ( \Exception $e ) { return \IPS\Theme::i()->getTemplate( 'transactions', 'nexus', 'admin' )->braintreeData( NULL, 'error' ); } return \IPS\Theme::i()->getTemplate( 'transactions', 'nexus', 'admin' )->braintreeData( $braintreeData ); } /** * Extra data to show on the ACP transaction page for a dispute * * @param \IPS\nexus\Transaction $transaction Transaction * @param array $ref Dispute log data * @return string */ public function disputeData( \IPS\nexus\Transaction $transaction, $log ) { if ( isset( $log['ref'] ) ) { try { $response = $this->gateway()->dispute()->find( $log['ref'] ); } catch ( \Exception $e ) { return \IPS\Theme::i()->getTemplate( 'transactions', 'nexus', 'admin' )->braintreeDispute( $transaction, $log, NULL, TRUE ); } return \IPS\Theme::i()->getTemplate( 'transactions', 'nexus', 'admin' )->braintreeDispute( $transaction, $log, $response ); } } /** * Run any gateway-specific anti-fraud checks and return status for transaction * This is only called if our local anti-fraud rules have not matched * * @param \IPS\nexus\Transaction $transaction Transaction * @return string */ public function fraudCheck( \IPS\nexus\Transaction $transaction ) { try { $response = $this->gateway()->transaction()->find( $transaction->gw_id ); if ( isset( $response->riskData ) and isset( $response->riskData->decision ) and $response->riskData->decision === 'Review' ) { return $transaction::STATUS_HELD; } return $transaction::STATUS_PAID; } catch ( \Exception $e ) { return $transaction::STATUS_PAID; } } /** * URL to view transaction in gateway * * @param \IPS\nexus\Transaction $transaction Transaction * @return \IPS\Http\Url|NULL */ public function gatewayUrl( \IPS\nexus\Transaction $transaction ) { $settings = json_decode( $this->settings, TRUE ); return \IPS\Http\Url::external( "https://" . ( \IPS\NEXUS_TEST_GATEWAYS ? 'sandbox.' : '' ) . "braintreegateway.com/merchants/{$settings['merchant_id']}/transactions/{$transaction->gw_id}" ); } /* !ACP Configuration */ /** * [Node] Add/Edit Form * * @param \IPS\Helpers\Form $form The form * @return void */ public function form( &$form ) { $form->addHeader('braintree_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\Select( 'paymethod_countries', ( $this->id and $this->countries !== '*' ) ? explode( ',', $this->countries ) : '*', FALSE, array( 'options' => array_map( function( $val ) { return "country-{$val}"; }, array_combine( \IPS\GeoLocation::$countries, \IPS\GeoLocation::$countries ) ), 'multiple' => TRUE, 'unlimited' => '*', 'unlimitedLang' => 'no_restriction' ) ) ); $this->settings( $form ); } /** * Settings * * @param \IPS\Helpers\Form $form The form * @return void */ public function settings( &$form ) { $settings = json_decode( $this->settings, TRUE ); \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'admin_payments.js', 'nexus', 'admin' ) ); $form->attributes['data-controller'] = 'nexus.admin.payments.braintreeSetup'; if ( isset( \IPS\Request::i()->_getClientToken ) ) { try { $_settings = array( 'merchant_id' => \IPS\Request::i()->merchant_id, 'public_key' => \IPS\Request::i()->public_key, 'private_key' => \IPS\Request::i()->private_key ); $tokens = array(); foreach ( $this->gateway( $_settings )->merchantAccount()->all() as $merchantAccount ) { $tokens[ (string) $merchantAccount->currencyIsoCode ] = $this->gateway()->clientToken()->generate( array( 'merchantAccountId' => $merchantAccount->id ) ); } \IPS\Output::i()->json( array( 'success' => true, 'tokens' => $tokens ) ); } catch ( \Exception $e ) { \IPS\Output::i()->json( array( 'success' => false, 'message' => \IPS\Member::loggedIn()->language()->addToStack('braintree_auth_error') ) ); } } $form->addHeader('braintree_keys'); $form->addMessage('braintree_keys_blurb'); $form->add( new \IPS\Helpers\Form\Text( 'braintree_merchant_id', $settings ? $settings['merchant_id'] : NULL, TRUE ) ); $form->add( new \IPS\Helpers\Form\Text( 'braintree_public_key', $settings ? $settings['public_key'] : NULL, TRUE ) ); $form->add( new \IPS\Helpers\Form\Text( 'braintree_private_key', $settings ? $settings['private_key'] : NULL, TRUE ) ); $form->addHeader('braintree_type_header'); $formId = $this->id ? "form_{$this->id}" : 'gw'; $form->add( new \IPS\Helpers\Form\Radio( 'braintree_type', isset( $settings['type'] ) ? $settings['type'] : 'paypal', TRUE, array( 'options' => array( 'paypal' => 'braintree_type_paypal', 'card' => 'braintree_type_card', 'venmo' => 'braintree_type_venmo', 'applepay' => 'braintree_type_applepay', 'googlepay' => 'braintree_type_googlepay', ), 'toggles' => array( 'paypal' => array( 'braintree_paypal_vault', 'braintree_paypal_credit' ), 'card' => array( 'braintree_cards', 'braintree_3d_secure', 'braintree_advanced_fraud' ), 'venmo' => array( 'braintree_venmo_vault', 'braintree_venmo_profile' ), 'applepay' => array( 'braintree_advanced_fraud' ), 'googlepay' => array( 'braintree_googlepay_merchant', 'braintree_advanced_fraud' ), ) ) ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'braintree_paypal_vault', ( $settings and isset( $settings['paypal_vault'] ) ) ? $settings['paypal_vault'] : TRUE, FALSE, array(), NULL, NULL, NULL, 'braintree_paypal_vault' ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'braintree_paypal_credit', ( $settings and isset( $settings['paypal_credit'] ) ) ? $settings['paypal_credit'] : FALSE, FALSE, array(), NULL, NULL, NULL, 'braintree_paypal_credit' ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'braintree_venmo_vault', ( $settings and isset( $settings['venmo_vault'] ) ) ? $settings['venmo_vault'] : TRUE, FALSE, array(), NULL, NULL, NULL, 'braintree_venmo_vault' ) ); $form->add( new \IPS\Helpers\Form\Text( 'braintree_venmo_profile', ( $settings and isset( $settings['venmo_profile'] ) ) ? $settings['venmo_profile'] : NULL, FALSE, array( 'nullLang' => 'braintree_venmo_profile_null' ), NULL, NULL, NULL, 'braintree_venmo_profile' ) ); $form->add( new \IPS\Helpers\Form\Text( 'braintree_googlepay_merchant', ( $settings and isset( $settings['googlepay_merchant'] ) ) ? $settings['googlepay_merchant'] : NULL, NULL, array(), NULL, NULL, NULL, 'braintree_googlepay_merchant' ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'braintree_cards', ( $settings and isset( $settings['cards'] ) ) ? $settings['cards'] : TRUE, FALSE, array(), NULL, NULL, NULL, 'braintree_cards' ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'braintree_3d_secure', isset( $settings['3d_secure'] ) ? $settings['3d_secure'] : TRUE, FALSE, array(), NULL, NULL, NULL, 'braintree_3d_secure' ) ); $form->add( new \IPS\Helpers\Form\YesNo( 'braintree_advanced_fraud', isset( $settings['advanced_fraud'] ) ? $settings['advanced_fraud'] : FALSE, FALSE, array(), NULL, NULL, NULL, 'braintree_advanced_fraud' ) ); $form->addHeader('braintree_webhook_header'); $form->addMessage( 'braintree_webhook', '', TRUE, 'braintree_webhook' ); \IPS\Member::loggedIn()->language()->words["braintree_webhook"] = sprintf( \IPS\Member::loggedIn()->language()->get('braintree_webhook'), (string) \IPS\Http\Url::internal( 'applications/nexus/interface/gateways/braintree.php', 'interface' ) ); } /** * Test Settings * * @param array $settings Settings * @return array * @throws \InvalidArgumentException */ public function testSettings( $settings ) { parse_str( urldecode( \IPS\Request::i()->braintree_features ), $features ); try { $merchantAccounts = $this->gateway( $settings )->merchantAccount()->all(); foreach ( $merchantAccounts as $merchantAccount ) { if ( $settings['type'] != 'paypal' or isset( $features[ (string) $merchantAccount->currencyIsoCode ] ) and $features[ (string) $merchantAccount->currencyIsoCode ]['paypal'] === 'true' ) { $settings['merchant_accounts'][ (string) $merchantAccount->currencyIsoCode ]['id'] = $merchantAccount->id; if ( $settings['type'] == 'card' ) { $settings['merchant_accounts'][ (string) $merchantAccount->currencyIsoCode ]['cardTypes'] = array(); foreach ( $features[ (string) $merchantAccount->currencyIsoCode ]['cardTypes'] as $type ) { switch ( $type ) { case 'Visa': $settings['merchant_accounts'][ (string) $merchantAccount->currencyIsoCode ]['cardTypes'][] = \IPS\nexus\CreditCard::TYPE_VISA; break; case 'American Express': $settings['merchant_accounts'][ (string) $merchantAccount->currencyIsoCode ]['cardTypes'][] = \IPS\nexus\CreditCard::TYPE_AMERICAN_EXPRESS; break; case 'MasterCard': $settings['merchant_accounts'][ (string) $merchantAccount->currencyIsoCode ]['cardTypes'][] = \IPS\nexus\CreditCard::TYPE_MASTERCARD; break; case 'Discover': $settings['merchant_accounts'][ (string) $merchantAccount->currencyIsoCode ]['cardTypes'][] = \IPS\nexus\CreditCard::TYPE_DISCOVER; break; case 'JCB': $settings['merchant_accounts'][ (string) $merchantAccount->currencyIsoCode ]['cardTypes'][] = \IPS\nexus\CreditCard::TYPE_JCB; break; } } } } } } catch ( \Exception $e ) { throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('braintree_auth_error') ); } return $settings; } /* !Utility Methods */ /** * @brief $gateway Braintree gateway */ protected $_gateway; /** * Get Braintree gateway * * @return \Braintree_Gateway */ public function gateway( $settings = NULL ) { if ( !$this->_gateway ) { require_once \IPS\ROOT_PATH . '/applications/nexus/sources/Gateway/Braintree/lib/autoload.php'; $settings = $settings ?: json_decode( $this->settings, TRUE ); $this->_gateway = new \Braintree_Gateway( array( 'environment' => \IPS\NEXUS_TEST_GATEWAYS ? 'sandbox' : 'production', 'merchantId' => $settings['merchant_id'], 'publicKey' => $settings['public_key'], 'privateKey' => $settings['private_key'] ) ); } return $this->_gateway; } /** * Customer-readable error message from processor code * * @param string $code Processor response code * @return string */ public static function errorMessageFromCode( $code ) { $errorMessage = 'gateway_err'; if ( \in_array( $code, array( '2000', '2001', '2002', '2003', '2012', '2013', '2014', '2015', '2019', '2020', '2021', '2027', '2038', '2039', '2041', '2043', '2044', '2046', '2050', '2053', '2054', '2057', '2059', '2060', '2071', '2074', '2075', '2082', '2083', '2086', '2098', '2099' ) ) ) { $errorMessage = 'card_refused'; } elseif ( \in_array( $code, array( '2004', '2005', '2006', '2008', '2010', '2072', '2093' ) ) ) { $errorMessage = 'card_information_invalid'; } elseif ( \in_array( $code, array( '2007', '2009', '2022', '2029', '2032', '2047', '2051', '2061', '2062', '2076', '2084' ) ) ) { $errorMessage = 'payment_refused'; } return $errorMessage; } /** * Get short code for PayPal State * * @param \IPS\GeoLocation $address * @return NULL|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; } }