From 88c9fc1ac1d00c157c93fd939959a2f51023aae6 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Wed, 11 May 2016 22:55:37 -0400 Subject: [PATCH 01/12] Add WePay configuration UI --- .../Controllers/AccountGatewayController.php | 140 +++++++++++++++++- app/Http/routes.php | 9 ++ app/Libraries/Utils.php | 18 +++ .../Repositories/AccountGatewayRepository.php | 2 +- app/Services/AccountGatewayService.php | 66 ++++++++- app/Services/DatatableService.php | 9 +- composer.json | 3 +- composer.lock | 57 ++++++- resources/lang/en/texts.php | 16 ++ .../views/accounts/account_gateway.blade.php | 51 ++++++- 10 files changed, 355 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index a4fd57c7e5f4..c430fb61d816 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -11,6 +11,7 @@ use Validator; use stdClass; use URL; use Utils; +use WePay; use App\Models\Gateway; use App\Models\Account; use App\Models\AccountGateway; @@ -44,6 +45,10 @@ class AccountGatewayController extends BaseController $accountGateway = AccountGateway::scope($publicId)->firstOrFail(); $config = $accountGateway->getConfig(); + if ($accountGateway->gateway_id == GATEWAY_WEPAY) { + return Redirect::to('gateways'); + } + foreach ($config as $field => $value) { $config->$field = str_repeat('*', strlen($value)); } @@ -57,7 +62,6 @@ class AccountGatewayController extends BaseController $data['paymentTypeId'] = $accountGateway->getPaymentType(); $data['selectGateways'] = Gateway::where('id', '=', $accountGateway->gateway_id)->get(); - return View::make('accounts.account_gateway', $data); } @@ -87,7 +91,6 @@ class AccountGatewayController extends BaseController ->where('id', '!=', GATEWAY_GOCARDLESS) ->where('id', '!=', GATEWAY_DWOLLA) ->where('id', '!=', GATEWAY_STRIPE) - ->where('id', '!=', GATEWAY_WEPAY) ->orderBy('name')->get(); $data['hiddenFields'] = Gateway::$hiddenFields; @@ -101,7 +104,8 @@ class AccountGatewayController extends BaseController private function getViewModel($accountGateway = false) { $selectedCards = $accountGateway ? $accountGateway->accepted_credit_cards : 0; - $account = Auth::user()->account; + $user = Auth::user(); + $account =$user->account; $paymentTypes = []; foreach (Gateway::$paymentTypes as $type) { @@ -172,6 +176,7 @@ class AccountGatewayController extends BaseController return [ 'paymentTypes' => $paymentTypes, 'account' => $account, + 'user' => $user, 'accountGateway' => $accountGateway, 'config' => false, 'gateways' => $gateways, @@ -203,6 +208,10 @@ class AccountGatewayController extends BaseController $paymentType = Input::get('payment_type_id'); $gatewayId = Input::get('gateway_id'); + if ($gatewayId == GATEWAY_WEPAY) { + return $this->setupWePay(); + } + if ($paymentType == PAYMENT_TYPE_PAYPAL) { $gatewayId = GATEWAY_PAYPAL_EXPRESS; } elseif ($paymentType == PAYMENT_TYPE_BITCOIN) { @@ -352,4 +361,129 @@ class AccountGatewayController extends BaseController } } + protected function getWePayUpdateUri($accountGateway) + { + if ($accountGateway->gateway_id != GATEWAY_WEPAY) { + return null; + } + + $wepay = Utils::setupWePay($accountGateway); + + $update_uri_data = $wepay->request('account/get_update_uri', array( + 'account_id' => $accountGateway->getConfig()->accountId, + 'mode' => 'iframe', + 'redirect_uri' => URL::to('/gateways'), + )); + + return $update_uri_data->uri; + } + + protected function setupWePay() + { + $user = Auth::user(); + $account = $user->account; + + $validator = Validator::make(Input::all(), array( + 'company_name' => 'required', + 'description' => 'required', + 'tos_agree' => 'required', + 'first_name' => 'required', + 'last_name' => 'required', + 'email' => 'required', + )); + + if ($validator->fails()) { + return Redirect::to('gateways/create') + ->withErrors($validator) + ->withInput(); + } + + try{ + $wepay = Utils::setupWePay(); + + $wepayUser = $wepay->request('user/register/', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'email' => Input::get('email'), + 'first_name' => Input::get('first_name'), + 'last_name' => Input::get('last_name'), + 'original_ip' => \Request::getClientIp(true), + 'original_device' => \Request::server('HTTP_USER_AGENT'), + 'tos_acceptance_time' => time(), + 'redirect_uri' => URL::to('/gateways'), + 'callback_uri' => URL::to('https://sometechie.ngrok.io/paymenthook/'.$account->account_key.'/'.GATEWAY_WEPAY), + 'scope' => 'manage_accounts,collect_payments,view_user,preapprove_payments,send_money', + )); + + $accessToken = $wepayUser->access_token; + $accessTokenExpires = $wepayUser->expires_in ? (time() + $wepayUser->expires_in) : null; + + $wepay = new WePay($accessToken); + + $wepayAccount = $wepay->request('account/create/', array( + 'name' => Input::get('company_name'), + 'description' => Input::get('description'), + 'theme_object' => json_decode(WEPAY_THEME), + )); + + try { + $wepay->request('user/send_confirmation/', []); + $confirmationRequired = true; + } catch(\WePayException $ex){ + if ($ex->getMessage() == 'This access_token is already approved.') { + $confirmationRequired = false; + } else { + throw $ex; + } + } + + $accountGateway = AccountGateway::createNew(); + $accountGateway->gateway_id = GATEWAY_WEPAY; + $accountGateway->setConfig(array( + 'userId' => $wepayUser->user_id, + 'accessToken' => $accessToken, + 'tokenType' => $wepayUser->token_type, + 'tokenExpires' => $accessTokenExpires, + 'accountId' => $wepayAccount->account_id, + 'testMode' => WEPAY_ENVIRONMENT == WEPAY_STAGING, + )); + $account->account_gateways()->save($accountGateway); + + if ($confirmationRequired) { + Session::flash('message', trans('texts.created_wepay_confirmation_required')); + } else { + $updateUri = $wepay->request('/account/get_update_uri', array( + 'account_id' => $wepayAccount->account_id, + 'redirect_uri' => URL::to('gateways'), + )); + + return Redirect::to($updateUri->uri); + } + + return Redirect::to("gateways"); + } catch (\WePayException $e) { + Session::flash('error', $e->getMessage()); + return Redirect::to('gateways/create') + ->withInput(); + } + } + + + public function resendConfirmation($publicId = false) + { + $accountGateway = AccountGateway::scope($publicId)->firstOrFail(); + + if ($accountGateway->gateway_id == GATEWAY_WEPAY) { + try { + $wepay = Utils::setupWePay($accountGateway); + $wepay->request('user/send_confirmation', []); + + Session::flash('message', trans('texts.resent_confirmation_email')); + } catch (\WePayException $e) { + Session::flash('error', $e->getMessage()); + } + } + + return Redirect::to("gateways/{$accountGateway->public_id}/edit"); + } } diff --git a/app/Http/routes.php b/app/Http/routes.php index 128a1e9e2ec6..292d6128a070 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -242,6 +242,7 @@ Route::group([ Route::post('/import_csv', 'ImportController@doImportCSV'); Route::resource('gateways', 'AccountGatewayController'); + Route::get('gateways/{public_id}/resend_confirmation', 'AccountGatewayController@resendConfirmation'); Route::get('api/gateways', array('as'=>'api.gateways', 'uses'=>'AccountGatewayController@getDatatable')); Route::post('account_gateways/bulk', 'AccountGatewayController@bulk'); @@ -744,6 +745,14 @@ if (!defined('CONTACT_EMAIL')) { // Pro users who started paying on or before this date will be able to manage users define('PRO_USERS_GRANDFATHER_DEADLINE', '2016-05-15'); + + // WePay + define('WEPAY_PRODUCTION', 'production'); + define('WEPAY_STAGING', 'staging'); + define('WEPAY_CLIENT_ID', env('WEPAY_CLIENT_ID')); + define('WEPAY_CLIENT_SECRET', env('WEPAY_CLIENT_SECRET')); + define('WEPAY_ENVIRONMENT', env('WEPAY_ENVIRONMENT', WEPAY_PRODUCTION)); + define('WEPAY_THEME', env('WEPAY_THEME','{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}')); $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index c02c719169a4..8e54926e8045 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -15,6 +15,7 @@ use Log; use DateTime; use stdClass; use Carbon; +use WePay; use App\Models\Currency; @@ -990,4 +991,21 @@ class Utils return $url; } + + public static function setupWePay($accountGateway = null) + { + if (WePay::getEnvironment() == 'none') { + if (WEPAY_ENVIRONMENT == WEPAY_STAGING) { + WePay::useStaging(WEPAY_CLIENT_ID, WEPAY_CLIENT_SECRET); + } else { + WePay::useProduction(WEPAY_CLIENT_ID, WEPAY_CLIENT_SECRET); + } + } + + if ($accountGateway) { + return new WePay($accountGateway->getConfig()->accessToken); + } else { + return new WePay(null); + } + } } diff --git a/app/Ninja/Repositories/AccountGatewayRepository.php b/app/Ninja/Repositories/AccountGatewayRepository.php index b61f854f2dc5..b011ebda765b 100644 --- a/app/Ninja/Repositories/AccountGatewayRepository.php +++ b/app/Ninja/Repositories/AccountGatewayRepository.php @@ -19,6 +19,6 @@ class AccountGatewayRepository extends BaseRepository ->join('gateways', 'gateways.id', '=', 'account_gateways.gateway_id') ->where('account_gateways.deleted_at', '=', null) ->where('account_gateways.account_id', '=', $accountId) - ->select('account_gateways.public_id', 'gateways.name', 'account_gateways.deleted_at', 'account_gateways.gateway_id'); + ->select('account_gateways.id', 'account_gateways.public_id', 'gateways.name', 'account_gateways.deleted_at', 'account_gateways.gateway_id'); } } diff --git a/app/Services/AccountGatewayService.php b/app/Services/AccountGatewayService.php index bd5635e0dd52..c21902c87374 100644 --- a/app/Services/AccountGatewayService.php +++ b/app/Services/AccountGatewayService.php @@ -2,6 +2,7 @@ use URL; use App\Models\Gateway; +use App\Models\AccountGateway; use App\Services\BaseService; use App\Ninja\Repositories\AccountGatewayRepository; @@ -41,7 +42,38 @@ class AccountGatewayService extends BaseService [ 'name', function ($model) { - return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); + if ($model->gateway_id != GATEWAY_WEPAY) { + return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); + } else { + $accountGateway = AccountGateway::find($model->id); + $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGING ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; + $wepayAccountId = $accountGateway->getConfig()->accountId; + $linkText = $model->name; + $url = $endpoint.'account/'.$wepayAccountId; + $wepay = \Utils::setupWepay($accountGateway); + $html = link_to($url, $linkText, array('target'=>'_blank'))->toHtml(); + + try { + $wepayAccount = $wepay->request('/account', array('account_id' => $wepayAccountId)); + if ($wepayAccount->state == 'action_required') { + $updateUri = $wepay->request('/account/get_update_uri', array( + 'account_id' => $wepayAccountId, + 'redirect_uri' => URL::to('gateways'), + )); + + $linkText .= ' ('.trans('texts.action_required').')'; + $url = $updateUri->uri; + $html = "{$linkText}"; + $model->setupUrl = $url; + } elseif ($wepayAccount->state == 'pending') { + $linkText .= ' ('.trans('texts.resend_confirmation_email').')'; + $model->resendConfirmationUrl = $url = URL::to("gateways/{$accountGateway->public_id}/resend_confirmation"); + $html = link_to($url, $linkText)->toHtml(); + } + } catch(\WePayException $ex){} + + return $html; + } } ], [ @@ -60,6 +92,38 @@ class AccountGatewayService extends BaseService uctrans('texts.edit_gateway'), function ($model) { return URL::to("gateways/{$model->public_id}/edit"); + }, + function($model) { + return $model->gateway_id != GATEWAY_WEPAY; + } + ], [ + uctrans('texts.resend_confirmation_email'), + function ($model) { + return $model->resendConfirmationUrl; + }, + function($model) { + return $model->gateway_id == GATEWAY_WEPAY && !empty($model->resendConfirmationUrl); + } + ], [ + uctrans('texts.finish_setup'), + function ($model) { + return $model->setupUrl; + }, + function($model) { + return $model->gateway_id == GATEWAY_WEPAY && !empty($model->setupUrl); + } + ] , [ + uctrans('texts.manage_wepay_account'), + function ($model) { + $accountGateway = AccountGateway::find($model->id); + $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGING ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; + return array( + 'url' => $endpoint.'account/'.$accountGateway->getConfig()->accountId, + 'attributes' => 'target="_blank"' + ); + }, + function($model) { + return $model->gateway_id == GATEWAY_WEPAY; } ] ]; diff --git a/app/Services/DatatableService.php b/app/Services/DatatableService.php index 3d9001a095b7..222a7b4d7cb6 100644 --- a/app/Services/DatatableService.php +++ b/app/Services/DatatableService.php @@ -82,7 +82,14 @@ class DatatableService $lastIsDivider = true; } else { - $str .= "
  • {$value}
  • "; + $urlVal = $url($model); + $urlStr = is_string($urlVal) ? $urlVal : $urlVal['url']; + $attributes = ''; + if (!empty($urlVal['attributes'])) { + $attributes = ' '.$urlVal['attributes']; + } + + $str .= "
  • {$value}
  • "; $hasAction = true; $lastIsDivider = false; } diff --git a/composer.json b/composer.json index 01c6a26f51cf..cff2d85bcbba 100644 --- a/composer.json +++ b/composer.json @@ -74,7 +74,8 @@ "league/flysystem-rackspace": "~1.0", "barracudanetworks/archivestream-php": "^1.0", "omnipay/braintree": "~2.0@dev", - "gatepay/FedACHdir": "dev-master@dev" + "gatepay/FedACHdir": "dev-master@dev", + "wepay/php-sdk": "^0.2" }, "require-dev": { "phpunit/phpunit": "~4.0", diff --git a/composer.lock b/composer.lock index e167cfb38e9c..d694f04f6960 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "7139e4aedb2ac151079c50ee5c17f93c", - "content-hash": "a314d6c0a16785dd2395a7fd73cdc76d", + "hash": "b2471aea1af5ef67a1379ad95b5138f7", + "content-hash": "df30a311df0341933d4ff2c3aa5974a6", "packages": [ { "name": "agmscode/omnipay-agms", @@ -505,7 +505,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/e97ed532f09e290b91ff7713b785ed7ab11d0812", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/f7b31bdbdceaaea930c71df20e4180b0b7172b4a", "reference": "e97ed532f09e290b91ff7713b785ed7ab11d0812", "shasum": "" }, @@ -2785,7 +2785,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/labs7in0/omnipay-wechat/zipball/40c9f86df6573ad98ae1dd0d29712ccbc789a74e", + "url": "https://api.github.com/repos/labs7in0/omnipay-wechat/zipball/c8d80c3b48bae2bab071f283f75b1cd8624ed3c7", "reference": "40c9f86df6573ad98ae1dd0d29712ccbc789a74e", "shasum": "" }, @@ -8057,6 +8057,53 @@ ], "time": "2016-02-25 10:29:59" }, + { + "name": "wepay/php-sdk", + "version": "0.2.7", + "source": { + "type": "git", + "url": "https://github.com/wepay/PHP-SDK.git", + "reference": "31bfcdd97d2c9c33c9c09129638ae31840822182" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wepay/PHP-SDK/zipball/31bfcdd97d2c9c33c9c09129638ae31840822182", + "reference": "31bfcdd97d2c9c33c9c09129638ae31840822182", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "wepay.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "WePay", + "email": "api@wepay.com" + } + ], + "description": "WePay APIv2 SDK for PHP", + "keywords": [ + "payment", + "sdk", + "wepay" + ], + "time": "2015-08-14 19:42:37" + }, { "name": "wildbit/laravel-postmark-provider", "version": "3.0.0", @@ -10024,4 +10071,4 @@ "prefer-lowest": false, "platform": [], "platform-dev": [] -} \ No newline at end of file +} diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 0aebc8b87903..25f9fdd6fabb 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1293,6 +1293,22 @@ $LANG = array( 'no_payment_method_specified' => 'No payment method specified', + + + // WePay + 'wepay' => 'WePay', + 'sign_up_with_wepay' => 'Sign up with WePay', + 'use_another_provider' => 'Use another provider', + 'company_name' => 'Company Name', + 'wepay_company_name_help' => 'This will appear on client\'s credit card statements.', + 'wepay_description_help' => 'The purpose of this account.', + 'wepay_tos_agree' => 'I agree to the :link.', + 'wepay_tos_link_text' => 'WePay Terms of Service', + 'resend_confirmation_email' => 'Resend Confirmation Email', + 'manage_wepay_account' => 'Manage WePay Account', + 'action_required' => 'Action Required', + 'finish_setup' => 'Finish Setup', + 'created_wepay_confirmation_required' => 'Please check your email and confirm your email address with WePay.' ); return $LANG; diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index ade161f5b65f..72acd1cff05a 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -5,15 +5,53 @@ @include('accounts.nav', ['selected' => ACCOUNT_PAYMENTS]) - {!! Former::open($url)->method($method)->rule()->addClass('warn-on-exit') !!} - {!! Former::populateField('token_billing_type_id', $account->token_billing_type_id) !!} - + @if(!$accountGateway && WEPAY_CLIENT_ID) + {!! Former::open($url)->method($method)->rules(array( + 'first_name' => 'required', + 'last_name' => 'required', + 'email' => 'required', + 'description' => 'required', + 'company_name' => 'required', + 'tos_agree' => 'required', + ))->addClass('warn-on-exit') !!} + {!! Former::populateField('company_name', $account->getDisplayName()) !!} + {!! Former::populateField('first_name', $user->first_name) !!} + {!! Former::populateField('last_name', $user->last_name) !!} + {!! Former::populateField('email', $user->email) !!} +
    +
    +

    {!! trans('texts.online_payments') !!}

    +
    +
    + {!! Former::text('first_name') !!} + {!! Former::text('last_name') !!} + {!! Former::text('email') !!} + {!! Former::text('company_name')->help('wepay_company_name_help')->maxlength(255) !!} + {!! Former::text('description')->help('wepay_description_help') !!} + {!! Former::checkbox('tos_agree')->label(' ')->text(trans('texts.wepay_tos_agree', + ['link'=>''.trans('texts.wepay_tos_link_text').''] + ))->value('true') !!} +
    + {!! Button::primary(trans('texts.sign_up_with_wepay')) + ->submit() + ->large() !!}

    + {{ trans('texts.use_another_provider') }} +
    +
    +
    + + + {!! Former::close() !!} + @endif +

    {!! trans($title) !!}

    + {!! Former::open($url)->method($method)->rule()->addClass('warn-on-exit') !!} + {!! Former::populateField('token_billing_type_id', $account->token_billing_type_id) !!} @if ($accountGateway) {!! Former::populateField('gateway_id', $accountGateway->gateway_id) !!} @@ -173,7 +211,6 @@
    @endif
    -
    @@ -183,6 +220,7 @@ $countGateways > 0 ? Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/settings/online_payments'))->appendIcon(Icon::create('remove-circle')) : false, Button::success(trans('texts.save'))->submit()->large()->appendIcon(Icon::create('floppy-disk'))) !!} {!! Former::close() !!} + From ba000546c4531b4669cdb574453523f8dd22663e Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Thu, 12 May 2016 11:09:07 -0400 Subject: [PATCH 02/12] Better WePay switching --- app/Http/Controllers/AccountController.php | 7 ++++ .../Controllers/AccountGatewayController.php | 20 +++++++++- app/Http/routes.php | 1 + app/Models/AccountGateway.php | 2 +- resources/lang/en/texts.php | 4 +- .../views/accounts/account_gateway.blade.php | 39 +------------------ .../account_gateway_switch_wepay.blade.php | 7 ++++ .../partials/account_gateway_wepay.blade.php | 39 +++++++++++++++++++ resources/views/accounts/payments.blade.php | 6 +++ 9 files changed, 85 insertions(+), 40 deletions(-) create mode 100644 resources/views/accounts/account_gateway_switch_wepay.blade.php create mode 100644 resources/views/accounts/partials/account_gateway_wepay.blade.php diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 5ce42356bf83..053d5cb66a58 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -430,7 +430,14 @@ class AccountController extends BaseController if ($count == 0) { return Redirect::to('gateways/create'); } else { + $switchToWepay = WEPAY_CLIENT_ID && !$account->getGatewayConfig(GATEWAY_WEPAY); + + if ($switchToWepay && $account->token_billing_type_id != TOKEN_BILLING_DISABLED) { + $switchToWepay = !$account->getGatewayConfig(GATEWAY_BRAINTREE) && !$account->getGatewayConfig(GATEWAY_STRIPE); + } + return View::make('accounts.payments', [ + 'showSwitchToWepay' => $switchToWepay, 'showAdd' => $count < count(Gateway::$paymentTypes), 'title' => trans('texts.online_payments') ]); diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index c430fb61d816..a192538480e9 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -410,7 +410,7 @@ class AccountGatewayController extends BaseController 'original_ip' => \Request::getClientIp(true), 'original_device' => \Request::server('HTTP_USER_AGENT'), 'tos_acceptance_time' => time(), - 'redirect_uri' => URL::to('/gateways'), + 'redirect_uri' => URL::to('gateways'), 'callback_uri' => URL::to('https://sometechie.ngrok.io/paymenthook/'.$account->account_key.'/'.GATEWAY_WEPAY), 'scope' => 'manage_accounts,collect_payments,view_user,preapprove_payments,send_money', )); @@ -437,6 +437,10 @@ class AccountGatewayController extends BaseController } } + if (($gateway = $account->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD)) || ($gateway = $account->getGatewayByType(PAYMENT_TYPE_STRIPE))) { + $gateway->delete(); + } + $accountGateway = AccountGateway::createNew(); $accountGateway->gateway_id = GATEWAY_WEPAY; $accountGateway->setConfig(array( @@ -486,4 +490,18 @@ class AccountGatewayController extends BaseController return Redirect::to("gateways/{$accountGateway->public_id}/edit"); } + + public function switchToWepay() + { + $data = self::getViewModel(); + $data['url'] = 'gateways'; + $data['method'] = 'POST'; + unset($data['gateways']); + + if ( ! \Request::secure() && ! Utils::isNinjaDev()) { + Session::flash('warning', trans('texts.enable_https')); + } + + return View::make('accounts.account_gateway_switch_wepay', $data); + } } diff --git a/app/Http/routes.php b/app/Http/routes.php index 292d6128a070..2c12c215cfd3 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -243,6 +243,7 @@ Route::group([ Route::resource('gateways', 'AccountGatewayController'); Route::get('gateways/{public_id}/resend_confirmation', 'AccountGatewayController@resendConfirmation'); + Route::get('gateways/switch/wepay', 'AccountGatewayController@switchToWepay'); Route::get('api/gateways', array('as'=>'api.gateways', 'uses'=>'AccountGatewayController@getDatatable')); Route::post('account_gateways/bulk', 'AccountGatewayController@bulk'); diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php index 6de95e8593cf..0c281856be66 100644 --- a/app/Models/AccountGateway.php +++ b/app/Models/AccountGateway.php @@ -77,7 +77,7 @@ class AccountGateway extends EntityModel return !empty($this->getConfigField('enableAch')); } - public function getPayPAlEnabled() + public function getPayPalEnabled() { return !empty($this->getConfigField('enablePayPal')); } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 25f9fdd6fabb..ea3ea349f31d 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1308,7 +1308,9 @@ $LANG = array( 'manage_wepay_account' => 'Manage WePay Account', 'action_required' => 'Action Required', 'finish_setup' => 'Finish Setup', - 'created_wepay_confirmation_required' => 'Please check your email and confirm your email address with WePay.' + 'created_wepay_confirmation_required' => 'Please check your email and confirm your email address with WePay.', + 'switch_to_wepay' => 'Switch to WePay', + 'switch' => 'Switch', ); return $LANG; diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index 72acd1cff05a..49635e95db38 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -5,43 +5,8 @@ @include('accounts.nav', ['selected' => ACCOUNT_PAYMENTS]) - @if(!$accountGateway && WEPAY_CLIENT_ID) - {!! Former::open($url)->method($method)->rules(array( - 'first_name' => 'required', - 'last_name' => 'required', - 'email' => 'required', - 'description' => 'required', - 'company_name' => 'required', - 'tos_agree' => 'required', - ))->addClass('warn-on-exit') !!} - {!! Former::populateField('company_name', $account->getDisplayName()) !!} - {!! Former::populateField('first_name', $user->first_name) !!} - {!! Former::populateField('last_name', $user->last_name) !!} - {!! Former::populateField('email', $user->email) !!} -
    -
    -

    {!! trans('texts.online_payments') !!}

    -
    -
    - {!! Former::text('first_name') !!} - {!! Former::text('last_name') !!} - {!! Former::text('email') !!} - {!! Former::text('company_name')->help('wepay_company_name_help')->maxlength(255) !!} - {!! Former::text('description')->help('wepay_description_help') !!} - {!! Former::checkbox('tos_agree')->label(' ')->text(trans('texts.wepay_tos_agree', - ['link'=>''.trans('texts.wepay_tos_link_text').''] - ))->value('true') !!} -
    - {!! Button::primary(trans('texts.sign_up_with_wepay')) - ->submit() - ->large() !!}

    - {{ trans('texts.use_another_provider') }} -
    -
    -
    - - - {!! Former::close() !!} + @if(!$accountGateway && WEPAY_CLIENT_ID && !$account->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD) && !$account->getGatewayByType(PAYMENT_TYPE_STRIPE)) + @include('accounts.partials.account_gateway_wepay') @endif
    diff --git a/resources/views/accounts/account_gateway_switch_wepay.blade.php b/resources/views/accounts/account_gateway_switch_wepay.blade.php new file mode 100644 index 000000000000..ae851881bed0 --- /dev/null +++ b/resources/views/accounts/account_gateway_switch_wepay.blade.php @@ -0,0 +1,7 @@ +@extends('header') + +@section('content') + @parent + @include('accounts.nav', ['selected' => ACCOUNT_PAYMENTS]) + @include('accounts.partials.account_gateway_wepay') +@stop \ No newline at end of file diff --git a/resources/views/accounts/partials/account_gateway_wepay.blade.php b/resources/views/accounts/partials/account_gateway_wepay.blade.php new file mode 100644 index 000000000000..edda271d1479 --- /dev/null +++ b/resources/views/accounts/partials/account_gateway_wepay.blade.php @@ -0,0 +1,39 @@ +{!! Former::open($url)->method($method)->rules(array( + 'first_name' => 'required', + 'last_name' => 'required', + 'email' => 'required', + 'description' => 'required', + 'company_name' => 'required', + 'tos_agree' => 'required', + ))->addClass('warn-on-exit') !!} +{!! Former::populateField('company_name', $account->getDisplayName()) !!} +{!! Former::populateField('first_name', $user->first_name) !!} +{!! Former::populateField('last_name', $user->last_name) !!} +{!! Former::populateField('email', $user->email) !!} +
    +
    +

    {!! trans('texts.online_payments') !!}

    +
    +
    + {!! Former::text('first_name') !!} + {!! Former::text('last_name') !!} + {!! Former::text('email') !!} + {!! Former::text('company_name')->help('wepay_company_name_help')->maxlength(255) !!} + {!! Former::text('description')->help('wepay_description_help') !!} + {!! Former::checkbox('tos_agree')->label(' ')->text(trans('texts.wepay_tos_agree', + ['link'=>''.trans('texts.wepay_tos_link_text').''] + ))->value('true') !!} +
    + {!! Button::primary(trans('texts.sign_up_with_wepay')) + ->submit() + ->large() !!} + @if(isset($gateways)) +

    + {{ trans('texts.use_another_provider') }} + @endif +
    +
    +
    + + +{!! Former::close() !!} \ No newline at end of file diff --git a/resources/views/accounts/payments.blade.php b/resources/views/accounts/payments.blade.php index d2fcde895200..9907fac6ecee 100644 --- a/resources/views/accounts/payments.blade.php +++ b/resources/views/accounts/payments.blade.php @@ -4,6 +4,12 @@ @parent @include('accounts.nav', ['selected' => ACCOUNT_PAYMENTS]) + @if ($showSwitchToWepay) + {!! Button::success(trans('texts.switch_to_wepay')) + ->asLinkTo(URL::to('/gateways/switch/wepay')) + ->appendIcon(Icon::create('circle-arrow-up')) !!} + @endif + @if ($showAdd) {!! Button::primary(trans('texts.add_gateway')) ->asLinkTo(URL::to('/gateways/create')) From ce04c994dda4cca616f5dd729236774ed2fbc508 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Fri, 13 May 2016 09:30:22 -0400 Subject: [PATCH 03/12] Support payments and storing cards via WePay. --- .../Controllers/AccountGatewayController.php | 2 +- app/Http/Controllers/PaymentController.php | 73 +++--- .../Controllers/PublicClientController.php | 7 +- app/Http/routes.php | 6 +- app/Libraries/Utils.php | 2 +- app/Models/Account.php | 2 + app/Services/AccountGatewayService.php | 4 +- app/Services/PaymentService.php | 210 +++++++++++----- .../payments/add_paymentmethod.blade.php | 234 +----------------- .../payments/paymentmethods_list.blade.php | 1 + .../payments/tokenization_braintree.blade.php | 61 +++++ .../payments/tokenization_stripe.blade.php | 154 ++++++++++++ .../payments/tokenization_wepay.blade.php | 52 ++++ 13 files changed, 480 insertions(+), 328 deletions(-) create mode 100644 resources/views/payments/tokenization_braintree.blade.php create mode 100644 resources/views/payments/tokenization_stripe.blade.php create mode 100644 resources/views/payments/tokenization_wepay.blade.php diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index a192538480e9..27d3d54464d2 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -449,7 +449,7 @@ class AccountGatewayController extends BaseController 'tokenType' => $wepayUser->token_type, 'tokenExpires' => $accessTokenExpires, 'accountId' => $wepayAccount->account_id, - 'testMode' => WEPAY_ENVIRONMENT == WEPAY_STAGING, + 'testMode' => WEPAY_ENVIRONMENT == WEPAY_STAGE, )); $account->account_gateways()->save($accountGateway); diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 8f09c2980a59..e14a9cb9cd69 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -152,6 +152,8 @@ class PaymentController extends BaseController $data = array(); + Session::put($invitation->id.'payment_ref', $invoice->id.'_'.uniqid()); + if ($paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) { if ($paymentType == PAYMENT_TYPE_TOKEN) { $useToken = true; @@ -198,6 +200,10 @@ class PaymentController extends BaseController $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); } + if(!empty($data['braintreeClientToken']) || $accountGateway->getPublishableStripeKey()|| $accountGateway->gateway_id == GATEWAY_WEPAY) { + $data['tokenize'] = true; + } + } else { if ($deviceData = Input::get('details')) { Session::put($invitation->id . 'device_data', $deviceData); @@ -405,7 +411,7 @@ class PaymentController extends BaseController 'last_name' => 'required', ]; - if ( ! Input::get('stripeToken') && ! Input::get('payment_method_nonce') && !(Input::get('plaidPublicToken') && Input::get('plaidAccountId'))) { + if ( ! Input::get('sourceToken') && !(Input::get('plaidPublicToken') && Input::get('plaidAccountId'))) { $rules = array_merge( $rules, [ @@ -459,8 +465,6 @@ class PaymentController extends BaseController $paymentType = Session::get($invitation->id . 'payment_type'); $accountGateway = $account->getGatewayByType($paymentType); $paymentMethod = null; - - if ($useToken) { if(!$sourceId) { @@ -492,21 +496,46 @@ class PaymentController extends BaseController $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway, $data); // check if we're creating/using a billing token + $tokenBillingSupported = false; if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + $tokenBillingSupported = true; + $customerReferenceParam = 'cardReference'; + if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && !Input::get('authorize_ach')) { Session::flash('error', trans('texts.ach_authorization_required')); return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); } + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { + $tokenBillingSupported = true; + $customerReferenceParam = 'paymentMethodToken'; + $deviceData = Input::get('device_data'); + if (!$deviceData) { + $deviceData = Session::get($invitation->id . 'device_data'); + } + + if($deviceData) { + $details['device_data'] = $deviceData; + } + } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { + $tokenBillingSupported = true; + $customerReferenceParam = false; + } + + if ($tokenBillingSupported) { if ($useToken) { - $details['customerReference'] = $customerReference; - unset($details['token']); - $details['cardReference'] = $sourceReference; + if ($customerReferenceParam) { + $details[$customerReferenceParam] = $customerReference; + } + $details['token'] = $sourceReference; + unset($details['card']); } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing') || $paymentType == PAYMENT_TYPE_STRIPE_ACH) { $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */); if ($token) { $details['token'] = $token; - $details['customerReference'] = $customerReference; + if ($customerReferenceParam) { + $details[$customerReferenceParam] = $customerReference; + } if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty(Input::get('plaidPublicToken')) ) { // The user needs to complete verification @@ -518,36 +547,6 @@ class PaymentController extends BaseController return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); } } - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $deviceData = Input::get('device_data'); - if (!$deviceData) { - $deviceData = Session::get($invitation->id . 'device_data'); - } - - if ($token = Input::get('payment_method_nonce')) { - $details['token'] = $token; - unset($details['card']); - } - - if ($useToken) { - $details['customerId'] = $customerReference; - $details['paymentMethodToken'] = $sourceReference; - unset($details['token']); - } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) { - $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */); - if ($token) { - $details['paymentMethodToken'] = $token; - $details['customerId'] = $customerReference; - unset($details['token']); - } else { - $this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway); - return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); - } - } - - if($deviceData) { - $details['deviceData'] = $deviceData; - } } $response = $gateway->purchase($details)->send(); diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index 405de63493b0..360f71e90fbf 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -859,7 +859,6 @@ class PublicClientController extends BaseController ]; if ($paymentType == PAYMENT_TYPE_STRIPE_ACH) { - $data['currencies'] = Cache::get('currencies'); } @@ -867,6 +866,10 @@ class PublicClientController extends BaseController $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); } + if(!empty($data['braintreeClientToken']) || $accountGateway->getPublishableStripeKey()|| $accountGateway->gateway_id == GATEWAY_WEPAY) { + $data['tokenize'] = true; + } + return View::make('payments.add_paymentmethod', $data); } @@ -882,7 +885,7 @@ class PublicClientController extends BaseController $account = $client->account; $accountGateway = $account->getGatewayByType($paymentType); - $sourceToken = $accountGateway->gateway_id == GATEWAY_STRIPE ? Input::get('stripeToken'):Input::get('payment_method_nonce'); + $sourceToken = Input::get('sourceToken'); if (!PaymentController::processPaymentClientDetails($client, $accountGateway, $paymentType)) { return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv')); diff --git a/app/Http/routes.php b/app/Http/routes.php index 2c12c215cfd3..3a02960d8005 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -562,6 +562,10 @@ if (!defined('CONTACT_EMAIL')) { define('GATEWAY_WEPAY', 60); define('GATEWAY_BRAINTREE', 61); + // The customer exists, but only as a local concept + // The remote gateway doesn't understand the concept of customers + define('CUSTOMER_REFERENCE_LOCAL', 'local'); + define('EVENT_CREATE_CLIENT', 1); define('EVENT_CREATE_INVOICE', 2); define('EVENT_CREATE_QUOTE', 3); @@ -749,7 +753,7 @@ if (!defined('CONTACT_EMAIL')) { // WePay define('WEPAY_PRODUCTION', 'production'); - define('WEPAY_STAGING', 'staging'); + define('WEPAY_STAGE', 'stage'); define('WEPAY_CLIENT_ID', env('WEPAY_CLIENT_ID')); define('WEPAY_CLIENT_SECRET', env('WEPAY_CLIENT_SECRET')); define('WEPAY_ENVIRONMENT', env('WEPAY_ENVIRONMENT', WEPAY_PRODUCTION)); diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 8e54926e8045..dc92f6e65125 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -995,7 +995,7 @@ class Utils public static function setupWePay($accountGateway = null) { if (WePay::getEnvironment() == 'none') { - if (WEPAY_ENVIRONMENT == WEPAY_STAGING) { + if (WEPAY_ENVIRONMENT == WEPAY_STAGE) { WePay::useStaging(WEPAY_CLIENT_ID, WEPAY_CLIENT_SECRET); } else { WePay::useProduction(WEPAY_CLIENT_ID, WEPAY_CLIENT_SECRET); diff --git a/app/Models/Account.php b/app/Models/Account.php index 29a9abbd09be..fb2c39b951db 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -1245,6 +1245,8 @@ class Account extends Eloquent return GATEWAY_STRIPE; } elseif ($this->isGatewayConfigured(GATEWAY_BRAINTREE)) { return GATEWAY_BRAINTREE; + } elseif ($this->isGatewayConfigured(GATEWAY_WEPAY)) { + return GATEWAY_WEPAY; } else { return false; } diff --git a/app/Services/AccountGatewayService.php b/app/Services/AccountGatewayService.php index c21902c87374..fe5427b966bb 100644 --- a/app/Services/AccountGatewayService.php +++ b/app/Services/AccountGatewayService.php @@ -46,7 +46,7 @@ class AccountGatewayService extends BaseService return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); } else { $accountGateway = AccountGateway::find($model->id); - $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGING ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; + $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; $wepayAccountId = $accountGateway->getConfig()->accountId; $linkText = $model->name; $url = $endpoint.'account/'.$wepayAccountId; @@ -116,7 +116,7 @@ class AccountGatewayService extends BaseService uctrans('texts.manage_wepay_account'), function ($model) { $accountGateway = AccountGateway::find($model->id); - $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGING ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; + $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; return array( 'url' => $endpoint.'account/'.$accountGateway->getConfig()->accountId, 'attributes' => 'target="_blank"' diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index b97bbbd67de4..626366a469cd 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -9,6 +9,7 @@ use Cache; use Omnipay; use Session; use CreditCard; +use WePay; use App\Models\Payment; use App\Models\PaymentMethod; use App\Models\Account; @@ -30,7 +31,8 @@ class PaymentService extends BaseService protected static $refundableGateways = array( GATEWAY_STRIPE, - GATEWAY_BRAINTREE + GATEWAY_BRAINTREE, + GATEWAY_WEPAY, ); public function __construct(PaymentRepository $paymentRepo, AccountRepository $accountRepo, DatatableService $datatableService) @@ -95,9 +97,9 @@ class PaymentService extends BaseService $data['ButtonSource'] = 'InvoiceNinja_SP'; }; - if ($input && $accountGateway->isGateway(GATEWAY_STRIPE)) { - if (!empty($input['stripeToken'])) { - $data['token'] = $input['stripeToken']; + if ($input) { + if (!empty($input['sourceToken'])) { + $data['token'] = $input['sourceToken']; unset($data['card']); } elseif (!empty($input['plaidPublicToken'])) { $data['plaidPublicToken'] = $input['plaidPublicToken']; @@ -106,6 +108,10 @@ class PaymentService extends BaseService } } + if ($accountGateway->isGateway(GATEWAY_WEPAY) && $transactionId = Session::get($invitation->id.'payment_ref')) { + $data['transaction_id'] = $transactionId; + } + return $data; } @@ -266,6 +272,17 @@ class PaymentService extends BaseService if (!$response->isSuccessful()) { return $response->getMessage(); } + } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { + try { + $wepay = Utils::setupWePay($accountGateway); + $wepay->request('/credit_card/delete', [ + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => $paymentMethod->source_reference, + ]); + } catch (\WePayException $ex){ + return $ex->getMessage(); + } } $paymentMethod->delete(); @@ -291,16 +308,16 @@ class PaymentService extends BaseService { $customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return paramenter */); - if ($customerReference) { + if ($customerReference && $customerReference != CUSTOMER_REFERENCE_LOCAL) { $details['customerReference'] = $customerReference; - if ($accountGateway->gateway->id == GATEWAY_STRIPE) { + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { $customerResponse = $gateway->fetchCustomer(array('customerReference' => $customerReference))->send(); if (!$customerResponse->isSuccessful()) { $customerReference = null; // The customer might not exist anymore } - } elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) { + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { $customer = $gateway->findCustomer($customerReference)->send()->getData(); if (!($customer instanceof \Braintree\Customer)) { @@ -309,7 +326,7 @@ class PaymentService extends BaseService } } - if ($accountGateway->gateway->id == GATEWAY_STRIPE) { + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { if (!empty($details['plaidPublicToken'])) { $plaidResult = $this->getPlaidToken($accountGateway, $details['plaidPublicToken'], $details['plaidAccountId']); @@ -355,7 +372,7 @@ class PaymentService extends BaseService return; } } - } elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) { + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { if (!$customerReference) { $tokenResponse = $gateway->createCustomer(array('customerData' => array()))->send(); if ($tokenResponse->isSuccessful()) { @@ -377,6 +394,31 @@ class PaymentService extends BaseService return; } } + } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { + $wepay = Utils::setupWePay($accountGateway); + + try { + $wepay->request('credit_card/authorize', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => $details['token'], + )); + + // Get the card details + $tokenResponse = $wepay->request('credit_card', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => $details['token'], + )); + + $customerReference = CUSTOMER_REFERENCE_LOCAL; + $sourceReference = $details['token']; + } catch (\WePayException $ex) { + $this->lastError = $ex->getMessage(); + return; + } + } else { + return null; } if ($customerReference) { @@ -394,7 +436,7 @@ class PaymentService extends BaseService $accountGatewayToken->token = $customerReference; $accountGatewayToken->save(); - $paymentMethod = $this->createPaymentMethodFromGatewayResponse($tokenResponse, $accountGateway, $accountGatewayToken, $contactId); + $paymentMethod = $this->convertPaymentMethodFromGatewayResponse($tokenResponse, $accountGateway, $accountGatewayToken, $contactId); } else { $this->lastError = $tokenResponse->getMessage(); @@ -456,8 +498,24 @@ class PaymentService extends BaseService return $paymentMethod; } + + public function convertPaymentMethodFromWePay($source, $accountGatewayToken = null, $paymentMethod = null) { + // Creating a new one or updating an existing one + if (!$paymentMethod) { + $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + } + + $paymentMethod->payment_type_id = $this->parseCardType($source->credit_card_name); + $paymentMethod->last4 = $source->last_four; + $paymentMethod->expiration = $source->expiration_year . '-' . $source->expiration_month . '-00'; + $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); + + $paymentMethod->source_reference = $source->credit_card_id; + + return $paymentMethod; + } - public function createPaymentMethodFromGatewayResponse($gatewayResponse, $accountGateway, $accountGatewayToken = null, $contactId = null) { + public function convertPaymentMethodFromGatewayResponse($gatewayResponse, $accountGateway, $accountGatewayToken = null, $contactId = null, $existingPaymentMethod = null) { if ($accountGateway->gateway_id == GATEWAY_STRIPE) { $data = $gatewayResponse->getData(); if (!empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) { @@ -470,7 +528,7 @@ class PaymentService extends BaseService } if ($source) { - $paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken); + $paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken, $existingPaymentMethod); } } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { $data = $gatewayResponse->getData(); @@ -478,7 +536,12 @@ class PaymentService extends BaseService if (!empty($data->transaction)) { $transaction = $data->transaction; - $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + if ($existingPaymentMethod) { + $paymentMethod = $existingPaymentMethod; + } else { + $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + } + if ($transaction->paymentInstrumentType == 'credit_card') { $card = $transaction->creditCardDetails; $paymentMethod->last4 = $card->last4; @@ -490,9 +553,20 @@ class PaymentService extends BaseService } $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); } elseif (!empty($data->paymentMethod)) { - $paymentMethod = $this->convertPaymentMethodFromBraintree($data->paymentMethod, $accountGatewayToken); + $paymentMethod = $this->convertPaymentMethodFromBraintree($data->paymentMethod, $accountGatewayToken, $existingPaymentMethod); } + } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { + if ($gatewayResponse instanceof \Omnipay\WePay\Message\CustomCheckoutResponse) { + $wepay = \Utils::setupWePay($accountGateway); + $gatewayResponse = $wepay->request('credit_card', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => $gatewayResponse->getData()['payment_method']['credit_card']['id'], + )); + + } + $paymentMethod = $this->convertPaymentMethodFromWePay($gatewayResponse, $accountGatewayToken, $existingPaymentMethod); } if (!empty($paymentMethod) && $accountGatewayToken && $contactId) { @@ -566,43 +640,49 @@ class PaymentService extends BaseService $payment->payment_type_id = $this->detectCardType($card->getNumber()); } + $savePaymentMethod = !empty($paymentMethod); + + // This will convert various gateway's formats to a known format + $paymentMethod = $this->convertPaymentMethodFromGatewayResponse($purchaseResponse, $accountGateway, null, null, $paymentMethod); + + // If this is a stored payment method, we'll update it with the latest info + if ($savePaymentMethod) { + $paymentMethod->save(); + } + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { $data = $purchaseResponse->getData(); - $source = !empty($data['source'])?$data['source']:$data['card']; - $payment->payment_status_id = $data['status'] == 'succeeded' ? PAYMENT_STATUS_COMPLETED : PAYMENT_STATUS_PENDING; - - if ($source) { - $payment->last4 = $source['last4']; - - if ($source['object'] == 'bank_account') { - $payment->routing_number = $source['routing_number']; - $payment->payment_type_id = PAYMENT_TYPE_ACH; - } - else{ - $payment->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-00'; - $payment->payment_type_id = $this->parseCardType($source['brand']); - } - } - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $transaction = $purchaseResponse->getData()->transaction; - if ($transaction->paymentInstrumentType == 'credit_card') { - $card = $transaction->creditCardDetails; - $payment->last4 = $card->last4; - $payment->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00'; - $payment->payment_type_id = $this->parseCardType($card->cardType); - } elseif ($transaction->paymentInstrumentType == 'paypal_account') { - $payment->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; - $payment->email = $transaction->paypalDetails->payerEmail; - } - } - - if ($payerId) { - $payment->payer_id = $payerId; } if ($paymentMethod) { - $payment->payment_method_id = $paymentMethod->id; + if ($paymentMethod->last4) { + $payment->last4 = $paymentMethod->last4; + } + + if ($paymentMethod->expiration) { + $payment->expiration = $paymentMethod->expiration; + } + + if ($paymentMethod->routing_number) { + $payment->routing_number = $paymentMethod->routing_number; + } + + if ($paymentMethod->payment_type_id) { + $payment->payment_type_id = $paymentMethod->payment_type_id; + } + + if ($paymentMethod->email) { + $payment->email = $paymentMethod->email; + } + + if ($payerId) { + $payment->payer_id = $payerId; + } + + if ($savePaymentMethod) { + $payment->payment_method_id = $paymentMethod->id; + } } $payment->save(); @@ -665,20 +745,29 @@ class PaymentService extends BaseService private function parseCardType($cardName) { $cardTypes = array( - 'Visa' => PAYMENT_TYPE_VISA, - 'American Express' => PAYMENT_TYPE_AMERICAN_EXPRESS, - 'MasterCard' => PAYMENT_TYPE_MASTERCARD, - 'Discover' => PAYMENT_TYPE_DISCOVER, - 'JCB' => PAYMENT_TYPE_JCB, - 'Diners Club' => PAYMENT_TYPE_DINERS, - 'Carte Blanche' => PAYMENT_TYPE_CARTE_BLANCHE, - 'China UnionPay' => PAYMENT_TYPE_UNIONPAY, - 'Laser' => PAYMENT_TYPE_LASER, - 'Maestro' => PAYMENT_TYPE_MAESTRO, - 'Solo' => PAYMENT_TYPE_SOLO, - 'Switch' => PAYMENT_TYPE_SWITCH + 'visa' => PAYMENT_TYPE_VISA, + 'americanexpress' => PAYMENT_TYPE_AMERICAN_EXPRESS, + 'amex' => PAYMENT_TYPE_AMERICAN_EXPRESS, + 'mastercard' => PAYMENT_TYPE_MASTERCARD, + 'discover' => PAYMENT_TYPE_DISCOVER, + 'jcb' => PAYMENT_TYPE_JCB, + 'dinersclub' => PAYMENT_TYPE_DINERS, + 'carteblanche' => PAYMENT_TYPE_CARTE_BLANCHE, + 'chinaunionpay' => PAYMENT_TYPE_UNIONPAY, + 'unionpay' => PAYMENT_TYPE_UNIONPAY, + 'laser' => PAYMENT_TYPE_LASER, + 'maestro' => PAYMENT_TYPE_MAESTRO, + 'solo' => PAYMENT_TYPE_SOLO, + 'switch' => PAYMENT_TYPE_SWITCH ); + $cardName = strtolower(str_replace(array(' ', '-', '_'), '', $cardName)); + + if (empty($cardTypes[$cardName]) && 1 == preg_match('/^('.implode('|', array_keys($cardTypes)).')/', $cardName, $matches)) { + // Some gateways return extra stuff after the card name + $cardName = $matches[1]; + } + if (!empty($cardTypes[$cardName])) { return $cardTypes[$cardName]; } else { @@ -736,10 +825,9 @@ class PaymentService extends BaseService $details = $this->getPaymentDetails($invitation, $accountGateway); $details['customerReference'] = $token; - if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - $details['cardReference'] = $defaultPaymentMethod->source_reference; - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $details['paymentMethodToken'] = $defaultPaymentMethod->source_reference; + $details['token'] = $defaultPaymentMethod->source_reference; + if ($accountGateway->gateway_id == GATEWAY_WEPAY) { + $details['transaction_id'] = 'autobill_'.$invoice->id; } // submit purchase/get response diff --git a/resources/views/payments/add_paymentmethod.blade.php b/resources/views/payments/add_paymentmethod.blade.php index 7c5ca2b956cf..e4d5d3c0a590 100644 --- a/resources/views/payments/add_paymentmethod.blade.php +++ b/resources/views/payments/add_paymentmethod.blade.php @@ -3,221 +3,11 @@ @section('head') @parent @if (!empty($braintreeClientToken)) - - + @include('payments.tokenization_braintree') @elseif (isset($accountGateway) && $accountGateway->getPublishableStripeKey()) - - + @include('payments.tokenization_stripe') + @elseif (isset($accountGateway) && $accountGateway->gateway_id == GATEWAY_WEPAY) + @include('payments.tokenization_wepay') @else @endif - @stop @section('content') @@ -283,7 +72,6 @@ {{ Former::populateField('email', $contact->email) }} @if (!$client->country_id && $client->account->country_id) {{ Former::populateField('country_id', $client->account->country_id) }} - {{ Former::populateField('country', $client->account->country->iso_3166_2) }} @endif @if (!$client->currency_id && $client->account->currency_id) {{ Former::populateField('currency_id', $client->account->currency_id) }} @@ -442,9 +230,9 @@ ))->inline()->label(trans('texts.account_holder_type')); !!} {!! Former::text('account_holder_name') ->label(trans('texts.account_holder_name')) !!} - {!! Former::select('country') + {!! Former::select('country_id') ->label(trans('texts.country_id')) - ->fromQuery($countries, 'name', 'iso_3166_2') + ->fromQuery($countries, 'name', 'id') ->addGroupClass('country-select') !!} {!! Former::select('currency') ->label(trans('texts.currency_id')) @@ -485,7 +273,7 @@

    {{ trans('texts.paypal') }}

    {{$paypalDetails->firstName}} {{$paypalDetails->lastName}}
    {{$paypalDetails->email}}
    - + @@ -515,7 +303,7 @@ @if (!empty($braintreeClientToken))
    @else - {!! Former::text($accountGateway->getPublishableStripeKey() ? '' : 'card_number') + {!! Former::text(!empty($tokenize) ? '' : 'card_number') ->id('card_number') ->placeholder(trans('texts.card_number')) ->autocomplete('cc-number') @@ -526,7 +314,7 @@ @if (!empty($braintreeClientToken))
    @else - {!! Former::text($accountGateway->getPublishableStripeKey() ? '' : 'cvv') + {!! Former::text(!empty($tokenize) ? '' : 'cvv') ->id('cvv') ->placeholder(trans('texts.cvv')) ->autocomplete('off') @@ -539,7 +327,7 @@ @if (!empty($braintreeClientToken))
    @else - {!! Former::select($accountGateway->getPublishableStripeKey() ? '' : 'expiration_month') + {!! Former::select(!empty($tokenize) ? '' : 'expiration_month') ->id('expiration_month') ->autocomplete('cc-exp-month') ->placeholder(trans('texts.expiration_month')) @@ -562,7 +350,7 @@ @if (!empty($braintreeClientToken))
    @else - {!! Former::select($accountGateway->getPublishableStripeKey() ? '' : 'expiration_year') + {!! Former::select(!empty($tokenize) ? '' : 'expiration_year') ->id('expiration_year') ->autocomplete('cc-exp-year') ->placeholder(trans('texts.expiration_year')) diff --git a/resources/views/payments/paymentmethods_list.blade.php b/resources/views/payments/paymentmethods_list.blade.php index 0dc04a988e58..79e8faae5bd1 100644 --- a/resources/views/payments/paymentmethods_list.blade.php +++ b/resources/views/payments/paymentmethods_list.blade.php @@ -183,5 +183,6 @@ function setDefault(sourceId) { $('#default_id').val(sourceId); $('#defaultSourceForm').submit() + return false; } \ No newline at end of file diff --git a/resources/views/payments/tokenization_braintree.blade.php b/resources/views/payments/tokenization_braintree.blade.php new file mode 100644 index 000000000000..f6f6e5921706 --- /dev/null +++ b/resources/views/payments/tokenization_braintree.blade.php @@ -0,0 +1,61 @@ + + \ No newline at end of file diff --git a/resources/views/payments/tokenization_stripe.blade.php b/resources/views/payments/tokenization_stripe.blade.php new file mode 100644 index 000000000000..325393061866 --- /dev/null +++ b/resources/views/payments/tokenization_stripe.blade.php @@ -0,0 +1,154 @@ + + \ No newline at end of file diff --git a/resources/views/payments/tokenization_wepay.blade.php b/resources/views/payments/tokenization_wepay.blade.php new file mode 100644 index 000000000000..4f130d69e01b --- /dev/null +++ b/resources/views/payments/tokenization_wepay.blade.php @@ -0,0 +1,52 @@ + + \ No newline at end of file From 947cb4a6f710c2ff4a13bbcad0de48b2f3e85bc6 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 14 May 2016 17:23:20 -0400 Subject: [PATCH 04/12] Better WePay configuration support --- app/Http/Controllers/AccountController.php | 4 +- .../Controllers/AccountGatewayController.php | 113 ++++++++---------- app/Models/Account.php | 31 +++++ .../Repositories/AccountGatewayRepository.php | 12 +- app/Services/AccountGatewayService.php | 26 ++-- app/Services/DatatableService.php | 35 +++--- public/built.js | 2 +- public/js/script.js | 2 +- resources/lang/en/texts.php | 2 + .../views/accounts/account_gateway.blade.php | 2 +- resources/views/accounts/payments.blade.php | 13 ++ resources/views/list.blade.php | 2 +- 12 files changed, 144 insertions(+), 100 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 053d5cb66a58..bf047ded36c3 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -1,5 +1,6 @@ account; $account->load('account_gateways'); $count = count($account->account_gateways); + $trashedCount = AccountGateway::scope($account->id)->withTrashed()->count(); if ($accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE)) { if (! $accountGateway->getPublishableStripeKey()) { @@ -427,7 +429,7 @@ class AccountController extends BaseController } } - if ($count == 0) { + if ($trashedCount == 0) { return Redirect::to('gateways/create'); } else { $switchToWepay = WEPAY_CLIENT_ID && !$account->getGatewayConfig(GATEWAY_WEPAY); diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 27d3d54464d2..9a12ca11c5f9 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -45,10 +45,6 @@ class AccountGatewayController extends BaseController $accountGateway = AccountGateway::scope($publicId)->firstOrFail(); $config = $accountGateway->getConfig(); - if ($accountGateway->gateway_id == GATEWAY_WEPAY) { - return Redirect::to('gateways'); - } - foreach ($config as $field => $value) { $config->$field = str_repeat('*', strlen($value)); } @@ -109,31 +105,7 @@ class AccountGatewayController extends BaseController $paymentTypes = []; foreach (Gateway::$paymentTypes as $type) { - if ($accountGateway || !$account->getGatewayByType($type)) { - if ($type == PAYMENT_TYPE_CREDIT_CARD && $account->getGatewayByType(PAYMENT_TYPE_STRIPE)) { - // Stripe is already handling credit card payments - continue; - } - - if ($type == PAYMENT_TYPE_STRIPE && $account->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD)) { - // Another gateway is already handling credit card payments - continue; - } - - if ($type == PAYMENT_TYPE_DIRECT_DEBIT && $stripeGateway = $account->getGatewayByType(PAYMENT_TYPE_STRIPE)) { - if (!empty($stripeGateway->getAchEnabled())) { - // Stripe is already handling ACH payments - continue; - } - } - - if ($type == PAYMENT_TYPE_PAYPAL && $braintreeGateway = $account->getGatewayConfig(GATEWAY_BRAINTREE)) { - if (!empty($braintreeGateway->getPayPalEnabled())) { - // PayPal is already enabled - continue; - } - } - + if ($accountGateway || $account->canAddGateway($type)) { $paymentTypes[$type] = $type == PAYMENT_TYPE_CREDIT_CARD ? trans('texts.other_providers'): trans('texts.'.strtolower($type)); if ($type == PAYMENT_TYPE_BITCOIN) { @@ -162,7 +134,7 @@ class AccountGatewayController extends BaseController foreach ($gateways as $gateway) { $fields = $gateway->getFields(); asort($fields); - $gateway->fields = $fields; + $gateway->fields = $gateway->id == GATEWAY_WEPAY ? [] : $fields; if ($accountGateway && $accountGateway->gateway_id == $gateway->id) { $accountGateway->fields = $gateway->fields; } @@ -193,7 +165,7 @@ class AccountGatewayController extends BaseController $ids = Input::get('bulk_public_id'); $count = $this->accountGatewayService->bulk($ids, $action); - Session::flash('message', trans('texts.archived_account_gateway')); + Session::flash('message', trans("texts.{$action}d_account_gateway")); return Redirect::to('settings/' . ACCOUNT_PAYMENTS); } @@ -208,10 +180,6 @@ class AccountGatewayController extends BaseController $paymentType = Input::get('payment_type_id'); $gatewayId = Input::get('gateway_id'); - if ($gatewayId == GATEWAY_WEPAY) { - return $this->setupWePay(); - } - if ($paymentType == PAYMENT_TYPE_PAYPAL) { $gatewayId = GATEWAY_PAYPAL_EXPRESS; } elseif ($paymentType == PAYMENT_TYPE_BITCOIN) { @@ -245,14 +213,16 @@ class AccountGatewayController extends BaseController } } - foreach ($fields as $field => $details) { - if (!in_array($field, $optional)) { - if (strtolower($gateway->name) == 'beanstream') { - if (in_array($field, ['merchant_id', 'passCode'])) { - $rules[$gateway->id.'_'.$field] = 'required'; + if ($gatewayId != GATEWAY_WEPAY) { + foreach ($fields as $field => $details) { + if (!in_array($field, $optional)) { + if (strtolower($gateway->name) == 'beanstream') { + if (in_array($field, ['merchant_id', 'passCode'])) { + $rules[$gateway->id . '_' . $field] = 'required'; + } + } else { + $rules[$gateway->id . '_' . $field] = 'required'; } - } else { - $rules[$gateway->id.'_'.$field] = 'required'; } } } @@ -274,20 +244,29 @@ class AccountGatewayController extends BaseController } else { $accountGateway = AccountGateway::createNew(); $accountGateway->gateway_id = $gatewayId; + + if ($gatewayId == GATEWAY_WEPAY && !$this->setupWePay($accountGateway, $wepayResponse)) { + return $wepayResponse; + } } $config = new stdClass(); - foreach ($fields as $field => $details) { - $value = trim(Input::get($gateway->id.'_'.$field)); - // if the new value is masked use the original value - if ($oldConfig && $value && $value === str_repeat('*', strlen($value))) { - $value = $oldConfig->$field; - } - if (!$value && ($field == 'testMode' || $field == 'developerMode')) { - // do nothing - } else { - $config->$field = $value; + + if ($gatewayId != GATEWAY_WEPAY) { + foreach ($fields as $field => $details) { + $value = trim(Input::get($gateway->id . '_' . $field)); + // if the new value is masked use the original value + if ($oldConfig && $value && $value === str_repeat('*', strlen($value))) { + $value = $oldConfig->$field; + } + if (!$value && ($field == 'testMode' || $field == 'developerMode')) { + // do nothing + } else { + $config->$field = $value; + } } + } else { + $config = clone $oldConfig; } $publishableKey = Input::get('publishable_key'); @@ -349,15 +328,18 @@ class AccountGatewayController extends BaseController $account->save(); } - if ($accountGatewayPublicId) { - $message = trans('texts.updated_gateway'); + if(isset($wepayResponse)) { + return $wepayResponse; } else { - $message = trans('texts.created_gateway'); + if ($accountGatewayPublicId) { + $message = trans('texts.updated_gateway'); + } else { + $message = trans('texts.created_gateway'); + } + + Session::flash('message', $message); + return Redirect::to("gateways/{$accountGateway->public_id}/edit"); } - - Session::flash('message', $message); - - return Redirect::to("gateways/{$accountGateway->public_id}/edit"); } } @@ -378,7 +360,7 @@ class AccountGatewayController extends BaseController return $update_uri_data->uri; } - protected function setupWePay() + protected function setupWePay($accountGateway, &$response) { $user = Auth::user(); $account = $user->account; @@ -441,7 +423,6 @@ class AccountGatewayController extends BaseController $gateway->delete(); } - $accountGateway = AccountGateway::createNew(); $accountGateway->gateway_id = GATEWAY_WEPAY; $accountGateway->setConfig(array( 'userId' => $wepayUser->user_id, @@ -451,7 +432,6 @@ class AccountGatewayController extends BaseController 'accountId' => $wepayAccount->account_id, 'testMode' => WEPAY_ENVIRONMENT == WEPAY_STAGE, )); - $account->account_gateways()->save($accountGateway); if ($confirmationRequired) { Session::flash('message', trans('texts.created_wepay_confirmation_required')); @@ -461,14 +441,17 @@ class AccountGatewayController extends BaseController 'redirect_uri' => URL::to('gateways'), )); - return Redirect::to($updateUri->uri); + $response = Redirect::to($updateUri->uri); + return true; } - return Redirect::to("gateways"); + $response = Redirect::to("gateways/{$accountGateway->public_id}/edit"); + return true; } catch (\WePayException $e) { Session::flash('error', $e->getMessage()); - return Redirect::to('gateways/create') + $response = Redirect::to('gateways/create') ->withInput(); + return false; } } diff --git a/app/Models/Account.php b/app/Models/Account.php index fb2c39b951db..c489d21b180b 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -1412,6 +1412,37 @@ class Account extends Eloquent public function getFontFolders(){ return array_map(function($item){return $item['folder'];}, $this->getFontsData()); } + + public function canAddGateway($type){ + if($this->getGatewayByType($type)) { + return false; + } + if ($type == PAYMENT_TYPE_CREDIT_CARD && $this->getGatewayByType(PAYMENT_TYPE_STRIPE)) { + // Stripe is already handling credit card payments + return false; + } + + if ($type == PAYMENT_TYPE_STRIPE && $this->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD)) { + // Another gateway is already handling credit card payments + return false; + } + + if ($type == PAYMENT_TYPE_DIRECT_DEBIT && $stripeGateway = $this->getGatewayByType(PAYMENT_TYPE_STRIPE)) { + if (!empty($stripeGateway->getAchEnabled())) { + // Stripe is already handling ACH payments + return false; + } + } + + if ($type == PAYMENT_TYPE_PAYPAL && $braintreeGateway = $this->getGatewayConfig(GATEWAY_BRAINTREE)) { + if (!empty($braintreeGateway->getPayPalEnabled())) { + // PayPal is already enabled + return false; + } + } + + return true; + } } Account::updated(function ($account) { diff --git a/app/Ninja/Repositories/AccountGatewayRepository.php b/app/Ninja/Repositories/AccountGatewayRepository.php index b011ebda765b..286c8416a21a 100644 --- a/app/Ninja/Repositories/AccountGatewayRepository.php +++ b/app/Ninja/Repositories/AccountGatewayRepository.php @@ -15,10 +15,14 @@ class AccountGatewayRepository extends BaseRepository public function find($accountId) { - return DB::table('account_gateways') + $query = DB::table('account_gateways') ->join('gateways', 'gateways.id', '=', 'account_gateways.gateway_id') - ->where('account_gateways.deleted_at', '=', null) - ->where('account_gateways.account_id', '=', $accountId) - ->select('account_gateways.id', 'account_gateways.public_id', 'gateways.name', 'account_gateways.deleted_at', 'account_gateways.gateway_id'); + ->where('account_gateways.account_id', '=', $accountId); + + if (!\Session::get('show_trash:gateway')) { + $query->where('account_gateways.deleted_at', '=', null); + } + + return $query->select('account_gateways.id', 'account_gateways.public_id', 'gateways.name', 'account_gateways.deleted_at', 'account_gateways.gateway_id'); } } diff --git a/app/Services/AccountGatewayService.php b/app/Services/AccountGatewayService.php index fe5427b966bb..a992da5c82c9 100644 --- a/app/Services/AccountGatewayService.php +++ b/app/Services/AccountGatewayService.php @@ -42,7 +42,9 @@ class AccountGatewayService extends BaseService [ 'name', function ($model) { - if ($model->gateway_id != GATEWAY_WEPAY) { + if ($model->deleted_at) { + return $model->name; + } elseif ($model->gateway_id != GATEWAY_WEPAY) { return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); } else { $accountGateway = AccountGateway::find($model->id); @@ -89,20 +91,12 @@ class AccountGatewayService extends BaseService { return [ [ - uctrans('texts.edit_gateway'), - function ($model) { - return URL::to("gateways/{$model->public_id}/edit"); - }, - function($model) { - return $model->gateway_id != GATEWAY_WEPAY; - } - ], [ uctrans('texts.resend_confirmation_email'), function ($model) { return $model->resendConfirmationUrl; }, function($model) { - return $model->gateway_id == GATEWAY_WEPAY && !empty($model->resendConfirmationUrl); + return !$model->deleted_at && $model->gateway_id == GATEWAY_WEPAY && !empty($model->resendConfirmationUrl); } ], [ uctrans('texts.finish_setup'), @@ -110,9 +104,17 @@ class AccountGatewayService extends BaseService return $model->setupUrl; }, function($model) { - return $model->gateway_id == GATEWAY_WEPAY && !empty($model->setupUrl); + return !$model->deleted_at && $model->gateway_id == GATEWAY_WEPAY && !empty($model->setupUrl); } ] , [ + uctrans('texts.edit_gateway'), + function ($model) { + return URL::to("gateways/{$model->public_id}/edit"); + }, + function($model) { + return !$model->deleted_at; + } + ], [ uctrans('texts.manage_wepay_account'), function ($model) { $accountGateway = AccountGateway::find($model->id); @@ -123,7 +125,7 @@ class AccountGatewayService extends BaseService ); }, function($model) { - return $model->gateway_id == GATEWAY_WEPAY; + return !$model->deleted_at && $model->gateway_id == GATEWAY_WEPAY; } ] ]; diff --git a/app/Services/DatatableService.php b/app/Services/DatatableService.php index 222a7b4d7cb6..df4c9d93d55d 100644 --- a/app/Services/DatatableService.php +++ b/app/Services/DatatableService.php @@ -60,11 +60,7 @@ class DatatableService $str .= '
    '; } - $str .= ''; + if (!empty($dropdown_contents)) { + $str .= ''; }); } diff --git a/public/built.js b/public/built.js index 14f82399435f..06790ce1e6ed 100644 --- a/public/built.js +++ b/public/built.js @@ -30933,7 +30933,7 @@ function truncate(string, length){ // Show/hide the 'Select' option in the datalists function actionListHandler() { - $('tbody tr').mouseover(function() { + $('tbody tr .tr-action').closest('tr').mouseover(function() { $(this).closest('tr').find('.tr-action').show(); $(this).closest('tr').find('.tr-status').hide(); }).mouseout(function() { diff --git a/public/js/script.js b/public/js/script.js index a76e6d37cd0e..a66c48768bb6 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -1042,7 +1042,7 @@ function truncate(string, length){ // Show/hide the 'Select' option in the datalists function actionListHandler() { - $('tbody tr').mouseover(function() { + $('tbody tr .tr-action').closest('tr').mouseover(function() { $(this).closest('tr').find('.tr-action').show(); $(this).closest('tr').find('.tr-status').hide(); }).mouseout(function() { diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index ea3ea349f31d..f1adcabadaa3 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1311,6 +1311,8 @@ $LANG = array( 'created_wepay_confirmation_required' => 'Please check your email and confirm your email address with WePay.', 'switch_to_wepay' => 'Switch to WePay', 'switch' => 'Switch', + 'restore_account_gateway' => 'Restore Gateway', + 'restored_account_gateway' => 'Successfully restored gateway', ); return $LANG; diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index 49635e95db38..4bcbc1cc0fb6 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -94,7 +94,7 @@ {!! Former::text('publishable_key') !!} @endif - @if ($gateway->id == GATEWAY_STRIPE || $gateway->id == GATEWAY_BRAINTREE) + @if ($gateway->id == GATEWAY_STRIPE || $gateway->id == GATEWAY_BRAINTREE || $gateway->id == GATEWAY_WEPAY) {!! Former::select('token_billing_type_id') ->options($tokenBillingOptions) ->help(trans('texts.token_billing_help')) !!} diff --git a/resources/views/accounts/payments.blade.php b/resources/views/accounts/payments.blade.php index 9907fac6ecee..997190ec4f0a 100644 --- a/resources/views/accounts/payments.blade.php +++ b/resources/views/accounts/payments.blade.php @@ -8,7 +8,12 @@ {!! Button::success(trans('texts.switch_to_wepay')) ->asLinkTo(URL::to('/gateways/switch/wepay')) ->appendIcon(Icon::create('circle-arrow-up')) !!} +   @endif + @if ($showAdd) {!! Button::primary(trans('texts.add_gateway')) @@ -34,6 +39,14 @@ @stop \ No newline at end of file diff --git a/resources/views/list.blade.php b/resources/views/list.blade.php index 93588fba9806..02faad273954 100644 --- a/resources/views/list.blade.php +++ b/resources/views/list.blade.php @@ -26,7 +26,7 @@   + -onli
    Date: Sat, 14 May 2016 22:22:06 -0400 Subject: [PATCH 05/12] WePay fixes; support account updater --- .env.example | 10 +++- .../Controllers/AccountGatewayController.php | 2 +- app/Http/Controllers/PaymentController.php | 46 +++++++++++++++++++ app/Http/routes.php | 1 + app/Services/PaymentService.php | 15 ++++-- .../views/accounts/account_gateway.blade.php | 2 +- .../payments/tokenization_wepay.blade.php | 14 +++++- 7 files changed, 81 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 41ce2039f7e5..dd797f50d0ec 100644 --- a/.env.example +++ b/.env.example @@ -67,4 +67,12 @@ API_SECRET=password #MAX_DOCUMENT_SIZE # KB #MAX_EMAIL_DOCUMENTS_SIZE # Total KB #MAX_ZIP_DOCUMENTS_SIZE # Total KB (uncompressed) -#DOCUMENT_PREVIEW_SIZE # Pixels \ No newline at end of file +#DOCUMENT_PREVIEW_SIZE # Pixels + +WEPAY_CLIENT_ID= +WEPAY_CLIENT_SECRET= +WEPAY_AUTO_UPDATE=true # Requires permission from WePay +WEPAY_ENVIRONMENT=production # production or stage + +# See https://www.wepay.com/developer/reference/structures#theme +WEPAY_THEME={"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}')); \ No newline at end of file diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 9a12ca11c5f9..34a378379d21 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -393,7 +393,7 @@ class AccountGatewayController extends BaseController 'original_device' => \Request::server('HTTP_USER_AGENT'), 'tos_acceptance_time' => time(), 'redirect_uri' => URL::to('gateways'), - 'callback_uri' => URL::to('https://sometechie.ngrok.io/paymenthook/'.$account->account_key.'/'.GATEWAY_WEPAY), + 'callback_uri' => URL::to(env('WEBHOOK_PREFIX','').'paymenthook/'.$account->account_key.'/'.GATEWAY_WEPAY), 'scope' => 'manage_accounts,collect_payments,view_user,preapprove_payments,send_money', )); diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index e14a9cb9cd69..c30436cd3a80 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -790,6 +790,8 @@ class PaymentController extends BaseController switch($gatewayId) { case GATEWAY_STRIPE: return $this->handleStripeWebhook($accountGateway); + case GATEWAY_WEPAY: + return $this->handleWePayWebhook($accountGateway); default: return response()->json([ 'message' => 'Unsupported gateway', @@ -797,6 +799,50 @@ class PaymentController extends BaseController } } + protected function handleWePayWebhook($accountGateway) { + $data = Input::all(); + $accountId = $accountGateway->account_id; + + foreach (array_keys($data) as $key) { + if ('_id' == substr($key, -3)) { + $objectType = substr($key, 0, -3); + $objectId = $data[$key]; + break; + } + } + + if (!isset($objectType)) { + return response()->json([ + 'message' => 'Could not find object id parameter', + ], 400); + } + + if ($objectType == 'credit_card') { + $paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $objectId)->first(); + + if (!$paymentMethod) { + return array('message' => 'Unknown payment method'); + } + + $wepay = \Utils::setupWePay($accountGateway); + $source = $wepay->request('credit_card', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => intval($objectId), + )); + + if ($source->state == 'deleted') { + $paymentMethod->delete(); + } else { + $this->paymentService->convertPaymentMethodFromWePay($source, null, $paymentMethod)->save(); + } + + return array('message' => 'Processed successfully'); + } else { + return array('message' => 'Ignoring event'); + } + } + protected function handleStripeWebhook($accountGateway) { $eventId = Input::get('id'); $eventType= Input::get('type'); diff --git a/app/Http/routes.php b/app/Http/routes.php index 3a02960d8005..4f362a3c6df6 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -756,6 +756,7 @@ if (!defined('CONTACT_EMAIL')) { define('WEPAY_STAGE', 'stage'); define('WEPAY_CLIENT_ID', env('WEPAY_CLIENT_ID')); define('WEPAY_CLIENT_SECRET', env('WEPAY_CLIENT_SECRET')); + define('WEPAY_AUTO_UPDATE', env('WEPAY_AUTO_UPDATE', false)); define('WEPAY_ENVIRONMENT', env('WEPAY_ENVIRONMENT', WEPAY_PRODUCTION)); define('WEPAY_THEME', env('WEPAY_THEME','{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}')); diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 626366a469cd..11d9cda41e9f 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -278,7 +278,7 @@ class PaymentService extends BaseService $wepay->request('/credit_card/delete', [ 'client_id' => WEPAY_CLIENT_ID, 'client_secret' => WEPAY_CLIENT_SECRET, - 'credit_card_id' => $paymentMethod->source_reference, + 'credit_card_id' => intval($paymentMethod->source_reference), ]); } catch (\WePayException $ex){ return $ex->getMessage(); @@ -401,14 +401,21 @@ class PaymentService extends BaseService $wepay->request('credit_card/authorize', array( 'client_id' => WEPAY_CLIENT_ID, 'client_secret' => WEPAY_CLIENT_SECRET, - 'credit_card_id' => $details['token'], + 'credit_card_id' => intval($details['token']), )); - // Get the card details + // Update the callback uri and get the card details + $wepay->request('credit_card/modify', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => intval($details['token']), + 'auto_update' => WEPAY_AUTO_UPDATE, + 'callback_uri' => URL::to(env('WEBHOOK_PREFIX','').'paymenthook/'.$client->account->account_key.'/'.GATEWAY_WEPAY), + )); $tokenResponse = $wepay->request('credit_card', array( 'client_id' => WEPAY_CLIENT_ID, 'client_secret' => WEPAY_CLIENT_SECRET, - 'credit_card_id' => $details['token'], + 'credit_card_id' => intval($details['token']), )); $customerReference = CUSTOMER_REFERENCE_LOCAL; diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index 4bcbc1cc0fb6..155ef6579f69 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -104,7 +104,7 @@
    - +
    {!! trans('texts.stripe_webhook_help', [ 'link'=>''.trans('texts.stripe_webhook_help_link_text').'' ]) !!}
    diff --git a/resources/views/payments/tokenization_wepay.blade.php b/resources/views/payments/tokenization_wepay.blade.php index 4f130d69e01b..5d41cfc9fc69 100644 --- a/resources/views/payments/tokenization_wepay.blade.php +++ b/resources/views/payments/tokenization_wepay.blade.php @@ -3,6 +3,7 @@ $(function() { var countries = {!! $countries->pluck('iso_3166_2','id') !!}; WePay.set_endpoint('{{ WEPAY_ENVIRONMENT }}'); + var $form = $('.payment-form'); $('.payment-form').submit(function(event) { var data = { client_id: {{ WEPAY_CLIENT_ID }}, @@ -27,9 +28,11 @@ } // Not including state/province, since WePay wants 2-letter codes and users enter the full name - var response = WePay.credit_card.create(data, function(response) { - var $form = $('.payment-form'); + // Disable the submit button to prevent repeated clicks + $form.find('button').prop('disabled', true); + $('#js-error-message').hide(); + var response = WePay.credit_card.create(data, function(response) { if (response.error) { // Show the errors on the form var error = response.error_description; @@ -45,6 +48,13 @@ } }); + if (response.error) { + // Show the errors on the form + var error = response.error_description; + $form.find('button').prop('disabled', false); + $('#js-error-message').text(error).fadeIn(); + } + // Prevent the form from submitting with the default action return false; }); From 9afd0741f73f09779202d8bd7547e13340bc1e3e Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 14 May 2016 22:32:27 -0400 Subject: [PATCH 06/12] Better WePay setup UI --- .../Controllers/AccountGatewayController.php | 10 +++++++--- .../partials/account_gateway_wepay.blade.php | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 34a378379d21..fef9a511636e 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -87,6 +87,7 @@ class AccountGatewayController extends BaseController ->where('id', '!=', GATEWAY_GOCARDLESS) ->where('id', '!=', GATEWAY_DWOLLA) ->where('id', '!=', GATEWAY_STRIPE) + ->where('id', '!=', GATEWAY_WEPAY) ->orderBy('name')->get(); $data['hiddenFields'] = Gateway::$hiddenFields; @@ -245,8 +246,11 @@ class AccountGatewayController extends BaseController $accountGateway = AccountGateway::createNew(); $accountGateway->gateway_id = $gatewayId; - if ($gatewayId == GATEWAY_WEPAY && !$this->setupWePay($accountGateway, $wepayResponse)) { - return $wepayResponse; + if ($gatewayId == GATEWAY_WEPAY) { + if(!$this->setupWePay($accountGateway, $wepayResponse)) { + return $wepayResponse; + } + $oldConfig = $accountGateway->getConfig(); } } @@ -265,7 +269,7 @@ class AccountGatewayController extends BaseController $config->$field = $value; } } - } else { + } elseif($oldConfig) { $config = clone $oldConfig; } diff --git a/resources/views/accounts/partials/account_gateway_wepay.blade.php b/resources/views/accounts/partials/account_gateway_wepay.blade.php index edda271d1479..4d6832a8c1f5 100644 --- a/resources/views/accounts/partials/account_gateway_wepay.blade.php +++ b/resources/views/accounts/partials/account_gateway_wepay.blade.php @@ -10,6 +10,9 @@ {!! Former::populateField('first_name', $user->first_name) !!} {!! Former::populateField('last_name', $user->last_name) !!} {!! Former::populateField('email', $user->email) !!} +{!! Former::populateField('show_address', 1) !!} +{!! Former::populateField('update_address', 1) !!} +{!! Former::populateField('token_billing_type_id', $account->token_billing_type_id) !!}

    {!! trans('texts.online_payments') !!}

    @@ -20,6 +23,23 @@ {!! Former::text('email') !!} {!! Former::text('company_name')->help('wepay_company_name_help')->maxlength(255) !!} {!! Former::text('description')->help('wepay_description_help') !!} + {!! Former::select('token_billing_type_id') + ->options($tokenBillingOptions) + ->help(trans('texts.token_billing_help')) !!} + {!! Former::checkbox('show_address') + ->label(trans('texts.billing_address')) + ->text(trans('texts.show_address_help')) + ->addGroupClass('gateway-option') !!} + {!! Former::checkbox('update_address') + ->label(' ') + ->text(trans('texts.update_address_help')) + ->addGroupClass('gateway-option') !!} + {!! Former::checkboxes('creditCardTypes[]') + ->label('Accepted Credit Cards') + ->checkboxes($creditCardTypes) + ->class('creditcard-types') + ->addGroupClass('gateway-option') + !!} {!! Former::checkbox('tos_agree')->label(' ')->text(trans('texts.wepay_tos_agree', ['link'=>''.trans('texts.wepay_tos_link_text').''] ))->value('true') !!} From 8bc8b384c5513a741b9036e010f58f682f0e2bd1 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 14 May 2016 22:38:09 -0400 Subject: [PATCH 07/12] Fix auto bill error when the user hasn't entered payment information --- app/Services/PaymentService.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 11d9cda41e9f..4425adeebaa3 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -821,6 +821,11 @@ class PaymentService extends BaseService $invitation = $invoice->invitations->first(); $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); + + if (!$accountGatewayToken) { + return false; + } + $defaultPaymentMethod = $accountGatewayToken->default_payment_method; if (!$invitation || !$token || !$defaultPaymentMethod) { From 65feee34e3ab7a1531f146ae38fc8dbaa18de5f1 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 14 May 2016 22:43:55 -0400 Subject: [PATCH 08/12] Fox expiration off-by-one error --- app/Services/PaymentService.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 4425adeebaa3..4a4a757720de 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -471,7 +471,7 @@ class PaymentService extends BaseService $paymentMethod->setRelation('currency', $currency); } } elseif ($source['object'] == 'card') { - $paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-00'; + $paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-01'; $paymentMethod->payment_type_id = $this->parseCardType($source['brand']); } else { return null; @@ -491,7 +491,7 @@ class PaymentService extends BaseService if ($source instanceof \Braintree\CreditCard) { $paymentMethod->payment_type_id = $this->parseCardType($source->cardType); $paymentMethod->last4 = $source->last4; - $paymentMethod->expiration = $source->expirationYear . '-' . $source->expirationMonth . '-00'; + $paymentMethod->expiration = $source->expirationYear . '-' . $source->expirationMonth . '-01'; } elseif ($source instanceof \Braintree\PayPalAccount) { $paymentMethod->email = $source->email; $paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; @@ -514,7 +514,7 @@ class PaymentService extends BaseService $paymentMethod->payment_type_id = $this->parseCardType($source->credit_card_name); $paymentMethod->last4 = $source->last_four; - $paymentMethod->expiration = $source->expiration_year . '-' . $source->expiration_month . '-00'; + $paymentMethod->expiration = $source->expiration_year . '-' . $source->expiration_month . '-01'; $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); $paymentMethod->source_reference = $source->credit_card_id; @@ -552,7 +552,7 @@ class PaymentService extends BaseService if ($transaction->paymentInstrumentType == 'credit_card') { $card = $transaction->creditCardDetails; $paymentMethod->last4 = $card->last4; - $paymentMethod->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00'; + $paymentMethod->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-01'; $paymentMethod->payment_type_id = $this->parseCardType($card->cardType); } elseif ($transaction->paymentInstrumentType == 'paypal_account') { $paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; From 919aec2192971cc2d7f143ce00a19d83b756c2e2 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 14 May 2016 23:03:39 -0400 Subject: [PATCH 09/12] Better refund handling --- app/Listeners/ActivityListener.php | 12 ++--- app/Services/PaymentService.php | 81 +++++++++++++++++++++--------- resources/views/list.blade.php | 2 +- 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/app/Listeners/ActivityListener.php b/app/Listeners/ActivityListener.php index 1df8edeb6848..14ba0da50ec9 100644 --- a/app/Listeners/ActivityListener.php +++ b/app/Listeners/ActivityListener.php @@ -307,8 +307,8 @@ class ActivityListener $this->activityRepo->create( $payment, ACTIVITY_TYPE_DELETE_PAYMENT, - $payment->amount, - $payment->amount * -1 + $payment->amount - $payment->refunded, + ($payment->amount - $payment->refunded) * -1 ); } @@ -343,8 +343,8 @@ class ActivityListener $this->activityRepo->create( $payment, ACTIVITY_TYPE_FAILED_PAYMENT, - $payment->amount, - $payment->amount * -1 + ($payment->amount - $payment->refunded), + ($payment->amount - $payment->refunded) * -1 ); } @@ -367,8 +367,8 @@ class ActivityListener $this->activityRepo->create( $payment, ACTIVITY_TYPE_RESTORE_PAYMENT, - $event->fromDeleted ? $payment->amount * -1 : 0, - $event->fromDeleted ? $payment->amount : 0 + $event->fromDeleted ? ($payment->amount - $payment->refunded) * -1 : 0, + $event->fromDeleted ? ($payment->amount - $payment->refunded) : 0 ); } } diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 4a4a757720de..32fafc045c01 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -1048,41 +1048,76 @@ class PaymentService extends BaseService if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) { $gateway = $this->createGateway($accountGateway); - $refund = $gateway->refund(array( - 'transactionReference' => $payment->transaction_reference, - 'amount' => $amount, - )); - $response = $refund->send(); - - if ($response->isSuccessful()) { - $payment->recordRefund($amount); + + if ($accountGateway->gateway_id != GATEWAY_WEPAY) { + $refund = $gateway->refund(array( + 'transactionReference' => $payment->transaction_reference, + 'amount' => $amount, + )); + $response = $refund->send(); + + if ($response->isSuccessful()) { + $payment->recordRefund($amount); + } else { + $data = $response->getData(); + + if ($data instanceof \Braintree\Result\Error) { + $error = $data->errors->deepAll()[0]; + if ($error && $error->code == 91506) { + if ($amount == $payment->amount) { + // This is an unsettled transaction; try to void it + $void = $gateway->void(array( + 'transactionReference' => $payment->transaction_reference, + )); + $response = $void->send(); + + if ($response->isSuccessful()) { + $payment->markVoided(); + } + } else { + $this->error('Unknown', 'Partial refund not allowed for unsettled transactions.', $accountGateway); + return false; + } + } + } + + if (!$response->isSuccessful()) { + $this->error('Unknown', $response->getMessage(), $accountGateway); + return false; + } + } } else { - $data = $response->getData(); + $wepay = \Utils::setupWePay($accountGateway); - if ($data instanceof \Braintree\Result\Error) { - $error = $data->errors->deepAll()[0]; - if ($error && $error->code == 91506) { + try { + $wepay->request('checkout/refund', array( + 'checkout_id' => intval($payment->transaction_reference), + 'refund_reason' => 'Refund issued by merchant.', + 'amount' => $amount, + )); + $payment->recordRefund($amount); + } catch (\WePayException $ex) { + if ($ex->getCode() == 4004) { if ($amount == $payment->amount) { - // This is an unsettled transaction; try to void it - $void = $gateway->void(array( - 'transactionReference' => $payment->transaction_reference, - )); - $response = $void->send(); - - if ($response->isSuccessful()) { + try { + // This is an uncaptured transaction; try to cancel it + $wepay->request('checkout/cancel', array( + 'checkout_id' => intval($payment->transaction_reference), + 'cancel_reason' => 'Refund issued by merchant.', + )); $payment->markVoided(); + } catch (\WePayException $ex) { + $this->error('Unknown', $ex->getMessage(), $accountGateway); } } else { $this->error('Unknown', 'Partial refund not allowed for unsettled transactions.', $accountGateway); return false; } + } else { + $this->error('Unknown', $ex->getMessage(), $accountGateway); } } - if (!$response->isSuccessful()) { - $this->error('Unknown', $response->getMessage(), $accountGateway); - return false; - } } } else { $payment->recordRefund($amount); diff --git a/resources/views/list.blade.php b/resources/views/list.blade.php index 02faad273954..93588fba9806 100644 --- a/resources/views/list.blade.php +++ b/resources/views/list.blade.php @@ -26,7 +26,7 @@  -onli +
    Date: Sat, 14 May 2016 23:15:22 -0400 Subject: [PATCH 10/12] Make autobill setting numbers match token settings --- app/Http/routes.php | 8 ++++---- app/Ninja/Repositories/InvoiceRepository.php | 2 +- .../2016_04_23_182223_payments_changes.php | 11 ++++++++++- resources/views/invoices/edit.blade.php | 16 ++++++++-------- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index 4f362a3c6df6..8a69b8d53e38 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -707,10 +707,10 @@ if (!defined('CONTACT_EMAIL')) { define('RESELLER_REVENUE_SHARE', 'A'); define('RESELLER_LIMITED_USERS', 'B'); - define('AUTO_BILL_OFF', 0); - define('AUTO_BILL_OPT_IN', 1); - define('AUTO_BILL_OPT_OUT', 2); - define('AUTO_BILL_ALWAYS', 3); + define('AUTO_BILL_OFF', 1); + define('AUTO_BILL_OPT_IN', 2); + define('AUTO_BILL_OPT_OUT', 3); + define('AUTO_BILL_ALWAYS', 4); // These must be lowercase define('PLAN_FREE', 'free'); diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index fd493ba16d93..979304ed01a0 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -324,7 +324,7 @@ class InvoiceRepository extends BaseRepository $invoice->start_date = Utils::toSqlDate($data['start_date']); $invoice->end_date = Utils::toSqlDate($data['end_date']); $invoice->client_enable_auto_bill = isset($data['client_enable_auto_bill']) && $data['client_enable_auto_bill'] ? true : false; - $invoice->auto_bill = isset($data['auto_bill']) ? intval($data['auto_bill']) : 0; + $invoice->auto_bill = isset($data['auto_bill']) ? intval($data['auto_bill']) : AUTO_BILL_OFF; if ($invoice->auto_bill < AUTO_BILL_OFF || $invoice->auto_bill > AUTO_BILL_ALWAYS ) { $invoice->auto_bill = AUTO_BILL_OFF; diff --git a/database/migrations/2016_04_23_182223_payments_changes.php b/database/migrations/2016_04_23_182223_payments_changes.php index 1c277d8da6b2..35a1b6134e9b 100644 --- a/database/migrations/2016_04_23_182223_payments_changes.php +++ b/database/migrations/2016_04_23_182223_payments_changes.php @@ -78,6 +78,11 @@ class PaymentsChanges extends Migration ->where('auto_bill', '=', 1) ->update(array('client_enable_auto_bill' => 1, 'auto_bill' => AUTO_BILL_OPT_OUT)); + \DB::table('invoices') + ->where('auto_bill', '=', 0) + ->where('is_recurring', '=', 1) + ->update(array('auto_bill' => AUTO_BILL_OFF)); + Schema::table('account_gateway_tokens', function($table) { @@ -113,11 +118,15 @@ class PaymentsChanges extends Migration $table->dropColumn('payment_method_id'); }); + \DB::table('invoices') + ->where('auto_bill', '=', AUTO_BILL_OFF) + ->update(array('auto_bill' => 0)); + \DB::table('invoices') ->where(function($query){ $query->where('auto_bill', '=', AUTO_BILL_ALWAYS); $query->orwhere(function($query){ - $query->where('auto_bill', '!=', AUTO_BILL_OFF); + $query->where('auto_bill', '!=', 0); $query->where('client_enable_auto_bill', '=', 1); }); }) diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 5e503b1d7687..82204e7aca4b 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -166,24 +166,24 @@ @if($account->getTokenGatewayId()) -
    +
    {!! Former::select('auto_bill') - ->data_bind("value: auto_bill, valueUpdate: 'afterkeydown', event:{change:function(){if(auto_bill()==1)client_enable_auto_bill(0);if(auto_bill()==2)client_enable_auto_bill(1)}}") + ->data_bind("value: auto_bill, valueUpdate: 'afterkeydown', event:{change:function(){if(auto_bill()==".AUTO_BILL_OPT_IN.")client_enable_auto_bill(0);if(auto_bill()==".AUTO_BILL_OPT_OUT.")client_enable_auto_bill(1)}}") ->options([ - 0 => trans('texts.off'), - 1 => trans('texts.opt_in'), - 2 => trans('texts.opt_out'), - 3 => trans('texts.always'), + AUTO_BILL_OFF => trans('texts.off'), + AUTO_BILL_OPT_IN => trans('texts.opt_in'), + AUTO_BILL_OPT_OUT => trans('texts.opt_out'), + AUTO_BILL_ALWAYS => trans('texts.always'), ]) !!}
    -
    +
    {{trans('texts.auto_bill')}}
    {{trans('texts.opted_in')}} - ({{trans('texts.disable')}})
    -
    +
    {{trans('texts.auto_bill')}}
    {{trans('texts.opted_out')}} - ({{trans('texts.enable')}}) From a482c63ee3f9737c77e7dee33659a3fc3672806d Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sun, 15 May 2016 16:27:56 -0400 Subject: [PATCH 11/12] Payments bug fixes --- app/Http/Controllers/PaymentController.php | 27 +++++----- .../Controllers/PublicClientController.php | 12 +++-- app/Models/Payment.php | 2 +- app/Models/PaymentMethod.php | 2 +- app/Services/PaymentService.php | 23 ++++---- .../partials/account_gateway_wepay.blade.php | 2 +- .../payments/add_paymentmethod.blade.php | 52 +++++++++---------- .../payments/paymentmethods_list.blade.php | 2 +- .../payments/tokenization_braintree.blade.php | 8 ++- 9 files changed, 72 insertions(+), 58 deletions(-) diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index c30436cd3a80..69553f58d5c1 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -411,7 +411,7 @@ class PaymentController extends BaseController 'last_name' => 'required', ]; - if ( ! Input::get('sourceToken') && !(Input::get('plaidPublicToken') && Input::get('plaidAccountId'))) { + if ( !Input::get('sourceToken') && !(Input::get('plaidPublicToken') && Input::get('plaidAccountId'))) { $rules = array_merge( $rules, [ @@ -439,7 +439,7 @@ class PaymentController extends BaseController $validator = Validator::make(Input::all(), $rules); if ($validator->fails()) { - return false; + return $validator; } if ($requireAddress && $accountGateway->update_address) { @@ -465,7 +465,7 @@ class PaymentController extends BaseController $paymentType = Session::get($invitation->id . 'payment_type'); $accountGateway = $account->getGatewayByType($paymentType); $paymentMethod = null; - + if ($useToken) { if(!$sourceId) { Session::flash('error', trans('texts.no_payment_method_specified')); @@ -477,12 +477,13 @@ class PaymentController extends BaseController } } - if (!static::processPaymentClientDetails($client, $accountGateway, $paymentType, $onSite)) { + if (($validator = static::processPaymentClientDetails($client, $accountGateway, $paymentType, $onSite)) !== true) { return Redirect::to('payment/'.$invitationKey) ->withErrors($validator) ->withInput(Request::except('cvv')); } + try { // For offsite payments send the client's details on file // If we're using a token then we don't need to send any other data @@ -497,9 +498,10 @@ class PaymentController extends BaseController // check if we're creating/using a billing token $tokenBillingSupported = false; + $sourceReferenceParam = 'token'; if ($accountGateway->gateway_id == GATEWAY_STRIPE) { $tokenBillingSupported = true; - $customerReferenceParam = 'cardReference'; + $customerReferenceParam = 'customerReference'; if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && !Input::get('authorize_ach')) { Session::flash('error', trans('texts.ach_authorization_required')); @@ -507,7 +509,8 @@ class PaymentController extends BaseController } } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { $tokenBillingSupported = true; - $customerReferenceParam = 'paymentMethodToken'; + $sourceReferenceParam = 'paymentMethodToken'; + $customerReferenceParam = 'customerId'; $deviceData = Input::get('device_data'); if (!$deviceData) { @@ -527,12 +530,12 @@ class PaymentController extends BaseController if ($customerReferenceParam) { $details[$customerReferenceParam] = $customerReference; } - $details['token'] = $sourceReference; + $details[$sourceReferenceParam] = $sourceReference; unset($details['card']); } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing') || $paymentType == PAYMENT_TYPE_STRIPE_ACH) { $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */); if ($token) { - $details['token'] = $token; + $details[$sourceReferenceParam] = $token; if ($customerReferenceParam) { $details[$customerReferenceParam] = $customerReference; } @@ -568,7 +571,7 @@ class PaymentController extends BaseController if (!$ref) { $this->error('No-Ref', $response->getMessage(), $accountGateway); - if ($onSite) { + if ($onSite && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) { return Redirect::to('payment/'.$invitationKey) ->withInput(Request::except('cvv')); } else { @@ -596,7 +599,7 @@ class PaymentController extends BaseController $response->redirect(); } else { $this->error('Unknown', $response->getMessage(), $accountGateway); - if ($onSite) { + if ($onSite && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) { return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); } else { return Redirect::to('view/'.$invitationKey); @@ -604,7 +607,7 @@ class PaymentController extends BaseController } } catch (\Exception $e) { $this->error('Uncaught', false, $accountGateway, $e); - if ($onSite) { + if ($onSite && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) { return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); } else { return Redirect::to('view/'.$invitationKey); @@ -759,7 +762,7 @@ class PaymentController extends BaseController 'message' => $data, ], 500); } elseif (!empty($data)) { - return $data; + return response()->json($data); } return response()->json([ diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index 360f71e90fbf..bf12d808fa31 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -16,7 +16,7 @@ use Redirect; use App\Models\Gateway; use App\Models\Invitation; use App\Models\Document; -use App\ModelsPaymentMethod; +use App\Models\PaymentMethod; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\ActivityRepository; @@ -175,8 +175,10 @@ class PublicClientController extends BaseController $code = htmlentities(str_replace(' ', '', strtolower($paymentMethod->payment_type->name))); if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) { - if($paymentMethod->bank_data) { + if ($paymentMethod->bank_data) { $html = '
    ' . htmlentities($paymentMethod->bank_data->name) . '
    '; + } else { + $html = ''.trans('; } } elseif ($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL) { $html = ''.trans('; @@ -887,8 +889,10 @@ class PublicClientController extends BaseController $accountGateway = $account->getGatewayByType($paymentType); $sourceToken = Input::get('sourceToken'); - if (!PaymentController::processPaymentClientDetails($client, $accountGateway, $paymentType)) { - return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv')); + if (($validator = PaymentController::processPaymentClientDetails($client, $accountGateway, $paymentType)) !== true) { + return Redirect::to('client/paymentmethods/add/' . $typeLink) + ->withErrors($validator) + ->withInput(Request::except('cvv')); } if ($sourceToken) { diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 9171c9d22c69..85a10acafbbe 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -167,7 +167,7 @@ class Payment extends EntityModel return ENTITY_PAYMENT; } - public function getBankData() + public function getBankDataAttribute() { if (!$this->routing_number) { return null; diff --git a/app/Models/PaymentMethod.php b/app/Models/PaymentMethod.php index 07c6d2e19e1f..a2f17b722ae6 100644 --- a/app/Models/PaymentMethod.php +++ b/app/Models/PaymentMethod.php @@ -63,7 +63,7 @@ class PaymentMethod extends EntityModel return $this->hasMany('App\Models\Payments'); } - public function getBankData() + public function getBankDataAttribute() { if (!$this->routing_number) { return null; diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 32fafc045c01..d1a0a29a7154 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -131,7 +131,7 @@ class PaymentService extends BaseService $data['cvv'] = $input['cvv']; } - if (isset($input['country_id'])) { + if (isset($input['address1'])) { $country = Country::find($input['country_id']); $data = array_merge($data, [ @@ -222,7 +222,7 @@ class PaymentService extends BaseService public function verifyClientPaymentMethod($client, $publicId, $amount1, $amount2) { - $token = $client->getGatewayToken($accountGateway); + $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); if ($accountGateway->gateway_id != GATEWAY_STRIPE) { return 'Unsupported gateway'; } @@ -238,15 +238,18 @@ class PaymentService extends BaseService 'amounts[]=' . intval($amount1) . '&amounts[]=' . intval($amount2) ); - if (!is_string($result)) { - $paymentMethod->status = PAYMENT_METHOD_STATUS_VERIFIED; - $paymentMethod->save(); - - if (!$paymentMethod->account_gateway_token->default_payment_method_id) { - $paymentMethod->account_gateway_token->default_payment_method_id = $paymentMethod->id; - $paymentMethod->account_gateway_token->save(); - } + if (is_string($result)) { + return $result; } + + $paymentMethod->status = PAYMENT_METHOD_STATUS_VERIFIED; + $paymentMethod->save(); + + if (!$paymentMethod->account_gateway_token->default_payment_method_id) { + $paymentMethod->account_gateway_token->default_payment_method_id = $paymentMethod->id; + $paymentMethod->account_gateway_token->save(); + } + return true; } diff --git a/resources/views/accounts/partials/account_gateway_wepay.blade.php b/resources/views/accounts/partials/account_gateway_wepay.blade.php index 4d6832a8c1f5..803fc37ed103 100644 --- a/resources/views/accounts/partials/account_gateway_wepay.blade.php +++ b/resources/views/accounts/partials/account_gateway_wepay.blade.php @@ -49,7 +49,7 @@ ->large() !!} @if(isset($gateways))

    - {{ trans('texts.use_another_provider') }} + {{ trans('texts.use_another_provider') }} @endif
    diff --git a/resources/views/payments/add_paymentmethod.blade.php b/resources/views/payments/add_paymentmethod.blade.php index e4d5d3c0a590..c5a5b1eab36c 100644 --- a/resources/views/payments/add_paymentmethod.blade.php +++ b/resources/views/payments/add_paymentmethod.blade.php @@ -130,7 +130,6 @@
    {!! Former::text('first_name') ->placeholder(trans('texts.first_name')) - ->autocomplete('given-name') ->label('') !!}
    @@ -230,33 +229,32 @@ ))->inline()->label(trans('texts.account_holder_type')); !!} {!! Former::text('account_holder_name') ->label(trans('texts.account_holder_name')) !!} - {!! Former::select('country_id') - ->label(trans('texts.country_id')) - ->fromQuery($countries, 'name', 'id') - ->addGroupClass('country-select') !!} - {!! Former::select('currency') - ->label(trans('texts.currency_id')) - ->fromQuery($currencies, 'name', 'code') - ->addGroupClass('currency-select') !!} - {!! Former::text('') - ->id('routing_number') - ->label(trans('texts.routing_number')) !!} -
    -
    -
    -
    -
    - {!! Former::text('') - ->id('account_number') - ->label(trans('texts.account_number')) !!} - {!! Former::text('') - ->id('confirm_account_number') - ->label(trans('texts.confirm_account_number')) !!} - {!! Former::checkbox('authorize_ach') - ->text(trans('texts.ach_authorization', ['company'=>$account->getDisplayName()])) - ->label(' ') !!} + {!! Former::select('country_id') + ->label(trans('texts.country_id')) + ->fromQuery($countries, 'name', 'id') + ->addGroupClass('country-select') !!} + {!! Former::select('currency') + ->label(trans('texts.currency_id')) + ->fromQuery($currencies, 'name', 'code') + ->addGroupClass('currency-select') !!} + {!! Former::text('') + ->id('routing_number') + ->label(trans('texts.routing_number')) !!} +
    +
    +
    +
    + {!! Former::text('') + ->id('account_number') + ->label(trans('texts.account_number')) !!} + {!! Former::text('') + ->id('confirm_account_number') + ->label(trans('texts.confirm_account_number')) !!}
    + {!! Former::checkbox('authorize_ach') + ->text(trans('texts.ach_authorization', ['company'=>$account->getDisplayName()])) + ->label(' ') !!}
    {!! Button::success(strtoupper(trans('texts.add_account'))) ->submit() @@ -441,7 +439,7 @@ $('#routing_number, #country').on('change keypress keyup keydown paste', function(){setTimeout(function () { var routingNumber = $('#routing_number').val().replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); - if (routingNumber.length != 9 || $("#country").val() != 'US' || routingNumberCache[routingNumber] === false) { + if (routingNumber.length != 9 || $("#country_id").val() != 840 || routingNumberCache[routingNumber] === false) { $('#bank_name').hide(); } else if (routingNumberCache[routingNumber]) { $('#bank_name').empty().append(routingNumberCache[routingNumber]).show(); diff --git a/resources/views/payments/paymentmethods_list.blade.php b/resources/views/payments/paymentmethods_list.blade.php index 79e8faae5bd1..3b0d7745d608 100644 --- a/resources/views/payments/paymentmethods_list.blade.php +++ b/resources/views/payments/paymentmethods_list.blade.php @@ -60,7 +60,7 @@ @endif @if($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) @if($paymentMethod->bank_data) - {{ $paymentMethod->bank_data }} + {{ $paymentMethod->bank_data->name }} @endif @if($paymentMethod->status == PAYMENT_METHOD_STATUS_NEW) ({{trans('texts.complete_verification')}}) diff --git a/resources/views/payments/tokenization_braintree.blade.php b/resources/views/payments/tokenization_braintree.blade.php index f6f6e5921706..3588ff9a67a0 100644 --- a/resources/views/payments/tokenization_braintree.blade.php +++ b/resources/views/payments/tokenization_braintree.blade.php @@ -1,6 +1,7 @@