From e905b295105c1a326ec375fff54591ee7538411f Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 17 May 2016 14:09:39 -0400 Subject: [PATCH 01/15] Partial WePay ACH support --- .../Controllers/AccountGatewayController.php | 2 +- .../Controllers/PublicClientController.php | 19 +++--- app/Http/routes.php | 1 + app/Models/Account.php | 48 +++++---------- resources/lang/en/texts.php | 2 +- .../views/accounts/account_gateway.blade.php | 58 ++++++++++++------- .../partials/account_gateway_wepay.blade.php | 12 ++++ .../payments/add_paymentmethod.blade.php | 2 +- .../payments/paymentmethods_list.blade.php | 24 ++++++++ 9 files changed, 103 insertions(+), 65 deletions(-) diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 9b02bfe56a24..202e3f35fcf2 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -301,7 +301,7 @@ class AccountGatewayController extends BaseController $config->plaidPublicKey = $oldConfig->plaidPublicKey; } - if ($gatewayId == GATEWAY_STRIPE) { + if ($gatewayId == GATEWAY_STRIPE || $gatewayId == GATEWAY_WEPAY) { $config->enableAch = boolval(Input::get('enable_ach')); } diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index bf12d808fa31..29d67135e953 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -293,6 +293,7 @@ class PublicClientController extends BaseController 'color' => $color, 'account' => $account, 'client' => $client, + 'contact' => $invitation->contact, 'clientFontUrl' => $account->getFontsUrl(), 'gateway' => $account->getTokenGateway(), 'paymentMethods' => $this->paymentService->getClientPaymentMethods($client), @@ -757,6 +758,7 @@ class PublicClientController extends BaseController 'account' => $account, 'color' => $account->primary_color ? $account->primary_color : '#0b4d78', 'client' => $client, + 'contact' => $invitation->contact, 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'paymentMethods' => $paymentMethods, @@ -828,20 +830,21 @@ class PublicClientController extends BaseController $accountGateway = $invoice->client->account->getTokenGateway(); $gateway = $accountGateway->gateway; - if ($token && $paymentType == PAYMENT_TYPE_BRAINTREE_PAYPAL) { - $sourceReference = $this->paymentService->createToken($this->paymentService->createGateway($accountGateway), array('token'=>$token), $accountGateway, $client, $invitation->contact_id); + if ($token) { + if ($paymentType == PAYMENT_TYPE_BRAINTREE_PAYPAL) { + $sourceReference = $this->paymentService->createToken($this->paymentService->createGateway($accountGateway), array('token' => $token), $accountGateway, $client, $invitation->contact_id); - if(empty($sourceReference)) { - $this->paymentMethodError('Token-No-Ref', $this->paymentService->lastError, $accountGateway); - } else { - Session::flash('message', trans('texts.payment_method_added')); + if (empty($sourceReference)) { + $this->paymentMethodError('Token-No-Ref', $this->paymentService->lastError, $accountGateway); + } else { + Session::flash('message', trans('texts.payment_method_added')); + } + return redirect()->to($account->enable_client_portal ? '/client/dashboard' : '/client/paymentmethods/'); } - return redirect()->to($account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/'); } $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); - $data = [ 'showBreadcrumbs' => false, 'client' => $client, diff --git a/app/Http/routes.php b/app/Http/routes.php index 46fe5cea836b..3b210671725e 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -665,6 +665,7 @@ if (!defined('CONTACT_EMAIL')) { define('PAYMENT_TYPE_STRIPE_CREDIT_CARD', 'PAYMENT_TYPE_STRIPE_CREDIT_CARD'); define('PAYMENT_TYPE_STRIPE_ACH', 'PAYMENT_TYPE_STRIPE_ACH'); define('PAYMENT_TYPE_BRAINTREE_PAYPAL', 'PAYMENT_TYPE_BRAINTREE_PAYPAL'); + define('PAYMENT_TYPE_WEPAY_ACH', 'PAYMENT_TYPE_WEPAY_ACH'); define('PAYMENT_TYPE_CREDIT_CARD', 'PAYMENT_TYPE_CREDIT_CARD'); define('PAYMENT_TYPE_DIRECT_DEBIT', 'PAYMENT_TYPE_DIRECT_DEBIT'); define('PAYMENT_TYPE_BITCOIN', 'PAYMENT_TYPE_BITCOIN'); diff --git a/app/Models/Account.php b/app/Models/Account.php index c489d21b180b..de1328862b71 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -379,26 +379,27 @@ class Account extends Eloquent return $format; } - public function getGatewayByType($type = PAYMENT_TYPE_ANY) + public function getGatewayByType($type = PAYMENT_TYPE_ANY, $exceptFor = null) { if ($type == PAYMENT_TYPE_STRIPE_ACH || $type == PAYMENT_TYPE_STRIPE_CREDIT_CARD) { $type = PAYMENT_TYPE_STRIPE; } - if ($type == PAYMENT_TYPE_BRAINTREE_PAYPAL) { - $gateway = $this->getGatewayConfig(GATEWAY_BRAINTREE); - - if (!$gateway || !$gateway->getPayPalEnabled()){ - return false; - } - return $gateway; - } - foreach ($this->account_gateways as $gateway) { + if ($exceptFor && ($gateway->id == $exceptFor->id)) { + continue; + } + if (!$type || $type == PAYMENT_TYPE_ANY) { return $gateway; } elseif ($gateway->isPaymentType($type)) { return $gateway; + } elseif ($type == PAYMENT_TYPE_CREDIT_CARD && $gateway->isPaymentType(PAYMENT_TYPE_STRIPE)) { + return $gateway; + } elseif ($type == PAYMENT_TYPE_DIRECT_DEBIT && $gateway->getAchEnabled()) { + return $gateway; + } elseif ($type == PAYMENT_TYPE_PAYPAL && $gateway->getPayPalEnabled()) { + return $gateway; } } @@ -1414,32 +1415,13 @@ class Account extends Eloquent } public function canAddGateway($type){ + if ($type == PAYMENT_TYPE_STRIPE) { + $type == PAYMENT_TYPE_CREDIT_CARD; + } + 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; } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index e766e13afbc0..1edde462a025 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1219,7 +1219,7 @@ $LANG = array( 'ach' => 'ACH', 'enable_ach' => 'Enable ACH', 'stripe_ach_help' => 'ACH support must also be enabled at Stripe.', - 'stripe_ach_disabled' => 'Another gateway is already configured for direct debit.', + 'ach_disabled' => 'Another gateway is already configured for direct debit.', 'plaid' => 'Plaid', 'client_id' => 'Client Id', diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index 155ef6579f69..f019edce93dc 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -113,7 +113,7 @@ @endif @if ($gateway->id == GATEWAY_BRAINTREE) - @if ($account->getGatewayByType(PAYMENT_TYPE_PAYPAL)) + @if ($account->getGatewayByType(PAYMENT_TYPE_PAYPAL, isset($accountGateway)?$accountGateway:null)) {!! Former::checkbox('enable_paypal') ->label(trans('texts.paypal')) ->text(trans('texts.braintree_enable_paypal')) @@ -149,33 +149,49 @@ ->class('creditcard-types') ->addGroupClass('gateway-option') !!} -
- @if ($account->getGatewayByType(PAYMENT_TYPE_DIRECT_DEBIT)) + @if(isset($accountGateway) && $accountGateway->gateway_id == GATEWAY_WEPAY) + @if ($account->getGatewayByType(PAYMENT_TYPE_DIRECT_DEBIT, $accountGateway)) {!! Former::checkbox('enable_ach') ->label(trans('texts.ach')) ->text(trans('texts.enable_ach')) ->value(null) ->disabled(true) - ->help(trans('texts.stripe_ach_disabled')) !!} + ->help(trans('texts.ach_disabled')) !!} @else - {!! Former::checkbox('enable_ach') - ->label(trans('texts.ach')) - ->text(trans('texts.enable_ach')) - ->help(trans('texts.stripe_ach_help')) !!} -
-
-
-

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

-
{{trans('texts.plaid_optional')}}
-
-
- {!! Former::text('plaid_client_id')->label(trans('texts.client_id')) !!} - {!! Former::text('plaid_secret')->label(trans('texts.secret')) !!} - {!! Former::text('plaid_public_key')->label(trans('texts.public_key')) - ->help(trans('texts.plaid_environment_help')) !!} -
+ {!! Former::checkbox('enable_ach') + ->label(trans('texts.ach')) + ->text(trans('texts.enable_ach')) !!} @endif -
+ + @elseif(!isset($accountGateway) || $accountGateway->gateway_id == GATEWAY_STRIPE) +
+ @if ($account->getGatewayByType(PAYMENT_TYPE_DIRECT_DEBIT, isset($accountGateway)?$accountGateway:null)) + {!! Former::checkbox('enable_ach') + ->label(trans('texts.ach')) + ->text(trans('texts.enable_ach')) + ->value(null) + ->disabled(true) + ->help(trans('texts.ach_disabled')) !!} + @else + {!! Former::checkbox('enable_ach') + ->label(trans('texts.ach')) + ->text(trans('texts.enable_ach')) + ->help(trans('texts.stripe_ach_help')) !!} +
+
+
+

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

+
{{trans('texts.plaid_optional')}}
+
+
+ {!! Former::text('plaid_client_id')->label(trans('texts.client_id')) !!} + {!! Former::text('plaid_secret')->label(trans('texts.secret')) !!} + {!! Former::text('plaid_public_key')->label(trans('texts.public_key')) + ->help(trans('texts.plaid_environment_help')) !!} +
+ @endif +
+ @endif diff --git a/resources/views/accounts/partials/account_gateway_wepay.blade.php b/resources/views/accounts/partials/account_gateway_wepay.blade.php index a5ccd7f01abc..0ccce3deded0 100644 --- a/resources/views/accounts/partials/account_gateway_wepay.blade.php +++ b/resources/views/accounts/partials/account_gateway_wepay.blade.php @@ -53,6 +53,18 @@ ->label('Accepted Credit Cards') ->checkboxes($creditCardTypes) ->class('creditcard-types') !!} + @if ($account->getGatewayByType(PAYMENT_TYPE_DIRECT_DEBIT)) + {!! Former::checkbox('enable_ach') + ->label(trans('texts.ach')) + ->text(trans('texts.enable_ach')) + ->value(null) + ->disabled(true) + ->help(trans('texts.ach_disabled')) !!} + @else + {!! Former::checkbox('enable_ach') + ->label(trans('texts.ach')) + ->text(trans('texts.enable_ach')) !!} + @endif {!! Former::checkbox('tos_agree')->label(' ')->text(trans('texts.wepay_tos_agree', ['link'=>''.trans('texts.wepay_tos_link_text').''] ))->value('true') !!} diff --git a/resources/views/payments/add_paymentmethod.blade.php b/resources/views/payments/add_paymentmethod.blade.php index c5a5b1eab36c..f0ecfbbed58d 100644 --- a/resources/views/payments/add_paymentmethod.blade.php +++ b/resources/views/payments/add_paymentmethod.blade.php @@ -124,7 +124,7 @@

 
 

- @if($paymentType != PAYMENT_TYPE_STRIPE_ACH && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) + @if($paymentType != PAYMENT_TYPE_STRIPE_ACH && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL && $paymentType != PAYMENT_TYPE_WEPAY_ACH)

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

diff --git a/resources/views/payments/paymentmethods_list.blade.php b/resources/views/payments/paymentmethods_list.blade.php index 6c068b5f915a..77789b8f5c83 100644 --- a/resources/views/payments/paymentmethods_list.blade.php +++ b/resources/views/payments/paymentmethods_list.blade.php @@ -48,6 +48,24 @@ }) }); +@elseif($gateway->gateway_id == GATEWAY_WEPAY && $gateway->getAchEnabled()) + + + @endif @if(!empty($paymentMethods)) @foreach ($paymentMethods as $paymentMethod) @@ -88,8 +106,14 @@ ->asLinkTo(URL::to('/client/paymentmethods/add/'.($gateway->getPaymentType() == PAYMENT_TYPE_STRIPE ? 'stripe_credit_card' : 'credit_card'))) !!} @if($gateway->getACHEnabled())   + @if($gateway->gateway_id == GATEWAY_STRIPE) {!! Button::success(strtoupper(trans('texts.add_bank_account'))) ->asLinkTo(URL::to('/client/paymentmethods/add/stripe_ach')) !!} + @elseif($gateway->gateway_id == GATEWAY_WEPAY) + {!! Button::success(strtoupper(trans('texts.add_bank_account'))) + ->withAttributes(['id'=>'add-ach']) + ->asLinkTo(URL::to('/client/paymentmethods/add/wepay_ach')) !!} + @endif @endif @if($gateway->getPayPalEnabled())   From 2aed1354e9afb5a3f72606a28b23bf8ccb83f80c Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 24 May 2016 11:47:03 -0400 Subject: [PATCH 02/15] Update wepay driver version --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index d5513297fd3f..a2888a4263ac 100644 --- a/composer.lock +++ b/composer.lock @@ -977,12 +977,12 @@ "source": { "type": "git", "url": "https://github.com/sometechie/omnipay-wepay.git", - "reference": "2964730018e9fccf0bb0e449065940cad3ca6719" + "reference": "fb0e6c9824d15ba74cd6f75421b318e87a9e1822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sometechie/omnipay-wepay/zipball/2964730018e9fccf0bb0e449065940cad3ca6719", - "reference": "2964730018e9fccf0bb0e449065940cad3ca6719", + "url": "https://api.github.com/repos/sometechie/omnipay-wepay/zipball/fb0e6c9824d15ba74cd6f75421b318e87a9e1822", + "reference": "fb0e6c9824d15ba74cd6f75421b318e87a9e1822", "shasum": "" }, "require": { @@ -1014,7 +1014,7 @@ "support": { "source": "https://github.com/sometechie/omnipay-wepay/tree/additional-calls" }, - "time": "2016-05-18 18:12:17" + "time": "2016-05-23 15:01:20" }, { "name": "container-interop/container-interop", From 5958cd2c5ea1689f7d9e1d601138105b5b096b5f Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 24 May 2016 13:04:55 -0400 Subject: [PATCH 03/15] Don't request update URI from WePay --- app/Ninja/Datatables/AccountGatewayDatatable.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/Ninja/Datatables/AccountGatewayDatatable.php b/app/Ninja/Datatables/AccountGatewayDatatable.php index ad16fc39878a..fe6a8df0df7a 100644 --- a/app/Ninja/Datatables/AccountGatewayDatatable.php +++ b/app/Ninja/Datatables/AccountGatewayDatatable.php @@ -29,18 +29,13 @@ class AccountGatewayDatatable extends EntityDatatable $wepayState = isset($config->state)?$config->state:null; $linkText = $model->name; $url = $endpoint.'account/'.$wepayAccountId; - $wepay = \Utils::setupWepay($accountGateway); $html = link_to($url, $linkText, array('target'=>'_blank'))->toHtml(); try { if ($wepayState == 'action_required') { - $updateUri = $wepay->request('/account/get_update_uri', array( - 'account_id' => $wepayAccountId, - 'redirect_uri' => URL::to('gateways'), - )); - + $updateUri = $endpoint.'api/account_update/'.$wepayAccountId.'?redirect_uri='.urlencode(URL::to('gateways')); $linkText .= ' ('.trans('texts.action_required').')'; - $url = $updateUri->uri; + $url = $updateUri; $html = "{$linkText}"; $model->setupUrl = $url; } elseif ($wepayState == 'pending') { From f8835268b42ab37691464927a5e4fa50f918b8af Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 24 May 2016 14:16:42 -0400 Subject: [PATCH 04/15] Update DB structure --- app/Models/Contact.php | 9 +++ app/Models/Payment.php | 20 +++++++ app/Models/PaymentMethod.php | 20 +++++++ app/Ninja/Repositories/ContactRepository.php | 1 + .../2016_05_24_164847_wepay_ach.php | 57 +++++++++++++++++++ 5 files changed, 107 insertions(+) create mode 100644 database/migrations/2016_05_24_164847_wepay_ach.php diff --git a/app/Models/Contact.php b/app/Models/Contact.php index 9c86c4ce5b84..e0e967dcde77 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -60,6 +60,15 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa } } + public function getContactKeyAttribute($contact_key) + { + if (empty($contact_key) && $this->id) { + $this->contact_key = $contact_key = str_random(RANDOM_KEY_LENGTH); + static::where('id', $this->id)->update(array('contact_key' => $contact_key)); + } + return $contact_key; + } + public function getFullName() { if ($this->first_name || $this->last_name) { diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 59f1e1e9823e..677c5c0ea0c1 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -180,10 +180,30 @@ class Payment extends EntityModel return PaymentMethod::lookupBankData($this->routing_number); } + public function getBankNameAttribute($bank_name) + { + if ($bank_name) { + return $bank_name; + } + $bankData = $this->bank_data; + + return $bankData?$bankData->name:null; + } + public function getLast4Attribute($value) { return $value ? str_pad($value, 4, '0', STR_PAD_LEFT) : null; } + + public function getIpAddressAttribute($value) + { + return !$value?$value:inet_ntop($value); + } + + public function setIpAddressAttribute($value) + { + $this->attributes['ip_address'] = inet_pton($value); + } } Payment::creating(function ($payment) { diff --git a/app/Models/PaymentMethod.php b/app/Models/PaymentMethod.php index a2f17b722ae6..7110403ab828 100644 --- a/app/Models/PaymentMethod.php +++ b/app/Models/PaymentMethod.php @@ -71,6 +71,16 @@ class PaymentMethod extends EntityModel return static::lookupBankData($this->routing_number); } + public function getBankNameAttribute($bank_name) + { + if ($bank_name) { + return $bank_name; + } + $bankData = $this->bank_data; + + return $bankData?$bankData->name:null; + } + public function getLast4Attribute($value) { return $value ? str_pad($value, 4, '0', STR_PAD_LEFT) : null; @@ -148,6 +158,16 @@ class PaymentMethod extends EntityModel return null; } } + + public function getIpAddressAttribute($value) + { + return !$value?$value:inet_ntop($value); + } + + public function setIpAddressAttribute($value) + { + $this->attributes['ip_address'] = inet_pton($value); + } } PaymentMethod::deleting(function($paymentMethod) { diff --git a/app/Ninja/Repositories/ContactRepository.php b/app/Ninja/Repositories/ContactRepository.php index 49b73e91a664..50d15af2b21e 100644 --- a/app/Ninja/Repositories/ContactRepository.php +++ b/app/Ninja/Repositories/ContactRepository.php @@ -14,6 +14,7 @@ class ContactRepository extends BaseRepository $contact->send_invoice = true; $contact->client_id = $data['client_id']; $contact->is_primary = Contact::scope()->where('client_id', '=', $contact->client_id)->count() == 0; + $contact->contact_key = str_random(RANDOM_KEY_LENGTH); } else { $contact = Contact::scope($publicId)->firstOrFail(); } diff --git a/database/migrations/2016_05_24_164847_wepay_ach.php b/database/migrations/2016_05_24_164847_wepay_ach.php new file mode 100644 index 000000000000..b8e6ca752a0e --- /dev/null +++ b/database/migrations/2016_05_24_164847_wepay_ach.php @@ -0,0 +1,57 @@ +string('contact_key')->index()->default(null); + }); + + Schema::table('payment_methods', function($table) + { + $table->string('bank_name')->nullable(); + }); + + Schema::table('payments', function($table) + { + $table->string('bank_name')->nullable(); + }); + + Schema::table('accounts', function($table) + { + $table->boolean('auto_bill_on_due_date')->default(false); + }); + + DB::statement('ALTER TABLE `payments` ADD `ip_address` VARBINARY(16)'); + DB::statement('ALTER TABLE `payment_methods` ADD `ip_address` VARBINARY(16)'); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function(Blueprint $table) { + $table->dropColumn('contact_key'); + }); + + Schema::table('payments', function($table) { + $table->dropColumn('ip_address'); + }); + + Schema::table('payment_methods', function($table) { + $table->dropColumn('ip_address'); + }); + } +} From 724f5738aa7138cef7466f8d05bde9ff994d2916 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 24 May 2016 17:02:28 -0400 Subject: [PATCH 05/15] Add contact_key --- .../Controllers/ClientAuth/AuthController.php | 34 ++-- .../ClientAuth/PasswordController.php | 76 +++++---- .../Controllers/ClientPortalController.php | 146 +++++++++--------- app/Http/Middleware/Authenticate.php | 59 ++++--- app/Http/routes.php | 2 + app/Models/Contact.php | 5 + resources/lang/en/texts.php | 4 + resources/views/clientauth/login.blade.php | 2 +- resources/views/clientauth/password.blade.php | 8 + resources/views/clientauth/reset.blade.php | 10 +- .../views/clientauth/sessionexpired.blade.php | 79 ++++++++++ resources/views/clients/show.blade.php | 3 +- .../views/emails/client_password.blade.php | 2 +- 13 files changed, 283 insertions(+), 147 deletions(-) create mode 100644 resources/views/clientauth/sessionexpired.blade.php diff --git a/app/Http/Controllers/ClientAuth/AuthController.php b/app/Http/Controllers/ClientAuth/AuthController.php index e3951b5464b8..24fc85222872 100644 --- a/app/Http/Controllers/ClientAuth/AuthController.php +++ b/app/Http/Controllers/ClientAuth/AuthController.php @@ -10,7 +10,7 @@ use App\Events\UserLoggedIn; use App\Http\Controllers\Controller; use App\Ninja\Repositories\AccountRepository; use App\Services\AuthService; -use App\Models\Invitation; +use App\Models\Contact; use Illuminate\Foundation\Auth\AuthenticatesUsers; class AuthController extends Controller { @@ -22,16 +22,13 @@ class AuthController extends Controller { public function showLoginForm() { - $data = array( - ); + $data = array(); - $invitation_key = session('invitation_key'); - if($invitation_key){ - $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); - if ($invitation && !$invitation->is_deleted) { - $invoice = $invitation->invoice; - $client = $invoice->client; - $account = $client->account; + $contactKey = session('contact_key'); + if($contactKey){ + $contact = Contact::where('contact_key', '=', $contactKey)->first(); + if ($contact && !$contact->is_deleted) { + $account = $contact->account; $data['account'] = $account; $data['clientFontUrl'] = $account->getFontsUrl(); @@ -51,12 +48,12 @@ class AuthController extends Controller { { $credentials = $request->only('password'); $credentials['id'] = null; - - $invitation_key = session('invitation_key'); - if($invitation_key){ - $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); - if ($invitation && !$invitation->is_deleted) { - $credentials['id'] = $invitation->contact_id; + + $contactKey = session('contact_key'); + if($contactKey){ + $contact = Contact::where('contact_key', '=', $contactKey)->first(); + if ($contact && !$contact->is_deleted) { + $credentials['id'] = $contact->id; } } @@ -75,4 +72,9 @@ class AuthController extends Controller { 'password' => 'required', ]); } + + public function getSessionExpired() + { + return view('clientauth.sessionexpired'); + } } diff --git a/app/Http/Controllers/ClientAuth/PasswordController.php b/app/Http/Controllers/ClientAuth/PasswordController.php index 822764315a0e..af3d97029edf 100644 --- a/app/Http/Controllers/ClientAuth/PasswordController.php +++ b/app/Http/Controllers/ClientAuth/PasswordController.php @@ -6,6 +6,7 @@ use Illuminate\Foundation\Auth\ResetsPasswords; use Illuminate\Http\Request; use Illuminate\Mail\Message; use Illuminate\Support\Facades\Password; +use App\Models\Contact; use App\Models\Invitation; @@ -42,16 +43,16 @@ class PasswordController extends Controller { public function showLinkRequestForm() { $data = array(); - $invitation_key = session('invitation_key'); - if($invitation_key){ - $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); - if ($invitation && !$invitation->is_deleted) { - $invoice = $invitation->invoice; - $client = $invoice->client; - $account = $client->account; + $contactKey = session('contact_key'); + if($contactKey){ + $contact = Contact::where('contact_key', '=', $contactKey)->first(); + if ($contact && !$contact->is_deleted) { + $account = $contact->account; $data['account'] = $account; $data['clientFontUrl'] = $account->getFontsUrl(); } + } else { + return \Redirect::to('/client/sessionexpired'); } return view('clientauth.password')->with($data); @@ -67,16 +68,16 @@ class PasswordController extends Controller { { $broker = $this->getBroker(); - $contact_id = null; - $invitation_key = session('invitation_key'); - if($invitation_key){ - $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); - if ($invitation && !$invitation->is_deleted) { - $contact_id = $invitation->contact_id; + $contactId = null; + $contactKey = session('contact_key'); + if($contactKey){ + $contact = Contact::where('contact_key', '=', $contactKey)->first(); + if ($contact && !$contact->is_deleted) { + $contactId = $contact->id; } } - $response = Password::broker($broker)->sendResetLink(array('id'=>$contact_id), function (Message $message) { + $response = Password::broker($broker)->sendResetLink(array('id'=>$contactId), function (Message $message) { $message->subject($this->getEmailSubject()); }); @@ -96,27 +97,36 @@ class PasswordController extends Controller { * If no token is present, display the link request form. * * @param \Illuminate\Http\Request $request - * @param string|null $invitation_key + * @param string|null $key * @param string|null $token * @return \Illuminate\Http\Response */ - public function showResetForm(Request $request, $invitation_key = null, $token = null) + public function showResetForm(Request $request, $key = null, $token = null) { if (is_null($token)) { return $this->getEmail(); } - $data = compact('token', 'invitation_key'); - $invitation_key = session('invitation_key'); - if($invitation_key){ - $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); - if ($invitation && !$invitation->is_deleted) { - $invoice = $invitation->invoice; - $client = $invoice->client; - $account = $client->account; + $data = compact('token'); + if($key) { + $contact = Contact::where('contact_key', '=', $key)->first(); + if ($contact && !$contact->is_deleted) { + $account = $contact->account; + $data['contact_key'] = $contact->contact_key; + } else { + // Maybe it's an invitation key + $invitation = Invitation::where('invitation_key', '=', $key)->first(); + if ($invitation && !$invitation->is_deleted) { + $account = $invitation->account; + $data['contact_key'] = $invitation->contact->contact_key; + } + } + if (!empty($account)) { $data['account'] = $account; $data['clientFontUrl'] = $account->getFontsUrl(); + } else { + return \Redirect::to('/client/sessionexpired'); } } @@ -131,13 +141,13 @@ class PasswordController extends Controller { * If no token is present, display the link request form. * * @param \Illuminate\Http\Request $request - * @param string|null $invitation_key + * @param string|null $key * @param string|null $token * @return \Illuminate\Http\Response */ - public function getReset(Request $request, $invitation_key = null, $token = null) + public function getReset(Request $request, $key = null, $token = null) { - return $this->showResetForm($request, $invitation_key, $token); + return $this->showResetForm($request, $key, $token); } /** @@ -155,12 +165,12 @@ class PasswordController extends Controller { ); $credentials['id'] = null; - - $invitation_key = $request->input('invitation_key'); - if($invitation_key){ - $invitation = Invitation::where('invitation_key', '=', $invitation_key)->first(); - if ($invitation && !$invitation->is_deleted) { - $credentials['id'] = $invitation->contact_id; + + $contactKey = session('contact_key'); + if($contactKey){ + $contact = Contact::where('contact_key', '=', $contactKey)->first(); + if ($contact && !$contact->is_deleted) { + $credentials['id'] = $contact->id; } } diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index 56dd183b19fe..578b960b1160 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -17,6 +17,7 @@ use App\Models\Gateway; use App\Models\Invitation; use App\Models\Document; use App\Models\PaymentMethod; +use App\Models\Contact; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\ActivityRepository; @@ -70,7 +71,7 @@ class ClientPortalController extends BaseController } Session::put($invitationKey, true); // track this invitation has been seen - Session::put('invitation_key', $invitationKey); // track current invitation + Session::put('contact_key', $invitation->contact->contact_key);// track current contact $account->loadLocalizationSettings($client); @@ -161,6 +162,18 @@ class ClientPortalController extends BaseController return View::make('invoices.view', $data); } + + public function contactIndex($contactKey) { + if (!$contact = Contact::where('contact_key', '=', $contactKey)->first()) { + return $this->returnError(); + } + + $client = $contact->client; + + Session::put('contact_key', $contactKey);// track current contact + + return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/invoices/'); + } private function getPaymentTypes($client, $invitation) { @@ -277,13 +290,12 @@ class ClientPortalController extends BaseController public function dashboard() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $account = $invitation->account; - $invoice = $invitation->invoice; - $client = $invoice->client; + $client = $contact->client; + $account = $client->account; $color = $account->primary_color ? $account->primary_color : '#0b4d78'; if (!$account->enable_client_portal || !$account->enable_client_portal_dashboard) { @@ -310,12 +322,13 @@ class ClientPortalController extends BaseController public function activityDatatable() { - if (!$invitation = $this->getInvitation()) { - return false; + if (!$contact = $this->getContact()) { + return $this->returnError(); } - $invoice = $invitation->invoice; - $query = $this->activityRepo->findByClientId($invoice->client_id); + $client = $contact->client; + + $query = $this->activityRepo->findByClientId($client->id); $query->where('activities.adjustment', '!=', 0); return Datatable::query($query) @@ -341,11 +354,11 @@ class ClientPortalController extends BaseController public function recurringInvoiceIndex() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $account = $invitation->account; + $account = $contact->account; if (!$account->enable_client_portal) { return $this->returnError(); @@ -356,7 +369,7 @@ class ClientPortalController extends BaseController $data = [ 'color' => $color, 'account' => $account, - 'client' => $invitation->invoice->client, + 'client' => $contact->client, 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.recurring_invoices'), 'entityType' => ENTITY_RECURRING_INVOICE, @@ -368,11 +381,11 @@ class ClientPortalController extends BaseController public function invoiceIndex() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $account = $invitation->account; + $account = $contact->account; if (!$account->enable_client_portal) { return $this->returnError(); @@ -383,7 +396,7 @@ class ClientPortalController extends BaseController $data = [ 'color' => $color, 'account' => $account, - 'client' => $invitation->invoice->client, + 'client' => $contact->client, 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.invoices'), 'entityType' => ENTITY_INVOICE, @@ -395,29 +408,30 @@ class ClientPortalController extends BaseController public function invoiceDatatable() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return ''; } - return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_INVOICE, Input::get('sSearch')); + return $this->invoiceRepo->getClientDatatable($contact->id, ENTITY_INVOICE, Input::get('sSearch')); } public function recurringInvoiceDatatable() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return ''; } - return $this->invoiceRepo->getClientRecurringDatatable($invitation->contact_id); + return $this->invoiceRepo->getClientRecurringDatatable($contact->id); } public function paymentIndex() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $account = $invitation->account; + + $account = $contact->account; if (!$account->enable_client_portal) { return $this->returnError(); @@ -438,10 +452,10 @@ class ClientPortalController extends BaseController public function paymentDatatable() { - if (!$invitation = $this->getInvitation()) { - return false; + if (!$contact = $this->getContact()) { + return $this->returnError(); } - $payments = $this->paymentRepo->findForContact($invitation->contact->id, Input::get('sSearch')); + $payments = $this->paymentRepo->findForContact($contact->id, Input::get('sSearch')); return Datatable::query($payments) ->addColumn('invoice_number', function ($model) { return $model->invitation_key ? link_to('/view/'.$model->invitation_key, $model->invoice_number)->toHtml() : $model->invoice_number; }) @@ -502,11 +516,11 @@ class ClientPortalController extends BaseController public function quoteIndex() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $account = $invitation->account; + $account = $contact->account; if (!$account->enable_client_portal) { return $this->returnError(); @@ -528,20 +542,20 @@ class ClientPortalController extends BaseController public function quoteDatatable() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return false; } - return $this->invoiceRepo->getClientDatatable($invitation->contact_id, ENTITY_QUOTE, Input::get('sSearch')); + return $this->invoiceRepo->getClientDatatable($contact->id, ENTITY_QUOTE, Input::get('sSearch')); } public function documentIndex() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $account = $invitation->account; + $account = $contact->account; if (!$account->enable_client_portal) { return $this->returnError(); @@ -563,11 +577,11 @@ class ClientPortalController extends BaseController public function documentDatatable() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return false; } - return $this->documentRepo->getClientDatatable($invitation->contact_id, ENTITY_DOCUMENT, Input::get('sSearch')); + return $this->documentRepo->getClientDatatable($contact->id, ENTITY_DOCUMENT, Input::get('sSearch')); } private function returnError($error = false) @@ -578,36 +592,28 @@ class ClientPortalController extends BaseController ]); } - private function getInvitation() - { - $invitationKey = session('invitation_key'); + private function getContact() { + $contactKey = session('contact_key'); - if (!$invitationKey) { + if (!$contactKey) { return false; } - $invitation = Invitation::where('invitation_key', '=', $invitationKey)->first(); + $contact = Contact::where('contact_key', '=', $contactKey)->first(); - if (!$invitation || $invitation->is_deleted) { + if (!$contact || $contact->is_deleted) { return false; } - $invoice = $invitation->invoice; - - if (!$invoice || $invoice->is_deleted) { - return false; - } - - return $invitation; + return $contact; } public function getDocumentVFSJS($publicId, $name){ - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $clientId = $invitation->invoice->client_id; - $document = Document::scope($publicId, $invitation->account_id)->first(); + $document = Document::scope($publicId, $contact->account_id)->first(); if(!$document->isPDFEmbeddable()){ @@ -615,9 +621,9 @@ class ClientPortalController extends BaseController } $authorized = false; - if($document->expense && $document->expense->client_id == $invitation->invoice->client_id){ + if($document->expense && $document->expense->client_id == $contact->client_id){ $authorized = true; - } else if($document->invoice && $document->invoice->client_id == $invitation->invoice->client_id){ + } else if($document->invoice && $document->invoice->client_id ==$contact->client_id){ $authorized = true; } @@ -692,7 +698,7 @@ class ClientPortalController extends BaseController return $this->returnError(); } - Session::put('invitation_key', $invitationKey); // track current invitation + Session::put('contact_key', $invitation->contact->contact_key);// track current contact $invoice = $invitation->invoice; @@ -725,7 +731,7 @@ class ClientPortalController extends BaseController return $this->returnError(); } - Session::put('invitation_key', $invitationKey); // track current invitation + Session::put('contact_key', $invitation->contact->contact_key);// track current contact $clientId = $invitation->invoice->client_id; $document = Document::scope($publicId, $invitation->account_id)->firstOrFail(); @@ -746,11 +752,11 @@ class ClientPortalController extends BaseController public function paymentMethods() { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $client = $invitation->invoice->client; + $client = $contact->client; $account = $client->account; $paymentMethods = $this->paymentService->getClientPaymentMethods($client); @@ -780,11 +786,11 @@ class ClientPortalController extends BaseController $amount1 = Input::get('verification1'); $amount2 = Input::get('verification2'); - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $client = $invitation->invoice->client; + $client = $contact->client; $result = $this->paymentService->verifyClientPaymentMethod($client, $publicId, $amount1, $amount2); if (is_string($result)) { @@ -798,11 +804,11 @@ class ClientPortalController extends BaseController public function removePaymentMethod($publicId) { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $client = $invitation->invoice->client; + $client = $contact->client; $result = $this->paymentService->removeClientPaymentMethod($client, $publicId); if (is_string($result)) { @@ -816,17 +822,16 @@ class ClientPortalController extends BaseController public function addPaymentMethod($paymentType, $token=false) { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } - $invoice = $invitation->invoice; - $client = $invitation->invoice->client; + $client = $contact->client; $account = $client->account; $typeLink = $paymentType; $paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType); - $accountGateway = $invoice->client->account->getTokenGateway(); + $accountGateway = $client->account->getTokenGateway(); $gateway = $accountGateway->gateway; if ($token && $paymentType == PAYMENT_TYPE_BRAINTREE_PAYPAL) { @@ -846,7 +851,7 @@ class ClientPortalController extends BaseController $data = [ 'showBreadcrumbs' => false, 'client' => $client, - 'contact' => $invitation->contact, + 'contact' => $contact, 'gateway' => $gateway, 'accountGateway' => $accountGateway, 'acceptedCreditCardTypes' => $acceptedCreditCardTypes, @@ -878,13 +883,14 @@ class ClientPortalController extends BaseController public function postAddPaymentMethod($paymentType) { - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } + $client = $contact->client; + $typeLink = $paymentType; $paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType); - $client = $invitation->invoice->client; $account = $client->account; $accountGateway = $account->getGatewayByType($paymentType); @@ -929,12 +935,13 @@ class ClientPortalController extends BaseController } public function setDefaultPaymentMethod(){ - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } + $client = $contact->client; + $validator = Validator::make(Input::all(), array('source' => 'required')); - $client = $invitation->invoice->client; if ($validator->fails()) { return Redirect::to($client->account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/'); } @@ -963,12 +970,13 @@ class ClientPortalController extends BaseController } public function setAutoBill(){ - if (!$invitation = $this->getInvitation()) { + if (!$contact = $this->getContact()) { return $this->returnError(); } + $client = $contact->client; + $validator = Validator::make(Input::all(), array('public_id' => 'required')); - $client = $invitation->invoice->client; if ($validator->fails()) { return Redirect::to('client/invoices/recurring'); diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 81fde62439e2..ff10d0d06156 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -20,37 +20,50 @@ class Authenticate { $authenticated = Auth::guard($guard)->check(); if($guard == 'client' && !empty($request->invitation_key)){ - $old_key = session('invitation_key'); - if($old_key && $old_key != $request->invitation_key){ - if($this->getInvitationContactId($old_key) != $this->getInvitationContactId($request->invitation_key)){ + $contact_key = session('contact_key'); + if($contact_key) { + $contact = $this->getContact($contact_key); + $invitation = $this->getInvitation($request->invitation_key); + + if ($contact->id != $invitation->contact_id) { // This is a different client; reauthenticate $authenticated = false; Auth::guard($guard)->logout(); } - } - Session::put('invitation_key', $request->invitation_key); + Session::put('contact_key', $contact->contact_key); + } } if($guard=='client'){ - $invitation_key = session('invitation_key'); - $account_id = $this->getInvitationAccountId($invitation_key); + if (!empty($request->contact_key)) { + $contact_key = $request->contact_key; + Session::put('contact_key', $contact_key); + } else { + $contact_key = session('contact_key'); + } + + if ($contact_key) { + $contact = $this->getContact($contact_key); + $account = $contact->account; + } elseif (!empty($request->invitation_key)) { + $invitation = $this->getInvitation($request->invitation_key); + $account = $invitation->account; + } else { + return \Redirect::to('client/sessionexpired'); + } - if(Auth::guard('user')->check() && Auth::user('user')->account_id === $account_id){ + if(Auth::guard('user')->check() && Auth::user('user')->account_id === $account->id){ // This is an admin; let them pretend to be a client $authenticated = true; } // Does this account require portal passwords? - $account = Account::whereId($account_id)->first(); if($account && (!$account->enable_portal_password || !$account->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD))){ $authenticated = true; } - if(!$authenticated){ - $contact = Contact::whereId($this->getInvitationContactId($invitation_key))->first(); - if($contact && !$contact->password){ - $authenticated = true; - } + if(!$authenticated && $contact && !$contact->password){ + $authenticated = true; } } @@ -76,16 +89,12 @@ class Authenticate { } else return null; } - - protected function getInvitationContactId($key){ - $invitation = $this->getInvitation($key); - - return $invitation?$invitation->contact_id:null; - } - - protected function getInvitationAccountId($key){ - $invitation = $this->getInvitation($key); - - return $invitation?$invitation->account_id:null; + + protected function getContact($key){ + $contact = Contact::withTrashed()->where('contact_key', '=', $key)->first(); + if ($contact && !$contact->is_deleted) { + return $contact; + } + else return null; } } diff --git a/app/Http/routes.php b/app/Http/routes.php index 84ec2552bb30..4611bb1abdd5 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -57,6 +57,7 @@ Route::group(['middleware' => 'auth:client'], function() { Route::get('client/documents', 'ClientPortalController@documentIndex'); Route::get('client/payments', 'ClientPortalController@paymentIndex'); Route::get('client/dashboard', 'ClientPortalController@dashboard'); + Route::get('client/dashboard/{contact_key}', 'ClientPortalController@contactIndex'); Route::get('client/documents/js/{documents}/{filename}', 'ClientPortalController@getDocumentVFSJS'); Route::get('client/documents/{invitation_key}/{documents}/{filename?}', 'ClientPortalController@getDocument'); Route::get('client/documents/{invitation_key}/{filename?}', 'ClientPortalController@getInvoiceDocumentsZip'); @@ -101,6 +102,7 @@ Route::get('/user/confirm/{code}', 'UserController@confirm'); Route::get('/client/login', array('as' => 'login', 'uses' => 'ClientAuth\AuthController@getLogin')); Route::post('/client/login', array('as' => 'login', 'uses' => 'ClientAuth\AuthController@postLogin')); Route::get('/client/logout', array('as' => 'logout', 'uses' => 'ClientAuth\AuthController@getLogout')); +Route::get('/client/sessionexpired', array('as' => 'logout', 'uses' => 'ClientAuth\AuthController@getSessionExpired')); Route::get('/client/recover_password', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@getEmail')); Route::post('/client/recover_password', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@postEmail')); Route::get('/client/password/reset/{invitation_key}/{token}', array('as' => 'forgot', 'uses' => 'ClientAuth\PasswordController@getReset')); diff --git a/app/Models/Contact.php b/app/Models/Contact.php index e0e967dcde77..e47211e5133e 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -77,4 +77,9 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa return ''; } } + + public function getLinkAttribute() + { + return \URL::to('client/dashboard/' . $this->contact_key); + } } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 2b9f2e5f4aec..a5b74f646ff1 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1311,6 +1311,10 @@ $LANG = array( 'new_start_date' => 'New start date', 'security' => 'Security', 'see_whats_new' => 'See what\'s new in v:version', + + 'view_dashboard' => 'View Dashboard', + 'client_session_expired' => 'Session Expired', + 'client_session_expired_message' => 'Your session has expired. Please click the link in your email again.' ); return $LANG; diff --git a/resources/views/clientauth/login.blade.php b/resources/views/clientauth/login.blade.php index e5baee7c05aa..690f14bd8df7 100644 --- a/resources/views/clientauth/login.blade.php +++ b/resources/views/clientauth/login.blade.php @@ -61,7 +61,7 @@ @section('body')
- @include('partials.warn_session', ['redirectTo' => '/client/login']) + @include('partials.warn_session', ['redirectTo' => '/client/sessionexpired']) {!! Former::open('client/login') ->rules(['password' => 'required']) diff --git a/resources/views/clientauth/password.blade.php b/resources/views/clientauth/password.blade.php index 9b08937ed1bd..41334fb32995 100644 --- a/resources/views/clientauth/password.blade.php +++ b/resources/views/clientauth/password.blade.php @@ -45,6 +45,14 @@ .form-signin .form-control:focus { z-index: 2; } + + .modal-header a:link, + .modal-header a:visited, + .modal-header a:hover, + .modal-header a:active { + text-decoration: none; + color: white; + } @stop diff --git a/resources/views/clientauth/reset.blade.php b/resources/views/clientauth/reset.blade.php index f8f0924a0cbc..fe384391127b 100644 --- a/resources/views/clientauth/reset.blade.php +++ b/resources/views/clientauth/reset.blade.php @@ -45,6 +45,14 @@ .form-signin .form-control:focus { z-index: 2; } + + .modal-header a:link, + .modal-header a:visited, + .modal-header a:hover, + .modal-header a:active { + text-decoration: none; + color: white; + } @stop @@ -70,7 +78,7 @@
- +

{!! Former::password('password')->placeholder(trans('texts.password'))->raw() !!} diff --git a/resources/views/clientauth/sessionexpired.blade.php b/resources/views/clientauth/sessionexpired.blade.php new file mode 100644 index 000000000000..1fca7a79b4a7 --- /dev/null +++ b/resources/views/clientauth/sessionexpired.blade.php @@ -0,0 +1,79 @@ +@extends('public.header') + +@section('head') + @parent + + +@endsection + +@section('body') +

+ +
+@endsection \ No newline at end of file diff --git a/resources/views/clients/show.blade.php b/resources/views/clients/show.blade.php index 57d5ffbbdd7e..7be5b4649b7b 100644 --- a/resources/views/clients/show.blade.php +++ b/resources/views/clients/show.blade.php @@ -145,7 +145,8 @@ @endif @if ($contact->phone) {{ $contact->phone }}
- @endif + @endif + {{ trans('texts.view_dashboard') }}
@endforeach
diff --git a/resources/views/emails/client_password.blade.php b/resources/views/emails/client_password.blade.php index 9a586b3b5eb6..24f08e95f466 100644 --- a/resources/views/emails/client_password.blade.php +++ b/resources/views/emails/client_password.blade.php @@ -8,7 +8,7 @@
@include('partials.email_button', [ - 'link' => URL::to("client/password/reset/".session('invitation_key')."/{$token}"), + 'link' => URL::to("client/password/reset/".session('contact_key')."/{$token}"), 'field' => 'reset', 'color' => '#36c157', ]) From 29c4f1697073d1cc0c97bbc99c26e0f641d8b8d8 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 24 May 2016 17:45:38 -0400 Subject: [PATCH 06/15] Fix contact authentication --- app/Http/Middleware/Authenticate.php | 44 ++++++++++++++++------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index ff10d0d06156..1b38969fe929 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -19,22 +19,29 @@ class Authenticate { { $authenticated = Auth::guard($guard)->check(); - if($guard == 'client' && !empty($request->invitation_key)){ - $contact_key = session('contact_key'); - if($contact_key) { - $contact = $this->getContact($contact_key); - $invitation = $this->getInvitation($request->invitation_key); - - if ($contact->id != $invitation->contact_id) { - // This is a different client; reauthenticate - $authenticated = false; - Auth::guard($guard)->logout(); - } - Session::put('contact_key', $contact->contact_key); - } - } - if($guard=='client'){ + if(!empty($request->invitation_key)){ + $contact_key = session('contact_key'); + if($contact_key) { + $contact = $this->getContact($contact_key); + $invitation = $this->getInvitation($request->invitation_key); + + if (!$invitation) { + return response()->view('error', [ + 'error' => trans('texts.invoice_not_found'), + 'hideHeader' => true, + ]); + } + + if ($contact->id != $invitation->contact_id) { + // This is a different client; reauthenticate + $authenticated = false; + Auth::guard($guard)->logout(); + } + Session::put('contact_key', $invitation->contact->contact_key); + } + } + if (!empty($request->contact_key)) { $contact_key = $request->contact_key; Session::put('contact_key', $contact_key); @@ -44,14 +51,15 @@ class Authenticate { if ($contact_key) { $contact = $this->getContact($contact_key); - $account = $contact->account; } elseif (!empty($request->invitation_key)) { $invitation = $this->getInvitation($request->invitation_key); - $account = $invitation->account; + $contact = $invitation->contact; + Session::put('contact_key', $contact->contact_key); } else { return \Redirect::to('client/sessionexpired'); } - + $account = $contact->account; + if(Auth::guard('user')->check() && Auth::user('user')->account_id === $account->id){ // This is an admin; let them pretend to be a client $authenticated = true; From 1f11d88d6b23bf5752c510e803ad4a6903e299a9 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 24 May 2016 18:00:59 -0400 Subject: [PATCH 07/15] Store ip addresses for payments and payment methods --- app/Http/Controllers/ClientPortalController.php | 4 ++-- app/Http/Controllers/PaymentController.php | 2 +- app/Services/PaymentService.php | 10 ++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index 578b960b1160..50058c8bc809 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -835,7 +835,7 @@ class ClientPortalController extends BaseController $gateway = $accountGateway->gateway; if ($token && $paymentType == PAYMENT_TYPE_BRAINTREE_PAYPAL) { - $sourceReference = $this->paymentService->createToken($this->paymentService->createGateway($accountGateway), array('token'=>$token), $accountGateway, $client, $invitation->contact_id); + $sourceReference = $this->paymentService->createToken($this->paymentService->createGateway($accountGateway), array('token'=>$token), $accountGateway, $client, $contact->id); if(empty($sourceReference)) { $this->paymentMethodError('Token-No-Ref', $this->paymentService->lastError, $accountGateway); @@ -916,7 +916,7 @@ class ClientPortalController extends BaseController if (!empty($details)) { $gateway = $this->paymentService->createGateway($accountGateway); - $sourceReference = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id); + $sourceReference = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $contact->id); } else { return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv')); } diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 6a5742fabe2b..07d20a1571b9 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -667,7 +667,7 @@ class PaymentController extends BaseController } elseif (method_exists($gateway, 'completePurchase') && !$accountGateway->isGateway(GATEWAY_TWO_CHECKOUT) && !$accountGateway->isGateway(GATEWAY_CHECKOUT_COM)) { - $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway); + $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway, array()); $response = $this->paymentService->completePurchase($gateway, $accountGateway, $details, $token); diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index b227b466413c..c80e193b82de 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -88,6 +88,10 @@ class PaymentService extends BaseService 'transactionType' => 'Purchase', ]; + if ($input !== null) { + $data['ip_address'] = \Request::ip(); + } + if ($accountGateway->isGateway(GATEWAY_PAYPAL_EXPRESS) || $accountGateway->isGateway(GATEWAY_PAYPAL_PRO)) { $data['ButtonSource'] = 'InvoiceNinja_SP'; }; @@ -442,6 +446,8 @@ class PaymentService extends BaseService $accountGatewayToken->save(); $paymentMethod = $this->convertPaymentMethodFromGatewayResponse($tokenResponse, $accountGateway, $accountGatewayToken, $contactId); + $paymentMethod->ip_address = \Request::ip(); + $paymentMethod->save(); } else { $this->lastError = $tokenResponse->getMessage(); @@ -644,6 +650,10 @@ class PaymentService extends BaseService $payment->payment_type_id = $this->detectCardType($card->getNumber()); } + if (!empty($paymentDetails['ip_address'])) { + $payment->ip_address = $paymentDetails['ip_address']; + } + $savePaymentMethod = !empty($paymentMethod); // This will convert various gateway's formats to a known format From a5fdb88d79899c311b59d2bee4eeedce05ec4f3f Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 24 May 2016 22:49:06 -0400 Subject: [PATCH 08/15] Add support for auto bill on due date --- .../Commands/SendRecurringInvoices.php | 38 +++++++++++++- app/Models/Invoice.php | 14 +++++ app/Models/Payment.php | 10 ---- app/Models/PaymentMethod.php | 10 +--- app/Ninja/Repositories/InvoiceRepository.php | 2 +- app/Services/PaymentService.php | 52 +++++++++++++++++-- .../2016_05_24_164847_wepay_ach.php | 19 ++++--- 7 files changed, 114 insertions(+), 31 deletions(-) diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index abf493d1ca54..6debedcd4001 100644 --- a/app/Console/Commands/SendRecurringInvoices.php +++ b/app/Console/Commands/SendRecurringInvoices.php @@ -8,6 +8,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use App\Ninja\Mailers\ContactMailer as Mailer; use App\Ninja\Repositories\InvoiceRepository; +use App\Services\PaymentService; use App\Models\Invoice; use App\Models\InvoiceItem; use App\Models\Invitation; @@ -18,13 +19,15 @@ class SendRecurringInvoices extends Command protected $description = 'Send recurring invoices'; protected $mailer; protected $invoiceRepo; + protected $paymentService; - public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo) + public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, PaymentService $paymentService) { parent::__construct(); $this->mailer = $mailer; $this->invoiceRepo = $invoiceRepo; + $this->paymentService = $paymentService; } public function fire() @@ -53,6 +56,39 @@ class SendRecurringInvoices extends Command } } + $delayedAutoBillInvoices = Invoice::with('account.timezone', 'recurring_invoice', 'invoice_items', 'client', 'user') + ->leftJoin('invoices as recurring_invoice', 'invoices.recurring_invoice_id', '=', 'recurring_invoice.id') + ->whereRaw('invoices.is_deleted IS FALSE AND invoices.deleted_at IS NULL AND invoices.is_recurring IS FALSE + AND invoices.balance > 0 AND invoices.due_date = ? + AND (recurring_invoice.auto_bill = ? OR (recurring_invoice.auto_bill != ? AND recurring_invoice.client_enable_auto_bill IS TRUE))', + array($today->format('Y-m-d'), AUTO_BILL_ALWAYS, AUTO_BILL_OFF)) + ->orderBy('invoices.id', 'asc') + ->get(); + $this->info(count($delayedAutoBillInvoices).' due recurring auto bill invoice instance(s) found'); + + foreach ($delayedAutoBillInvoices as $invoice) { + $autoBill = $invoice->getAutoBillEnabled(); + $billNow = false; + + if ($autoBill && !$invoice->isPaid()) { + $billNow = $invoice->account->auto_bill_on_due_date; + + if (!$billNow) { + $paymentMethod = $this->invoiceService->getClientDefaultPaymentMethod($invoice->client); + + if ($paymentMethod && $paymentMethod->requiresDelayedAutoBill()) { + $billNow = true; + } + } + } + + $this->info('Processing Invoice '.$invoice->id.' - Should bill '.($billNow ? 'YES' : 'NO')); + + if ($billNow) { + $this->paymentService->autoBillInvoice($invoice); + } + } + $this->info('Done'); } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 02fdc73f99bd..1c9a9baf62cc 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -934,6 +934,20 @@ class Invoice extends EntityModel implements BalanceAffecting } return false; } + + public function getAutoBillEnabled() { + if (!$this->is_recurring) { + $recurInvoice = $this->recurring_invoice; + } else { + $recurInvoice = $this; + } + + if (!$recurInvoice) { + return false; + } + + return $recurInvoice->auto_bill == AUTO_BILL_ALWAYS || ($recurInvoice->auto_bill != AUTO_BILL_OFF && $recurInvoice->client_enable_auto_bill); + } } Invoice::creating(function ($invoice) { diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 677c5c0ea0c1..288bc566a8f1 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -194,16 +194,6 @@ class Payment extends EntityModel { return $value ? str_pad($value, 4, '0', STR_PAD_LEFT) : null; } - - public function getIpAddressAttribute($value) - { - return !$value?$value:inet_ntop($value); - } - - public function setIpAddressAttribute($value) - { - $this->attributes['ip_address'] = inet_pton($value); - } } Payment::creating(function ($payment) { diff --git a/app/Models/PaymentMethod.php b/app/Models/PaymentMethod.php index 7110403ab828..c18d9d2b8bae 100644 --- a/app/Models/PaymentMethod.php +++ b/app/Models/PaymentMethod.php @@ -159,14 +159,8 @@ class PaymentMethod extends EntityModel } } - public function getIpAddressAttribute($value) - { - return !$value?$value:inet_ntop($value); - } - - public function setIpAddressAttribute($value) - { - $this->attributes['ip_address'] = inet_pton($value); + public function requiresDelayedAutoBill(){ + return $this->payment_type_id == PAYMENT_TYPE_DIRECT_DEBIT; } } diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index 979304ed01a0..6bbd4f9fa639 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -815,7 +815,7 @@ class InvoiceRepository extends BaseRepository $recurInvoice->last_sent_date = date('Y-m-d'); $recurInvoice->save(); - if ($recurInvoice->auto_bill == AUTO_BILL_ALWAYS || ($recurInvoice->auto_bill != AUTO_BILL_OFF && $recurInvoice->client_enable_auto_bill)) { + if ($recurInvoice->getAutoBillEnabled() && !$recurInvoice->account->auto_bill_on_due_date) { if ($this->paymentService->autoBillInvoice($invoice)) { // update the invoice reference to match its actual state // this is to ensure a 'payment received' email is sent diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index c80e193b82de..40feac81f157 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -89,7 +89,7 @@ class PaymentService extends BaseService ]; if ($input !== null) { - $data['ip_address'] = \Request::ip(); + $data['ip'] = \Request::ip(); } if ($accountGateway->isGateway(GATEWAY_PAYPAL_EXPRESS) || $accountGateway->isGateway(GATEWAY_PAYPAL_PRO)) { @@ -446,7 +446,7 @@ class PaymentService extends BaseService $accountGatewayToken->save(); $paymentMethod = $this->convertPaymentMethodFromGatewayResponse($tokenResponse, $accountGateway, $accountGatewayToken, $contactId); - $paymentMethod->ip_address = \Request::ip(); + $paymentMethod->ip = \Request::ip(); $paymentMethod->save(); } else { @@ -650,8 +650,8 @@ class PaymentService extends BaseService $payment->payment_type_id = $this->detectCardType($card->getNumber()); } - if (!empty($paymentDetails['ip_address'])) { - $payment->ip_address = $paymentDetails['ip_address']; + if (!empty($paymentDetails['ip'])) { + $payment->ip = $paymentDetails['ip']; } $savePaymentMethod = !empty($paymentMethod); @@ -839,6 +839,38 @@ class PaymentService extends BaseService return false; } + if ($defaultPaymentMethod->requiresDelayedAutoBill()) { + $invoiceDate = DateTime::createFromFormat('Y-m-d', $invoice_date); + $minDueDate = clone $invoiceDate; + $minDueDate->modify('+10 days'); + + if (DateTime::create() < $minDueDate) { + // Can't auto bill now + return false; + } + + $firstUpdate = \App\Models\Activities::where('invoice_id', '=', $invoice->id) + ->where('activity_type_id', '=', ACTIVITY_TYPE_UPDATE_INVOICE) + ->first(); + + if ($firstUpdate) { + $backup = json_decode($firstUpdate->json_backup); + + if ($backup->balance != $invoice->balance || $backup->due_date != $invoice->due_date) { + // It's changed since we sent the email can't bill now + return false; + } + } + + $invoicePayments = \App\Models\Activities::where('invoice_id', '=', $invoice->id) + ->where('activity_type_id', '=', ACTIVITY_TYPE_CREATE_PAYMENT); + + if ($invoicePayments->count()) { + // ACH requirements are strict; don't auto bill this + return false; + } + } + // setup the gateway/payment info $details = $this->getPaymentDetails($invitation, $accountGateway); $details['customerReference'] = $token; @@ -859,6 +891,18 @@ class PaymentService extends BaseService } } + public function getClientDefaultPaymentMethod($client) { + $this->getClientPaymentMethods($client); + + $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); + + if (!$accountGatewayToken) { + return false; + } + + return $accountGatewayToken->default_payment_method; + } + public function getDatatable($clientPublicId, $search) { $datatable = new PaymentDatatable( ! $clientPublicId, $clientPublicId); diff --git a/database/migrations/2016_05_24_164847_wepay_ach.php b/database/migrations/2016_05_24_164847_wepay_ach.php index b8e6ca752a0e..cb1b79433321 100644 --- a/database/migrations/2016_05_24_164847_wepay_ach.php +++ b/database/migrations/2016_05_24_164847_wepay_ach.php @@ -13,26 +13,25 @@ class WePayAch extends Migration public function up() { Schema::table('contacts', function(Blueprint $table) { - $table->string('contact_key')->index()->default(null); + $table->string('contact_key')->nullable()->default(null)->index()->unique(); }); Schema::table('payment_methods', function($table) { $table->string('bank_name')->nullable(); + $table->string('ip')->nullable(); }); Schema::table('payments', function($table) { $table->string('bank_name')->nullable(); + $table->string('ip')->nullable(); }); Schema::table('accounts', function($table) { $table->boolean('auto_bill_on_due_date')->default(false); }); - - DB::statement('ALTER TABLE `payments` ADD `ip_address` VARBINARY(16)'); - DB::statement('ALTER TABLE `payment_methods` ADD `ip_address` VARBINARY(16)'); } /** @@ -43,15 +42,21 @@ class WePayAch extends Migration public function down() { Schema::table('contacts', function(Blueprint $table) { - $table->dropColumn('contact_key'); + $table->dropColumn('contact_key'); }); Schema::table('payments', function($table) { - $table->dropColumn('ip_address'); + $table->dropColumn('bank_name'); + $table->dropColumn('ip'); }); Schema::table('payment_methods', function($table) { - $table->dropColumn('ip_address'); + $table->dropColumn('bank_name'); + $table->dropColumn('ip'); + }); + + Schema::table('accounts', function($table) { + $table->dropColumn('auto_bill_on_due_date'); }); } } From 9d4b42a5142690d6b250390959c2f668b7f8518c Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 24 May 2016 23:14:19 -0400 Subject: [PATCH 09/15] Mention in the invoice notification that the invoice will be auto billed on the due date --- .../Commands/SendRecurringInvoices.php | 24 ++++++++++++++++++- app/Ninja/Mailers/ContactMailer.php | 20 ++++++++++++++++ app/Services/TemplateService.php | 1 + resources/lang/en/texts.php | 8 ++++++- .../templates_and_reminders.blade.php | 1 + 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index 6debedcd4001..2362184e8ccd 100644 --- a/app/Console/Commands/SendRecurringInvoices.php +++ b/app/Console/Commands/SendRecurringInvoices.php @@ -51,6 +51,28 @@ class SendRecurringInvoices extends Command $invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice); if ($invoice && !$invoice->isPaid()) { + $invoice->account->auto_bill_on_due_date; + + $autoBillLater = false; + if ($invoice->account->auto_bill_on_due_date) { + $autoBillLater = true; + } elseif ($paymentMethod = $this->paymentService->getClientDefaultPaymentMethod($invoice->client)) { + if ($paymentMethod && $paymentMethod->requiresDelayedAutoBill()) { + $autoBillLater = true; + } + } + + if($autoBillLater) { + if (empty($paymentMethod)) { + $paymentMethod = $this->paymentService->getClientDefaultPaymentMethod($invoice->client); + } + + if($paymentMethod) { + $invoice->autoBillPaymentMethod = $paymentMethod; + } + } + + $this->info('Sending Invoice'); $this->mailer->sendInvoice($invoice); } @@ -74,7 +96,7 @@ class SendRecurringInvoices extends Command $billNow = $invoice->account->auto_bill_on_due_date; if (!$billNow) { - $paymentMethod = $this->invoiceService->getClientDefaultPaymentMethod($invoice->client); + $paymentMethod = $this->paymentService->getClientDefaultPaymentMethod($invoice->client); if ($paymentMethod && $paymentMethod->requiresDelayedAutoBill()) { $billNow = true; diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index 224d48785aea..9710fbb1df55 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -30,6 +30,7 @@ class ContactMailer extends Mailer 'viewButton', 'paymentLink', 'paymentButton', + 'autoBill', ]; public function __construct(TemplateService $templateService) @@ -106,6 +107,20 @@ class ContactMailer extends Mailer return $response; } + private function createAutoBillNotifyString($paymentMethod) { + if ($paymentMethod->payment_type_id == PAYMENT_TYPE_DIRECT_DEBIT) { + $paymentMethodString = trans('texts.auto_bill_payment_method_bank', ['bank'=>$paymentMethod->getBankName(), 'last4'=>$paymentMethod->last4]); + } elseif ($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL) { + $paymentMethodString = trans('texts.auto_bill_payment_method_paypal', ['email'=>$paymentMethod->email]); + } else { + $code = str_replace(' ', '', strtolower($paymentMethod->payment_type->name)); + $cardType = trans("texts.card_" . $code); + $paymentMethodString = trans('texts.auto_bill_payment_method_credit_card', ['type'=>$cardType,'last4'=>$paymentMethod->last4]); + } + + return trans('texts.auto_bill_notification', ['payment_method'=>$paymentMethodString]); + } + private function sendInvitation($invitation, $invoice, $body, $subject, $pdfString, $documentStrings) { $client = $invoice->client; @@ -137,6 +152,11 @@ class ContactMailer extends Mailer 'amount' => $invoice->getRequestedAmount() ]; + if ($invoice->autoBillPaymentMethod) { + // Let the client know they'll be billed later + $variables['autobill'] = $this->createAutoBillNotifyString($invoice->autoBillPaymentMethod); + } + if (empty($invitation->contact->password) && $account->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $account->enable_portal_password && $account->send_portal_password) { // The contact needs a password $variables['password'] = $password = $this->generatePassword(); diff --git a/app/Services/TemplateService.php b/app/Services/TemplateService.php index 5a41c705352d..cc3e1becc031 100644 --- a/app/Services/TemplateService.php +++ b/app/Services/TemplateService.php @@ -51,6 +51,7 @@ class TemplateService '$customInvoice1' => $account->custom_invoice_text_label1, '$customInvoice2' => $account->custom_invoice_text_label2, '$documents' => $documentsHTML, + '$autoBill' => empty($data['autobill'])?'':$data['autobill'], ]; // Add variables for available payment types diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index a5b74f646ff1..0443f37515ac 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1314,7 +1314,13 @@ $LANG = array( 'view_dashboard' => 'View Dashboard', 'client_session_expired' => 'Session Expired', - 'client_session_expired_message' => 'Your session has expired. Please click the link in your email again.' + 'client_session_expired_message' => 'Your session has expired. Please click the link in your email again.', + + 'auto_bill_notification' => 'This invoice will automatically be billed to :payment_method on the due date.', + 'auto_bill_payment_method_bank' => 'your :bank account ending in :last4', + 'auto_bill_payment_method_credit_card' => 'your :type card ending in :last4', + 'auto_bill_payment_method_paypal' => 'your PayPal account (:email)', + 'auto_bill_notification_placeholder' => 'This invoice will automatically be billed to your Visa card ending in 4242 on the due date.' ); return $LANG; diff --git a/resources/views/accounts/templates_and_reminders.blade.php b/resources/views/accounts/templates_and_reminders.blade.php index 87453e80dae6..aee11968b469 100644 --- a/resources/views/accounts/templates_and_reminders.blade.php +++ b/resources/views/accounts/templates_and_reminders.blade.php @@ -266,6 +266,7 @@ '{!! Form::flatButton('view_invoice', '#0b4d78') !!}$password', "{{ URL::to('/payment/...') }}$password", '{!! Form::flatButton('pay_now', '#36c157') !!}$password', + '{{ trans('texts.auto_bill_notification_placeholder') }}', ]; // Add blanks for custom values From c701c5563c03cc907f8ba13af28971db97b69c3d Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Wed, 25 May 2016 10:36:40 -0400 Subject: [PATCH 10/15] Add payment settings block --- app/Http/Controllers/AccountController.php | 25 ++++++++++++++++++- .../Controllers/AccountGatewayController.php | 11 -------- resources/lang/en/texts.php | 6 ++++- .../views/accounts/account_gateway.blade.php | 7 ------ .../partials/account_gateway_wepay.blade.php | 4 --- resources/views/accounts/payments.blade.php | 23 +++++++++++++++++ 6 files changed, 52 insertions(+), 24 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 49955e184b89..b9e3d9691d06 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -430,10 +430,17 @@ class AccountController extends BaseController $switchToWepay = !$account->getGatewayConfig(GATEWAY_BRAINTREE) && !$account->getGatewayConfig(GATEWAY_STRIPE); } + $tokenBillingOptions = []; + for ($i=1; $i<=4; $i++) { + $tokenBillingOptions[$i] = trans("texts.token_billing_{$i}"); + } + return View::make('accounts.payments', [ 'showSwitchToWepay' => $switchToWepay, 'showAdd' => $count < count(Gateway::$paymentTypes), - 'title' => trans('texts.online_payments') + 'title' => trans('texts.online_payments'), + 'tokenBillingOptions' => $tokenBillingOptions, + 'account' => $account, ]); } } @@ -661,6 +668,8 @@ class AccountController extends BaseController return AccountController::saveDetails(); } elseif ($section === ACCOUNT_LOCALIZATION) { return AccountController::saveLocalization(); + } elseif ($section == ACCOUNT_PAYMENTS) { + return self::saveOnlinePayments(); } elseif ($section === ACCOUNT_NOTIFICATIONS) { return AccountController::saveNotifications(); } elseif ($section === ACCOUNT_EXPORT) { @@ -1137,6 +1146,20 @@ class AccountController extends BaseController return Redirect::to('settings/'.ACCOUNT_LOCALIZATION); } + private function saveOnlinePayments() + { + $account = Auth::user()->account; + $account->token_billing_type_id = Input::get('token_billing_type_id'); + $account->auto_bill_on_due_date = boolval(Input::get('auto_bill_on_due_date')); + $account->save(); + + event(new UserSettingsChanged()); + + Session::flash('message', trans('texts.updated_settings')); + + return Redirect::to('settings/'.ACCOUNT_PAYMENTS); + } + public function removeLogo() { $account = Auth::user()->account; diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 765fbc78a9d6..e613b2047b4b 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -141,11 +141,6 @@ class AccountGatewayController extends BaseController } } - $tokenBillingOptions = []; - for ($i=1; $i<=4; $i++) { - $tokenBillingOptions[$i] = trans("texts.token_billing_{$i}"); - } - return [ 'paymentTypes' => $paymentTypes, 'account' => $account, @@ -154,7 +149,6 @@ class AccountGatewayController extends BaseController 'config' => false, 'gateways' => $gateways, 'creditCardTypes' => $creditCards, - 'tokenBillingOptions' => $tokenBillingOptions, 'countGateways' => count($currentGateways) ]; } @@ -327,11 +321,6 @@ class AccountGatewayController extends BaseController $account->account_gateways()->save($accountGateway); } - if (Input::get('token_billing_type_id')) { - $account->token_billing_type_id = Input::get('token_billing_type_id'); - $account->save(); - } - if(isset($wepayResponse)) { return $wepayResponse; } else { diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 0443f37515ac..7d491f57d62a 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1320,7 +1320,11 @@ $LANG = array( 'auto_bill_payment_method_bank' => 'your :bank account ending in :last4', 'auto_bill_payment_method_credit_card' => 'your :type card ending in :last4', 'auto_bill_payment_method_paypal' => 'your PayPal account (:email)', - 'auto_bill_notification_placeholder' => 'This invoice will automatically be billed to your Visa card ending in 4242 on the due date.' + 'auto_bill_notification_placeholder' => 'This invoice will automatically be billed to your Visa card ending in 4242 on the due date.', + 'payment_settings' => 'Payment Settings', + + 'auto_bill_on_due_date' => 'Auto bill on due date instead of send date', + 'auto_bill_ach_date_help' => 'ACH auto bill will always happen on the due date', ); return $LANG; diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index cfad9f21a6b6..fb5f674d27e7 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -16,7 +16,6 @@
{!! 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) !!} @@ -99,12 +98,6 @@ {!! Former::text('publishable_key') !!} @endif - @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')) !!} - @endif - @if ($gateway->id == GATEWAY_STRIPE)
diff --git a/resources/views/accounts/partials/account_gateway_wepay.blade.php b/resources/views/accounts/partials/account_gateway_wepay.blade.php index bdbe974aa5e9..7b6729a7d65c 100644 --- a/resources/views/accounts/partials/account_gateway_wepay.blade.php +++ b/resources/views/accounts/partials/account_gateway_wepay.blade.php @@ -16,7 +16,6 @@ {!! 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') !!}

@@ -40,9 +39,6 @@ ->text(trans('texts.accept_debit_cards')) !!}
@endif - {!! 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')) !!} diff --git a/resources/views/accounts/payments.blade.php b/resources/views/accounts/payments.blade.php index 997190ec4f0a..a2f289c782ad 100644 --- a/resources/views/accounts/payments.blade.php +++ b/resources/views/accounts/payments.blade.php @@ -4,6 +4,29 @@ @parent @include('accounts.nav', ['selected' => ACCOUNT_PAYMENTS]) + {!! Former::open()->addClass('warn-on-exit') !!} + {!! Former::populateField('token_billing_type_id', $account->token_billing_type_id) !!} + {!! Former::populateField('auto_bill_on_due_date', $account->auto_bill_on_due_date) !!} + + +
+
+

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

+
+
+ + {!! Former::select('token_billing_type_id') + ->options($tokenBillingOptions) + ->help(trans('texts.token_billing_help')) !!} + {!! Former::checkbox('auto_bill_on_due_date') + ->label(trans('texts.auto_bill')) + ->text(trans('texts.auto_bill_on_due_date')) + ->help(trans('texts.auto_bill_ach_date_help')) !!} + {!! Former::actions( Button::success(trans('texts.save'))->submit()->appendIcon(Icon::create('floppy-disk')) ) !!} +
+
+ {!! Former::close() !!} + @if ($showSwitchToWepay) {!! Button::success(trans('texts.switch_to_wepay')) ->asLinkTo(URL::to('/gateways/switch/wepay')) From d19a3715f95ff8a2b1e67fa6bc77437a163b4810 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Wed, 25 May 2016 11:07:20 -0400 Subject: [PATCH 11/15] Warn user when editing an ACH auto bill invoice --- .../Commands/SendRecurringInvoices.php | 22 +++---------------- app/Http/Controllers/InvoiceController.php | 9 +++++++- app/Services/PaymentService.php | 6 +++++ resources/lang/en/texts.php | 1 + resources/views/invoices/edit.blade.php | 8 +++++++ 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index 2362184e8ccd..d07645bf9bc2 100644 --- a/app/Console/Commands/SendRecurringInvoices.php +++ b/app/Console/Commands/SendRecurringInvoices.php @@ -54,20 +54,12 @@ class SendRecurringInvoices extends Command $invoice->account->auto_bill_on_due_date; $autoBillLater = false; - if ($invoice->account->auto_bill_on_due_date) { + if ($invoice->account->auto_bill_on_due_date || $this->paymentService->getClientRequiresDelayedAutoBill($invoice->client)) { $autoBillLater = true; - } elseif ($paymentMethod = $this->paymentService->getClientDefaultPaymentMethod($invoice->client)) { - if ($paymentMethod && $paymentMethod->requiresDelayedAutoBill()) { - $autoBillLater = true; - } } if($autoBillLater) { - if (empty($paymentMethod)) { - $paymentMethod = $this->paymentService->getClientDefaultPaymentMethod($invoice->client); - } - - if($paymentMethod) { + if($paymentMethod = $this->paymentService->getClientDefaultPaymentMethod($invoice->client)) { $invoice->autoBillPaymentMethod = $paymentMethod; } } @@ -93,15 +85,7 @@ class SendRecurringInvoices extends Command $billNow = false; if ($autoBill && !$invoice->isPaid()) { - $billNow = $invoice->account->auto_bill_on_due_date; - - if (!$billNow) { - $paymentMethod = $this->paymentService->getClientDefaultPaymentMethod($invoice->client); - - if ($paymentMethod && $paymentMethod->requiresDelayedAutoBill()) { - $billNow = true; - } - } + $billNow = $invoice->account->auto_bill_on_due_date || $this->paymentService->getClientRequiresDelayedAutoBill($invoice->client); } $this->info('Processing Invoice '.$invoice->id.' - Should bill '.($billNow ? 'YES' : 'NO')); diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 9a0dadf494c4..9fb049d3a47d 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -26,6 +26,7 @@ use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\DocumentRepository; use App\Services\InvoiceService; +use App\Services\PaymentService; use App\Services\RecurringInvoiceService; use App\Http\Requests\InvoiceRequest; @@ -39,10 +40,11 @@ class InvoiceController extends BaseController protected $clientRepo; protected $documentRepo; protected $invoiceService; + protected $paymentService; protected $recurringInvoiceService; protected $entityType = ENTITY_INVOICE; - public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService, DocumentRepository $documentRepo, RecurringInvoiceService $recurringInvoiceService) + public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService, DocumentRepository $documentRepo, RecurringInvoiceService $recurringInvoiceService, PaymentService $paymentService) { // parent::__construct(); @@ -51,6 +53,7 @@ class InvoiceController extends BaseController $this->clientRepo = $clientRepo; $this->invoiceService = $invoiceService; $this->recurringInvoiceService = $recurringInvoiceService; + $this->paymentService = $paymentService; } public function index() @@ -196,6 +199,10 @@ class InvoiceController extends BaseController 'lastSent' => $lastSent); $data = array_merge($data, self::getViewModel($invoice)); + if ($invoice->isSent() && $invoice->getAutoBillEnabled() && !$invoice->isPaid()) { + $data['autoBillChangeWarning'] = $this->paymentService->getClientRequiresDelayedAutoBill($invoice->client); + } + if ($clone) { $data['formIsChanged'] = true; } diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 40feac81f157..0f1ede8b3fa8 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -903,6 +903,12 @@ class PaymentService extends BaseService return $accountGatewayToken->default_payment_method; } + public function getClientRequiresDelayedAutoBill($client) { + $defaultPaymentMethod = $this->getClientDefaultPaymentMethod($client); + + return $defaultPaymentMethod?$defaultPaymentMethod->requiresDelayedAutoBill():null; + } + public function getDatatable($clientPublicId, $search) { $datatable = new PaymentDatatable( ! $clientPublicId, $clientPublicId); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 7d491f57d62a..307c37c1a152 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1325,6 +1325,7 @@ $LANG = array( 'auto_bill_on_due_date' => 'Auto bill on due date instead of send date', 'auto_bill_ach_date_help' => 'ACH auto bill will always happen on the due date', + 'warn_change_auto_bill' => 'Due to NACHA rules, changes to this invoice may prevent ACH auto bill.', ); return $LANG; diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 3ca6acafba59..630bd733aed7 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1200,6 +1200,10 @@ } function onSaveClick() { + @if(!empty($autoBillChangeWarning)) + if(!confirm("{!! trans('texts.warn_change_auto_bill') !!}"))return; + @endif + if (model.invoice().is_recurring()) { // warn invoice will be emailed when saving new recurring invoice if ({{ $invoice->exists() ? 'false' : 'true' }}) { @@ -1324,6 +1328,10 @@ @if ($invoice->id) function onPaymentClick() { + @if(!empty($autoBillChangeWarning)) + if(!confirm("{!! trans('texts.warn_change_auto_bill') !!}"))return; + @endif + window.location = '{{ URL::to('payments/create/' . $invoice->client->public_id . '/' . $invoice->public_id ) }}'; } From c9f00274b1d4c164c514cb0eef78b34f51798514 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Wed, 25 May 2016 15:04:58 -0400 Subject: [PATCH 12/15] Support storing WePay bank tokens --- .../Controllers/ClientPortalController.php | 44 ++++++--- app/Http/Controllers/PaymentController.php | 30 ++++-- app/Models/Account.php | 4 + app/Ninja/Datatables/PaymentDatatable.php | 13 ++- app/Ninja/Repositories/PaymentRepository.php | 2 + app/Services/PaymentService.php | 94 +++++++++++++------ resources/lang/en/texts.php | 8 +- .../payments/add_paymentmethod.blade.php | 50 ++++++++-- .../payments/paymentmethods_list.blade.php | 9 +- 9 files changed, 190 insertions(+), 64 deletions(-) diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index 50058c8bc809..2bb550759b1c 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -189,8 +189,8 @@ class ClientPortalController extends BaseController $html = ''; if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) { - if ($paymentMethod->bank_data) { - $html = '
' . htmlentities($paymentMethod->bank_data->name) . '
'; + if ($paymentMethod->bank_name) { + $html = '
' . htmlentities($paymentMethod->bank_name) . '
'; } else { $html = ''.trans('; } @@ -304,6 +304,7 @@ class ClientPortalController extends BaseController $data = [ 'color' => $color, + 'contact' => $contact, 'account' => $account, 'client' => $client, 'clientFontUrl' => $account->getFontsUrl(), @@ -472,9 +473,16 @@ class ClientPortalController extends BaseController return $model->email; } } elseif ($model->last4) { - $bankData = PaymentMethod::lookupBankData($model->routing_number); - if (is_object($bankData)) { - return $bankData->name.'  •••' . $model->last4; + if($model->bank_name) { + $bankName = $model->bank_name; + } else { + $bankData = PaymentMethod::lookupBankData($model->routing_number); + if($bankData) { + $bankName = $bankData->name; + } + } + if (!empty($bankName)) { + return $bankName.'  •••' . $model->last4; } elseif($model->last4) { return '' . htmlentities($card_type) . '  •••' . $model->last4; } @@ -762,6 +770,7 @@ class ClientPortalController extends BaseController $data = array( 'account' => $account, + 'contact' => $contact, 'color' => $account->primary_color ? $account->primary_color : '#0b4d78', 'client' => $client, 'clientViewCSS' => $account->clientViewCSS(), @@ -835,7 +844,7 @@ class ClientPortalController extends BaseController $gateway = $accountGateway->gateway; if ($token && $paymentType == PAYMENT_TYPE_BRAINTREE_PAYPAL) { - $sourceReference = $this->paymentService->createToken($this->paymentService->createGateway($accountGateway), array('token'=>$token), $accountGateway, $client, $contact->id); + $sourceReference = $this->paymentService->createToken($paymentType, $this->paymentService->createGateway($accountGateway), array('token'=>$token), $accountGateway, $client, $contact->id); if(empty($sourceReference)) { $this->paymentMethodError('Token-No-Ref', $this->paymentService->lastError, $accountGateway); @@ -864,8 +873,12 @@ class ClientPortalController extends BaseController 'clientFontUrl' => $account->getFontsUrl(), 'showAddress' => $accountGateway->show_address, 'paymentTitle' => trans('texts.add_payment_method'), + 'sourceId' => $token, ]; + $details = json_decode(Input::get('details')); + $data['details'] = $details; + if ($paymentType == PAYMENT_TYPE_STRIPE_ACH) { $data['currencies'] = Cache::get('currencies'); } @@ -874,7 +887,7 @@ class ClientPortalController extends BaseController $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); } - if(!empty($data['braintreeClientToken']) || $accountGateway->getPublishableStripeKey()|| $accountGateway->gateway_id == GATEWAY_WEPAY) { + if(!empty($data['braintreeClientToken']) || $accountGateway->getPublishableStripeKey()|| ($accountGateway->gateway_id == GATEWAY_WEPAY && $paymentType != PAYMENT_TYPE_WEPAY_ACH)) { $data['tokenize'] = true; } @@ -897,7 +910,7 @@ class ClientPortalController extends BaseController $sourceToken = Input::get('sourceToken'); if (($validator = PaymentController::processPaymentClientDetails($client, $accountGateway, $paymentType)) !== true) { - return Redirect::to('client/paymentmethods/add/' . $typeLink) + return Redirect::to('client/paymentmethods/add/' . $typeLink.'/'.$sourceToken) ->withErrors($validator) ->withInput(Request::except('cvv')); } @@ -909,21 +922,26 @@ class ClientPortalController extends BaseController $details = array('plaidPublicToken' => Input::get('plaidPublicToken'), 'plaidAccountId' => Input::get('plaidAccountId')); } - if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && !Input::get('authorize_ach')) { + if (($paymentType == PAYMENT_TYPE_STRIPE_ACH || $paymentType == PAYMENT_TYPE_WEPAY_ACH) && !Input::get('authorize_ach')) { Session::flash('error', trans('texts.ach_authorization_required')); - return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv')); + return Redirect::to('client/paymentmethods/add/' . $typeLink.'/'.$sourceToken)->withInput(Request::except('cvv')); + } + + if ($paymentType == PAYMENT_TYPE_WEPAY_ACH && !Input::get('tos_agree')) { + Session::flash('error', trans('texts.wepay_payment_tos_agree_required')); + return Redirect::to('client/paymentmethods/add/' . $typeLink.'/'.$sourceToken)->withInput(Request::except('cvv')); } if (!empty($details)) { $gateway = $this->paymentService->createGateway($accountGateway); - $sourceReference = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $contact->id); + $sourceReference = $this->paymentService->createToken($paymentType, $gateway, $details, $accountGateway, $client, $contact->id); } else { - return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv')); + return Redirect::to('client/paymentmethods/add/' . $typeLink.'/'.$sourceToken)->withInput(Request::except('cvv')); } if(empty($sourceReference)) { $this->paymentMethodError('Token-No-Ref', $this->paymentService->lastError, $accountGateway); - return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv')); + return Redirect::to('client/paymentmethods/add/' . $typeLink.'/'.$sourceToken)->withInput(Request::except('cvv')); } else if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty($usingPlaid) ) { // The user needs to complete verification Session::flash('message', trans('texts.bank_account_verification_next_steps')); diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 07d20a1571b9..f62f24f51348 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -136,7 +136,6 @@ class PaymentController extends BaseController public function show_payment($invitationKey, $paymentType = false, $sourceId = false) { - $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail(); $invoice = $invitation->invoice; $client = $invoice->client; @@ -154,6 +153,9 @@ class PaymentController extends BaseController Session::put($invitation->id.'payment_ref', $invoice->id.'_'.uniqid()); + $details = json_decode(Input::get('details')); + $data['details'] = $details; + if ($paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) { if ($paymentType == PAYMENT_TYPE_TOKEN) { $useToken = true; @@ -205,16 +207,14 @@ class PaymentController extends BaseController } } else { - if ($deviceData = Input::get('details')) { + if ($deviceData = Input::get('device_data')) { Session::put($invitation->id . 'device_data', $deviceData); } Session::put($invitation->id . 'payment_type', PAYMENT_TYPE_BRAINTREE_PAYPAL); - $paypalDetails = json_decode(Input::get('details')); - if (!$sourceId || !$paypalDetails) { + if (!$sourceId || !$details) { return Redirect::to('view/'.$invitationKey); } - $data['paypalDetails'] = $paypalDetails; } $data += [ @@ -405,7 +405,7 @@ class PaymentController extends BaseController } public static function processPaymentClientDetails($client, $accountGateway, $paymentType, $onSite = true){ - $rules = $paymentType == PAYMENT_TYPE_STRIPE_ACH ? [] : [ + $rules = ($paymentType == PAYMENT_TYPE_STRIPE_ACH || $paymentType == PAYMENT_TYPE_WEPAY_ACH)? [] : [ 'first_name' => 'required', 'last_name' => 'required', ]; @@ -422,7 +422,7 @@ class PaymentController extends BaseController ); } - $requireAddress = $accountGateway->show_address && $paymentType != PAYMENT_TYPE_STRIPE_ACH && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL; + $requireAddress = $accountGateway->show_address && $paymentType != PAYMENT_TYPE_STRIPE_ACH && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL && $paymentType != PAYMENT_TYPE_WEPAY_ACH; if ($requireAddress) { $rules = array_merge($rules, [ @@ -473,6 +473,21 @@ class PaymentController extends BaseController $customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return parameter*/); $paymentMethod = PaymentMethod::scope($sourceId, $account->id, $accountGatewayToken->id)->firstOrFail(); $sourceReference = $paymentMethod->source_reference; + + // What type of payment is this? + if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) { + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + $paymentType = PAYMENT_TYPE_STRIPE_ACH; + } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { + $paymentType = PAYMENT_TYPE_WEPAY_ACH; + } + } elseif ($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL && $accountGateway->gateway_id == GATEWAY_BRAINTREE) { + $paymentType = PAYMENT_TYPE_BRAINTREE_PAYPAL; + } elseif ($accountGateway->gateway_id == GATEWAY_STRIPE) { + $paymentType = PAYMENT_TYPE_STRIPE_CREDIT_CARD; + } else { + $paymentType = PAYMENT_TYPE_CREDIT_CARD; + } } } @@ -494,6 +509,7 @@ class PaymentController extends BaseController $gateway = $this->paymentService->createGateway($accountGateway); $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway, $data); + $details['paymentType'] = $paymentType; // check if we're creating/using a billing token $tokenBillingSupported = false; diff --git a/app/Models/Account.php b/app/Models/Account.php index de1328862b71..98237ce02e25 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -385,6 +385,10 @@ class Account extends Eloquent $type = PAYMENT_TYPE_STRIPE; } + if ($type == PAYMENT_TYPE_WEPAY_ACH) { + return $this->getGatewayConfig(GATEWAY_WEPAY); + } + foreach ($this->account_gateways as $gateway) { if ($exceptFor && ($gateway->id == $exceptFor->id)) { continue; diff --git a/app/Ninja/Datatables/PaymentDatatable.php b/app/Ninja/Datatables/PaymentDatatable.php index 22006bf742e1..92473c7fd257 100644 --- a/app/Ninja/Datatables/PaymentDatatable.php +++ b/app/Ninja/Datatables/PaymentDatatable.php @@ -65,9 +65,16 @@ class PaymentDatatable extends EntityDatatable return $model->email; } } elseif ($model->last4) { - $bankData = PaymentMethod::lookupBankData($model->routing_number); - if (is_object($bankData)) { - return $bankData->name.'  •••' . $model->last4; + if($model->bank_name) { + $bankName = $model->bank_name; + } else { + $bankData = PaymentMethod::lookupBankData($model->routing_number); + if($bankData) { + $bankName = $bankData->name; + } + } + if (!empty($bankName)) { + return $bankName.'  •••' . $model->last4; } elseif($model->last4) { return '' . htmlentities($card_type) . '  •••' . $model->last4; } diff --git a/app/Ninja/Repositories/PaymentRepository.php b/app/Ninja/Repositories/PaymentRepository.php index ef6c88096405..498f0d1d6912 100644 --- a/app/Ninja/Repositories/PaymentRepository.php +++ b/app/Ninja/Repositories/PaymentRepository.php @@ -58,6 +58,7 @@ class PaymentRepository extends BaseRepository 'payments.last4', 'payments.email', 'payments.routing_number', + 'payments.bank_name', 'invoices.is_deleted as invoice_is_deleted', 'gateways.name as gateway_name', 'gateways.id as gateway_id', @@ -129,6 +130,7 @@ class PaymentRepository extends BaseRepository 'payments.last4', 'payments.email', 'payments.routing_number', + 'payments.bank_name', 'payments.payment_status_id', 'payment_statuses.name as payment_status_name' ); diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 0f1ede8b3fa8..77f489049f68 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -306,7 +306,7 @@ class PaymentService extends BaseService return true; } - public function createToken($gateway, $details, $accountGateway, $client, $contactId, &$customerReference = null, &$paymentMethod = null) + public function createToken($paymentType, $gateway, $details, $accountGateway, $client, $contactId, &$customerReference = null, &$paymentMethod = null) { $customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return paramenter */); @@ -398,27 +398,36 @@ class PaymentService extends BaseService } } 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' => intval($details['token']), - )); + if ($paymentType == PAYMENT_TYPE_WEPAY_ACH) { + // Persist bank details + $tokenResponse = $wepay->request('/payment_bank/persist', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'payment_bank_id' => intval($details['token']), + )); + } else { + // Authorize credit card + $wepay->request('credit_card/authorize', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => intval($details['token']), + )); - // 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' => $accountGateway->getWebhookUrl(), - )); - $tokenResponse = $wepay->request('credit_card', array( - 'client_id' => WEPAY_CLIENT_ID, - 'client_secret' => WEPAY_CLIENT_SECRET, - 'credit_card_id' => intval($details['token']), - )); + // 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' => $accountGateway->getWebhookUrl(), + )); + $tokenResponse = $wepay->request('credit_card', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => intval($details['token']), + )); + } $customerReference = CUSTOMER_REFERENCE_LOCAL; $sourceReference = $details['token']; @@ -516,12 +525,29 @@ class PaymentService extends BaseService $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 . '-01'; - $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); + if ($source->payment_bank_id) { + $paymentMethod->payment_type_id = PAYMENT_TYPE_ACH; + $paymentMethod->last4 = $source->account_last_four; + $paymentMethod->bank_name = $source->bank_name; + $paymentMethod->source_reference = $source->payment_bank_id; - $paymentMethod->source_reference = $source->credit_card_id; + switch($source->state) { + case 'new': + case 'pending': + $paymentMethod->status = 'new'; + break; + case 'authorized': + $paymentMethod->status = 'verified'; + break; + } + } else { + $paymentMethod->last4 = $source->last_four; + $paymentMethod->payment_type_id = $this->parseCardType($source->credit_card_name); + $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; + } return $paymentMethod; } @@ -570,10 +596,12 @@ class PaymentService extends BaseService } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { if ($gatewayResponse instanceof \Omnipay\WePay\Message\CustomCheckoutResponse) { $wepay = \Utils::setupWePay($accountGateway); - $gatewayResponse = $wepay->request('credit_card', array( + $paymentMethodType = $gatewayResponse->getData()['payment_method']['type']; + + $gatewayResponse = $wepay->request($paymentMethodType, array( 'client_id' => WEPAY_CLIENT_ID, 'client_secret' => WEPAY_CLIENT_SECRET, - 'credit_card_id' => $gatewayResponse->getData()['payment_method']['credit_card']['id'], + $paymentMethodType.'_id' => $gatewayResponse->getData()['payment_method'][$paymentMethodType]['id'], )); } @@ -690,6 +718,10 @@ class PaymentService extends BaseService $payment->email = $paymentMethod->email; } + if ($paymentMethod->bank_name) { + $payment->bank_name = $paymentMethod->bank_name; + } + if ($payerId) { $payment->payer_id = $payerId; } @@ -876,6 +908,7 @@ class PaymentService extends BaseService $details['customerReference'] = $token; $details['token'] = $defaultPaymentMethod->source_reference; + $details['paymentType'] = $defaultPaymentMethod->payment_type_id; if ($accountGateway->gateway_id == GATEWAY_WEPAY) { $details['transaction_id'] = 'autobill_'.$invoice->id; } @@ -1117,6 +1150,13 @@ class PaymentService extends BaseService $details['applicationFee'] = $this->calculateApplicationFee($accountGateway, $details['amount']); $details['feePayer'] = WEPAY_FEE_PAYER; $details['callbackUri'] = $accountGateway->getWebhookUrl(); + if(isset($details['paymentType'])) { + if($details['paymentType'] == PAYMENT_TYPE_ACH || $details['paymentType'] == PAYMENT_TYPE_WEPAY_ACH) { + $details['paymentMethodType'] = 'payment_bank'; + } + + unset($details['paymentType']); + } } $response = $gateway->purchase($details)->send(); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 5218c16fe504..c0f4f97f4ec0 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1257,7 +1257,7 @@ $LANG = array( 'plaid_linked_status' => 'Your bank account at :bank', 'add_payment_method' => 'Add Payment Method', 'account_holder_type' => 'Account Holder Type', - 'ach_authorization' => 'I authorize :company to electronically debit my account and, if necessary, electronically credit my account to correct erroneous debits.', + 'ach_authorization' => 'I authorize :company to use my bank account for future payments and, if necessary, electronically credit my account to correct erroneous debits. I understand that I may cancel this authorization at any time by removing the payment method or by contacting :email.', 'ach_authorization_required' => 'You must consent to ACH transactions.', 'off' => 'Off', 'opt_in' => 'Opt-in', @@ -1327,6 +1327,12 @@ $LANG = array( 'auto_bill_on_due_date' => 'Auto bill on due date instead of send date', 'auto_bill_ach_date_help' => 'ACH auto bill will always happen on the due date', 'warn_change_auto_bill' => 'Due to NACHA rules, changes to this invoice may prevent ACH auto bill.', + + 'bank_account' => 'Bank Account', + 'payment_processed_through_wepay' => 'ACH payments will be processed using WePay.', + 'wepay_payment_tos_agree' => 'I agree to the WePay :terms and :privacy_policy.', + 'privacy_policy' => 'Privacy Policy', + 'wepay_payment_tos_agree_required' => 'You must agree to the WePay Terms of Service and Privacy Policy.', ); return $LANG; diff --git a/resources/views/payments/add_paymentmethod.blade.php b/resources/views/payments/add_paymentmethod.blade.php index e17624af2211..db734f2299a4 100644 --- a/resources/views/payments/add_paymentmethod.blade.php +++ b/resources/views/payments/add_paymentmethod.blade.php @@ -6,7 +6,7 @@ @include('payments.tokenization_braintree') @elseif (isset($accountGateway) && $accountGateway->getPublishableStripeKey()) @include('payments.tokenization_stripe') - @elseif (isset($accountGateway) && $accountGateway->gateway_id == GATEWAY_WEPAY) + @elseif (isset($accountGateway) && $accountGateway->gateway_id == GATEWAY_WEPAY && $paymentType != PAYMENT_TYPE_WEPAY_ACH) @include('payments.tokenization_wepay') @else + @elseif(!empty($enableWePayACH)) + + @endif @stop diff --git a/resources/views/payments/paymentmethods_list.blade.php b/resources/views/payments/paymentmethods_list.blade.php index 3a6e96715897..153a9847bffa 100644 --- a/resources/views/payments/paymentmethods_list.blade.php +++ b/resources/views/payments/paymentmethods_list.blade.php @@ -50,20 +50,25 @@ @elseif($gateway->gateway_id == GATEWAY_WEPAY && $gateway->getAchEnabled()) -