From b8170f0324018c0ed6a7e9f692bf305787c80906 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 23 Apr 2016 16:40:19 -0400 Subject: [PATCH 01/37] Support refunds from the admin UI --- app/Events/PaymentWasRefunded.php | 25 +++++ app/Http/Controllers/AccountController.php | 9 +- app/Http/Controllers/PaymentController.php | 6 +- .../Controllers/PublicClientController.php | 33 +++++- app/Http/routes.php | 7 ++ app/Listeners/ActivityListener.php | 13 +++ app/Listeners/CreditListener.php | 20 +++- app/Listeners/InvoiceListener.php | 15 ++- app/Models/Activity.php | 2 + app/Models/Payment.php | 52 +++++++++ app/Models/PaymentStatus.php | 8 ++ app/Ninja/Repositories/ActivityRepository.php | 1 + app/Ninja/Repositories/PaymentRepository.php | 16 ++- app/Providers/EventServiceProvider.php | 5 + app/Services/ActivityService.php | 4 +- app/Services/PaymentService.php | 104 ++++++++++++++++++ .../2016_04_23_182223_payments_changes.php | 49 +++++++++ database/seeds/DatabaseSeeder.php | 1 + database/seeds/PaymentStatusSeeder.php | 36 ++++++ database/seeds/UpdateSeeder.php | 1 + resources/lang/en/texts.php | 14 +++ resources/views/list.blade.php | 56 +++++++++- 22 files changed, 457 insertions(+), 20 deletions(-) create mode 100644 app/Events/PaymentWasRefunded.php create mode 100644 app/Models/PaymentStatus.php create mode 100644 database/migrations/2016_04_23_182223_payments_changes.php create mode 100644 database/seeds/PaymentStatusSeeder.php diff --git a/app/Events/PaymentWasRefunded.php b/app/Events/PaymentWasRefunded.php new file mode 100644 index 000000000000..54eeeabc3259 --- /dev/null +++ b/app/Events/PaymentWasRefunded.php @@ -0,0 +1,25 @@ +payment = $payment; + $this->refundAmount = $refundAmount; + } + +} diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 3f8917b65427..cf0e4508610e 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -182,14 +182,7 @@ class AccountController extends BaseController if ($account->company->payment) { $payment = $account->company->payment; - - $gateway = $this->paymentService->createGateway($payment->account_gateway); - $refund = $gateway->refund(array( - 'transactionReference' => $payment->transaction_reference, - 'amount' => $payment->amount - )); - $refund->send(); - $payment->delete(); + $this->paymentService->refund($payment); Session::flash('message', trans('texts.plan_refunded')); \Log::info("Refunded Plan Payment: {$account->name} - {$user->email}"); } else { diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 1f53b4a0bc54..f5ecc89f8e8e 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -57,6 +57,7 @@ class PaymentController extends BaseController 'method', 'payment_amount', 'payment_date', + 'status', '' ]), )); @@ -630,11 +631,12 @@ class PaymentController extends BaseController public function bulk() { $action = Input::get('action'); + $amount = Input::get('amount'); $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); - $count = $this->paymentService->bulk($ids, $action); + $count = $this->paymentService->bulk($ids, $action, array('amount'=>$amount)); if ($count > 0) { - $message = Utils::pluralize($action.'d_payment', $count); + $message = Utils::pluralize($action=='refund'?'refunded_payment':$action.'d_payment', $count); Session::flash('message', $message); } diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index 32d6d53c3887..d756bd7eaaa0 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -252,6 +252,9 @@ class PublicClientController extends BaseController 'invoice' => trans('texts.invoice') . ' ' . $model->invoice, 'contact' => Utils::getClientDisplayName($model), 'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''), + 'credit' => $model->payment_amount ? Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) : '', + 'payment_amount' => $model->payment_amount ? Utils::formatMoney($model->payment_amount, $model->currency_id, $model->country_id) : null, + 'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null, ]; return trans("texts.activity_{$model->activity_type_id}", $data); @@ -321,7 +324,7 @@ class PublicClientController extends BaseController 'clientFontUrl' => $account->getFontsUrl(), 'entityType' => ENTITY_PAYMENT, 'title' => trans('texts.payments'), - 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date']) + 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date', 'status']) ]; return response()->view('public_list', $data); @@ -340,8 +343,36 @@ class PublicClientController extends BaseController ->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? 'Online payment' : ''); }) ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); }) ->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); }) + ->addColumn('status', function ($model) { return $this->getPaymentStatusLabel($model); }) ->make(); } + + private function getPaymentStatusLabel($model) + { + $label = trans("texts.status_" . strtolower($model->payment_status_name)); + $class = 'default'; + switch ($model->payment_status_id) { + case PAYMENT_STATUS_PENDING: + $class = 'info'; + break; + case PAYMENT_STATUS_COMPLETED: + $class = 'success'; + break; + case PAYMENT_STATUS_FAILED: + $class = 'danger'; + break; + case PAYMENT_STATUS_PARTIALLY_REFUNDED: + $label = trans('texts.status_partially_refunded_amount', [ + 'amount' => Utils::formatMoney($model->refunded, $model->currency_id, $model->country_id), + ]); + $class = 'primary'; + break; + case PAYMENT_STATUS_REFUNDED: + $class = 'default'; + break; + } + return "

$label

"; + } public function quoteIndex() { diff --git a/app/Http/routes.php b/app/Http/routes.php index 48f06e5e7b3d..25041048f102 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -398,6 +398,7 @@ if (!defined('CONTACT_EMAIL')) { //define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); + define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 39); define('ACTIVITY_TYPE_CREATE_CREDIT', 14); //define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); @@ -474,6 +475,12 @@ if (!defined('CONTACT_EMAIL')) { define('INVOICE_STATUS_APPROVED', 4); define('INVOICE_STATUS_PARTIAL', 5); define('INVOICE_STATUS_PAID', 6); + + define('PAYMENT_STATUS_PENDING', 1); + define('PAYMENT_STATUS_FAILED', 2); + define('PAYMENT_STATUS_COMPLETED', 3); + define('PAYMENT_STATUS_PARTIALLY_REFUNDED', 4); + define('PAYMENT_STATUS_REFUNDED', 5); define('PAYMENT_TYPE_CREDIT', 1); define('CUSTOM_DESIGN', 11); diff --git a/app/Listeners/ActivityListener.php b/app/Listeners/ActivityListener.php index 52c2e26f9027..811d1050b9cb 100644 --- a/app/Listeners/ActivityListener.php +++ b/app/Listeners/ActivityListener.php @@ -22,6 +22,7 @@ use App\Events\QuoteInvitationWasViewed; use App\Events\QuoteInvitationWasApproved; use App\Events\PaymentWasCreated; use App\Events\PaymentWasDeleted; +use App\Events\PaymentWasRefunded; use App\Events\PaymentWasArchived; use App\Events\PaymentWasRestored; use App\Events\CreditWasCreated; @@ -309,6 +310,18 @@ class ActivityListener ); } + public function refundedPayment(PaymentWasRefunded $event) + { + $payment = $event->payment; + + $this->activityRepo->create( + $payment, + ACTIVITY_TYPE_REFUNDED_PAYMENT, + $event->refundAmount, + $event->refundAmount * -1 + ); + } + public function archivedPayment(PaymentWasArchived $event) { if ($event->payment->is_deleted) { diff --git a/app/Listeners/CreditListener.php b/app/Listeners/CreditListener.php index bed71a47f59d..5c2ce2539d80 100644 --- a/app/Listeners/CreditListener.php +++ b/app/Listeners/CreditListener.php @@ -3,6 +3,7 @@ use Carbon; use App\Models\Credit; use App\Events\PaymentWasDeleted; +use App\Events\PaymentWasRefunded; use App\Ninja\Repositories\CreditRepository; class CreditListener @@ -26,7 +27,24 @@ class CreditListener $credit = Credit::createNew(); $credit->client_id = $payment->client_id; $credit->credit_date = Carbon::now()->toDateTimeString(); - $credit->balance = $credit->amount = $payment->amount; + $credit->balance = $credit->amount = $payment->amount - $payment->refunded; + $credit->private_notes = $payment->transaction_reference; + $credit->save(); + } + + public function refundedPayment(PaymentWasRefunded $event) + { + $payment = $event->payment; + + // if the payment was from a credit we need to refund the credit + if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) { + return; + } + + $credit = Credit::createNew(); + $credit->client_id = $payment->client_id; + $credit->credit_date = Carbon::now()->toDateTimeString(); + $credit->balance = $credit->amount = $event->refundAmount; $credit->private_notes = $payment->transaction_reference; $credit->save(); } diff --git a/app/Listeners/InvoiceListener.php b/app/Listeners/InvoiceListener.php index 91f3de8cc213..290a8f4a0426 100644 --- a/app/Listeners/InvoiceListener.php +++ b/app/Listeners/InvoiceListener.php @@ -7,6 +7,7 @@ use App\Events\InvoiceWasUpdated; use App\Events\InvoiceWasCreated; use App\Events\PaymentWasCreated; use App\Events\PaymentWasDeleted; +use App\Events\PaymentWasRefunded; use App\Events\PaymentWasRestored; use App\Events\InvoiceInvitationWasViewed; @@ -58,7 +59,17 @@ class InvoiceListener { $payment = $event->payment; $invoice = $payment->invoice; - $adjustment = $payment->amount; + $adjustment = $payment->amount - $payment->refunded; + + $invoice->updateBalances($adjustment); + $invoice->updatePaidStatus(); + } + + public function refundedPayment(PaymentWasRefunded $event) + { + $payment = $event->payment; + $invoice = $payment->invoice; + $adjustment = $event->refundAmount; $invoice->updateBalances($adjustment); $invoice->updatePaidStatus(); @@ -72,7 +83,7 @@ class InvoiceListener $payment = $event->payment; $invoice = $payment->invoice; - $adjustment = $payment->amount * -1; + $adjustment = ($payment->amount - $payment->refunded) * -1; $invoice->updateBalances($adjustment); $invoice->updatePaidStatus(); diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 921c037d5f69..2fa9e6e8e349 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -70,6 +70,8 @@ class Activity extends Eloquent 'quote' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null, 'contact' => $contactId ? $client->getDisplayName() : $user->getDisplayName(), 'payment' => $payment ? $payment->transaction_reference : null, + 'payment_amount' => $payment ? $account->formatMoney($payment->amount, $payment) : null, + 'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : asdf, 'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null, ]; diff --git a/app/Models/Payment.php b/app/Models/Payment.php index a2e8b2591fe3..22924cc61297 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -1,7 +1,9 @@ belongsTo('App\Models\PaymentType'); + } + + public function payment_status() + { + return $this->belongsTo('App\Models\PaymentStatus'); } public function getRoute() @@ -69,6 +76,51 @@ class Payment extends EntityModel return trim("payment {$this->transaction_reference}"); } + public function isPending() + { + return $this->payment_status_id = PAYMENT_STATUS_PENDING; + } + + public function isFailed() + { + return $this->payment_status_id = PAYMENT_STATUS_FAILED; + } + + public function isCompleted() + { + return $this->payment_status_id >= PAYMENT_STATUS_COMPLETED; + } + + public function isPartiallyRefunded() + { + return $this->payment_status_id == PAYMENT_STATUS_PARTIALLY_REFUNDED; + } + + public function isRefunded() + { + return $this->payment_status_id == PAYMENT_STATUS_REFUNDED; + } + + public function recordRefund($amount = null) + { + if (!$this->isRefunded()) { + if (!$amount) { + $amount = $this->amount; + } + + $new_refund = min($this->amount, $this->refunded + $amount); + $refund_change = $new_refund - $this->refunded; + + if ($refund_change) { + $this->refunded = $new_refund; + $this->payment_status_id = $this->refunded == $this->amount ? PAYMENT_STATUS_REFUNDED : PAYMENT_STATUS_PARTIALLY_REFUNDED; + $this->save(); + + Event::fire(new PaymentWasRefunded($this, $refund_change)); + } + } + } + public function getEntityType() { return ENTITY_PAYMENT; diff --git a/app/Models/PaymentStatus.php b/app/Models/PaymentStatus.php new file mode 100644 index 000000000000..fdb1e0f43d5b --- /dev/null +++ b/app/Models/PaymentStatus.php @@ -0,0 +1,8 @@ +join('clients', 'clients.id', '=', 'payments.client_id') ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') ->join('contacts', 'contacts.client_id', '=', 'clients.id') + ->join('payment_statuses', 'payment_statuses.id', '=', 'payments.payment_status_id') ->leftJoin('payment_types', 'payment_types.id', '=', 'payments.payment_type_id') ->leftJoin('account_gateways', 'account_gateways.id', '=', 'payments.account_gateway_id') ->leftJoin('gateways', 'gateways.id', '=', 'account_gateways.gateway_id') @@ -39,6 +40,8 @@ class PaymentRepository extends BaseRepository 'clients.user_id as client_user_id', 'payments.amount', 'payments.payment_date', + 'payments.payment_status_id', + 'payments.payment_type_id', 'invoices.public_id as invoice_public_id', 'invoices.user_id as invoice_user_id', 'invoices.invoice_number', @@ -50,8 +53,11 @@ class PaymentRepository extends BaseRepository 'payments.deleted_at', 'payments.is_deleted', 'payments.user_id', + 'payments.refunded', 'invoices.is_deleted as invoice_is_deleted', - 'gateways.name as gateway_name' + 'gateways.name as gateway_name', + 'gateways.id as gateway_id', + 'payment_statuses.name as payment_status_name' ); if (!\Session::get('show_trash:payment')) { @@ -78,6 +84,7 @@ class PaymentRepository extends BaseRepository ->join('clients', 'clients.id', '=', 'payments.client_id') ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') ->join('contacts', 'contacts.client_id', '=', 'clients.id') + ->join('payment_statuses', 'payment_statuses.id', '=', 'payments.payment_status_id') ->leftJoin('invitations', function ($join) { $join->on('invitations.invoice_id', '=', 'invoices.id') ->on('invitations.contact_id', '=', 'contacts.id'); @@ -104,7 +111,10 @@ class PaymentRepository extends BaseRepository 'contacts.last_name', 'contacts.email', 'payment_types.name as payment_type', - 'payments.account_gateway_id' + 'payments.account_gateway_id', + 'payments.refunded', + 'payments.payment_status_id', + 'payment_statuses.name as payment_status_name' ); if ($filter) { @@ -189,6 +199,4 @@ class PaymentRepository extends BaseRepository parent::restore($payment); } - - } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index bb4e94d79546..3ce5fcfa29e0 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -107,6 +107,11 @@ class EventServiceProvider extends ServiceProvider { 'App\Listeners\InvoiceListener@deletedPayment', 'App\Listeners\CreditListener@deletedPayment', ], + 'App\Events\PaymentWasRefunded' => [ + 'App\Listeners\ActivityListener@refundedPayment', + 'App\Listeners\InvoiceListener@refundedPayment', + 'App\Listeners\CreditListener@refundedPayment', + ], 'App\Events\PaymentWasRestored' => [ 'App\Listeners\ActivityListener@restoredPayment', 'App\Listeners\InvoiceListener@restoredPayment', diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index d3b08b997189..6f6ef4b35956 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -44,7 +44,9 @@ class ActivityService extends BaseService 'quote' => $model->invoice ? link_to('/quotes/' . $model->invoice_public_id, $model->invoice)->toHtml() : null, 'contact' => $model->contact_id ? link_to('/clients/' . $model->client_public_id, Utils::getClientDisplayName($model))->toHtml() : Utils::getPersonDisplayName($model->user_first_name, $model->user_last_name, $model->user_email), 'payment' => $model->payment ?: '', - 'credit' => Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) + 'credit' => $model->payment_amount ? Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) : '', + 'payment_amount' => $model->payment_amount ? Utils::formatMoney($model->payment_amount, $model->currency_id, $model->country_id) : null, + 'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null ]; return trans("texts.activity_{$model->activity_type_id}", $data); diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 61af6cd2336c..8e24370bd930 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -23,6 +23,10 @@ class PaymentService extends BaseService { public $lastError; protected $datatableService; + + protected static $refundableGateways = array( + GATEWAY_STRIPE + ); public function __construct(PaymentRepository $paymentRepo, AccountRepository $accountRepo, DatatableService $datatableService) { @@ -375,6 +379,12 @@ class PaymentService extends BaseService function ($model) { return Utils::dateToString($model->payment_date); } + ], + [ + 'payment_status_name', + function ($model) use ($entityType) { + return self::getStatusLabel($entityType, $model); + } ] ]; } @@ -390,9 +400,103 @@ class PaymentService extends BaseService function ($model) { return Payment::canEditItem($model); } + ], + [ + trans('texts.refund_payment'), + function ($model) { + $max_refund = number_format($model->amount - $model->refunded, 2); + $formatted = Utils::formatMoney($max_refund, $model->currency_id, $model->country_id); + $symbol = Utils::getFromCache($model->currency_id, 'currencies')->symbol; + return "javascript:showRefundModal({$model->public_id}, '{$max_refund}', '{$formatted}', '{$symbol}')"; + }, + function ($model) { + return Payment::canEditItem($model) && ( + ($model->transaction_reference && in_array($model->gateway_id , static::$refundableGateways)) + || $model->payment_type_id == PAYMENT_TYPE_CREDIT + ); + } ] ]; } + + public function bulk($ids, $action, $params = array()) + { + if ($action == 'refund') { + if ( ! $ids ) { + return 0; + } + $payments = $this->getRepo()->findByPublicIdsWithTrashed($ids); + foreach ($payments as $payment) { + if($payment->canEdit()){ + if(!empty($params['amount'])) { + $this->refund($payment, floatval($params['amount'])); + } else { + $this->refund($payment); + } + } + } + + return count($payments); + } else { + return parent::bulk($ids, $action); + } + } + + private function getStatusLabel($entityType, $model) + { + $label = trans("texts.status_" . strtolower($model->payment_status_name)); + $class = 'default'; + switch ($model->payment_status_id) { + case PAYMENT_STATUS_PENDING: + $class = 'info'; + break; + case PAYMENT_STATUS_COMPLETED: + $class = 'success'; + break; + case PAYMENT_STATUS_FAILED: + $class = 'danger'; + break; + case PAYMENT_STATUS_PARTIALLY_REFUNDED: + $label = trans('texts.status_partially_refunded_amount', [ + 'amount' => Utils::formatMoney($model->refunded, $model->currency_id, $model->country_id), + ]); + $class = 'primary'; + break; + case PAYMENT_STATUS_REFUNDED: + $class = 'default'; + break; + } + return "

$label

"; + } + + public function refund($payment, $amount = null) { + if (!$amount) { + $amount = $payment->amount; + } + + $amount = min($amount, $payment->amount - $payment->refunded); + + if (!$amount) { + return; + } + + if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) { + $accountGateway = $this->createGateway($payment->account_gateway); + $refund = $accountGateway->refund(array( + 'transactionReference' => $payment->transaction_reference, + 'amount' => $amount, + )); + $response = $refund->send(); + + if ($response->isSuccessful()) { + $payment->recordRefund($amount); + } else { + $this->error('Unknown', $response->getMessage(), $accountGateway); + } + } else { + $payment->recordRefund($amount); + } + } } diff --git a/database/migrations/2016_04_23_182223_payments_changes.php b/database/migrations/2016_04_23_182223_payments_changes.php new file mode 100644 index 000000000000..641ec06e2c2d --- /dev/null +++ b/database/migrations/2016_04_23_182223_payments_changes.php @@ -0,0 +1,49 @@ +increments('id'); + $table->string('name'); + }); + + (new \PaymentStatusSeeder())->run(); + + Schema::table('payments', function($table) + { + $table->decimal('refunded', 13, 2); + $table->unsignedInteger('payment_status_id')->default(3); + $table->foreign('payment_status_id')->references('id')->on('payment_statuses'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('payments', function($table) + { + $table->dropColumn('refunded'); + $table->dropForeign('payments_payment_status_id_foreign'); + $table->dropColumn('payment_status_id'); + }); + + Schema::dropIfExists('payment_statuses'); + } +} diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php index 8791f30e71fb..753a1ce54e30 100644 --- a/database/seeds/DatabaseSeeder.php +++ b/database/seeds/DatabaseSeeder.php @@ -19,6 +19,7 @@ class DatabaseSeeder extends Seeder $this->call('FontsSeeder'); $this->call('BanksSeeder'); $this->call('InvoiceStatusSeeder'); + $this->call('PaymentStatusSeeder'); $this->call('CurrenciesSeeder'); $this->call('DateFormatsSeeder'); $this->call('InvoiceDesignsSeeder'); diff --git a/database/seeds/PaymentStatusSeeder.php b/database/seeds/PaymentStatusSeeder.php new file mode 100644 index 000000000000..8e93d57798fd --- /dev/null +++ b/database/seeds/PaymentStatusSeeder.php @@ -0,0 +1,36 @@ +createPaymentStatuses(); + + Eloquent::reguard(); + } + + private function createPaymentStatuses() + { + $statuses = [ + ['id' => '1', 'name' => 'Pending'], + ['id' => '2', 'name' => 'Failed'], + ['id' => '3', 'name' => 'Completed'], + ['id' => '4', 'name' => 'Partially Refunded'], + ['id' => '5', 'name' => 'Refunded'], + ]; + + foreach ($statuses as $status) { + $record = PaymentStatus::find($status['id']); + if ($record) { + $record->name = $status['name']; + $record->save(); + } else { + PaymentStatus::create($status); + } + } + } +} diff --git a/database/seeds/UpdateSeeder.php b/database/seeds/UpdateSeeder.php index e4d43f78e512..e5335e311b42 100644 --- a/database/seeds/UpdateSeeder.php +++ b/database/seeds/UpdateSeeder.php @@ -15,6 +15,7 @@ class UpdateSeeder extends Seeder $this->call('FontsSeeder'); $this->call('BanksSeeder'); $this->call('InvoiceStatusSeeder'); + $this->call('PaymentStatusSeeder'); $this->call('CurrenciesSeeder'); $this->call('DateFormatsSeeder'); $this->call('InvoiceDesignsSeeder'); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 37478c5d73e3..ae717b297b23 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1176,6 +1176,20 @@ $LANG = array( 'page_size' => 'Page Size', 'live_preview_disabled' => 'Live preview has been disabled to support selected font', 'invoice_number_padding' => 'Padding', + + // Payment updates + 'refund_payment' => 'Refund Payment', + 'refund_max' => 'Max:', + 'refund' => 'Refund', + 'are_you_sure_refund' => 'Refund selected payments?', + 'status_pending' => 'Pending', + 'status_completed' => 'Completed', + 'status_failed' => 'Failed', + 'status_partially_refunded' => 'Partially Refunded', + 'status_partially_refunded_amount' => ':amount Refunded', + 'status_refunded' => 'Refunded', + 'refunded_payment' => 'Refunded Payment', + 'activity_39' => ':user refunded :adjustment of a :payment_amount payment (:payment)', ); diff --git a/resources/views/list.blade.php b/resources/views/list.blade.php index 8e8ba97116d7..07db6b5e6b91 100644 --- a/resources/views/list.blade.php +++ b/resources/views/list.blade.php @@ -53,7 +53,41 @@ ->setOptions('sPaginationType', 'bootstrap') ->setOptions('aaSorting', [[isset($sortCol) ? $sortCol : '1', 'desc']]) ->render('datatable') !!} - + + @if ($entityType == ENTITY_PAYMENT) + + @endif + {!! Former::close() !!} + + @elseif ($accountGateway->getPublishableStripeKey()) From c536bd8569ccf8335987794f3a516797d606ad48 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Fri, 29 Apr 2016 17:50:21 -0400 Subject: [PATCH 10/37] Add basic ACH support --- .../Controllers/AccountGatewayController.php | 2 +- .../Controllers/ClientAuth/AuthController.php | 5 +- .../ClientAuth/PasswordController.php | 9 +- app/Http/Controllers/PaymentController.php | 149 ++++- .../Controllers/PublicClientController.php | 286 ++++++-- app/Http/routes.php | 12 +- app/Models/Account.php | 4 + app/Ninja/Repositories/PaymentRepository.php | 3 + app/Services/PaymentService.php | 254 ++++++-- composer.json | 25 +- composer.lock | 29 +- public/images/credit_cards/ach.png | Bin 0 -> 1949 bytes resources/lang/en/texts.php | 36 +- resources/views/clientauth/login.blade.php | 2 +- resources/views/clientauth/password.blade.php | 2 +- resources/views/clientauth/reset.blade.php | 2 +- resources/views/invoices/view.blade.php | 6 + resources/views/master.blade.php | 2 +- .../payments/add_paymentmethod.blade.php | 611 ++++++++++++++++++ resources/views/payments/payment.blade.php | 401 ++++++++---- .../views/payments/paymentmethods.blade.php | 149 +++++ resources/views/public/header.blade.php | 21 +- 22 files changed, 1746 insertions(+), 264 deletions(-) create mode 100644 public/images/credit_cards/ach.png create mode 100644 resources/views/payments/add_paymentmethod.blade.php create mode 100644 resources/views/payments/paymentmethods.blade.php diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 08b2d1355f80..1ef8d61e98b8 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -117,7 +117,7 @@ class AccountGatewayController extends BaseController } } - $paymentTypes[$type] = trans('texts.'.strtolower($type)); + $paymentTypes[$type] = $type == PAYMENT_TYPE_CREDIT_CARD ? trans('texts.other_providers'): trans('texts.'.strtolower($type)); if ($type == PAYMENT_TYPE_BITCOIN) { $paymentTypes[$type] .= ' - BitPay'; diff --git a/app/Http/Controllers/ClientAuth/AuthController.php b/app/Http/Controllers/ClientAuth/AuthController.php index ed5f199ac255..e3951b5464b8 100644 --- a/app/Http/Controllers/ClientAuth/AuthController.php +++ b/app/Http/Controllers/ClientAuth/AuthController.php @@ -32,9 +32,8 @@ class AuthController extends Controller { $invoice = $invitation->invoice; $client = $invoice->client; $account = $client->account; - - $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); - $data['clientViewCSS'] = $account->clientViewCSS(); + + $data['account'] = $account; $data['clientFontUrl'] = $account->getFontsUrl(); } } diff --git a/app/Http/Controllers/ClientAuth/PasswordController.php b/app/Http/Controllers/ClientAuth/PasswordController.php index 35576b47ae39..822764315a0e 100644 --- a/app/Http/Controllers/ClientAuth/PasswordController.php +++ b/app/Http/Controllers/ClientAuth/PasswordController.php @@ -49,9 +49,7 @@ class PasswordController extends Controller { $invoice = $invitation->invoice; $client = $invoice->client; $account = $client->account; - - $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); - $data['clientViewCSS'] = $account->clientViewCSS(); + $data['account'] = $account; $data['clientFontUrl'] = $account->getFontsUrl(); } } @@ -116,9 +114,8 @@ class PasswordController extends Controller { $invoice = $invitation->invoice; $client = $invoice->client; $account = $client->account; - - $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); - $data['clientViewCSS'] = $account->clientViewCSS(); + + $data['account'] = $account; $data['clientFontUrl'] = $account->getFontsUrl(); } } diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index f9c497c64ea8..fadcdd0394b4 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -137,7 +137,7 @@ class PaymentController extends BaseController ]; } - public function show_payment($invitationKey, $paymentType = false) + 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(); @@ -155,27 +155,31 @@ class PaymentController extends BaseController if ($paymentType == PAYMENT_TYPE_TOKEN) { $useToken = true; - $paymentType = PAYMENT_TYPE_CREDIT_CARD; + $accountGateway = $invoice->client->account->getTokenGateway(); + $paymentType = $accountGateway->getPaymentType(); + } else { + $accountGateway = $invoice->client->account->getGatewayByType($paymentType); } + Session::put($invitation->id . 'payment_type', $paymentType); - $accountGateway = $invoice->client->account->getGatewayByType($paymentType); $gateway = $accountGateway->gateway; $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); - - // Handle offsite payments - if ($useToken || $paymentType != PAYMENT_TYPE_CREDIT_CARD + $isOffsite = ($paymentType != PAYMENT_TYPE_CREDIT_CARD && $accountGateway->getPaymentType() != PAYMENT_TYPE_STRIPE) || $gateway->id == GATEWAY_EWAY || $gateway->id == GATEWAY_TWO_CHECKOUT || $gateway->id == GATEWAY_PAYFAST - || $gateway->id == GATEWAY_MOLLIE) { + || $gateway->id == GATEWAY_MOLLIE; + + // Handle offsite payments + if ($useToken || $isOffsite) { if (Session::has('error')) { Session::reflash(); return Redirect::to('view/'.$invitationKey); } else { - return self::do_payment($invitationKey, false, $useToken); + return self::do_payment($invitationKey, false, $useToken, $sourceId); } } @@ -189,22 +193,24 @@ class PaymentController extends BaseController 'gateway' => $gateway, 'accountGateway' => $accountGateway, 'acceptedCreditCardTypes' => $acceptedCreditCardTypes, + 'paymentType' => $paymentType, 'countries' => Cache::get('countries'), 'currencyId' => $client->getCurrencyId(), 'currencyCode' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD'), 'account' => $client->account, - 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), - 'hideHeader' => $account->isNinjaAccount(), - 'clientViewCSS' => $account->clientViewCSS(), - 'clientFontUrl' => $account->getFontsUrl(), + 'clientFontUrl' => $client->account->getFontsUrl(), 'showAddress' => $accountGateway->show_address, ]; - if ($gateway->id = GATEWAY_BRAINTREE) { + if ($paymentType == PAYMENT_TYPE_STRIPE_ACH) { + $data['currencies'] = Cache::get('currencies'); + } + + if ($gateway->id == GATEWAY_BRAINTREE) { $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); } - return View::make('payments.payment', $data); + return View::make('payments.add_paymentmethod', $data); } public function show_license_payment() @@ -235,7 +241,7 @@ class PaymentController extends BaseController $account = $this->accountRepo->getNinjaAccount(); $account->load('account_gateways.gateway'); - $accountGateway = $account->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD); + $accountGateway = $account->getGatewayByType(PAYMENT_TYPE_STRIPE_CREDIT_CARD); $gateway = $accountGateway->gateway; $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); @@ -260,7 +266,7 @@ class PaymentController extends BaseController 'showAddress' => true, ]; - return View::make('payments.payment', $data); + return View::make('payments.add_paymentmethod', $data); } public function do_license_payment() @@ -291,7 +297,7 @@ class PaymentController extends BaseController $account = $this->accountRepo->getNinjaAccount(); $account->load('account_gateways.gateway'); - $accountGateway = $account->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD); + $accountGateway = $account->getGatewayByType(PAYMENT_TYPE_STRIPE_CREDIT_CARD); try { $affiliate = Affiliate::find(Session::get('affiliate_id')); @@ -367,13 +373,14 @@ class PaymentController extends BaseController } } - public function do_payment($invitationKey, $onSite = true, $useToken = false) + public function do_payment($invitationKey, $onSite = true, $useToken = false, $sourceId = false) { $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.currency', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail(); $invoice = $invitation->invoice; $client = $invoice->client; $account = $client->account; - $accountGateway = $account->getGatewayByType(Session::get($invitation->id . 'payment_type')); + $paymentType = Session::get($invitation->id . 'payment_type'); + $accountGateway = $account->getGatewayByType($paymentType); $rules = [ @@ -445,11 +452,20 @@ class PaymentController extends BaseController if ($useToken) { $details['customerReference'] = $client->getGatewayToken(); unset($details['token']); - } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) { - $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference); + if ($sourceId) { + $details['cardReference'] = $sourceId; + } + } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing') || $paymentType == PAYMENT_TYPE_STRIPE_ACH) { + $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */); if ($token) { $details['token'] = $token; $details['customerReference'] = $customerReference; + + if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty($usingPlaid) ) { + // The user needs to complete verification + Session::flash('message', trans('texts.bank_account_verification_next_steps')); + return Redirect::to('/client/paymentmethods'); + } } else { $this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway); return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); @@ -463,11 +479,15 @@ class PaymentController extends BaseController if ($useToken) { $details['customerId'] = $customerId = $client->getGatewayToken(); - $customer = $gateway->findCustomer($customerId)->send(); - $details['paymentMethodToken'] = $customer->getData()->paymentMethods[0]->token; + if (!$sourceId) { + $customer = $gateway->findCustomer($customerId)->send(); + $details['paymentMethodToken'] = $customer->getData()->paymentMethods[0]->token; + } else { + $details['paymentMethodToken'] = $sourceId; + } unset($details['token']); } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) { - $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference); + $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */); if ($token) { $details['paymentMethodToken'] = $token; $details['customerId'] = $customerReference; @@ -683,4 +703,85 @@ class PaymentController extends BaseController Session::flash('error', $message); Utils::logError("Payment Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message), 'PHP', true); } + + public function getBankInfo($routingNumber) { + if (strlen($routingNumber) != 9 || !preg_match('/\d{9}/', $routingNumber)) { + return response()->json([ + 'message' => 'Invalid routing number', + ], 400); + } + + $data = static::getBankData($routingNumber); + + if (is_string($data)) { + return response()->json([ + 'message' => $data, + ], 500); + } elseif (!empty($data)) { + return $data; + } + + return response()->json([ + 'message' => 'Bank not found', + ], 404); + } + + public static function getBankData($routingNumber) { + $cached = Cache::get('bankData:'.$routingNumber); + + if ($cached != null) { + return $cached == false ? null : $cached; + } + + $dataPath = base_path('vendor/gatepay/FedACHdir/FedACHdir.txt'); + + if (!file_exists($dataPath) || !$size = filesize($dataPath)) { + return 'Invalid data file'; + } + + $lineSize = 157; + $numLines = $size/$lineSize; + + if ($numLines % 1 != 0) { + // The number of lines should be an integer + return 'Invalid data file'; + } + + // Format: http://www.sco.ca.gov/Files-21C/Bank_Master_Interface_Information_Package.pdf + $file = fopen($dataPath, 'r'); + + // Binary search + $low = 0; + $high = $numLines - 1; + while ($low <= $high) { + $mid = floor(($low + $high) / 2); + + fseek($file, $mid * $lineSize); + $thisNumber = fread($file, 9); + + if ($thisNumber > $routingNumber) { + $high = $mid - 1; + } else if ($thisNumber < $routingNumber) { + $low = $mid + 1; + } else { + $data = array('routing_number' => $thisNumber); + fseek($file, 26, SEEK_CUR); + $data['name'] = trim(fread($file, 36)); + $data['address'] = trim(fread($file, 36)); + $data['city'] = trim(fread($file, 20)); + $data['state'] = fread($file, 2); + $data['zip'] = fread($file, 5).'-'.fread($file, 4); + $data['phone'] = fread($file, 10); + break; + } + } + + if (!empty($data)) { + Cache::put('bankData:'.$routingNumber, $data, 5); + return $data; + } else { + Cache::put('bankData:'.$routingNumber, false, 5); + return null; + } + } } diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index 28391cf7140e..868a116719e8 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -10,6 +10,8 @@ use Request; use Response; use Session; use Datatable; +use Validator; +use Cache; use App\Models\Gateway; use App\Models\Invitation; use App\Models\Document; @@ -94,7 +96,7 @@ class PublicClientController extends BaseController $paymentTypes = $this->getPaymentTypes($client, $invitation); $paymentURL = ''; - if (count($paymentTypes)) { + if (count($paymentTypes) == 1) { $paymentURL = $paymentTypes[0]['url']; if (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) { $paymentURL = URL::to($paymentURL); @@ -126,11 +128,6 @@ class PublicClientController extends BaseController 'account' => $account, 'showApprove' => $showApprove, 'showBreadcrumbs' => false, - 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), - 'hideHeader' => $account->isNinjaAccount() || !$account->enable_client_portal, - 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), - 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'invoice' => $invoice->hidePrivateFields(), 'invitation' => $invitation, @@ -161,23 +158,67 @@ class PublicClientController extends BaseController $paymentTypes = []; $account = $client->account; - if ($client->getGatewayToken()) { - $paymentTypes[] = [ - 'url' => URL::to("payment/{$invitation->invitation_key}/token"), 'label' => trans('texts.use_card_on_file') - ]; - } - foreach(Gateway::$paymentTypes as $type) { - if ($account->getGatewayByType($type)) { - $typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type)); - $url = URL::to("/payment/{$invitation->invitation_key}/{$typeLink}"); + $paymentMethods = $this->paymentService->getClientPaymentMethods($client); - // PayPal doesn't allow being run in an iframe so we need to open in new tab - if ($type === PAYMENT_TYPE_PAYPAL && $account->iframe_url) { - $url = 'javascript:window.open("'.$url.'", "_blank")'; + if ($paymentMethods) { + foreach ($paymentMethods as $paymentMethod) { + if ($paymentMethod['type']->id != PAYMENT_TYPE_ACH || $paymentMethod['status'] == 'verified') { + + if ($paymentMethod['type']->id == PAYMENT_TYPE_ACH) { + $html = '
'.htmlentities($paymentMethod['bank_name']).'
'; + } else { + $code = htmlentities(str_replace(' ', '', strtolower($paymentMethod['type']->name))); + $html = ''.trans('; + } + + if ($paymentMethod['type']->id != PAYMENT_TYPE_ACH) { + $html .= '
'.trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($paymentMethod['expiration'], false)->format('m/y'))).'
'; + } else { + $html .= '
'; + } + $html .= '•••'.$paymentMethod['last4'].'
'; + + $paymentTypes[] = [ + 'url' => URL::to("/payment/{$invitation->invitation_key}/token/".$paymentMethod['id']), + 'label' => $html, + ]; + } + } + } + + + foreach(Gateway::$paymentTypes as $type) { + if ($gateway = $account->getGatewayByType($type)) { + $types = array($type); + + if ($type == PAYMENT_TYPE_STRIPE) { + $types = array(PAYMENT_TYPE_STRIPE_CREDIT_CARD); + if ($gateway->getAchEnabled()) { + $types[] = PAYMENT_TYPE_STRIPE_ACH; + } + } + + foreach($types as $type) { + $typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type)); + $url = URL::to("/payment/{$invitation->invitation_key}/{$typeLink}"); + + // PayPal doesn't allow being run in an iframe so we need to open in new tab + if ($type === PAYMENT_TYPE_PAYPAL && $account->iframe_url) { + $url = 'javascript:window.open("' . $url . '", "_blank")'; + } + + if ($type == PAYMENT_TYPE_STRIPE_CREDIT_CARD) { + $label = trans('texts.' . strtolower(PAYMENT_TYPE_CREDIT_CARD)); + } elseif ($type == PAYMENT_TYPE_STRIPE_ACH) { + $label = trans('texts.' . strtolower(PAYMENT_TYPE_DIRECT_DEBIT)); + } else { + $label = trans('texts.' . strtolower($type)); + } + + $paymentTypes[] = [ + 'url' => $url, 'label' => $label + ]; } - $paymentTypes[] = [ - 'url' => $url, 'label' => trans('texts.'.strtolower($type)) - ]; } } @@ -224,9 +265,6 @@ class PublicClientController extends BaseController 'color' => $color, 'account' => $account, 'client' => $client, - 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), - 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), - 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), ]; @@ -248,7 +286,7 @@ class PublicClientController extends BaseController ->addColumn('activity_type_id', function ($model) { $data = [ 'client' => Utils::getClientDisplayName($model), - 'user' => $model->is_system ? ('' . trans('texts.system') . '') : ($model->user_first_name . ' ' . $model->user_last_name), + 'user' => $model->is_system ? ('' . trans('texts.system') . '') : ($model->user_first_name . ' ' . $model->user_last_name), 'invoice' => trans('texts.invoice') . ' ' . $model->invoice, 'contact' => Utils::getClientDisplayName($model), 'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''), @@ -280,10 +318,7 @@ class PublicClientController extends BaseController $data = [ 'color' => $color, - 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), - 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), - 'clientViewCSS' => $account->clientViewCSS(), + 'account' => $account, 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.invoices'), 'entityType' => ENTITY_INVOICE, @@ -317,10 +352,7 @@ class PublicClientController extends BaseController $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), - 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), - 'clientViewCSS' => $account->clientViewCSS(), + 'account' => $account, 'clientFontUrl' => $account->getFontsUrl(), 'entityType' => ENTITY_PAYMENT, 'title' => trans('texts.payments'), @@ -345,8 +377,17 @@ class PublicClientController extends BaseController if (!$model->last4) return ''; $code = str_replace(' ', '', strtolower($model->payment_type)); $card_type = trans("texts.card_" . $code); - $expiration = trans('texts.card_expiration', array('expires'=>Utils::fromSqlDate($model->expiration, false)->format('m/y'))); - return ''.htmlentities($card_type).'  •••'.$model->last4.' '.$expiration; + if ($model->payment_type_id != PAYMENT_TYPE_ACH) { + $expiration = trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($model->expiration, false)->format('m/y'))); + return '' . htmlentities($card_type) . '  •••' . $model->last4 . ' ' . $expiration; + } else { + $bankData = PaymentController::getBankData($model->routing_number); + if (is_array($bankData)) { + return $bankData['name'].'  •••' . $model->last4; + } else { + return '' . htmlentities($card_type) . '  •••' . $model->last4; + } + } }) ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); }) ->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); }) @@ -397,10 +438,7 @@ class PublicClientController extends BaseController $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), - 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), - 'clientViewCSS' => $account->clientViewCSS(), + 'account' => $account, 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.quotes'), 'entityType' => ENTITY_QUOTE, @@ -435,10 +473,7 @@ class PublicClientController extends BaseController $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), - 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), - 'clientViewCSS' => $account->clientViewCSS(), + 'account' => $account, 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.documents'), 'entityType' => ENTITY_DOCUMENT, @@ -632,4 +667,169 @@ class PublicClientController extends BaseController return DocumentController::getDownloadResponse($document); } + public function paymentMethods() + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + + $client = $invitation->invoice->client; + $account = $client->account; + $paymentMethods = $this->paymentService->getClientPaymentMethods($client); + + $data = array( + 'account' => $account, + 'color' => $account->primary_color ? $account->primary_color : '#0b4d78', + 'client' => $client, + 'clientViewCSS' => $account->clientViewCSS(), + 'clientFontUrl' => $account->getFontsUrl(), + 'paymentMethods' => $paymentMethods, + 'gateway' => $account->getTokenGateway(), + 'title' => trans('texts.payment_methods') + ); + + return response()->view('payments.paymentmethods', $data); + } + + public function verifyPaymentMethod() + { + $sourceId = Input::get('source_id'); + $amount1 = Input::get('verification1'); + $amount2 = Input::get('verification2'); + + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + + $client = $invitation->invoice->client; + $result = $this->paymentService->verifyClientPaymentMethod($client, $sourceId, $amount1, $amount2); + + if (is_string($result)) { + Session::flash('error', $result); + } else { + Session::flash('message', trans('texts.payment_method_verified')); + } + + return redirect()->to('/client/paymentmethods/'); + } + + public function removePaymentMethod($sourceId) + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + + $client = $invitation->invoice->client; + $result = $this->paymentService->removeClientPaymentMethod($client, $sourceId); + + if (is_string($result)) { + Session::flash('error', $result); + } else { + Session::flash('message', trans('texts.payment_method_removed')); + } + + return redirect()->to('/client/paymentmethods/'); + } + + public function addPaymentMethod($paymentType) + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + + $invoice = $invitation->invoice; + $client = $invitation->invoice->client; + $account = $client->account; + + $typeLink = $paymentType; + $paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType); + $accountGateway = $invoice->client->account->getTokenGateway(); + $gateway = $accountGateway->gateway; + $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); + + + $data = [ + 'showBreadcrumbs' => false, + 'client' => $client, + 'contact' => $invitation->contact, + 'gateway' => $gateway, + 'accountGateway' => $accountGateway, + 'acceptedCreditCardTypes' => $acceptedCreditCardTypes, + 'paymentType' => $paymentType, + 'countries' => Cache::get('countries'), + 'currencyId' => $client->getCurrencyId(), + 'currencyCode' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD'), + 'account' => $account, + 'url' => URL::to('client/paymentmethods/add/'.$typeLink), + 'clientFontUrl' => $account->getFontsUrl(), + 'showAddress' => $accountGateway->show_address, + ]; + + if ($paymentType == PAYMENT_TYPE_STRIPE_ACH) { + + $data['currencies'] = Cache::get('currencies'); + } + + if ($gateway->id == GATEWAY_BRAINTREE) { + $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); + } + + return View::make('payments.add_paymentmethod', $data); + } + + public function postAddPaymentMethod($paymentType) + { + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + + $paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType); + $client = $invitation->invoice->client; + $account = $client->account; + + $accountGateway = $account->getGatewayByType($paymentType); + $sourceToken = $accountGateway->gateway_id == GATEWAY_STRIPE ? Input::get('stripeToken'):Input::get('payment_method_nonce'); + + if ($sourceToken) { + $details = array('token' => $sourceToken); + $gateway = $this->paymentService->createGateway($accountGateway); + $sourceId = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id); + } else { + return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); + } + + if(empty($sourceId)) { + $this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway); + return Redirect::to('payment/'.$invitationKey)->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')); + return Redirect::to('client/paymentmethods/add/' . $paymentType); + } else { + Session::flash('message', trans('texts.payment_method_added')); + return redirect()->to('/client/paymentmethods/'); + } + } + + public function setDefaultPaymentMethod(){ + if (!$invitation = $this->getInvitation()) { + return $this->returnError(); + } + + $validator = Validator::make(Input::all(), array('source' => 'required')); + if ($validator->fails()) { + return Redirect::to('client/paymentmethods'); + } + + $client = $invitation->invoice->client; + $result = $this->paymentService->setClientDefaultPaymentMethod($client, Input::get('source')); + + if (is_string($result)) { + Session::flash('error', $result); + } else { + Session::flash('message', trans('texts.payment_method_set_as_default')); + } + + return redirect()->to('/client/paymentmethods/'); + } } diff --git a/app/Http/routes.php b/app/Http/routes.php index d6cd8f497aec..d21883c566b9 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -40,9 +40,15 @@ Route::group(['middleware' => 'auth:client'], function() { Route::get('download/{invitation_key}', 'PublicClientController@download'); Route::get('view', 'HomeController@viewLogo'); Route::get('approve/{invitation_key}', 'QuoteController@approve'); - Route::get('payment/{invitation_key}/{payment_type?}', 'PaymentController@show_payment'); + Route::get('payment/{invitation_key}/{payment_type?}/{source_id?}', 'PaymentController@show_payment'); Route::post('payment/{invitation_key}', 'PaymentController@do_payment'); Route::match(['GET', 'POST'], 'complete', 'PaymentController@offsite_payment'); + Route::get('client/paymentmethods', 'PublicClientController@paymentMethods'); + Route::post('client/paymentmethods/verify', 'PublicClientController@verifyPaymentMethod'); + Route::get('client/paymentmethods/add/{payment_type}', 'PublicClientController@addPaymentMethod'); + Route::post('client/paymentmethods/add/{payment_type}', 'PublicClientController@postAddPaymentMethod'); + Route::post('client/paymentmethods/default', 'PublicClientController@setDefaultPaymentMethod'); + Route::post('client/paymentmethods/{source_id}/remove', 'PublicClientController@removePaymentMethod'); Route::get('client/quotes', 'PublicClientController@quoteIndex'); Route::get('client/invoices', 'PublicClientController@invoiceIndex'); Route::get('client/documents', 'PublicClientController@documentIndex'); @@ -60,6 +66,7 @@ Route::group(['middleware' => 'auth:client'], function() { }); +Route::get('bank/{routing_number}', 'PaymentController@getBankInfo'); Route::get('license', 'PaymentController@show_license_payment'); Route::post('license', 'PaymentController@do_license_payment'); Route::get('claim_license', 'PaymentController@claim_license'); @@ -615,6 +622,7 @@ if (!defined('CONTACT_EMAIL')) { define('TOKEN_BILLING_ALWAYS', 4); define('PAYMENT_TYPE_CREDIT', 1); + define('PAYMENT_TYPE_ACH', 5); define('PAYMENT_TYPE_VISA', 6); define('PAYMENT_TYPE_MASTERCARD', 7); define('PAYMENT_TYPE_AMERICAN_EXPRESS', 8); @@ -633,6 +641,8 @@ if (!defined('CONTACT_EMAIL')) { define('PAYMENT_TYPE_PAYPAL', 'PAYMENT_TYPE_PAYPAL'); define('PAYMENT_TYPE_STRIPE', 'PAYMENT_TYPE_STRIPE'); + define('PAYMENT_TYPE_STRIPE_CREDIT_CARD', 'PAYMENT_TYPE_STRIPE_CREDIT_CARD'); + define('PAYMENT_TYPE_STRIPE_ACH', 'PAYMENT_TYPE_STRIPE_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 44b49d44e1fe..234d9a5b63ca 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -379,6 +379,10 @@ class Account extends Eloquent public function getGatewayByType($type = PAYMENT_TYPE_ANY) { + if ($type == PAYMENT_TYPE_STRIPE_ACH || $type == PAYMENT_TYPE_STRIPE_CREDIT_CARD) { + $type = PAYMENT_TYPE_STRIPE; + } + foreach ($this->account_gateways as $gateway) { if (!$type || $type == PAYMENT_TYPE_ANY) { return $gateway; diff --git a/app/Ninja/Repositories/PaymentRepository.php b/app/Ninja/Repositories/PaymentRepository.php index c1c8d3e851dd..221cea40ce99 100644 --- a/app/Ninja/Repositories/PaymentRepository.php +++ b/app/Ninja/Repositories/PaymentRepository.php @@ -56,6 +56,7 @@ class PaymentRepository extends BaseRepository 'payments.refunded', 'payments.expiration', 'payments.last4', + 'payments.routing_number', 'invoices.is_deleted as invoice_is_deleted', 'gateways.name as gateway_name', 'gateways.id as gateway_id', @@ -107,6 +108,7 @@ class PaymentRepository extends BaseRepository 'clients.public_id as client_public_id', 'payments.amount', 'payments.payment_date', + 'payments.payment_type_id', 'invoices.public_id as invoice_public_id', 'invoices.invoice_number', 'contacts.first_name', @@ -117,6 +119,7 @@ class PaymentRepository extends BaseRepository 'payments.refunded', 'payments.expiration', 'payments.last4', + 'payments.routing_number', 'payments.payment_status_id', 'payment_statuses.name as payment_status_name' ); diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 6bfcf2e12559..8ad8a990eb34 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -5,6 +5,7 @@ use Auth; use URL; use DateTime; use Event; +use Cache; use Omnipay; use Session; use CreditCard; @@ -13,6 +14,7 @@ use App\Models\Account; use App\Models\Country; use App\Models\Client; use App\Models\Invoice; +use App\Http\Controllers\PaymentController; use App\Models\AccountGatewayToken; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\AccountRepository; @@ -155,8 +157,159 @@ class PaymentService extends BaseService ]; } + public function getClientPaymentMethods($client) { + $token = $client->getGatewayToken($accountGateway); + if (!$token) { + return null; + } + + $gateway = $this->createGateway($accountGateway); + + $paymentMethods = array(); + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + $response = $gateway->fetchCustomer(array('customerReference' => $token))->send(); + if (!$response->isSuccessful()) { + return null; + } + + $data = $response->getData(); + $default_source = $data['default_source']; + $sources = isset($data['sources']) ? $data['sources']['data'] : $data['cards']['data']; + + $paymentTypes = Cache::get('paymentTypes'); + $currencies = Cache::get('currencies'); + foreach ($sources as $source) { + if ($source['object'] == 'bank_account') { + $paymentMethods[] = array( + 'id' => $source['id'], + 'default' => $source['id'] == $default_source, + 'type' => $paymentTypes->find(PAYMENT_TYPE_ACH), + 'currency' => $currencies->where('code', strtoupper($source['currency']))->first(), + 'last4' => $source['last4'], + 'routing_number' => $source['routing_number'], + 'bank_name' => $source['bank_name'], + 'status' => $source['status'], + ); + } elseif ($source['object'] == 'card') { + $paymentMethods[] = array( + 'id' => $source['id'], + 'default' => $source['id'] == $default_source, + 'type' => $paymentTypes->find($this->parseCardType($source['brand'])), + 'last4' => $source['last4'], + 'expiration' => $source['exp_year'] . '-' . $source['exp_month'] . '-00', + ); + } + } + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { + + } + + return $paymentMethods; + } + + public function verifyClientPaymentMethod($client, $sourceId, $amount1, $amount2) { + $token = $client->getGatewayToken($accountGateway); + if ($accountGateway->gateway_id != GATEWAY_STRIPE) { + return 'Unsupported gateway'; + } + + try{ + // Omnipay doesn't support verifying payment methods + // Also, it doesn't want to urlencode without putting numbers inside the brackets + $response = (new \GuzzleHttp\Client(['base_uri'=>'https://api.stripe.com/v1/']))->request( + 'POST', + 'customers/'.$token.'/sources/'.$sourceId.'/verify', + [ + 'body' => 'amounts[]='.intval($amount1).'&amounts[]='.intval($amount2), + 'headers' => ['content-type' => 'application/x-www-form-urlencoded'], + 'auth' => [$accountGateway->getConfig()->apiKey,''], + ] + ); + return json_decode($response->getBody(), true); + } catch (\GuzzleHttp\Exception\BadResponseException $e) { + $response = $e->getResponse(); + $body = json_decode($response->getBody(), true); + + if ($body && $body['error'] && $body['error']['type'] == 'invalid_request_error') { + return $body['error']['message']; + } + + return $e->getMessage(); + } + } + + public function removeClientPaymentMethod($client, $sourceId) { + $token = $client->getGatewayToken($accountGateway/* return parameter */); + if (!$token) { + return null; + } + + $gateway = $this->createGateway($accountGateway); + + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + $response = $gateway->deleteCard(array('customerReference' => $token, 'cardReference'=>$sourceId))->send(); + if (!$response->isSuccessful()) { + return $response->getMessage(); + } + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { + + } + + return true; + } + + public function setClientDefaultPaymentMethod($client, $sourceId) { + $token = $client->getGatewayToken($accountGateway/* return parameter */); + if (!$token) { + return null; + } + + $gateway = $this->createGateway($accountGateway); + + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + try{ + // Omnipay doesn't support setting a default source + $response = (new \GuzzleHttp\Client(['base_uri'=>'https://api.stripe.com/v1/']))->request( + 'POST', + 'customers/'.$token, + [ + 'body' => 'default_card='.$sourceId, + 'headers' => ['content-type' => 'application/x-www-form-urlencoded'], + 'auth' => [$accountGateway->getConfig()->apiKey,''], + ] + ); + return true; + } catch (\GuzzleHttp\Exception\BadResponseException $e) { + $response = $e->getResponse(); + $body = json_decode($response->getBody(), true); + + if ($body && $body['error'] && $body['error']['type'] == 'invalid_request_error') { + return $body['error']['message']; + } + + return $e->getMessage(); + } + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { + + } + + return true; + } + public function createToken($gateway, $details, $accountGateway, $client, $contactId, &$customerReference = null) { + $customerReference = $client->getGatewayToken(); + + if ($customerReference) { + $details['customerReference'] = $customerReference; + + $customerResponse = $gateway->fetchCustomer(array('customerReference'=>$customerReference))->send(); + + if (!$customerResponse->isSuccessful()){ + $customerReference = null; // The customer might not exist anymore + } + } + if ($accountGateway->gateway->id == GATEWAY_STRIPE) { $tokenResponse = $gateway->createCard($details)->send(); $cardReference = $tokenResponse->getCardReference(); @@ -170,10 +323,15 @@ class PaymentService extends BaseService } } } elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) { - $tokenResponse = $gateway->createCustomer(array('customerData'=>array()))->send(); + if (!$customerReference) { + $tokenResponse = $gateway->createCustomer(array('customerData' => array()))->send(); + if ($tokenResponse->isSuccessful()) { + $customerReference = $tokenResponse->getCustomerData()->id; + } + } - if ($tokenResponse->isSuccessful()) { - $details['customerId'] = $customerReference = $tokenResponse->getCustomerData()->id; + if ($customerReference) { + $details['customerId'] = $customerReference; $tokenResponse = $gateway->createPaymentMethod($details)->send(); $cardReference = $tokenResponse->getData()->paymentMethod->token; @@ -264,54 +422,26 @@ class PaymentService extends BaseService } if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - $card = $purchaseResponse->getSource(); - if (!$card) { - $card = $purchaseResponse->getCard(); - } + $data = $purchaseResponse->getData(); + $source = !empty($data['source'])?$data['source']:$data['card']; - if ($card) { - $payment->last4 = $card['last4']; - $payment->expiration = $card['exp_year'] . '-' . $card['exp_month'] . '-00'; - $stripe_card_types = array( - 'Visa' => PAYMENT_TYPE_VISA, - 'American Express' => PAYMENT_TYPE_AMERICAN_EXPRESS, - 'MasterCard' => PAYMENT_TYPE_MASTERCARD, - 'Discover' => PAYMENT_TYPE_DISCOVER, - 'JCB' => PAYMENT_TYPE_JCB, - 'Diners Club' => PAYMENT_TYPE_DINERS, - ); + if ($source) { + $payment->last4 = $source['last4']; - if (!empty($stripe_card_types[$card['brand']])) { - $payment->payment_type_id = $stripe_card_types[$card['brand']]; - } else { - $payment->payment_type_id = PAYMENT_TYPE_CREDIT_CARD_OTHER; + if ($source['object'] == 'bank_account') { + $payment->routing_number = $source['routing_number']; + $payment->payment_type_id = PAYMENT_TYPE_ACH; + } + else{ + $payment->expiration = $card['exp_year'] . '-' . $card['exp_month'] . '-00'; + $payment->payment_type_id = $this->parseCardType($card['brand']); } } } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { $card = $purchaseResponse->getData()->transaction->creditCardDetails; $payment->last4 = $card->last4; $payment->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00'; - - $braintree_card_types = array( - 'Visa' => PAYMENT_TYPE_VISA, - 'American Express' => PAYMENT_TYPE_AMERICAN_EXPRESS, - 'MasterCard' => PAYMENT_TYPE_MASTERCARD, - 'Discover' => PAYMENT_TYPE_DISCOVER, - 'JCB' => PAYMENT_TYPE_JCB, - 'Diners Club' => PAYMENT_TYPE_DINERS, - 'Carte Blanche' => PAYMENT_TYPE_CARTE_BLANCHE, - 'China UnionPay' => PAYMENT_TYPE_UNIONPAY, - 'Laser' => PAYMENT_TYPE_LASER, - 'Maestro' => PAYMENT_TYPE_MAESTRO, - 'Solo' => PAYMENT_TYPE_SOLO, - 'Switch' => PAYMENT_TYPE_SWITCH, - ); - - if (!empty($braintree_card_types[$card->cardType])) { - $payment->payment_type_id = $braintree_card_types[$card->cardType]; - } else { - $payment->payment_type_id = PAYMENT_TYPE_CREDIT_CARD_OTHER; - } + $payment->payment_type_id = $this->parseCardType($card->cardType); } if ($payerId) { @@ -375,6 +505,29 @@ class PaymentService extends BaseService return $payment; } + + private function parseCardType($cardName) { + $cardTypes = array( + 'Visa' => PAYMENT_TYPE_VISA, + 'American Express' => PAYMENT_TYPE_AMERICAN_EXPRESS, + 'MasterCard' => PAYMENT_TYPE_MASTERCARD, + 'Discover' => PAYMENT_TYPE_DISCOVER, + 'JCB' => PAYMENT_TYPE_JCB, + 'Diners Club' => PAYMENT_TYPE_DINERS, + 'Carte Blanche' => PAYMENT_TYPE_CARTE_BLANCHE, + 'China UnionPay' => PAYMENT_TYPE_UNIONPAY, + 'Laser' => PAYMENT_TYPE_LASER, + 'Maestro' => PAYMENT_TYPE_MAESTRO, + 'Solo' => PAYMENT_TYPE_SOLO, + 'Switch' => PAYMENT_TYPE_SWITCH + ); + + if (!empty($cardTypes[$cardName])) { + return $cardTypes[$cardName]; + } else { + return PAYMENT_TYPE_CREDIT_CARD_OTHER; + } + } private function detectCardType($number) { @@ -493,12 +646,21 @@ class PaymentService extends BaseService ], [ 'source', - function ($model) { + function ($model) { if (!$model->last4) return ''; $code = str_replace(' ', '', strtolower($model->payment_type)); $card_type = trans("texts.card_" . $code); - $expiration = trans('texts.card_expiration', array('expires'=>Utils::fromSqlDate($model->expiration, false)->format('m/y'))); - return ''.htmlentities($card_type).'  •••'.$model->last4.' '.$expiration; + if ($model->payment_type_id != PAYMENT_TYPE_ACH) { + $expiration = trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($model->expiration, false)->format('m/y'))); + return '' . htmlentities($card_type) . '  •••' . $model->last4 . ' ' . $expiration; + } else { + $bankData = PaymentController::getBankData($model->routing_number); + if (is_array($bankData)) { + return $bankData['name'].'  •••' . $model->last4; + } else { + return '' . htmlentities($card_type) . '  •••' . $model->last4; + } + } } ], [ diff --git a/composer.json b/composer.json index 8eafa6224f42..01c6a26f51cf 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "omnipay/mollie": "dev-master#22956c1a62a9662afa5f5d119723b413770ac525", "omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248", "omnipay/gocardless": "dev-master", - "omnipay/stripe": "2.3.2", + "omnipay/stripe": "dev-master", "laravel/framework": "5.2.*", "laravelcollective/html": "5.2.*", "laravelcollective/bus": "5.2.*", @@ -73,7 +73,8 @@ "league/flysystem-aws-s3-v3": "~1.0", "league/flysystem-rackspace": "~1.0", "barracudanetworks/archivestream-php": "^1.0", - "omnipay/braintree": "~2.0@dev" + "omnipay/braintree": "~2.0@dev", + "gatepay/FedACHdir": "dev-master@dev" }, "require-dev": { "phpunit/phpunit": "~4.0", @@ -122,5 +123,23 @@ }, "config": { "preferred-install": "dist" - } + }, + "repositories": [ + { + "type": "package", + "package": { + "name": "gatepay/FedACHdir", + "version": "dev-master", + "dist": { + "url": "https://github.com/gatepay/FedACHdir/archive/master.zip", + "type": "zip" + }, + "source": { + "url": "git@github.com:gatepay/FedACHdir.git", + "type": "git", + "reference": "origin/master" + } + } + } + ] } diff --git a/composer.lock b/composer.lock index 787493b08070..23b08f2d7e9d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "e2b43d6bd5e87dfb9b3be07e89a2bd9f", - "content-hash": "480134957ff37fd0f46b5d90909da660", + "hash": "039f9d8f2e342f6c05dadb3523883a47", + "content-hash": "fd558fd1e187969baf015eab8e288e5b", "packages": [ { "name": "agmscode/omnipay-agms", @@ -2011,6 +2011,17 @@ ], "time": "2015-01-16 08:41:13" }, + { + "name": "gatepay/FedACHdir", + "version": "dev-master", + "source": { + "type": "git", + "url": "git@github.com:gatepay/FedACHdir.git", + "reference": "origin/master" + }, + "type": "library", + "time": "2016-04-29 12:01:22" + }, { "name": "guzzle/guzzle", "version": "v3.8.1", @@ -5771,16 +5782,16 @@ }, { "name": "omnipay/stripe", - "version": "v2.3.2", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-stripe.git", - "reference": "fe05ddd73d9ae38ca026dbc270ca98aaf3f99da4" + "reference": "0ea7a647ee01e29c152814e11c2ea6307e5b0db9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-stripe/zipball/fe05ddd73d9ae38ca026dbc270ca98aaf3f99da4", - "reference": "fe05ddd73d9ae38ca026dbc270ca98aaf3f99da4", + "url": "https://api.github.com/repos/thephpleague/omnipay-stripe/zipball/0ea7a647ee01e29c152814e11c2ea6307e5b0db9", + "reference": "0ea7a647ee01e29c152814e11c2ea6307e5b0db9", "shasum": "" }, "require": { @@ -5824,7 +5835,7 @@ "payment", "stripe" ], - "time": "2016-03-19 08:35:06" + "time": "2016-04-26 08:34:50" }, { "name": "omnipay/targetpay", @@ -9985,6 +9996,7 @@ "omnipay/mollie": 20, "omnipay/2checkout": 20, "omnipay/gocardless": 20, + "omnipay/stripe": 20, "anahkiasen/former": 20, "chumper/datatable": 20, "intervention/image": 20, @@ -10005,7 +10017,8 @@ "labs7in0/omnipay-wechat": 20, "laracasts/presenter": 20, "jlapp/swaggervel": 20, - "omnipay/braintree": 20 + "omnipay/braintree": 20, + "gatepay/fedachdir": 20 }, "prefer-stable": false, "prefer-lowest": false, diff --git a/public/images/credit_cards/ach.png b/public/images/credit_cards/ach.png new file mode 100644 index 0000000000000000000000000000000000000000..16cf86bdb3ecf921944306c6b7fb6eff3b332497 GIT binary patch literal 1949 zcmV;O2V(e%P)y{D4^00%uuL_t(o!_}C7Y?W6T$3M?G=WTC$ zZ*Ol~?m84kaplJX%?cQ4JF7ybOq~89YX$^mEW}vRU=Ti|AR8^)AjXl?LTn9R!*5>fklAaj6ln#PoMtmmaSXeH(FZkTL<2?Z|>V? zA6mCAT~bo=cVNm`7#R!C^UBN1*DPDUeBGjk2B#ntruSdR={b9f2{rf9)6+>`Uy?1G zHm3iyW5>pHI{gzMd9B9u^*CElQ1I1;`|tnThRvH7R#(r`v66AP-4{6B@@G0fIDpA7 zq-fj(3L`OQe{#0E=SvIkI(p>Dx6YkAcLZqtUou=h|L(h=Yg)5rT3KnCl$MS|Db0!3 zUgquQ^^{f4BAx0(8HaFu0<%^;jOSTgH$@_Gp4ay5aUb8j>5a2z&n^c(ype#%S65eW z|Mu$D3-6dQQ?LccRn-kYoefRJYA=cF?s$HtmlGgJPTN`So(S|l1e4` z<+g3<7oLCq(Nrq+Fwi#=0BVFl3o0rq9$UA5ecQHQ{rcXT+B#K!%dI#8*i13`1#}*N z*9VzlMCm~sg$iPXi^}=ts_&SWcKmVe+?txxz=E-0SYol*@=B={|9eA@jZIZoh16PN+#BQg~|mFU_y}`hW*;h^tUOJz3182(&GJm zZYcN(P&i4%F58n1mL(TU=XZ@wwB$r?cGIh`x#Qb zU3~thyRo9L3OIPBKh;gTKSA@;Pp6-4ZeHKh)AJ~h8VTS@b8BmxS2s1)S5#C;JRT?C zaRLJ}BETOV-AC8yV@$05GJjZc2VdOsI)%4P8V0anJqy+)nd~HyxM&}H48XmxKFgAeaSv;>7 zH{HXFzkea!{N$7CIyyQw$fSx2`_hgbeq+zYdKM7@5&X(eBqIKFixLF!+hl4HR8V8# z-$z>LIeU^xpKHkeXA#7Ll>m4S8CDcni-;f00=S-!-r^yWM1U=uHevdb$)|Sh+*x|z z!i63{X{}XfXJ-b07{naTQddH7jVrxGrX;|7@~bm=0mA3TWX`QzU;V+QF^F({2A z;C8oDJ7>;3V9$W;HCzxGCl5?uG?~C?jfh~BLPT&J$N%PnAQ)qOumR9q;0i$-gEGNl za|!?5^Rn%nynGOa#={#Tz|C-LiZ%u%L3AgU~`^DDSy1c zNf$6$W5thytiwtpUYh=)NF0t~$2eH+V6j;Ph@gYo7~i;OU7zv5p{vP4#P6?64q#)j z8HUBTC(8IAo~>4-AJ7 zA3o|@d*&CLH?Q2gckhV-44*i4YDFXxSunGzYRKt`M5DuMArh?`l_Vn3=&<7dR87tF zBL@#o?>K*c74U;><89lwKjeDe7vkmRHIZ=mrn<{bUbk`k literal 0 HcmV?d00001 diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 467e836a2820..83fdb5481daa 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -502,7 +502,7 @@ $LANG = array( 'resend_confirmation' => 'Resend confirmation email', 'confirmation_resent' => 'The confirmation email was resent', 'gateway_help_42' => ':link to sign up for BitPay.
Note: use a Legacy API Key, not an API token.', - 'payment_type_credit_card' => 'Other Providers', + 'payment_type_credit_card' => 'Credit Card', 'payment_type_paypal' => 'PayPal', 'payment_type_bitcoin' => 'Bitcoin', 'knowledge_base' => 'Knowledge Base', @@ -1205,6 +1205,7 @@ $LANG = array( 'card_solo' => 'Solo', 'card_switch' => 'Switch', 'card_visacard' => 'Visa', + 'card_ach' => 'ACH', 'payment_type_stripe' => 'Stripe', 'ach' => 'ACH', @@ -1218,6 +1219,39 @@ $LANG = array( 'public_key' => 'Public Key', 'plaid_optional' => '(optional)', 'plaid_environment_help' => 'When a Stripe test key is given, Plaid\'s development environement (tartan) will be used.', + 'other_providers' => 'Other Providers', + 'country_not_supported' => 'That country is not supported.', + 'invalid_routing_number' => 'The routing number is not valid.', + 'invalid_account_number' => 'The account number is not valid.', + 'account_number_mismatch' => 'The account numbers do not match.', + 'missing_account_holder_type' => 'Please select an individual or company account.', + 'missing_account_holder_name' => 'Please enter the account holder\'s name.', + 'routing_number' => 'Routing Number', + 'confirm_account_number' => 'Confirm Account Number', + 'individual_account' => 'Individual Account', + 'company_account' => 'Company Account', + 'account_holder_name' => 'Account Holder Name', + 'add_account' => 'Add Account', + 'payment_methods' => 'Payment Methods', + 'complete_verification' => 'Complete Verification', + 'verification_amount1' => 'Amount 1', + 'verification_amount2' => 'Amount 2', + 'payment_method_verified' => 'Verification completed successfully', + 'verification_failed' => 'Verification Failed', + 'remove_payment_method' => 'Remove Payment Method', + 'confirm_remove_payment_method' => 'Are you sure you want to remove this payment method?', + 'remove' => 'Remove', + 'payment_method_removed' => 'Removed payment method.', + 'bank_account_verification_help' => 'We have made two deposits into your account with the description "VERIFICATION". These deposits will take 1-2 business days to appear on your statement. Please enter the amounts below.', + 'bank_account_verification_next_steps' => 'We have made two deposits into your account with the description "VERIFICATION". These deposits will take 1-2 business days to appear on your statement. + Once you have the amounts, come back to this payment methods page and click "Complete Verification" next to the account.', + 'unknown_bank' => 'Unknown Bank', + 'ach_verification_delay_help' => 'You will be able to use the account after completing verification. Verification usually takes 1-2 business days.', + 'add_credit_card' => 'Add Credit Card', + 'payment_method_added' => 'Added payment method.', + 'use_for_auto_bill' => 'Use For Autobill', + 'used_for_auto_bill' => 'Autobill Payment Method', + 'payment_method_set_as_default' => 'Set Autobill payment method.' ); return $LANG; diff --git a/resources/views/clientauth/login.blade.php b/resources/views/clientauth/login.blade.php index 6ad28a593831..e5baee7c05aa 100644 --- a/resources/views/clientauth/login.blade.php +++ b/resources/views/clientauth/login.blade.php @@ -69,7 +69,7 @@ {{ Former::populateField('remember', 'true') }}