From b8170f0324018c0ed6a7e9f692bf305787c80906 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 23 Apr 2016 16:40:19 -0400 Subject: [PATCH 001/386] 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 011/386] 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') }} From 147c51f6f9d94a84020e837d27db68c16994f05e Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 10 May 2016 22:39:54 +0300 Subject: [PATCH 038/386] Clean up diffs between branches --- app/Http/routes.php | 1 + composer.lock | 2 +- resources/views/public/header.blade.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index 5b383c0822c8..15f0b247a144 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -1,5 +1,6 @@ Date: Tue, 10 May 2016 22:41:30 +0300 Subject: [PATCH 039/386] Clean up diffs between branches --- resources/views/header.blade.php | 2 +- resources/views/public/header.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index e3bdf5f5be28..7b4fe014e71f 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -812,4 +812,4 @@

 

-@stop \ No newline at end of file +@stop diff --git a/resources/views/public/header.blade.php b/resources/views/public/header.blade.php index 017570e5f919..4f753b4a8d3e 100644 --- a/resources/views/public/header.blade.php +++ b/resources/views/public/header.blade.php @@ -189,4 +189,4 @@ - @stop \ No newline at end of file + @stop From 1dc692d57859893e8879b4929974619419073900 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 10 May 2016 22:51:17 +0300 Subject: [PATCH 040/386] Updated readme --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 93b30a8003d4..e435d9e90657 100644 --- a/readme.md +++ b/readme.md @@ -31,10 +31,11 @@ ### Features * Built using Laravel 5.2 * Live PDF generation using [pdfmake](http://pdfmake.org/) -* Integrates with 50+ payment providers with [OmniPay](https://github.com/thephpleague/omnipay) +* Integrates with 50+ payment providers with [Omnipay](https://github.com/thephpleague/omnipay) * Recurring invoices with auto-billing * Expenses and vendors * Tasks with time-tracking +* File Attachments * Multi-user/multi-company support * Tax rates and payment terms * Reminder emails From 88ec714cd561bc4cf3613f975170b556499f8428 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 10 May 2016 18:46:32 -0400 Subject: [PATCH 041/386] Store Stripe card summary locally --- app/Http/Controllers/PaymentController.php | 189 ++++----- .../Controllers/PublicClientController.php | 49 +-- app/Http/routes.php | 4 + app/Models/AccountGatewayToken.php | 14 + app/Models/Client.php | 7 +- app/Models/Payment.php | 16 +- app/Models/PaymentMethod.php | 157 ++++++++ app/Services/PaymentService.php | 372 ++++++++++-------- .../2016_05_10_144219_wepay_integration.php | 83 ++++ resources/lang/en/texts.php | 7 +- .../views/accounts/account_gateway.blade.php | 21 +- .../payments/paymentmethods_list.blade.php | 33 +- 12 files changed, 627 insertions(+), 325 deletions(-) create mode 100644 app/Models/PaymentMethod.php create mode 100644 database/migrations/2016_05_10_144219_wepay_integration.php diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 8618fafdba58..491bac77033a 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -20,6 +20,7 @@ use App\Models\PaymentType; use App\Models\License; use App\Models\Payment; use App\Models\Affiliate; +use App\Models\PaymentMethod; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\AccountRepository; @@ -406,6 +407,7 @@ class PaymentController extends BaseController $account = $client->account; $paymentType = Session::get($invitation->id . 'payment_type'); $accountGateway = $account->getGatewayByType($paymentType); + $paymentMethod = null; $rules = [ 'first_name' => 'required', @@ -435,6 +437,17 @@ class PaymentController extends BaseController 'country_id' => 'required', ]); } + + if ($useToken) { + if(!$sourceId) { + Session::flash('error', trans('texts.no_payment_method_specified')); + return Redirect::to('payment/' . $invitationKey)->withInput(Request::except('cvv')); + } else { + $customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return parameter*/); + $paymentMethod = PaymentMethod::scope($sourceId, $account->id, $accountGatewayToken->id)->firstOrFail(); + $sourceReference = $paymentMethod->source_reference; + } + } if ($onSite) { $validator = Validator::make(Input::all(), $rules); @@ -476,11 +489,9 @@ class PaymentController extends BaseController } if ($useToken) { - $details['customerReference'] = $client->getGatewayToken(); + $details['customerReference'] = $customerReference; unset($details['token']); - if ($sourceId) { - $details['cardReference'] = $sourceId; - } + $details['cardReference'] = $sourceReference; } 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) { @@ -509,13 +520,8 @@ class PaymentController extends BaseController } if ($useToken) { - $details['customerId'] = $customerId = $client->getGatewayToken(); - if (!$sourceId) { - $customer = $gateway->findCustomer($customerId)->send(); - $details['paymentMethodToken'] = $customer->getData()->paymentMethods[0]->token; - } else { - $details['paymentMethodToken'] = $sourceId; - } + $details['customerId'] = $customerReference; + $details['paymentMethodToken'] = $sourceReference; unset($details['token']); } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) { $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */); @@ -536,7 +542,6 @@ class PaymentController extends BaseController $response = $gateway->purchase($details)->send(); - if ($accountGateway->gateway_id == GATEWAY_EWAY) { $ref = $response->getData()['AccessCode']; } elseif ($accountGateway->gateway_id == GATEWAY_TWO_CHECKOUT) { @@ -563,7 +568,7 @@ class PaymentController extends BaseController } if ($response->isSuccessful()) { - $payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, null, $details, $response); + $payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, null, $details, $paymentMethod, $response); Session::flash('message', trans('texts.applied_payment')); if ($account->account_key == NINJA_ACCOUNT_KEY) { @@ -660,7 +665,7 @@ class PaymentController extends BaseController if ($response->isCancelled()) { // do nothing } elseif ($response->isSuccessful()) { - $payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, $payerId, $details, $purchaseResponse); + $payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, $payerId, $details, null, $purchaseResponse); Session::flash('message', trans('texts.applied_payment')); } else { $this->error('offsite', $response->getMessage(), $accountGateway); @@ -738,7 +743,7 @@ class PaymentController extends BaseController ], 400); } - $data = static::getBankData($routingNumber); + $data = PaymentMethod::lookupBankData($routingNumber); if (is_string($data)) { return response()->json([ @@ -753,75 +758,10 @@ class PaymentController extends BaseController ], 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; - } - } - public function handlePaymentWebhook($accountKey, $gatewayId) { $gatewayId = intval($gatewayId); - if ($gatewayId != GATEWAY_STRIPE) { - return response()->json([ - 'message' => 'Unsupported gateway', - ], 404); - } - $account = Account::where('accounts.account_key', '=', $accountKey)->first(); if (!$account) { @@ -830,27 +770,48 @@ class PaymentController extends BaseController ], 404); } + $accountGateway = $account->getGatewayConfig(intval($gatewayId)); + + if (!$accountGateway) { + return response()->json([ + 'message' => 'Unknown gateway', + ], 404); + } + + switch($gatewayId) { + case GATEWAY_STRIPE: + return $this->handleStripeWebhook($accountGateway); + default: + return response()->json([ + 'message' => 'Unsupported gateway', + ], 404); + } + } + + protected function handleStripeWebhook($accountGateway) { $eventId = Input::get('id'); $eventType= Input::get('type'); + $accountId = $accountGateway->account_id; if (!$eventId) { - return response()->json([ - 'message' => 'Missing event id', - ], 400); + return response()->json(['message' => 'Missing event id'], 400); } if (!$eventType) { - return response()->json([ - 'message' => 'Missing event type', - ], 400); + return response()->json(['message' => 'Missing event type'], 400); } - if (!in_array($eventType, array('charge.failed', 'charge.succeeded'))) { + $supportedEvents = array( + 'charge.failed', + 'charge.succeeded', + 'customer.source.updated', + 'customer.source.deleted', + ); + + if (!in_array($eventType, $supportedEvents)) { return array('message' => 'Ignoring event'); } - $accountGateway = $account->getGatewayConfig(intval($gatewayId)); - // Fetch the event directly from Stripe for security $eventDetails = $this->paymentService->makeStripeCall($accountGateway, 'GET', 'events/'.$eventId); @@ -861,33 +822,47 @@ class PaymentController extends BaseController } if ($eventType != $eventDetails['type']) { - return response()->json([ - 'message' => 'Event type mismatch', - ], 400); + return response()->json(['message' => 'Event type mismatch'], 400); } if (!$eventDetails['pending_webhooks']) { - return response()->json([ - 'message' => 'This is not a pending event', - ], 400); + return response()->json(['message' => 'This is not a pending event'], 400); } - $charge = $eventDetails['data']['object']; - $transactionRef = $charge['id']; - $payment = Payment::where('transaction_reference', '=', $transactionRef)->first(); + if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded') { + $charge = $eventDetails['data']['object']; + $transactionRef = $charge['id']; - if (!$payment) { - return array('message' => 'Unknown payment'); - } + $payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $transactionRef)->first(); - if ($eventType == 'charge.failed') { - if (!$payment->isFailed()) { - $payment->markFailed($charge['failure_message']); - $this->userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment); + if (!$payment) { + return array('message' => 'Unknown payment'); + } + + if ($eventType == 'charge.failed') { + if (!$payment->isFailed()) { + $payment->markFailed($charge['failure_message']); + $this->userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment); + } + } elseif ($eventType == 'charge.succeeded') { + $payment->markComplete(); + } + } elseif($eventType == 'customer.source.updated' || $eventType == 'customer.source.deleted') { + $source = $eventDetails['data']['object']; + $sourceRef = $source['id']; + + $paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $sourceRef)->first(); + + if (!$paymentMethod) { + return array('message' => 'Unknown payment method'); + } + + if ($eventType == 'customer.source.deleted') { + $paymentMethod->delete(); + } elseif ($eventType == 'customer.source.updated') { + $this->paymentService->convertPaymentMethodFromStripe($source, null, $paymentMethod)->save(); } - } elseif ($eventType == 'charge.succeeded') { - $payment->markComplete(); } return array('message' => 'Processed successfully'); diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index c5b22ee16204..b0bc58a45165 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -16,6 +16,7 @@ use Redirect; use App\Models\Gateway; use App\Models\Invitation; use App\Models\Document; +use App\ModelsPaymentMethod; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\ActivityRepository; @@ -170,28 +171,30 @@ class PublicClientController extends BaseController if ($paymentMethods) { foreach ($paymentMethods as $paymentMethod) { - if ($paymentMethod['type']->id != PAYMENT_TYPE_ACH || $paymentMethod['status'] == 'verified') { + if ($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED) { + $code = htmlentities(str_replace(' ', '', strtolower($paymentMethod->payment_type->name))); - if ($paymentMethod['type']->id == PAYMENT_TYPE_ACH) { - $html = '
'.htmlentities($paymentMethod['bank_name']).'
'; - } elseif ($paymentMethod['type']->id == PAYMENT_TYPE_ID_PAYPAL) { + if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) { + if($paymentMethod->bank_data) { + $html = '
' . htmlentities($paymentMethod->bank_data->name) . '
'; + } + } elseif ($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL) { $html = ''.trans('; } else { - $code = htmlentities(str_replace(' ', '', strtolower($paymentMethod['type']->name))); $html = ''.trans('; } - $url = URL::to("/payment/{$invitation->invitation_key}/token/".$paymentMethod['id']); + $url = URL::to("/payment/{$invitation->invitation_key}/token/".$paymentMethod->public_id); - if ($paymentMethod['type']->id == PAYMENT_TYPE_ID_PAYPAL) { - $html .= '  '.$paymentMethod['email'].''; + if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL) { + $html .= '  '.$paymentMethod->email.''; $url .= '#braintree_paypal'; - } elseif ($paymentMethod['type']->id != PAYMENT_TYPE_ACH) { - $html .= '
'.trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($paymentMethod['expiration'], false)->format('m/y'))).'
'; - $html .= '•••'.$paymentMethod['last4'].'
'; + } elseif ($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH) { + $html .= '
'.trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($paymentMethod->expiration, false)->format('m/y'))).'
'; + $html .= '•••'.$paymentMethod->last4.'
'; } else { $html .= '
'; - $html .= '•••'.$paymentMethod['last4'].'
'; + $html .= '•••'.$paymentMethod->last4.'
'; } $paymentTypes[] = [ @@ -452,9 +455,9 @@ class PublicClientController extends BaseController return $model->email; } } elseif ($model->last4) { - $bankData = PaymentController::getBankData($model->routing_number); - if (is_array($bankData)) { - return $bankData['name'].'  •••' . $model->last4; + $bankData = PaymentMethod::lookupBankData($model->routing_number); + if (is_object($bankData)) { + return $bankData->name.'  •••' . $model->last4; } elseif($model->last4) { return '' . htmlentities($card_type) . '  •••' . $model->last4; } @@ -770,7 +773,7 @@ class PublicClientController extends BaseController public function verifyPaymentMethod() { - $sourceId = Input::get('source_id'); + $publicId = Input::get('source_id'); $amount1 = Input::get('verification1'); $amount2 = Input::get('verification2'); @@ -779,7 +782,7 @@ class PublicClientController extends BaseController } $client = $invitation->invoice->client; - $result = $this->paymentService->verifyClientPaymentMethod($client, $sourceId, $amount1, $amount2); + $result = $this->paymentService->verifyClientPaymentMethod($client, $publicId, $amount1, $amount2); if (is_string($result)) { Session::flash('error', $result); @@ -790,14 +793,14 @@ class PublicClientController extends BaseController return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/'); } - public function removePaymentMethod($sourceId) + public function removePaymentMethod($publicId) { if (!$invitation = $this->getInvitation()) { return $this->returnError(); } $client = $invitation->invoice->client; - $result = $this->paymentService->removeClientPaymentMethod($client, $sourceId); + $result = $this->paymentService->removeClientPaymentMethod($client, $publicId); if (is_string($result)) { Session::flash('error', $result); @@ -824,9 +827,9 @@ class PublicClientController extends BaseController $gateway = $accountGateway->gateway; if ($token && $paymentType == PAYMENT_TYPE_BRAINTREE_PAYPAL) { - $sourceId = $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, $invitation->contact_id); - if(empty($sourceId)) { + if(empty($sourceReference)) { $this->paymentMethodError('Token-No-Ref', $this->paymentService->lastError, $accountGateway); } else { Session::flash('message', trans('texts.payment_method_added')); @@ -895,12 +898,12 @@ class PublicClientController extends BaseController if (!empty($details)) { $gateway = $this->paymentService->createGateway($accountGateway); - $sourceId = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id); + $sourceReference = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id); } else { return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv')); } - if(empty($sourceId)) { + if(empty($sourceReference)) { $this->paymentMethodError('Token-No-Ref', $this->paymentService->lastError, $accountGateway); return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv')); } else if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty($usingPlaid) ) { diff --git a/app/Http/routes.php b/app/Http/routes.php index 241049810960..e8d7f9bbeac3 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -647,6 +647,10 @@ if (!defined('CONTACT_EMAIL')) { define('PAYMENT_TYPE_SOLO', 22); define('PAYMENT_TYPE_SWITCH', 23); + define('PAYMENT_METHOD_STATUS_NEW', 'new'); + define('PAYMENT_METHOD_STATUS_VERIFICATION_FAILED', 'verification_failed'); + define('PAYMENT_METHOD_STATUS_VERIFIED', 'verified'); + 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'); diff --git a/app/Models/AccountGatewayToken.php b/app/Models/AccountGatewayToken.php index b706882af77d..06494325662e 100644 --- a/app/Models/AccountGatewayToken.php +++ b/app/Models/AccountGatewayToken.php @@ -8,4 +8,18 @@ class AccountGatewayToken extends Eloquent use SoftDeletes; protected $dates = ['deleted_at']; public $timestamps = true; + + protected $casts = [ + 'uses_local_payment_methods' => 'boolean', + ]; + + public function payment_methods() + { + return $this->hasMany('App\Models\PaymentMethod'); + } + + public function default_payment_method() + { + return $this->hasOne('App\Models\PaymentMethod', 'id', 'default_payment_method_id'); + } } \ No newline at end of file diff --git a/app/Models/Client.php b/app/Models/Client.php index bc6e94e649fc..0198d46c200d 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -261,7 +261,7 @@ class Client extends EntityModel } - public function getGatewayToken(&$accountGateway) + public function getGatewayToken(&$accountGateway = null, &$token = null) { $account = $this->account; @@ -272,7 +272,10 @@ class Client extends EntityModel if (!count($account->account_gateways)) { return false; } - $accountGateway = $account->getTokenGateway(); + + if (!$accountGateway){ + $accountGateway = $account->getTokenGateway(); + } if (!$accountGateway) { return false; diff --git a/app/Models/Payment.php b/app/Models/Payment.php index faa42e3c93f3..b0001569dcc8 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -8,6 +8,7 @@ use App\Events\PaymentWasVoided; use App\Events\PaymentCompleted; use App\Events\PaymentVoided; use App\Events\PaymentFailed; +use App\Models\PaymentMethod; use Laracasts\Presenter\PresentableTrait; class Payment extends EntityModel @@ -56,7 +57,12 @@ class Payment extends EntityModel public function payment_type() { return $this->belongsTo('App\Models\PaymentType'); - } + } + + public function payment_method() + { + return $this->belongsTo('App\Models\PaymentMethod'); + } public function payment_status() { @@ -160,6 +166,14 @@ class Payment extends EntityModel { return ENTITY_PAYMENT; } + + public function getBankData() + { + if (!$this->routing_number) { + return null; + } + return PaymentMethod::lookupBankData($this->routing_number); + } } Payment::creating(function ($payment) { diff --git a/app/Models/PaymentMethod.php b/app/Models/PaymentMethod.php new file mode 100644 index 000000000000..62072dca090f --- /dev/null +++ b/app/Models/PaymentMethod.php @@ -0,0 +1,157 @@ +account_id = $accountGatewayToken->account_id; + $entity->account_gateway_token_id = $accountGatewayToken->id; + + $lastEntity = static::scope(false, $entity->account_id); + + $lastEntity = $lastEntity->orderBy('public_id', 'DESC') + ->first(); + + if ($lastEntity) { + $entity->public_id = $lastEntity->public_id + 1; + } else { + $entity->public_id = 1; + } + + return $entity; + } + + public function account() + { + return $this->belongsTo('App\Models\Account'); + } + + public function contact() + { + return $this->belongsTo('App\Models\Contact'); + } + + public function account_gateway_token() + { + return $this->belongsTo('App\Models\AccountGatewayToken'); + } + + public function payment_type() + { + return $this->belongsTo('App\Models\PaymentType'); + } + + public function currency() + { + return $this->belongsTo('App\Models\Currency'); + } + + public function payments() + { + return $this->hasMany('App\Models\Payments'); + } + + public function getBankData() + { + if (!$this->routing_number) { + return null; + } + return static::lookupBankData($this->routing_number); + } + + public function scopeScope($query, $publicId = false, $accountId = false, $accountGatewayTokenId = false) + { + $query = parent::scopeScope($query, $publicId, $accountId); + + if ($accountGatewayTokenId) { + $query->where($this->getTable() . '.account_gateway_token_id', '=', $accountGatewayTokenId); + } + + return $query; + } + + public static function lookupBankData($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 = new \stdClass(); + $data->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; + } + } +} + +PaymentMethod::deleting(function($paymentMethod) { + $accountGatewayToken = $paymentMethod->account_gateway_token; + if ($accountGatewayToken->default_payment_method_id == $paymentMethod->id) { + $newDefault = $accountGatewayToken->payment_methods->first(function($i, $paymentMethdod) use ($accountGatewayToken){ + return $paymentMethdod->id != $accountGatewayToken->default_payment_method_id; + }); + $accountGatewayToken->default_payment_method_id = $newDefault ? $newDefault->id : null; + $accountGatewayToken->save(); + } +}); \ No newline at end of file diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 3f938141dd2b..101c79f92c34 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -10,6 +10,7 @@ use Omnipay; use Session; use CreditCard; use App\Models\Payment; +use App\Models\PaymentMethod; use App\Models\Account; use App\Models\Country; use App\Models\Client; @@ -26,7 +27,7 @@ class PaymentService extends BaseService { public $lastError; protected $datatableService; - + protected static $refundableGateways = array( GATEWAY_STRIPE, GATEWAY_BRAINTREE @@ -47,7 +48,7 @@ class PaymentService extends BaseService public function createGateway($accountGateway) { $gateway = Omnipay::create($accountGateway->gateway->provider); - $gateway->initialize((array) $accountGateway->getConfig()); + $gateway->initialize((array)$accountGateway->getConfig()); if ($accountGateway->isGateway(GATEWAY_DWOLLA)) { if ($gateway->getSandbox() && isset($_ENV['DWOLLA_SANDBOX_KEY']) && isset($_ENV['DWOLLA_SANSBOX_SECRET'])) { @@ -66,7 +67,7 @@ class PaymentService extends BaseService { $invoice = $invitation->invoice; $account = $invoice->account; - $key = $invoice->account_id.'-'.$invoice->invoice_number; + $key = $invoice->account_id . '-' . $invoice->invoice_number; $currencyCode = $invoice->client->currency ? $invoice->client->currency->code : ($invoice->account->currency ? $invoice->account->currency->code : 'USD'); if ($input) { @@ -94,7 +95,7 @@ class PaymentService extends BaseService $data['ButtonSource'] = 'InvoiceNinja_SP'; }; - if($input && $accountGateway->isGateway(GATEWAY_STRIPE)) { + if ($input && $accountGateway->isGateway(GATEWAY_STRIPE)) { if (!empty($input['stripeToken'])) { $data['token'] = $input['stripeToken']; unset($details['card']); @@ -118,12 +119,12 @@ class PaymentService extends BaseService 'expiryMonth' => isset($input['expiration_month']) ? $input['expiration_month'] : null, 'expiryYear' => isset($input['expiration_year']) ? $input['expiration_year'] : null, ]; - + // allow space until there's a setting to disable if (isset($input['cvv']) && $input['cvv'] != ' ') { $data['cvv'] = $input['cvv']; } - + if (isset($input['country_id'])) { $country = Country::find($input['country_id']); @@ -174,198 +175,134 @@ class PaymentService extends BaseService ]; } - public function getClientPaymentMethods($client) { - $token = $client->getGatewayToken($accountGateway); + public function getClientPaymentMethods($client) + { + $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); if (!$token) { return null; } - $gateway = $this->createGateway($accountGateway); - - $paymentMethods = array(); - if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + if (!$accountGatewayToken->uses_local_payment_methods && $accountGateway->gateway_id == GATEWAY_STRIPE) { + // Migrate Stripe data + $gateway = $this->createGateway($accountGateway); $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'); + // Load + $accountGatewayToken->payment_methods(); 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', - ); + $paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken); + if ($paymentMethod) { + $paymentMethod->save(); + } + + if ($data['default_source'] == $source['id']) { + $accountGatewayToken->default_payment_method_id = $paymentMethod->id; } } - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $response = $gateway->findCustomer($token)->send(); - $data = $response->getData(); - if (!($data instanceof \Braintree\Customer)) { - return null; - } - - $sources = $data->paymentMethods; - - $paymentTypes = Cache::get('paymentTypes'); - $currencies = Cache::get('currencies'); - foreach ($sources as $source) { - if ($source instanceof \Braintree\CreditCard) { - $paymentMethods[] = array( - 'id' => $source->token, - 'default' => $source->isDefault(), - 'type' => $paymentTypes->find($this->parseCardType($source->cardType)), - 'last4' => $source->last4, - 'expiration' => $source->expirationYear . '-' . $source->expirationMonth . '-00', - ); - } elseif ($source instanceof \Braintree\PayPalAccount) { - $paymentMethods[] = array( - 'id' => $source->token, - 'default' => $source->isDefault(), - 'type' => $paymentTypes->find(PAYMENT_TYPE_ID_PAYPAL), - 'email' => $source->email, - ); - } - } + $accountGatewayToken->uses_local_payment_methods = true; + $accountGatewayToken->save(); } - return $paymentMethods; + return $accountGatewayToken->payment_methods; } - public function verifyClientPaymentMethod($client, $sourceId, $amount1, $amount2) { + public function verifyClientPaymentMethod($client, $publicId, $amount1, $amount2) + { $token = $client->getGatewayToken($accountGateway); if ($accountGateway->gateway_id != GATEWAY_STRIPE) { return 'Unsupported gateway'; } + $paymentMethod = PaymentMethod::scope($publicId, $client->account_id, $accountGatewayToken->id)->firstOrFail(); // Omnipay doesn't support verifying payment methods // Also, it doesn't want to urlencode without putting numbers inside the brackets - return $this->makeStripeCall( + $result = $this->makeStripeCall( $accountGateway, 'POST', - 'customers/'.$token.'/sources/'.$sourceId.'/verify', - 'amounts[]='.intval($amount1).'&amounts[]='.intval($amount2) + 'customers/' . $token . '/sources/' . $paymentMethod->source_reference . '/verify', + 'amounts[]=' . intval($amount1) . '&amounts[]=' . intval($amount2) ); - } - public function removeClientPaymentMethod($client, $sourceId) { - $token = $client->getGatewayToken($accountGateway/* return parameter */); - if (!$token) { - return null; - } + if (!is_string($result)) { + $paymentMethod->status = PAYMENT_METHOD_STATUS_VERIFIED; + $paymentMethod->save(); - $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) { - // Make sure the source is owned by this client - $sources = $this->getClientPaymentMethods($client); - $ownsSource = false; - foreach ($sources as $source) { - if ($source['id'] == $sourceId) { - $ownsSource = true; - break; - } - } - if (!$ownsSource) { - return 'Unknown source'; - } - - $response = $gateway->deletePaymentMethod(array('token'=>$sourceId))->send(); - - if (!$response->isSuccessful()) { - return $response->getMessage(); + if (!$paymentMethod->account_gateway_token->default_payment_method_id) { + $paymentMethod->account_gateway_token->default_payment_method_id = $paymentMethod->id; + $paymentMethod->account_gateway_token->save(); } } - return true; } - 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) { - - return $this->makeStripeCall( - $accountGateway, - 'POST', - 'customers/'.$token, - 'default_card='.$sourceId - ); - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - // Make sure the source is owned by this client - $sources = $this->getClientPaymentMethods($client); - $ownsSource = false; - foreach ($sources as $source) { - if ($source['id'] == $sourceId) { - $ownsSource = true; - break; - } - } - if (!$ownsSource) { - return 'Unknown source'; - } - - $response = $gateway->updatePaymentMethod(array( - 'token' => $sourceId, - 'makeDefault' => true, - ))->send(); - - if (!$response->isSuccessful()) { - return $response->getMessage(); - } - } - - return true; - } - - public function createToken($gateway, $details, $accountGateway, $client, $contactId, &$customerReference = null) + public function removeClientPaymentMethod($client, $publicId) { - $customerReference = $client->getGatewayToken(); + $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); + if (!$token) { + return null; + } + + $paymentMethod = PaymentMethod::scope($publicId, $client->account_id, $accountGatewayToken->id)->firstOrFail(); + + $gateway = $this->createGateway($accountGateway); + + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + $response = $gateway->deleteCard(array('customerReference' => $token, 'cardReference' => $paymentMethod->source_reference))->send(); + if (!$response->isSuccessful()) { + return $response->getMessage(); + } + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { + $response = $gateway->deletePaymentMethod(array('token' => $paymentMethod->source_reference))->send(); + + if (!$response->isSuccessful()) { + return $response->getMessage(); + } + } + + $paymentMethod->delete(); + + return true; + } + + public function setClientDefaultPaymentMethod($client, $publicId) + { + $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); + if (!$token) { + return null; + } + + $paymentMethod = PaymentMethod::scope($publicId, $client->account_id, $accountGatewayToken->id)->firstOrFail(); + $paymentMethod->account_gateway_token->default_payment_method_id = $paymentMethod->id; + $paymentMethod->account_gateway_token->save(); + + return true; + } + + public function createToken($gateway, $details, $accountGateway, $client, $contactId, &$customerReference = null, &$paymentMethod = null) + { + $customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return paramenter */); if ($customerReference) { $details['customerReference'] = $customerReference; if ($accountGateway->gateway->id == GATEWAY_STRIPE) { - $customerResponse = $gateway->fetchCustomer(array('customerReference'=>$customerReference))->send(); + $customerResponse = $gateway->fetchCustomer(array('customerReference' => $customerReference))->send(); - if (!$customerResponse->isSuccessful()){ + if (!$customerResponse->isSuccessful()) { $customerReference = null; // The customer might not exist anymore } } elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) { $customer = $gateway->findCustomer($customerReference)->send()->getData(); - if (!($customer instanceof \Braintree\Customer)){ + if (!($customer instanceof \Braintree\Customer)) { $customerReference = null; // The customer might not exist anymore } } @@ -413,7 +350,7 @@ class PaymentService extends BaseService } else { $data = $tokenResponse->getData(); if ($data && $data['error'] && $data['error']['type'] == 'invalid_request_error') { - $this->lastError = $data['error']['message']; + $this->lastError = $data['error']['message']; return; } } @@ -435,7 +372,7 @@ class PaymentService extends BaseService if ($customerReference) { $token = AccountGatewayToken::where('client_id', '=', $client->id) - ->where('account_gateway_id', '=', $accountGateway->id)->first(); + ->where('account_gateway_id', '=', $accountGateway->id)->first(); if (!$token) { $token = new AccountGatewayToken(); @@ -447,6 +384,9 @@ class PaymentService extends BaseService $token->token = $customerReference; $token->save(); + + $paymentMethod = $this->createPaymentMethodFromGatewayResponse($tokenResponse, $accountGateway, $accountGatewayToken, $contactId); + } else { $this->lastError = $tokenResponse->getMessage(); } @@ -454,6 +394,104 @@ class PaymentService extends BaseService return $sourceReference; } + public function convertPaymentMethodFromStripe($source, $accountGatewayToken = null, $paymentMethod = null) { + // Creating a new one or updating an existing one + if (!$paymentMethod) { + $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + } + + $paymentMethod->last4 = $source['last4']; + $paymentMethod->source_reference = $source['id']; + + if ($source['object'] == 'bank_account') { + $paymentMethod->routing_number = $source['routing_number']; + $paymentMethod->payment_type_id = PAYMENT_TYPE_ACH; + $paymentMethod->status = $source['status']; + $currency = Cache::get('currencies')->where('code', strtoupper($source['currency']))->first(); + if ($currency) { + $paymentMethod->currency_id = $currency->id; + $paymentMethod->setRelation('currency', $currency); + } + } elseif ($source['object'] == 'card') { + $paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-00'; + $paymentMethod->payment_type_id = $this->parseCardType($source['brand']); + } else { + return null; + } + + $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); + + return $paymentMethod; + } + + public function convertPaymentMethodFromBraintree($source, $accountGatewayToken = null, $paymentMethod = null) { + // Creating a new one or updating an existing one + if (!$paymentMethod) { + $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + } + + if ($source instanceof \Braintree\CreditCard) { + $paymentMethod->payment_type_id = $this->parseCardType($source->cardType); + $paymentMethod->last4 = $source->last4; + $paymentMethod->expiration = $source->expirationYear . '-' . $source->expirationMonth . '-00'; + } elseif ($source instanceof \Braintree\PayPalAccount) { + $paymentMethod->email = $source->email; + $paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; + } else { + return null; + } + + $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); + + $paymentMethod->source_reference = $source->token; + + return $paymentMethod; + } + + public function createPaymentMethodFromGatewayResponse($gatewayResponse, $accountGateway, $accountGatewayToken = null, $contactId = null) { + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + $data = $gatewayResponse->getData(); + if(!empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) { + $source = $data; + } else { + $source = !empty($data['source']) ? $data['source'] : $data['card']; + } + + if ($source) { + $paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken); + } + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { + $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + + $transaction = $sourceResponse->getData()->transaction; + if ($transaction->paymentInstrumentType == 'credit_card') { + $card = $transaction->creditCardDetails; + $paymentMethod->last4 = $card->last4; + $paymentMethod->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00'; + $paymentMethod->payment_type_id = $this->parseCardType($card->cardType); + } elseif ($transaction->paymentInstrumentType == 'paypal_account') { + $paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; + $paymentMethod->email = $transaction->paypalDetails->payerEmail; + } + + $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); + } + + if (!empty($paymentMethod) && $accountGatewayToken && $contactId) { + $paymentMethod->account_gateway_token_id = $accountGatewayToken->id; + $paymentMethod->account_id = $accountGatewayToken->account_id; + $paymentMethod->contact_id = $contactId; + $paymentMethod->save(); + + if (!$paymentMethod->account_gateway_token->default_payment_method_id) { + $paymentMethod->account_gateway_token->default_payment_method_id = $paymentMethod->id; + $paymentMethod->account_gateway_token->save(); + } + } + + return $paymentMethod; + } + public function getCheckoutComToken($invitation) { $token = false; @@ -490,7 +528,7 @@ class PaymentService extends BaseService return $token; } - public function createPayment($invitation, $accountGateway, $ref, $payerId = null, $paymentDetails = null, $purchaseResponse = null) + public function createPayment($invitation, $accountGateway, $ref, $payerId = null, $paymentDetails = null, $paymentMethod = null, $purchaseResponse = null) { $invoice = $invitation->invoice; @@ -551,6 +589,10 @@ class PaymentService extends BaseService $payment->payer_id = $payerId; } + if ($paymentMethod) { + $payment->payment_method_id = $paymentMethod->id; + } + $payment->save(); // enable pro plan for hosted users @@ -665,28 +707,28 @@ class PaymentService extends BaseService public function autoBillInvoice($invoice) { $client = $invoice->client; - $account = $invoice->account; - $invitation = $invoice->invitations->first(); - $accountGateway = $account->getTokenGateway(); - $token = $client->getGatewayToken(); - if (!$invitation || !$accountGateway || !$token) { + // Make sure we've migrated in data from Stripe + $this->getClientPaymentMethods($client); + + $invitation = $invoice->invitations->first(); + $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); + $defaultPaymentMethod = $accountGatewayToken->default_payment_method; + + if (!$invitation || !$token || !$defaultPaymentMethod) { return false; } // setup the gateway/payment info $gateway = $this->createGateway($accountGateway); $details = $this->getPaymentDetails($invitation, $accountGateway); - $details['customerReference'] = $token; if ($accountGateway->gateway_id == GATEWAY_STRIPE) { $details['customerReference'] = $token; - + $details['cardReference'] = $defaultPaymentMethod->sourceReference; } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { $details['customerId'] = $token; - $customer = $gateway->findCustomer($token)->send()->getData(); - $defaultPaymentMethod = $customer->defaultPaymentMethod(); - $details['paymentMethodToken'] = $defaultPaymentMethod->token; + $details['paymentMethodToken'] = $defaultPaymentMethod->sourceReference; } // submit purchase/get response @@ -694,7 +736,7 @@ class PaymentService extends BaseService if ($response->isSuccessful()) { $ref = $response->getTransactionReference(); - return $this->createPayment($invitation, $accountGateway, $ref, null, $details, $response); + return $this->createPayment($invitation, $accountGateway, $ref, null, $details, $defaultPaymentMethod, $response); } else { return false; } @@ -761,9 +803,9 @@ class PaymentService extends BaseService return $model->email; } } elseif ($model->last4) { - $bankData = PaymentController::getBankData($model->routing_number); - if (is_array($bankData)) { - return $bankData['name'].'  •••' . $model->last4; + $bankData = PaymentMethod::lookupBankData($model->routing_number); + if (is_object($bankData)) { + return $bankData->name.'  •••' . $model->last4; } elseif($model->last4) { return '' . htmlentities($card_type) . '  •••' . $model->last4; } diff --git a/database/migrations/2016_05_10_144219_wepay_integration.php b/database/migrations/2016_05_10_144219_wepay_integration.php new file mode 100644 index 000000000000..c2a48afd8098 --- /dev/null +++ b/database/migrations/2016_05_10_144219_wepay_integration.php @@ -0,0 +1,83 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('contact_id')->nullable(); + $table->unsignedInteger('account_gateway_token_id'); + $table->unsignedInteger('payment_type_id'); + $table->string('source_reference'); + + $table->unsignedInteger('routing_number')->nullable(); + $table->smallInteger('last4')->unsigned()->nullable(); + $table->date('expiration')->nullable(); + $table->string('email')->nullable(); + $table->unsignedInteger('currency_id')->nullable(); + $table->string('status')->nullable(); + + $table->timestamps(); + $table->softDeletes(); + + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens'); + $table->foreign('payment_type_id')->references('id')->on('payment_types'); + $table->foreign('currency_id')->references('id')->on('currencies'); + + $table->unsignedInteger('public_id')->index(); + $table->unique( array('account_id','public_id') ); + }); + + Schema::table('payments', function($table) + { + $table->unsignedInteger('payment_method_id')->nullable(); + $table->foreign('payment_method_id')->references('id')->on('payment_methods'); + }); + + Schema::table('account_gateway_tokens', function($table) + { + $table->unsignedInteger('default_payment_method_id')->nullable(); + $table->foreign('default_payment_method_id')->references('id')->on('payment_methods'); + + $table->boolean('uses_local_payment_methods')->defalut(true); + }); + + \DB::table('account_gateway_tokens')->update(array('uses_local_payment_methods' => false)); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('payments', function($table) + { + $table->dropForeign('payments_payment_method_id_foreign'); + $table->dropColumn('payment_method_id'); + }); + + Schema::table('account_gateway_tokens', function($table) + { + $table->dropForeign('account_gateway_tokens_default_payment_method_id_foreign'); + $table->dropColumn('default_payment_method_id'); + $table->dropColumn('uses_local_payment_methods'); + }); + + Schema::dropIfExists('payment_methods'); + } +} diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 1a7d6a1267bb..32a31dca7d58 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1259,7 +1259,7 @@ $LANG = array( 'payment_method_set_as_default' => 'Set Autobill payment method.', 'activity_41' => ':payment_amount payment (:payment) failed', 'webhook_url' => 'Webhook URL', - 'stripe_webhook_help' => 'You must :link for ACH payment status to be updated.', + 'stripe_webhook_help' => 'You must :link.', 'stripe_webhook_help_link_text' => 'add this URL as an endpoint at Stripe', 'payment_method_error' => 'There was an error adding your payment methd. Please try again later.', 'notification_invoice_payment_failed_subject' => 'Payment failed for Invoice :invoice', @@ -1286,7 +1286,10 @@ $LANG = array( 'braintree_paypal_help' => 'You must also :link.', 'braintree_paypal_help_link_text' => 'link PayPal to your BrainTree account', 'token_billing_braintree_paypal' => 'Save payment details', - 'add_paypal_account' => 'Add PayPal Account' + 'add_paypal_account' => 'Add PayPal Account', + + + 'no_payment_method_specified' => 'No payment method specified', ); return $LANG; diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index bd5f100671a6..ade161f5b65f 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -97,6 +97,18 @@ ->help(trans('texts.token_billing_help')) !!} @endif + @if ($gateway->id == GATEWAY_STRIPE) +
+ +
+ +
{!! trans('texts.stripe_webhook_help', [ + 'link'=>''.trans('texts.stripe_webhook_help_link_text').'' + ]) !!}
+
+
+ @endif + @if ($gateway->id == GATEWAY_BRAINTREE) @if ($account->getGatewayByType(PAYMENT_TYPE_PAYPAL)) {!! Former::checkbox('enable_paypal') @@ -148,15 +160,6 @@ ->text(trans('texts.enable_ach')) ->help(trans('texts.stripe_ach_help')) !!}
-
- -
- -
{!! trans('texts.stripe_webhook_help', [ - 'link'=>''.trans('texts.stripe_webhook_help_link_text').'' - ]) !!}
-
-

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

diff --git a/resources/views/payments/paymentmethods_list.blade.php b/resources/views/payments/paymentmethods_list.blade.php index 3436315acf6a..1522f2ac0a94 100644 --- a/resources/views/payments/paymentmethods_list.blade.php +++ b/resources/views/payments/paymentmethods_list.blade.php @@ -53,30 +53,31 @@ @foreach ($paymentMethods as $paymentMethod)
- {{trans(name)))}}"> + {{trans(payment_type->name)))}}"> - @if(!empty($paymentMethod['last4'])) - •••••{{$paymentMethod['last4']}} + @if(!empty($paymentMethod->last4)) + •••••{{$paymentMethod->last4}} @endif - - @if($paymentMethod['type']->id == PAYMENT_TYPE_ACH) - {{ $paymentMethod['bank_name'] }} - @if($paymentMethod['status'] == 'new') - ({{trans('texts.complete_verification')}}) - @elseif($paymentMethod['status'] == 'verification_failed') + @if($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) + @if($paymentMethod->bank()) + {{ $paymentMethod->bank()->name }} + @endif + @if($paymentMethod->status == PAYMENT_METHOD_STATUS_NEW) + ({{trans('texts.complete_verification')}}) + @elseif($paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFICATION_FAILED) ({{trans('texts.verification_failed')}}) @endif - @elseif($paymentMethod['type']->id == PAYMENT_TYPE_ID_PAYPAL) - {{ $paymentMethod['email'] }} + @elseif($paymentMethod->type_id == PAYMENT_TYPE_ID_PAYPAL) + {{ $paymentMethod->email }} @else - {!! trans('texts.card_expiration', array('expires'=>Utils::fromSqlDate($paymentMethod['expiration'], false)->format('m/y'))) !!} + {!! trans('texts.card_expiration', array('expires'=>Utils::fromSqlDate($paymentMethod->expiration, false)->format('m/y'))) !!} @endif - @if($paymentMethod['default']) + @if($paymentMethod->id == $paymentMethod->account_gateway_token->default_payment_method_id) ({{trans('texts.used_for_auto_bill')}}) - @elseif($paymentMethod['type']->id != PAYMENT_TYPE_ACH || $paymentMethod['status'] == 'verified') - ({{trans('texts.use_for_auto_bill')}}) + @elseif($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED) + ({{trans('texts.use_for_auto_bill')}}) @endif - × + ×
@endforeach @endif From cc5cb091d7a431b486bcb9adc3cfd3ec671439f7 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 10 May 2016 19:16:53 -0400 Subject: [PATCH 042/386] Store Braintree card summary locally --- app/Services/PaymentService.php | 45 +++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 101c79f92c34..73d00b07e00e 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -359,6 +359,9 @@ class PaymentService extends BaseService $tokenResponse = $gateway->createCustomer(array('customerData' => array()))->send(); if ($tokenResponse->isSuccessful()) { $customerReference = $tokenResponse->getCustomerData()->id; + } else { + $this->lastError = $tokenResponse->getData()->message; + return; } } @@ -366,7 +369,12 @@ class PaymentService extends BaseService $details['customerId'] = $customerReference; $tokenResponse = $gateway->createPaymentMethod($details)->send(); - $sourceReference = $tokenResponse->getData()->paymentMethod->token; + if ($tokenResponse->isSuccessful()) { + $sourceReference = $tokenResponse->getData()->paymentMethod->token; + } else { + $this->lastError = $tokenResponse->getData()->message; + return; + } } } @@ -461,20 +469,26 @@ class PaymentService extends BaseService $paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken); } } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + $data = $gatewayResponse->getData(); - $transaction = $sourceResponse->getData()->transaction; - if ($transaction->paymentInstrumentType == 'credit_card') { - $card = $transaction->creditCardDetails; - $paymentMethod->last4 = $card->last4; - $paymentMethod->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00'; - $paymentMethod->payment_type_id = $this->parseCardType($card->cardType); - } elseif ($transaction->paymentInstrumentType == 'paypal_account') { - $paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; - $paymentMethod->email = $transaction->paypalDetails->payerEmail; + if (!empty($data->transaction)) { + $transaction = $data->transaction; + + $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + if ($transaction->paymentInstrumentType == 'credit_card') { + $card = $transaction->creditCardDetails; + $paymentMethod->last4 = $card->last4; + $paymentMethod->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00'; + $paymentMethod->payment_type_id = $this->parseCardType($card->cardType); + } elseif ($transaction->paymentInstrumentType == 'paypal_account') { + $paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; + $paymentMethod->email = $transaction->paypalDetails->payerEmail; + } + $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); + } elseif (!empty($data->paymentMethod)) { + $paymentMethod = $this->convertPaymentMethodFromBraintree($data->paymentMethod, $accountGatewayToken); } - $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); } if (!empty($paymentMethod) && $accountGatewayToken && $contactId) { @@ -722,13 +736,12 @@ class PaymentService extends BaseService // setup the gateway/payment info $gateway = $this->createGateway($accountGateway); $details = $this->getPaymentDetails($invitation, $accountGateway); + $details['customerReference'] = $token; if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - $details['customerReference'] = $token; - $details['cardReference'] = $defaultPaymentMethod->sourceReference; + $details['cardReference'] = $defaultPaymentMethod->source_reference; } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $details['customerId'] = $token; - $details['paymentMethodToken'] = $defaultPaymentMethod->sourceReference; + $details['paymentMethodToken'] = $defaultPaymentMethod->source_reference; } // submit purchase/get response From 0ca073291e7193c8b508a902a531938fd950acbb Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 10 May 2016 19:17:14 -0400 Subject: [PATCH 043/386] Make storing card summary locally part of payments changes --- .../2016_04_23_182223_payments_changes.php | 57 +++++++++++++ .../2016_05_10_144219_wepay_integration.php | 83 ------------------- 2 files changed, 57 insertions(+), 83 deletions(-) delete mode 100644 database/migrations/2016_05_10_144219_wepay_integration.php diff --git a/database/migrations/2016_04_23_182223_payments_changes.php b/database/migrations/2016_04_23_182223_payments_changes.php index 2e6cdcb6ec05..c250c897333d 100644 --- a/database/migrations/2016_04_23_182223_payments_changes.php +++ b/database/migrations/2016_04_23_182223_payments_changes.php @@ -22,6 +22,37 @@ class PaymentsChanges extends Migration (new \PaymentStatusSeeder())->run(); + Schema::dropIfExists('payment_methods'); + + Schema::create('payment_methods', function($table) + { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('contact_id')->nullable(); + $table->unsignedInteger('account_gateway_token_id'); + $table->unsignedInteger('payment_type_id'); + $table->string('source_reference'); + + $table->unsignedInteger('routing_number')->nullable(); + $table->smallInteger('last4')->unsigned()->nullable(); + $table->date('expiration')->nullable(); + $table->string('email')->nullable(); + $table->unsignedInteger('currency_id')->nullable(); + $table->string('status')->nullable(); + + $table->timestamps(); + $table->softDeletes(); + + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens'); + $table->foreign('payment_type_id')->references('id')->on('payment_types'); + $table->foreign('currency_id')->references('id')->on('currencies'); + + $table->unsignedInteger('public_id')->index(); + $table->unique( array('account_id','public_id') ); + }); + Schema::table('payments', function($table) { $table->decimal('refunded', 13, 2); @@ -33,6 +64,9 @@ class PaymentsChanges extends Migration $table->date('expiration')->nullable(); $table->text('gateway_error')->nullable(); $table->string('email')->nullable(); + + $table->unsignedInteger('payment_method_id')->nullable(); + $table->foreign('payment_method_id')->references('id')->on('payment_methods'); }); Schema::table('invoices', function($table) @@ -43,6 +77,17 @@ class PaymentsChanges extends Migration \DB::table('invoices') ->where('auto_bill', '=', 1) ->update(array('client_enable_auto_bill' => 1, 'auto_bill' => AUTO_BILL_OPT_OUT)); + + + Schema::table('account_gateway_tokens', function($table) + { + $table->unsignedInteger('default_payment_method_id')->nullable(); + $table->foreign('default_payment_method_id')->references('id')->on('payment_methods'); + + $table->boolean('uses_local_payment_methods')->defalut(true); + }); + + \DB::table('account_gateway_tokens')->update(array('uses_local_payment_methods' => false)); } /** @@ -63,6 +108,9 @@ class PaymentsChanges extends Migration $table->dropColumn('expiration'); $table->dropColumn('gateway_error'); $table->dropColumn('email'); + + $table->dropForeign('payments_payment_method_id_foreign'); + $table->dropColumn('payment_method_id'); }); \DB::table('invoices') @@ -84,5 +132,14 @@ class PaymentsChanges extends Migration }); Schema::dropIfExists('payment_statuses'); + + Schema::table('account_gateway_tokens', function($table) + { + $table->dropForeign('account_gateway_tokens_default_payment_method_id_foreign'); + $table->dropColumn('default_payment_method_id'); + $table->dropColumn('uses_local_payment_methods'); + }); + + Schema::dropIfExists('payment_methods'); } } diff --git a/database/migrations/2016_05_10_144219_wepay_integration.php b/database/migrations/2016_05_10_144219_wepay_integration.php deleted file mode 100644 index c2a48afd8098..000000000000 --- a/database/migrations/2016_05_10_144219_wepay_integration.php +++ /dev/null @@ -1,83 +0,0 @@ -increments('id'); - $table->unsignedInteger('account_id'); - $table->unsignedInteger('contact_id')->nullable(); - $table->unsignedInteger('account_gateway_token_id'); - $table->unsignedInteger('payment_type_id'); - $table->string('source_reference'); - - $table->unsignedInteger('routing_number')->nullable(); - $table->smallInteger('last4')->unsigned()->nullable(); - $table->date('expiration')->nullable(); - $table->string('email')->nullable(); - $table->unsignedInteger('currency_id')->nullable(); - $table->string('status')->nullable(); - - $table->timestamps(); - $table->softDeletes(); - - $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); - $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); - $table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens'); - $table->foreign('payment_type_id')->references('id')->on('payment_types'); - $table->foreign('currency_id')->references('id')->on('currencies'); - - $table->unsignedInteger('public_id')->index(); - $table->unique( array('account_id','public_id') ); - }); - - Schema::table('payments', function($table) - { - $table->unsignedInteger('payment_method_id')->nullable(); - $table->foreign('payment_method_id')->references('id')->on('payment_methods'); - }); - - Schema::table('account_gateway_tokens', function($table) - { - $table->unsignedInteger('default_payment_method_id')->nullable(); - $table->foreign('default_payment_method_id')->references('id')->on('payment_methods'); - - $table->boolean('uses_local_payment_methods')->defalut(true); - }); - - \DB::table('account_gateway_tokens')->update(array('uses_local_payment_methods' => false)); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('payments', function($table) - { - $table->dropForeign('payments_payment_method_id_foreign'); - $table->dropColumn('payment_method_id'); - }); - - Schema::table('account_gateway_tokens', function($table) - { - $table->dropForeign('account_gateway_tokens_default_payment_method_id_foreign'); - $table->dropColumn('default_payment_method_id'); - $table->dropColumn('uses_local_payment_methods'); - }); - - Schema::dropIfExists('payment_methods'); - } -} From b8514436a9a2df1406b25e41b5e0df1c7b4568cc Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 10 May 2016 19:17:41 -0400 Subject: [PATCH 044/386] Pad last 4 card digits to a string of length 4, since we're storing it as an int --- app/Models/Payment.php | 5 +++++ resources/views/payments/paymentmethods_list.blade.php | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/Models/Payment.php b/app/Models/Payment.php index b0001569dcc8..9171c9d22c69 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -174,6 +174,11 @@ class Payment extends EntityModel } return PaymentMethod::lookupBankData($this->routing_number); } + + public function getLast4Attribute($value) + { + return $value ? str_pad($value, 4, '0', STR_PAD_LEFT) : null; + } } Payment::creating(function ($payment) { diff --git a/resources/views/payments/paymentmethods_list.blade.php b/resources/views/payments/paymentmethods_list.blade.php index 1522f2ac0a94..78b2653236ba 100644 --- a/resources/views/payments/paymentmethods_list.blade.php +++ b/resources/views/payments/paymentmethods_list.blade.php @@ -67,9 +67,9 @@ @elseif($paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFICATION_FAILED) ({{trans('texts.verification_failed')}}) @endif - @elseif($paymentMethod->type_id == PAYMENT_TYPE_ID_PAYPAL) + @elseif($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL) {{ $paymentMethod->email }} - @else + @elseif($paymentMethod->expiration) {!! trans('texts.card_expiration', array('expires'=>Utils::fromSqlDate($paymentMethod->expiration, false)->format('m/y'))) !!} @endif @if($paymentMethod->id == $paymentMethod->account_gateway_token->default_payment_method_id) From 104264607bc135a0246428f0654b53603f2d4199 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 10 May 2016 19:17:55 -0400 Subject: [PATCH 045/386] Pad last 4 card digits to a string of length 4, since we're storing it as an int --- app/Models/PaymentMethod.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Models/PaymentMethod.php b/app/Models/PaymentMethod.php index 62072dca090f..07c6d2e19e1f 100644 --- a/app/Models/PaymentMethod.php +++ b/app/Models/PaymentMethod.php @@ -71,6 +71,11 @@ class PaymentMethod extends EntityModel return static::lookupBankData($this->routing_number); } + public function getLast4Attribute($value) + { + return $value ? str_pad($value, 4, '0', STR_PAD_LEFT) : null; + } + public function scopeScope($query, $publicId = false, $accountId = false, $accountGatewayTokenId = false) { $query = parent::scopeScope($query, $publicId, $accountId); From 6138e1132f07735145aec5d2099ad8ec16dcfaa1 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 10 May 2016 19:18:12 -0400 Subject: [PATCH 046/386] Make sure payments reference newly added payment methods --- app/Http/Controllers/PaymentController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 491bac77033a..d86f8684074f 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -493,7 +493,7 @@ class PaymentController extends BaseController unset($details['token']); $details['cardReference'] = $sourceReference; } 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 */); + $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */); if ($token) { $details['token'] = $token; $details['customerReference'] = $customerReference; @@ -524,7 +524,7 @@ class PaymentController extends BaseController $details['paymentMethodToken'] = $sourceReference; unset($details['token']); } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) { - $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */); + $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */); if ($token) { $details['paymentMethodToken'] = $token; $details['customerId'] = $customerReference; From dcf231c6a524fadcf3626cefbd2b457caea1a7aa Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 10 May 2016 21:30:52 -0400 Subject: [PATCH 047/386] Fix gateway ids --- app/Http/routes.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index e8d7f9bbeac3..33d28899e5f1 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -556,7 +556,8 @@ if (!defined('CONTACT_EMAIL')) { define('GATEWAY_DWOLLA', 43); define('GATEWAY_CHECKOUT_COM', 47); define('GATEWAY_CYBERSOURCE', 49); - define('GATEWAY_BRAINTREE', 62); + define('GATEWAY_WEPAY', 60); + define('GATEWAY_BRAINTREE', 61); define('EVENT_CREATE_CLIENT', 1); define('EVENT_CREATE_INVOICE', 2); From 258401715c6fddeb76c0933437bc081e0c1e451f Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 10 May 2016 22:15:30 -0400 Subject: [PATCH 048/386] Update billing address when adding payment method --- app/Http/Controllers/PaymentController.php | 53 +++++++++++-------- .../Controllers/PublicClientController.php | 5 ++ app/Services/PaymentService.php | 2 +- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index d86f8684074f..fead7b7b0121 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -399,16 +399,7 @@ class PaymentController extends BaseController } } - 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; - $paymentType = Session::get($invitation->id . 'payment_type'); - $accountGateway = $account->getGatewayByType($paymentType); - $paymentMethod = null; - + public static function processPaymentClientDetails($client, $accountGateway, $paymentType, $onSite = true){ $rules = [ 'first_name' => 'required', 'last_name' => 'required', @@ -437,17 +428,6 @@ class PaymentController extends BaseController 'country_id' => 'required', ]); } - - if ($useToken) { - if(!$sourceId) { - Session::flash('error', trans('texts.no_payment_method_specified')); - return Redirect::to('payment/' . $invitationKey)->withInput(Request::except('cvv')); - } else { - $customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return parameter*/); - $paymentMethod = PaymentMethod::scope($sourceId, $account->id, $accountGatewayToken->id)->firstOrFail(); - $sourceReference = $paymentMethod->source_reference; - } - } if ($onSite) { $validator = Validator::make(Input::all(), $rules); @@ -469,6 +449,37 @@ class PaymentController extends BaseController } } + return true; + } + + 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; + $paymentType = Session::get($invitation->id . 'payment_type'); + $accountGateway = $account->getGatewayByType($paymentType); + $paymentMethod = null; + + + + if ($useToken) { + if(!$sourceId) { + Session::flash('error', trans('texts.no_payment_method_specified')); + return Redirect::to('payment/' . $invitationKey)->withInput(Request::except('cvv')); + } else { + $customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return parameter*/); + $paymentMethod = PaymentMethod::scope($sourceId, $account->id, $accountGatewayToken->id)->firstOrFail(); + $sourceReference = $paymentMethod->source_reference; + } + } + + $result = static::processPaymentClientDetails($client, $accountGateway, $paymentType, $onSite); + if ($result !== true) { + return $result; + } + try { // For offsite payments send the client's details on file // If we're using a token then we don't need to send any other data diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index b0bc58a45165..b8c089379116 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -884,6 +884,11 @@ class PublicClientController extends BaseController $accountGateway = $account->getGatewayByType($paymentType); $sourceToken = $accountGateway->gateway_id == GATEWAY_STRIPE ? Input::get('stripeToken'):Input::get('payment_method_nonce'); + $result = PaymentController::processPaymentClientDetails($client, $accountGateway, $paymentType); + if ($result !== true) { + return $result; + } + if ($sourceToken) { $details = array('token' => $sourceToken); } elseif (Input::get('plaidPublicToken')) { diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 73d00b07e00e..d3ce97a7f935 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -98,7 +98,7 @@ class PaymentService extends BaseService if ($input && $accountGateway->isGateway(GATEWAY_STRIPE)) { if (!empty($input['stripeToken'])) { $data['token'] = $input['stripeToken']; - unset($details['card']); + unset($data['card']); } elseif (!empty($input['plaidPublicToken'])) { $data['plaidPublicToken'] = $input['plaidPublicToken']; $data['plaidAccountId'] = $input['plaidAccountId']; From 0f95799f33131e10d201c6bf02ea07aab1b861c0 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 11 May 2016 09:22:43 +0300 Subject: [PATCH 049/386] Improved 'return to app' button logic --- resources/views/invoices/view.blade.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/views/invoices/view.blade.php b/resources/views/invoices/view.blade.php index 3e5310ea40a6..e0a5cba5a2e4 100644 --- a/resources/views/invoices/view.blade.php +++ b/resources/views/invoices/view.blade.php @@ -25,10 +25,7 @@ @include('partials.checkout_com_payment') @else
- @if (Session::get('trackEventAction') === '/buy_pro_plan') - {!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}   - {!! Button::primary(trans('texts.return_to_app'))->asLinkTo(URL::to('/dashboard'))->large() !!} - @elseif ($invoice->is_quote) + @if ($invoice->is_quote) {!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!}   @if ($showApprove) {!! Button::success(trans('texts.approve'))->asLinkTo(URL::to('/approve/' . $invitation->invitation_key))->large() !!} @@ -40,8 +37,11 @@ @else {{ trans('texts.pay_now') }} @endif - @else + @else {!! Button::normal(trans('texts.download_pdf'))->withAttributes(['onclick' => 'onDownloadClick()'])->large() !!} + @if ($account->isNinjaAccount()) + {!! Button::primary(trans('texts.return_to_app'))->asLinkTo(URL::to('/dashboard'))->large() !!} + @endif @endif
From 981045b28b756390d63ffa1d32218722b2373182 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Wed, 11 May 2016 09:57:44 -0400 Subject: [PATCH 050/386] Better Stripe support --- app/Services/PaymentService.php | 10 ++-------- resources/views/payments/paymentmethods_list.blade.php | 2 ++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index d3ce97a7f935..3a662a08710d 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -558,14 +558,8 @@ class PaymentService extends BaseService if (!empty($paymentDetails['card'])) { $card = $paymentDetails['card']; - $payment->last4 = substr($card->number, -4); - $year = $card->expiryYear; - if (strlen($year) == 2) { - $year = '20' . $year; - } - - $payment->expiration = $year . '-' . $card->expiryMonth . '-00'; - $payment->payment_type_id = $this->detectCardType($card->number); + $payment->last4 = $card->getNumberLastFour(); + $payment->payment_type_id = $this->detectCardType($card->getNumber()); } if ($accountGateway->gateway_id == GATEWAY_STRIPE) { diff --git a/resources/views/payments/paymentmethods_list.blade.php b/resources/views/payments/paymentmethods_list.blade.php index 78b2653236ba..8745cadcce49 100644 --- a/resources/views/payments/paymentmethods_list.blade.php +++ b/resources/views/payments/paymentmethods_list.blade.php @@ -82,6 +82,7 @@ @endforeach @endif +@if($gateway->gateway_id != GATEWAY_STRIPE || $gateway->getPublishableStripeKey())
{!! Button::success(strtoupper(trans('texts.add_credit_card'))) ->asLinkTo(URL::to('/client/paymentmethods/add/'.($gateway->getPaymentType() == PAYMENT_TYPE_STRIPE ? 'stripe_credit_card' : 'credit_card'))) !!} @@ -98,6 +99,7 @@
@endif
+@endif From 110af6f28bee4d50492343bdcbe729c2752af74d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 12 May 2016 12:44:22 +0300 Subject: [PATCH 060/386] Fix for #859 --- app/Http/Controllers/ReportController.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index a63b16d9fcc4..4f162f260d37 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -248,11 +248,13 @@ class ReportController extends BaseController ->withArchived() ->with('contacts') ->with(['invoices' => function($query) use ($startDate, $endDate, $dateField) { - $query->withArchived(); - if ($dateField == FILTER_PAYMENT_DATE) { + $query->with('invoice_items')->withArchived(); + if ($dateField == FILTER_INVOICE_DATE) { $query->where('invoice_date', '>=', $startDate) ->where('invoice_date', '<=', $endDate) - ->whereHas('payments', function($query) use ($startDate, $endDate) { + ->with('payments'); + } else { + $query->whereHas('payments', function($query) use ($startDate, $endDate) { $query->where('payment_date', '>=', $startDate) ->where('payment_date', '<=', $endDate) ->withArchived(); @@ -260,9 +262,8 @@ class ReportController extends BaseController ->with(['payments' => function($query) use ($startDate, $endDate) { $query->where('payment_date', '>=', $startDate) ->where('payment_date', '<=', $endDate) - ->withArchived() - ->with('payment_type', 'account_gateway.gateway'); - }, 'invoice_items']); + ->withArchived(); + }]); } }]); From cca4e40ef58ef33f8fe692b0cef72a72176035ef Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 12 May 2016 12:44:44 +0300 Subject: [PATCH 061/386] Fix problem with migration --- database/migrations/2016_03_22_168362_add_documents.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2016_03_22_168362_add_documents.php b/database/migrations/2016_03_22_168362_add_documents.php index 3d0a44f20c25..233f420c198b 100644 --- a/database/migrations/2016_03_22_168362_add_documents.php +++ b/database/migrations/2016_03_22_168362_add_documents.php @@ -19,7 +19,7 @@ class AddDocuments extends Migration { $table->boolean('document_email_attachment')->default(1); }); - DB::table('accounts')->update(array('logo' => '')); + \DB::table('accounts')->update(array('logo' => '')); Schema::dropIfExists('documents'); Schema::create('documents', function($t) { From ba000546c4531b4669cdb574453523f8dd22663e Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Thu, 12 May 2016 11:09:07 -0400 Subject: [PATCH 062/386] Better WePay switching --- app/Http/Controllers/AccountController.php | 7 ++++ .../Controllers/AccountGatewayController.php | 20 +++++++++- app/Http/routes.php | 1 + app/Models/AccountGateway.php | 2 +- resources/lang/en/texts.php | 4 +- .../views/accounts/account_gateway.blade.php | 39 +------------------ .../account_gateway_switch_wepay.blade.php | 7 ++++ .../partials/account_gateway_wepay.blade.php | 39 +++++++++++++++++++ resources/views/accounts/payments.blade.php | 6 +++ 9 files changed, 85 insertions(+), 40 deletions(-) create mode 100644 resources/views/accounts/account_gateway_switch_wepay.blade.php create mode 100644 resources/views/accounts/partials/account_gateway_wepay.blade.php diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 5ce42356bf83..053d5cb66a58 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -430,7 +430,14 @@ class AccountController extends BaseController if ($count == 0) { return Redirect::to('gateways/create'); } else { + $switchToWepay = WEPAY_CLIENT_ID && !$account->getGatewayConfig(GATEWAY_WEPAY); + + if ($switchToWepay && $account->token_billing_type_id != TOKEN_BILLING_DISABLED) { + $switchToWepay = !$account->getGatewayConfig(GATEWAY_BRAINTREE) && !$account->getGatewayConfig(GATEWAY_STRIPE); + } + return View::make('accounts.payments', [ + 'showSwitchToWepay' => $switchToWepay, 'showAdd' => $count < count(Gateway::$paymentTypes), 'title' => trans('texts.online_payments') ]); diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index c430fb61d816..a192538480e9 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -410,7 +410,7 @@ class AccountGatewayController extends BaseController 'original_ip' => \Request::getClientIp(true), 'original_device' => \Request::server('HTTP_USER_AGENT'), 'tos_acceptance_time' => time(), - 'redirect_uri' => URL::to('/gateways'), + 'redirect_uri' => URL::to('gateways'), 'callback_uri' => URL::to('https://sometechie.ngrok.io/paymenthook/'.$account->account_key.'/'.GATEWAY_WEPAY), 'scope' => 'manage_accounts,collect_payments,view_user,preapprove_payments,send_money', )); @@ -437,6 +437,10 @@ class AccountGatewayController extends BaseController } } + if (($gateway = $account->getGatewayByType(PAYMENT_TYPE_CREDIT_CARD)) || ($gateway = $account->getGatewayByType(PAYMENT_TYPE_STRIPE))) { + $gateway->delete(); + } + $accountGateway = AccountGateway::createNew(); $accountGateway->gateway_id = GATEWAY_WEPAY; $accountGateway->setConfig(array( @@ -486,4 +490,18 @@ class AccountGatewayController extends BaseController return Redirect::to("gateways/{$accountGateway->public_id}/edit"); } + + public function switchToWepay() + { + $data = self::getViewModel(); + $data['url'] = 'gateways'; + $data['method'] = 'POST'; + unset($data['gateways']); + + if ( ! \Request::secure() && ! Utils::isNinjaDev()) { + Session::flash('warning', trans('texts.enable_https')); + } + + return View::make('accounts.account_gateway_switch_wepay', $data); + } } diff --git a/app/Http/routes.php b/app/Http/routes.php index 292d6128a070..2c12c215cfd3 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -243,6 +243,7 @@ Route::group([ Route::resource('gateways', 'AccountGatewayController'); Route::get('gateways/{public_id}/resend_confirmation', 'AccountGatewayController@resendConfirmation'); + Route::get('gateways/switch/wepay', 'AccountGatewayController@switchToWepay'); Route::get('api/gateways', array('as'=>'api.gateways', 'uses'=>'AccountGatewayController@getDatatable')); Route::post('account_gateways/bulk', 'AccountGatewayController@bulk'); diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php index 6de95e8593cf..0c281856be66 100644 --- a/app/Models/AccountGateway.php +++ b/app/Models/AccountGateway.php @@ -77,7 +77,7 @@ class AccountGateway extends EntityModel return !empty($this->getConfigField('enableAch')); } - public function getPayPAlEnabled() + public function getPayPalEnabled() { return !empty($this->getConfigField('enablePayPal')); } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 25f9fdd6fabb..ea3ea349f31d 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1308,7 +1308,9 @@ $LANG = array( 'manage_wepay_account' => 'Manage WePay Account', 'action_required' => 'Action Required', 'finish_setup' => 'Finish Setup', - 'created_wepay_confirmation_required' => 'Please check your email and confirm your email address with WePay.' + 'created_wepay_confirmation_required' => 'Please check your email and confirm your email address with WePay.', + 'switch_to_wepay' => 'Switch to WePay', + 'switch' => 'Switch', ); return $LANG; diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index 72acd1cff05a..49635e95db38 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -5,43 +5,8 @@ @include('accounts.nav', ['selected' => ACCOUNT_PAYMENTS]) - @if(!$accountGateway && WEPAY_CLIENT_ID) - {!! Former::open($url)->method($method)->rules(array( - 'first_name' => 'required', - 'last_name' => 'required', - 'email' => 'required', - 'description' => 'required', - 'company_name' => 'required', - 'tos_agree' => 'required', - ))->addClass('warn-on-exit') !!} - {!! Former::populateField('company_name', $account->getDisplayName()) !!} - {!! Former::populateField('first_name', $user->first_name) !!} - {!! Former::populateField('last_name', $user->last_name) !!} - {!! Former::populateField('email', $user->email) !!} -
-
-

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

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

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

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

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

+ {{ trans('texts.use_another_provider') }} + @endif +
+
+
+ + +{!! Former::close() !!} \ No newline at end of file diff --git a/resources/views/accounts/payments.blade.php b/resources/views/accounts/payments.blade.php index d2fcde895200..9907fac6ecee 100644 --- a/resources/views/accounts/payments.blade.php +++ b/resources/views/accounts/payments.blade.php @@ -4,6 +4,12 @@ @parent @include('accounts.nav', ['selected' => ACCOUNT_PAYMENTS]) + @if ($showSwitchToWepay) + {!! Button::success(trans('texts.switch_to_wepay')) + ->asLinkTo(URL::to('/gateways/switch/wepay')) + ->appendIcon(Icon::create('circle-arrow-up')) !!} + @endif + @if ($showAdd) {!! Button::primary(trans('texts.add_gateway')) ->asLinkTo(URL::to('/gateways/create')) From 3202d2480fc68f2fb41fabb168a6a97f7a7a70ff Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 12 May 2016 20:26:36 +0300 Subject: [PATCH 063/386] Fix for saving client portal setting in self host --- app/Http/Controllers/AccountController.php | 101 +++++++++++---------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 5ce42356bf83..97226ad29c2a 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -117,21 +117,21 @@ class AccountController extends BaseController if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { return false; } - + $invitation = $this->accountRepo->enablePlan(); return $invitation->invitation_key; } - + public function changePlan() { $user = Auth::user(); $account = $user->account; - + $plan = Input::get('plan'); $term = Input::get('plan_term'); - + $planDetails = $account->getPlanDetails(false, false); - + $credit = 0; if ($planDetails) { if ($planDetails['plan'] == PLAN_PRO && $plan == PLAN_ENTERPRISE) { @@ -141,7 +141,7 @@ class AccountController extends BaseController $pending_monthly = true; $term = PLAN_TERM_YEARLY; } - + $new_plan = array( 'plan' => PLAN_ENTERPRISE, 'term' => $term, @@ -169,7 +169,7 @@ class AccountController extends BaseController // Downgrade $refund_deadline = clone $planDetails['started']; $refund_deadline->modify('+30 days'); - + if ($plan == PLAN_FREE && $refund_deadline >= date_create()) { // Refund $account->company->plan = null; @@ -179,7 +179,7 @@ class AccountController extends BaseController $account->company->plan_paid = null; $account->company->pending_plan = null; $account->company->pending_term = null; - + if ($account->company->payment) { $payment = $account->company->payment; $this->paymentService->refund($payment); @@ -188,9 +188,9 @@ class AccountController extends BaseController } else { Session::flash('message', trans('texts.updated_plan')); } - + $account->company->save(); - + } else { $pending_change = array( 'plan' => $plan, @@ -198,18 +198,18 @@ class AccountController extends BaseController ); } } - + if (!empty($new_plan)) { $time_used = $planDetails['paid']->diff(date_create()); $days_used = $time_used->days; - + if ($time_used->invert) { // They paid in advance $days_used *= -1; } - + $days_total = $planDetails['paid']->diff($planDetails['expires'])->days; - + $percent_used = $days_used / $days_total; $old_plan_price = Account::$plan_prices[$planDetails['plan']][$planDetails['term']]; $credit = $old_plan_price * (1 - $percent_used); @@ -220,20 +220,20 @@ class AccountController extends BaseController 'term' => $term, ); } - + if (!empty($pending_change) && empty($new_plan)) { $account->company->pending_plan = $pending_change['plan']; $account->company->pending_term = $pending_change['term']; $account->company->save(); - + Session::flash('message', trans('texts.updated_plan')); } - + if (!empty($new_plan)) { $invitation = $this->accountRepo->enablePlan($new_plan['plan'], $new_plan['term'], $credit, !empty($pending_monthly)); return Redirect::to('view/'.$invitation->invitation_key); } - + return Redirect::to('/settings/'.ACCOUNT_MANAGEMENT, 301); } @@ -492,7 +492,7 @@ class AccountController extends BaseController $client->postal_code = trans('texts.postal_code'); $client->work_phone = trans('texts.work_phone'); $client->work_email = trans('texts.work_id'); - + $invoice->invoice_number = '0000'; $invoice->invoice_date = Utils::fromSqlDate(date('Y-m-d')); $invoice->account = json_decode($account->toJson()); @@ -510,7 +510,7 @@ class AccountController extends BaseController $invoiceItem->product_key = 'Item'; $document->base64 = 'data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAyAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNBCUAAAAAABAAAAAAAAAAAAAAAAAAAAAA/+4AIUFkb2JlAGTAAAAAAQMAEAMDBgkAAAW8AAALrQAAEWf/2wCEAAgGBgYGBggGBggMCAcIDA4KCAgKDhANDQ4NDRARDA4NDQ4MEQ8SExQTEg8YGBoaGBgjIiIiIycnJycnJycnJycBCQgICQoJCwkJCw4LDQsOEQ4ODg4REw0NDg0NExgRDw8PDxEYFhcUFBQXFhoaGBgaGiEhICEhJycnJycnJycnJ//CABEIAGQAlgMBIgACEQEDEQH/xADtAAABBQEBAAAAAAAAAAAAAAAAAQIDBAUGBwEBAAMBAQEAAAAAAAAAAAAAAAIDBAUBBhAAAQQCAQMDBQEAAAAAAAAAAgABAwQRBRIQIBMwIQYxIiMUFUARAAIBAgMFAwgHBwUBAAAAAAECAwARIRIEMUFRYROhIkIgcYGRsdFSIzDBMpKyFAVA4WJyM0MkUPGiU3OTEgABAgQBCQYEBwAAAAAAAAABEQIAITESAyBBUWFxkaGxIhAwgdEyE8HxYnLw4UJSgiMUEwEAAgIBAwQCAwEBAAAAAAABABEhMVFBYXEQgZGhILEwwdHw8f/aAAwDAQACEQMRAAAA9ScqiDlGjgRUUcqSCOVfTEeETZI/TABQBHCxAiDmcvz1O3rM7i7HG29J1nGW6c/ZO4i1ry9ZZwJOzk2Gc11N8YVe6FsZKEQqwR8v0vnEpz4isza7FaovCjNThxulztSxiz6597PwkfQ99R6vxT0S7N2yuXJpQceKrkIq3L9kK/OuR9F8rpjCsmdZXLUN+H0Obp9Hp8azkdPd1q58T21bV6XK6dcjW2UPGl0amXp5VdnIV3c5n6t508/srbbd+3Hbl2Ib8GXV2E59tXOvLwNmfv5sueVzWhPqsNggNdcKwOifnXlS4iDvkho4bP8ASEeyPrpZktFYLMbCPudZsNzzcsTdVc5CemqECqHoAEQBABXAOABAGtD0AH//2gAIAQIAAQUB9TkSnkPEFiKNhvcnhfysQuPbJwZijLkNUGZicWCZ3X1DsIRdZZlnKmPMnOImhsWBQSifR/o7sy+5fb0OIuU8EblCBxtFGQv14ssdjQxMXqf/2gAIAQMAAQUB9Qa5LwxipBck8bMjIY0BsXYJ4Q2QT2BdFK7uMGW/QJmKIo5OrimGZ0MDm4xjEw+PMhDibBi7Y6DjkIkT/iZn8uEzoSLBYdE7dcrzGmkFn68nx6n/2gAIAQEAAQUB9HCwsLHq5XJkxC/+ByZmsbSpCi2JG3GOM68rcOZOuU7IJuRJ+uFjsd8K1tCE55wIYpBYqrzHIAQlKdmty5KG6POC2RSTXwjUGxm8ywsLHX6KMJLrXNdLXCarQd4jeY5ZrHmLYwk0Vo5k85FJZlPjTOxYDySNa2H4wpTNYrLHZKQxhHJsHGzYsRFHe17KbYHI5tVZeGlxI67yOZmTx2wYbDpmsSu9iKCL49M/DtswNZrjb2GvjtW9XsY/EKliOSQXAXnaubRQ2JWoNJWvXbu1G0FmS0MOur+L+VPKNGs0FzvvaSjZUma8xwX5isVyhUFOWwUGg2LtV+OiSOnLAMNeig1tJ1Jr5RNor9Zq91pHz12N0dfTCtvbkcl7f6xr/wAjjvUKW3LgWv2VlRaXVg8NWnHG1aBNBaFmmtiQVDIJIJIyCyYEF1ibDSms9NlUa/THY7vXtb2tSzshj+JbBF8TeI/2vklNVvkVOeV61ck9SB1+qQLx3UVa9C47HDhHDJKEQw2eS5LKz0wzqbX1LCsfF6Mqajv6S/s7eurtmbeRg/EeS5LKyjCORnpCzxxNGsrksrKysrKysrKysrKysrKysrPXK917r3Xuvde/rf/aAAgBAgIGPwHvOlq6z0t3wbnNAFWg1+mS84LiQC6drJgfCJYTrf3UHlxhWA1T8GJ5KEF1aRb7YaD6cNovcmcn5xPDnXq6o9QaIQ9Z1S/OC3OyfgckXL/FxaeESBHjAkvARd7RxGNVtLgNJatYH+XG9p6+k9LdgFF2Q9uJhh7gJoUcQaEKoO8QUUJUGRG3slFSDrhQVifHsuY8jV6m7s3hDi9rsIn9Y6mH7tEe5h4oQuDNN2YIDDnPdc5yUCBBSU8jRsiuReGNu0pPvf/aAAgBAwIGPwHvFdLnEq6awBXWUhC8LojqcIlkETU6NEI5xJGq3eYJYiCpJQecJ7hI0Ycod/SVdS4pxcnKFb0pWrifhxgPUFuJ0+I05CgpEgHbacYAMytEoBXq+cG1zcMlM1x5+UTMzUhGkmEtKZ86iGNCMa1yyElHLtF1FnsijXN+kDdmi1zS3OLgUWJIn0JyHYhA5GJG7VQwhGZdkIM2Qh6vunzi4MC7Sm7IRe9//9oACAEBAQY/Af2u18eH7Bjsq2bO3wpjQUrldsRED3wvxGlkGpbvYAtgQeOHDzVYTdf+I7f+N/ZXcYX4Gx/CQeysYwfM1vxCspRkPP3j6MxQAYYGR9noG+i+q1Dtw8CUrRfNP2sO6gA8TE7qkeRMkUpvfHPMeWw5aMussuXBIr7uYW/qoJFpgzHYcAMOdXkyIN1+9b0sbVkXW7d+FhblsrLJKGTaGAC+uu4Q5pV1GQxObBk8J3X+g6rgvcmwZssY5ALiaZxNg7fZC4JzBONXn62olH/YTl7KJy5kG24GUEbBYbbbhXXDBpVwyKLqF3hicMaPX06cdpAvzzHGm6EkcEY4WUdgzH0CssbjUMONx3ud8ppRPpelN4Zdg9GXbSZFjY+IsQT90mo5XcRMD0mVAtrfFaszsGK3ubANy+ztxqOXiMfP5TPJgqgsTyFGXTuNPBISVVw5w43AIpfzMqzq++KS34lwodXSl5PCSc/Ze1dOJQFawyLhbje9hQSR3aTeLgKvIZb+2nZ5cbd1AM3o3UhddgtfxYbMBWWOMkbl/wBsTV54nEe0KFbtNArkj4bj7GolXTL8Ze1z671G6SNK4/qxnvxm+BymwtUulP8AbN18x8qSC9uopW/npYtVozLHGMomgN8Bh9miA/SnA7okGUE8G3dtG36fKrn+7G90B4gi+FWnMmYWsxxJvwzWvsoxh2yri4Pd5bi9Hpl5bDFU7q+ktc9lHoBQvEkAe+o1lkUByEkZTsW/xCpAJzB02ISFLgADZev8zRpqD8QBVv8A6Jann0yNplkFssq9RVIO0MmK7N4oMZBKhPe6FmHZa3qqPKdkdpBwPD6Bpf6L4szqbDmTfCsn6fqGmO54wV9m2upqcyse6WlNvRdhXSzJlOLMDm9GFZNMjytwQfXWX8uYv59nrx9lP+aPUbYFUlFHp2mguqTqxKLJK+LKP/VMfWKvKrsu5y5ZfWmFdTRytAx8UbYdtxQMpDFjhqYflSA7s4XBquttRz2NaunIpR+DeRJqiuYrgq8WOAoaiXVPEzYqkZCKOVt9X1DJPFsvKMp+8hqTStE0Er2xBDobG5FxY40kGi02nifZfMSSfNtr/OlcRHwxKO0A3q8smduDfL/FXTiQCPbbKHHrF6+WbH+B3TsufZRyTSfyu1/usR7ayPKM3wulj2VnAVGOJTZjxBGNZiuVvi+w331wPprLIbkbn7resd013hbz4fupbDYb38iTTE2z7DzGIoJrNN+ZjXDOO61h5rg0mp1Wmkk0yplEDG2Vt5wwNWH+NIdxJj9t1pZ/0/V5WQhk6gvzGI91fP0sesUeKI5W9X7qXTauJ9JM2AWYd0nhermNb+a3srxfeP118qdhyYBhWEkf81jf1Vnim658QfA+giulqUyNwbC/1GiLfLOOU7jypek3d8Q3Vw8r5sKt6PdV4i0Z5Yjtq2k1YmQbI5cfxe+ra39OLD44fd3qXSQaJ0uwJnlFsluFBSb2Fr+TldQw518pynLaO2rli7cT9Q/0r//aAAgBAgMBPxD8BHIj4/gUu+n/AKDL7Eqh2LDnpJp36uxcBVJSQBqzju2/1Mo/rVB3tkuO1ZHHZYne4pQ3+A1jS9SIA5pdrL6FN29E1HHIwAiNNrOl06RtUaBbO7u6gApbHBXuAv3EB7MGADleztFGRKsm7wY7RPX6jyyGlEcPVK65Tfd263KMLBdl5vh/uDZC0O5wdmKVo4YKKAOVMbNnutFAI9eEuQ4e6ahKuKj2+B/en0tbqrHmAfYICaGFNJdQyMh/5uV4l03drL4SfIR6aL1b1BlPXXmNhFlAM7NwL0U7zACUS0VtC3J6+u9zqhb2fqLSlI+JcuIO5SQ4R9ofyf/aAAgBAwMBPxD+RAWF0BeXwHuzQV9CbX26fUGyI3Q+OsxIrVsvtv6l5UovefjcHV637+PwAhSpEW03npcCcYFf6CUJoVSLxaKfBDaWsSw47vyTCEodeVls2/8AUQ7CBsMHauvOIZ9gwKrOdefH4MthVWOO9y9BzaCnDeJ8kzpIwbaLNkqtAQS0QFwTYlN+IQGULuC0pXHSWlpFWocCQV3A4dhwVblrrFrfXSZH08asO7MfiaKWfA2PeN7MUMgK5fu4Urrgge+T6jfLDqw7/wBkMAgG2DxzG9uzsd1xQBRbbbn1ENij2hXaE6AkMCOSsjnKOW/Qai9iTi/5f//aAAgBAQMBPxAIEqVKlSpUCEHoUiRjGX6BAlSpUqIIaIhUI6G34hXMIeiRjE9OkqB63HygG1aCOt3TKzCFkCino59iplOlzY8tvCMIxuwf0/mBqJ40DUb89L4/sgg43QRGuFT0ESVfo0gRlyha0dVlpKlKrm6raQySjYol1lVfgj8C3g6iJbHNxPeAW9yDaQdgrpMZAK1eq2o7Q7EFEVS8X6HaIQYrdr7U0YQobDxRja4mPhsgnSp/cLbjYA4K51OOKoU0zRiegjSEq4oFegvxGpy4QRr5JcRHqajXulVBqlghaxQnLR092G41E0g3djqcHWMXuExr0VmhZdW7FsLT+gynKYpXXjGV7wreJppoapXL7oQD0sBYvCAX4tIpESrHmFyooWQqCbMCN1vpBgtacBgtAYVZcF7afsYf9lQisQlRdvDkWyqGZBthXx7RPvKkUrlb5Q/CrdFT5neoWdIZSWgR/VBQwZ0nUGPeBAJdZvWE38qghbIlumjVcdMzdAL5o/BAVDYFa5xT2qVhDQIAA5pB+5aemryoxhX0jk3pALPvUXhzAK5y/XUnskCEqEqMLSHNUwwLAQBRotLMeIdlDn5FpRZUUm5R2ZJ7EpNZRMobAO5K5hOAUuBYHYG+8SddNHz0+EKEOCcKzlT1BZYb4uB90OpYUAVM2rcL3vCknNK+bjWGKs6bZa9oVhmRdpg/YWAAlUVJkcjdXD11Lgke0VcU2MbHfygaFKWEnTL5GJZzMyGuGMPMbSQlbPagPOZaKOHjusEyaLtXgeW3iK4+oDc4bNYnwcKiQaks/Caxh5wK7kdeZvb3LEJhAMqbKrhAqim522Qv5gPgqp9FxlL7mnZpXi3MxIMgDkG/ug65qHbsEF8zXvjwBFAU4jmwArRmKjV6XLdNd1TvoiF1X5vX/fMHBChWDvd+4paeJz4FDgzLjs70CdhHznQBjzv7Sxo8bd2NfcZmYNWs8RxQGYGe1+olGV9n7Z+0UPFyYwlYvmDNJctGQPGwnyQAWPv0haPhQ4abtsUxZfaFBalqvypK8pGizJpYO+aShBw+h2xgHf3CNeSAXzRnTRxS/szKo3P+IMAszsGE7iUiOwZy99tXZg3BCqz2L+qH0gU09RzxfaMDrstvwgKoDsPRrCLj7jcKSy6oH5pLZC0I+L/UPAvRNDQUa9oMU7aNedH3NWIKBWuO+m4lsAS60VfopKsCajNR6AT7l8D418EaQCisod0YIUK9U/PBh6loQegqKly/QfkBmNzMzM/i+jOk/9k='; - + $invoice->client = $client; $invoice->invoice_items = [$invoiceItem]; //$invoice->documents = $account->hasFeature(FEATURE_DOCUMENTS) ? [$document] : []; @@ -523,7 +523,7 @@ class AccountController extends BaseController $data['invoiceDesigns'] = InvoiceDesign::getDesigns(); $data['invoiceFonts'] = Cache::get('fonts'); $data['section'] = $section; - + $pageSizes = [ 'A0', 'A1', @@ -701,6 +701,13 @@ class AccountController extends BaseController private function saveClientPortal() { + $account = Auth::user()->account; + + $account->enable_client_portal = !!Input::get('enable_client_portal'); + $account->enable_client_portal_dashboard = !!Input::get('enable_client_portal_dashboard'); + $account->enable_portal_password = !!Input::get('enable_portal_password'); + $account->send_portal_password = !!Input::get('send_portal_password'); + // Only allowed for pro Invoice Ninja users or white labeled self-hosted users if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { $input_css = Input::get('client_view_css'); @@ -735,19 +742,13 @@ class AccountController extends BaseController $sanitized_css = $input_css; } - $account = Auth::user()->account; $account->client_view_css = $sanitized_css; - - $account->enable_client_portal = !!Input::get('enable_client_portal'); - $account->enable_client_portal_dashboard = !!Input::get('enable_client_portal_dashboard'); - $account->enable_portal_password = !!Input::get('enable_portal_password'); - $account->send_portal_password = !!Input::get('send_portal_password'); - - $account->save(); - - Session::flash('message', trans('texts.updated_settings')); } + $account->save(); + + Session::flash('message', trans('texts.updated_settings')); + return Redirect::to('settings/'.ACCOUNT_CLIENT_PORTAL); } @@ -1003,15 +1004,15 @@ class AccountController extends BaseController /* Logo image file */ if ($uploaded = Input::file('logo')) { $path = Input::file('logo')->getRealPath(); - + $disk = $account->getLogoDisk(); if ($account->hasLogo()) { $disk->delete($account->logo); } - + $extension = strtolower($uploaded->getClientOriginalExtension()); if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){ - $documentType = Document::$extraExtensions[$extension]; + $documentType = Document::$extraExtensions[$extension]; } else{ $documentType = $extension; @@ -1030,19 +1031,19 @@ class AccountController extends BaseController } else { if ($documentType != 'gif') { $account->logo = $account->account_key.'.'.$documentType; - + $imageSize = getimagesize($filePath); $account->logo_width = $imageSize[0]; $account->logo_height = $imageSize[1]; $account->logo_size = $size; - + // make sure image isn't interlaced if (extension_loaded('fileinfo')) { $image = Image::make($path); $image->interlace(false); $imageStr = (string) $image->encode($documentType); $disk->put($account->logo, $imageStr); - + $account->logo_size = strlen($imageStr); } else { $stream = fopen($filePath, 'r'); @@ -1055,12 +1056,12 @@ class AccountController extends BaseController $image->resize(200, 120, function ($constraint) { $constraint->aspectRatio(); }); - + $account->logo = $account->account_key.'.png'; $image = Image::canvas($image->width(), $image->height(), '#FFFFFF')->insert($image); $imageStr = (string) $image->encode('png'); $disk->put($account->logo, $imageStr); - + $account->logo_size = strlen($imageStr); $account->logo_width = $image->width(); $account->logo_height = $image->height(); @@ -1070,7 +1071,7 @@ class AccountController extends BaseController } } } - + $account->save(); } @@ -1140,7 +1141,7 @@ class AccountController extends BaseController $account = Auth::user()->account; if ($account->hasLogo()) { $account->getLogoDisk()->delete($account->logo); - + $account->logo = null; $account->logo_size = null; $account->logo_width = null; @@ -1245,7 +1246,7 @@ class AccountController extends BaseController $this->accountRepo->unlinkAccount($account); if ($account->company->accounts->count() == 1) { - $account->company->forceDelete(); + $account->company->forceDelete(); } $account->forceDelete(); @@ -1293,7 +1294,7 @@ class AccountController extends BaseController return Redirect::to("/settings/$section/", 301); } - + public function previewEmail(\App\Services\TemplateService $templateService) { $template = Input::get('template'); @@ -1301,29 +1302,29 @@ class AccountController extends BaseController ->invoices() ->withTrashed() ->first(); - + if ( ! $invoice) { return trans('texts.create_invoice_for_sample'); } - + $account = Auth::user()->account; - + // replace the variables with sample data $data = [ 'account' => $account, 'invoice' => $invoice, 'invitation' => $invoice->invitations->first(), 'client' => $invoice->client, - 'amount' => $invoice->amount + 'amount' => $invoice->amount ]; - - // create the email view + + // create the email view $view = 'emails.' . $account->getTemplateView(ENTITY_INVOICE) . '_html'; $data = array_merge($data, [ 'body' => $templateService->processVariables($template, $data), 'entityType' => ENTITY_INVOICE, ]); - + return Response::view($view, $data); } } From 6c4009d1efb34621ff3b1329f6ba08b41df4ba71 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 12 May 2016 20:27:24 +0300 Subject: [PATCH 064/386] Fixes for initial setup --- ...11_05_180133_confide_setup_users_table.php | 148 +++++++++--------- ...2016_01_04_175228_create_vendors_table.php | 4 +- ...2016_01_24_112646_add_bank_subaccounts.php | 8 +- ..._01_27_173015_add_header_footer_option.php | 20 ++- ...135956_add_source_currency_to_expenses.php | 8 +- ...214710_add_support_three_decimal_taxes.php | 4 +- ...3_23_215049_support_multiple_tax_rates.php | 34 ++-- 7 files changed, 121 insertions(+), 105 deletions(-) diff --git a/database/migrations/2013_11_05_180133_confide_setup_users_table.php b/database/migrations/2013_11_05_180133_confide_setup_users_table.php index 6bd69f503980..e9c10da1ad1e 100644 --- a/database/migrations/2013_11_05_180133_confide_setup_users_table.php +++ b/database/migrations/2013_11_05_180133_confide_setup_users_table.php @@ -9,37 +9,37 @@ class ConfideSetupUsersTable extends Migration { * @return void */ public function up() - { - Schema::dropIfExists('payment_terms'); - Schema::dropIfExists('themes'); - Schema::dropIfExists('credits'); + { + Schema::dropIfExists('payment_terms'); + Schema::dropIfExists('themes'); + Schema::dropIfExists('credits'); Schema::dropIfExists('activities'); Schema::dropIfExists('invitations'); Schema::dropIfExists('payments'); Schema::dropIfExists('account_gateways'); Schema::dropIfExists('invoice_items'); Schema::dropIfExists('products'); - Schema::dropIfExists('tax_rates'); + Schema::dropIfExists('tax_rates'); Schema::dropIfExists('contacts'); Schema::dropIfExists('invoices'); Schema::dropIfExists('password_reminders'); Schema::dropIfExists('clients'); Schema::dropIfExists('users'); Schema::dropIfExists('accounts'); - Schema::dropIfExists('currencies'); + Schema::dropIfExists('currencies'); Schema::dropIfExists('invoice_statuses'); Schema::dropIfExists('countries'); - Schema::dropIfExists('timezones'); - Schema::dropIfExists('frequencies'); - Schema::dropIfExists('date_formats'); - Schema::dropIfExists('datetime_formats'); + Schema::dropIfExists('timezones'); + Schema::dropIfExists('frequencies'); + Schema::dropIfExists('date_formats'); + Schema::dropIfExists('datetime_formats'); Schema::dropIfExists('sizes'); Schema::dropIfExists('industries'); Schema::dropIfExists('gateways'); Schema::dropIfExists('payment_types'); Schema::create('countries', function($table) - { + { $table->increments('id'); $table->string('capital', 255)->nullable(); $table->string('citizenship', 255)->nullable(); @@ -53,7 +53,7 @@ class ConfideSetupUsersTable extends Migration { $table->string('name', 255)->default(''); $table->string('region_code', 3)->default(''); $table->string('sub_region_code', 3)->default(''); - $table->boolean('eea')->default(0); + $table->boolean('eea')->default(0); }); Schema::create('themes', function($t) @@ -85,21 +85,21 @@ class ConfideSetupUsersTable extends Migration { Schema::create('date_formats', function($t) { $t->increments('id'); - $t->string('format'); - $t->string('picker_format'); - $t->string('label'); + $t->string('format'); + $t->string('picker_format'); + $t->string('label'); }); Schema::create('datetime_formats', function($t) { $t->increments('id'); - $t->string('format'); - $t->string('label'); + $t->string('format'); + $t->string('label'); }); Schema::create('currencies', function($t) { - $t->increments('id'); + $t->increments('id'); $t->string('name'); $t->string('symbol'); @@ -107,20 +107,20 @@ class ConfideSetupUsersTable extends Migration { $t->string('thousand_separator'); $t->string('decimal_separator'); $t->string('code'); - }); + }); Schema::create('sizes', function($t) { $t->increments('id'); $t->string('name'); - }); + }); Schema::create('industries', function($t) { $t->increments('id'); $t->string('name'); - }); - + }); + Schema::create('accounts', function($t) { $t->increments('id'); @@ -136,13 +136,13 @@ class ConfideSetupUsersTable extends Migration { $t->string('ip'); $t->string('account_key')->unique(); $t->timestamp('last_login')->nullable(); - + $t->string('address1')->nullable(); $t->string('address2')->nullable(); $t->string('city')->nullable(); $t->string('state')->nullable(); $t->string('postal_code')->nullable(); - $t->unsignedInteger('country_id')->nullable(); + $t->unsignedInteger('country_id')->nullable(); $t->text('invoice_terms')->nullable(); $t->text('email_footer')->nullable(); $t->unsignedInteger('industry_id')->nullable(); @@ -158,17 +158,17 @@ class ConfideSetupUsersTable extends Migration { $t->foreign('currency_id')->references('id')->on('currencies'); $t->foreign('industry_id')->references('id')->on('industries'); $t->foreign('size_id')->references('id')->on('sizes'); - }); - + }); + Schema::create('gateways', function($t) { $t->increments('id'); - $t->timestamps(); + $t->timestamps(); $t->string('name'); $t->string('provider'); $t->boolean('visible')->default(true); - }); + }); Schema::create('users', function($t) { @@ -206,31 +206,31 @@ class ConfideSetupUsersTable extends Migration { $t->unsignedInteger('gateway_id'); $t->timestamps(); $t->softDeletes(); - + $t->text('config'); $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $t->foreign('gateway_id')->references('id')->on('gateways'); $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); - + $t->unsignedInteger('public_id')->index(); $t->unique( array('account_id','public_id') ); - }); + }); Schema::create('password_reminders', function($t) { $t->string('email'); $t->timestamps(); - + $t->string('token'); - }); + }); Schema::create('clients', function($t) { $t->increments('id'); $t->unsignedInteger('user_id'); - $t->unsignedInteger('account_id')->index(); + $t->unsignedInteger('account_id')->index(); $t->unsignedInteger('currency_id')->nullable(); $t->timestamps(); $t->softDeletes(); @@ -255,14 +255,14 @@ class ConfideSetupUsersTable extends Migration { $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); - $t->foreign('country_id')->references('id')->on('countries'); - $t->foreign('industry_id')->references('id')->on('industries'); - $t->foreign('size_id')->references('id')->on('sizes'); + $t->foreign('country_id')->references('id')->on('countries'); + $t->foreign('industry_id')->references('id')->on('industries'); + $t->foreign('size_id')->references('id')->on('sizes'); $t->foreign('currency_id')->references('id')->on('currencies'); - + $t->unsignedInteger('public_id')->index(); $t->unique( array('account_id','public_id') ); - }); + }); Schema::create('contacts', function($t) { @@ -279,14 +279,14 @@ class ConfideSetupUsersTable extends Migration { $t->string('last_name')->nullable(); $t->string('email')->nullable(); $t->string('phone')->nullable(); - $t->timestamp('last_login')->nullable(); + $t->timestamp('last_login')->nullable(); - $t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade'); + $t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade'); $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');; $t->unsignedInteger('public_id')->nullable(); $t->unique( array('account_id','public_id') ); - }); + }); Schema::create('invoice_statuses', function($t) { @@ -325,15 +325,15 @@ class ConfideSetupUsersTable extends Migration { $t->timestamp('last_sent_date')->nullable(); $t->unsignedInteger('recurring_invoice_id')->index()->nullable(); - $t->string('tax_name'); - $t->decimal('tax_rate', 13, 2); + $t->string('tax_name1'); + $t->decimal('tax_rate1', 13, 3); $t->decimal('amount', 13, 2); $t->decimal('balance', 13, 2); - + $t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade'); - $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); - $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $t->foreign('invoice_status_id')->references('id')->on('invoice_statuses'); $t->foreign('recurring_invoice_id')->references('id')->on('invoices')->onDelete('cascade'); @@ -375,11 +375,11 @@ class ConfideSetupUsersTable extends Migration { $t->softDeletes(); $t->string('name'); - $t->decimal('rate', 13, 2); - - $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $t->decimal('rate', 13, 3); + + $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');; - + $t->unsignedInteger('public_id'); $t->unique( array('account_id','public_id') ); }); @@ -396,10 +396,10 @@ class ConfideSetupUsersTable extends Migration { $t->text('notes'); $t->decimal('cost', 13, 2); $t->decimal('qty', 13, 2)->nullable(); - - $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + + $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');; - + $t->unsignedInteger('public_id'); $t->unique( array('account_id','public_id') ); }); @@ -420,8 +420,8 @@ class ConfideSetupUsersTable extends Migration { $t->decimal('cost', 13, 2); $t->decimal('qty', 13, 2)->nullable(); - $t->string('tax_name')->nullable(); - $t->decimal('tax_rate', 13, 2)->nullable(); + $t->string('tax_name1')->nullable(); + $t->decimal('tax_rate1', 13, 3)->nullable(); $t->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade'); $t->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); @@ -458,10 +458,10 @@ class ConfideSetupUsersTable extends Migration { $t->foreign('account_gateway_id')->references('id')->on('account_gateways')->onDelete('cascade'); $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');; $t->foreign('payment_type_id')->references('id')->on('payment_types'); - + $t->unsignedInteger('public_id')->index(); $t->unique( array('account_id','public_id') ); - }); + }); Schema::create('credits', function($t) { @@ -471,21 +471,21 @@ class ConfideSetupUsersTable extends Migration { $t->unsignedInteger('user_id'); $t->timestamps(); $t->softDeletes(); - + $t->boolean('is_deleted')->default(false); $t->decimal('amount', 13, 2); $t->decimal('balance', 13, 2); $t->date('credit_date')->nullable(); $t->string('credit_number')->nullable(); $t->text('private_notes'); - + $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade'); $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');; - + $t->unsignedInteger('public_id')->index(); $t->unique( array('account_id','public_id') ); - }); + }); Schema::create('activities', function($t) { @@ -500,13 +500,13 @@ class ConfideSetupUsersTable extends Migration { $t->unsignedInteger('invoice_id')->nullable(); $t->unsignedInteger('credit_id')->nullable(); $t->unsignedInteger('invitation_id')->nullable(); - + $t->text('message')->nullable(); $t->text('json_backup')->nullable(); - $t->integer('activity_type_id'); + $t->integer('activity_type_id'); $t->decimal('adjustment', 13, 2)->nullable(); $t->decimal('balance', 13, 2)->nullable(); - + $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade'); }); @@ -519,9 +519,9 @@ class ConfideSetupUsersTable extends Migration { */ public function down() { - Schema::dropIfExists('payment_terms'); - Schema::dropIfExists('themes'); - Schema::dropIfExists('credits'); + Schema::dropIfExists('payment_terms'); + Schema::dropIfExists('themes'); + Schema::dropIfExists('credits'); Schema::dropIfExists('activities'); Schema::dropIfExists('invitations'); Schema::dropIfExists('payments'); @@ -535,16 +535,16 @@ class ConfideSetupUsersTable extends Migration { Schema::dropIfExists('clients'); Schema::dropIfExists('users'); Schema::dropIfExists('accounts'); - Schema::dropIfExists('currencies'); + Schema::dropIfExists('currencies'); Schema::dropIfExists('invoice_statuses'); Schema::dropIfExists('countries'); - Schema::dropIfExists('timezones'); - Schema::dropIfExists('frequencies'); - Schema::dropIfExists('date_formats'); - Schema::dropIfExists('datetime_formats'); + Schema::dropIfExists('timezones'); + Schema::dropIfExists('frequencies'); + Schema::dropIfExists('date_formats'); + Schema::dropIfExists('datetime_formats'); Schema::dropIfExists('sizes'); Schema::dropIfExists('industries'); - Schema::dropIfExists('gateways'); + Schema::dropIfExists('gateways'); Schema::dropIfExists('payment_types'); } } diff --git a/database/migrations/2016_01_04_175228_create_vendors_table.php b/database/migrations/2016_01_04_175228_create_vendors_table.php index 1295748b0801..5e8551004c20 100644 --- a/database/migrations/2016_01_04_175228_create_vendors_table.php +++ b/database/migrations/2016_01_04_175228_create_vendors_table.php @@ -57,7 +57,7 @@ class CreateVendorsTable extends Migration $table->foreign('vendor_id')->references('id')->on('vendors')->onDelete('cascade'); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); - + $table->unsignedInteger('public_id')->nullable(); $table->unique(array('account_id', 'public_id')); }); @@ -79,7 +79,7 @@ class CreateVendorsTable extends Migration $table->date('expense_date')->nullable(); $table->text('private_notes'); $table->text('public_notes'); - $table->unsignedInteger('currency_id')->nullable(); + $table->unsignedInteger('invoice_currency_id')->nullable(false); $table->boolean('should_be_invoiced')->default(true); // Relations diff --git a/database/migrations/2016_01_24_112646_add_bank_subaccounts.php b/database/migrations/2016_01_24_112646_add_bank_subaccounts.php index 92b283a52662..5c01fdd5de8b 100644 --- a/database/migrations/2016_01_24_112646_add_bank_subaccounts.php +++ b/database/migrations/2016_01_24_112646_add_bank_subaccounts.php @@ -35,13 +35,13 @@ class AddBankSubaccounts extends Migration { Schema::table('expenses', function($table) { - $table->string('transaction_id'); - $table->unsignedInteger('bank_id'); + $table->string('transaction_id')->nullable(); + $table->unsignedInteger('bank_id')->nullable(); }); Schema::table('vendors', function($table) { - $table->string('transaction_name'); + $table->string('transaction_name')->nullable(); }); } @@ -53,7 +53,7 @@ class AddBankSubaccounts extends Migration { public function down() { Schema::drop('bank_subaccounts'); - + Schema::table('expenses', function($table) { $table->dropColumn('transaction_id'); diff --git a/database/migrations/2016_01_27_173015_add_header_footer_option.php b/database/migrations/2016_01_27_173015_add_header_footer_option.php index 0cb690a17383..d840b31e8f13 100644 --- a/database/migrations/2016_01_27_173015_add_header_footer_option.php +++ b/database/migrations/2016_01_27_173015_add_header_footer_option.php @@ -25,20 +25,24 @@ class AddHeaderFooterOption extends Migration { $table->boolean('is_offsite'); $table->boolean('is_secure'); }); - + Schema::table('expenses', function($table) { - $table->string('transaction_id')->nullable()->change(); - $table->unsignedInteger('bank_id')->nullable()->change(); - }); + if (Schema::hasColumn('expenses', 'transaction_id')) { + $table->string('transaction_id')->nullable()->change(); + $table->unsignedInteger('bank_id')->nullable()->change(); + } + }); Schema::table('vendors', function($table) { - $table->string('transaction_name')->nullable()->change(); - }); - + if (Schema::hasColumn('vendors', 'transaction_name')) { + $table->string('transaction_name')->nullable()->change(); + } + }); + } - + /** * Reverse the migrations. * diff --git a/database/migrations/2016_02_01_135956_add_source_currency_to_expenses.php b/database/migrations/2016_02_01_135956_add_source_currency_to_expenses.php index 4fcfa367d6be..a4b1fa62436b 100644 --- a/database/migrations/2016_02_01_135956_add_source_currency_to_expenses.php +++ b/database/migrations/2016_02_01_135956_add_source_currency_to_expenses.php @@ -16,10 +16,12 @@ class AddSourceCurrencyToExpenses extends Migration $table->dropColumn('foreign_amount'); - $table->unsignedInteger('currency_id')->nullable(false)->change(); - $table->renameColumn('currency_id', 'invoice_currency_id'); - $table->unsignedInteger('expense_currency_id'); + if (Schema::hasColumn('expenses', 'currency_id')) { + $table->unsignedInteger('currency_id')->nullable(false)->change(); + $table->renameColumn('currency_id', 'invoice_currency_id'); + } + $table->unsignedInteger('expense_currency_id'); }); Schema::table('expenses', function (Blueprint $table) { diff --git a/database/migrations/2016_03_14_214710_add_support_three_decimal_taxes.php b/database/migrations/2016_03_14_214710_add_support_three_decimal_taxes.php index af35fc927564..339a29b521b7 100644 --- a/database/migrations/2016_03_14_214710_add_support_three_decimal_taxes.php +++ b/database/migrations/2016_03_14_214710_add_support_three_decimal_taxes.php @@ -11,7 +11,9 @@ class AddSupportThreeDecimalTaxes extends Migration { public function up() { Schema::table('tax_rates', function($table) { - $table->decimal('rate', 13, 3)->change(); + if (Schema::hasColumn('tax_rates', 'rate')) { + $table->decimal('rate', 13, 3)->change(); + } }); } /** diff --git a/database/migrations/2016_03_23_215049_support_multiple_tax_rates.php b/database/migrations/2016_03_23_215049_support_multiple_tax_rates.php index 4c2d8a29b931..cda2aef7b428 100644 --- a/database/migrations/2016_03_23_215049_support_multiple_tax_rates.php +++ b/database/migrations/2016_03_23_215049_support_multiple_tax_rates.php @@ -13,30 +13,38 @@ class SupportMultipleTaxRates extends Migration public function up() { Schema::table('invoices', function($table) { - $table->decimal('tax_rate', 13, 3)->change(); - }); + if (Schema::hasColumn('invoices', 'tax_rate')) { + $table->decimal('tax_rate', 13, 3)->change(); + } + }); Schema::table('invoice_items', function($table) { - $table->decimal('tax_rate', 13, 3)->change(); - }); - + if (Schema::hasColumn('invoice_items', 'tax_rate')) { + $table->decimal('tax_rate', 13, 3)->change(); + } + }); + Schema::table('invoices', function($table) { + if (Schema::hasColumn('invoices', 'tax_rate')) { $table->renameColumn('tax_rate', 'tax_rate1'); $table->renameColumn('tax_name', 'tax_name1'); - $table->string('tax_name2')->nullable(); - $table->decimal('tax_rate2', 13, 3); + } + $table->string('tax_name2')->nullable(); + $table->decimal('tax_rate2', 13, 3); }); Schema::table('invoice_items', function($table) { + if (Schema::hasColumn('invoice_items', 'tax_rate')) { $table->renameColumn('tax_rate', 'tax_rate1'); $table->renameColumn('tax_name', 'tax_name1'); - $table->string('tax_name2')->nullable(); - $table->decimal('tax_rate2', 13, 3); + } + $table->string('tax_name2')->nullable(); + $table->decimal('tax_rate2', 13, 3); }); - Schema::table('accounts', function($table) { - $table->boolean('enable_client_portal_dashboard')->default(true); - }); + Schema::table('accounts', function($table) { + $table->boolean('enable_client_portal_dashboard')->default(true); + }); } /** * Reverse the migrations. @@ -65,4 +73,4 @@ class SupportMultipleTaxRates extends Migration $table->dropColumn('enable_client_portal_dashboard'); }); } -} \ No newline at end of file +} From 0ce8a0d70aa580fa4ed12726b765ac082cec0d8c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 12 May 2016 20:31:31 +0300 Subject: [PATCH 065/386] Set initial key value to enable setup page to load w/o error --- config/app.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/app.php b/config/app.php index 4a75f65f9e5f..d9aa4ef00030 100644 --- a/config/app.php +++ b/config/app.php @@ -82,7 +82,7 @@ return [ | */ - 'key' => env('APP_KEY', ''), + 'key' => env('APP_KEY', 'SomeRandomString'), 'cipher' => env('APP_CIPHER', MCRYPT_RIJNDAEL_128), @@ -139,7 +139,7 @@ return [ 'Illuminate\Validation\ValidationServiceProvider', 'Illuminate\View\ViewServiceProvider', 'Illuminate\Broadcasting\BroadcastServiceProvider', - + /* * Additional Providers */ From 751b084a5922b7fba167fee7b11dde824bf7f7ee Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 12 May 2016 22:35:33 +0300 Subject: [PATCH 066/386] Fix javasript typo --- resources/views/payments/paymentmethods_list.blade.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/views/payments/paymentmethods_list.blade.php b/resources/views/payments/paymentmethods_list.blade.php index 0dc04a988e58..776cc942869c 100644 --- a/resources/views/payments/paymentmethods_list.blade.php +++ b/resources/views/payments/paymentmethods_list.blade.php @@ -63,7 +63,7 @@ {{ $paymentMethod->bank_data }} @endif @if($paymentMethod->status == PAYMENT_METHOD_STATUS_NEW) - ({{trans('texts.complete_verification')}}) + ({{trans('texts.complete_verification')}}) @elseif($paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFICATION_FAILED) ({{trans('texts.verification_failed')}}) @endif @@ -77,7 +77,7 @@ @elseif($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED) ({{trans('texts.use_for_auto_bill')}}) @endif - × + ×
@endforeach @endif @@ -184,4 +184,4 @@ $('#default_id').val(sourceId); $('#defaultSourceForm').submit() } - \ No newline at end of file + From 02b08402156c784977ad86692ddf0695fcb7c30d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 10:38:42 +0300 Subject: [PATCH 067/386] Fix for online payment test --- tests/acceptance/OnlinePaymentCest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/OnlinePaymentCest.php b/tests/acceptance/OnlinePaymentCest.php index 7e85f2cc6b5a..4621fecb7bb8 100644 --- a/tests/acceptance/OnlinePaymentCest.php +++ b/tests/acceptance/OnlinePaymentCest.php @@ -77,7 +77,7 @@ class OnlinePaymentCest $I->fillField(['name' => 'postal_code'], $this->faker->postcode); $I->selectDropdown($I, 'United States', '.country-select .dropdown-toggle'); */ - + $I->fillField('#card_number', '4242424242424242'); $I->fillField('#cvv', '1234'); $I->selectOption('#expiration_month', 12); @@ -94,10 +94,10 @@ class OnlinePaymentCest $I->selectDropdown($I, $clientEmail, '.client_select .dropdown-toggle'); $I->fillField('table.invoice-table tbody tr:nth-child(1) #product_key', $productKey); $I->click('table.invoice-table tbody tr:nth-child(1) .tt-selectable'); - $I->checkOption('#auto_bill'); + $I->selectOption('#auto_bill', 3); $I->executeJS('preparePdfData(\'email\')'); $I->wait(3); $I->see("$0.00"); } -} \ No newline at end of file +} From fba37171aebf67d62c368deaf1ac47bcf5aa9322 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 12:08:41 +0300 Subject: [PATCH 068/386] Support query counter in webapp as well as API --- app/Http/Controllers/BaseAPIController.php | 38 ++++++++-------------- app/Http/Kernel.php | 1 + app/Http/Middleware/QueryLogging.php | 38 ++++++++++++++++++++++ 3 files changed, 53 insertions(+), 24 deletions(-) create mode 100644 app/Http/Middleware/QueryLogging.php diff --git a/app/Http/Controllers/BaseAPIController.php b/app/Http/Controllers/BaseAPIController.php index 0718e95b91e4..4660e9b25e4c 100644 --- a/app/Http/Controllers/BaseAPIController.php +++ b/app/Http/Controllers/BaseAPIController.php @@ -61,40 +61,36 @@ class BaseAPIController extends Controller } $this->serializer = Request::get('serializer') ?: API_SERIALIZER_ARRAY; - + if ($this->serializer === API_SERIALIZER_JSON) { $this->manager->setSerializer(new JsonApiSerializer()); } else { $this->manager->setSerializer(new ArraySerializer()); } - - if (Utils::isNinjaDev()) { - \DB::enableQueryLog(); - } } protected function handleAction($request) - { + { $entity = $request->entity(); $action = $request->action; - + $repo = Utils::toCamelCase($this->entityType) . 'Repo'; - + $this->$repo->$action($entity); - + return $this->itemResponse($entity); } protected function listResponse($query) { $transformerClass = EntityModel::getTransformerName($this->entityType); - $transformer = new $transformerClass(Auth::user()->account, Input::get('serializer')); + $transformer = new $transformerClass(Auth::user()->account, Input::get('serializer')); $includes = $transformer->getDefaultIncludes(); $includes = $this->getRequestIncludes($includes); $query->with($includes); - + if ($updatedAt = Input::get('updated_at')) { $updatedAt = date('Y-m-d H:i:s', $updatedAt); $query->where(function($query) use ($includes, $updatedAt) { @@ -106,14 +102,14 @@ class BaseAPIController extends Controller } }); } - + if ($clientPublicId = Input::get('client_id')) { $filter = function($query) use ($clientPublicId) { $query->where('public_id', '=', $clientPublicId); }; $query->whereHas('client', $filter); } - + if ( ! Utils::hasPermission('view_all')){ if ($this->entityType == ENTITY_USER) { $query->where('id', '=', Auth::user()->id); @@ -121,7 +117,7 @@ class BaseAPIController extends Controller $query->where('user_id', '=', Auth::user()->id); } } - + $data = $this->createCollection($query, $transformer, $this->entityType); return $this->response($data); @@ -130,10 +126,10 @@ class BaseAPIController extends Controller protected function itemResponse($item) { $transformerClass = EntityModel::getTransformerName($this->entityType); - $transformer = new $transformerClass(Auth::user()->account, Input::get('serializer')); + $transformer = new $transformerClass(Auth::user()->account, Input::get('serializer')); $data = $this->createItem($item, $transformer, $this->entityType); - + return $this->response($data); } @@ -160,18 +156,12 @@ class BaseAPIController extends Controller } else { $resource = new Collection($query, $transformer, $entityType); } - + return $this->manager->createData($resource)->toArray(); } protected function response($response) { - if (Utils::isNinjaDev()) { - $count = count(\DB::getQueryLog()); - Log::info(Request::method() . ' - ' . Request::url() . ": $count queries"); - Log::info(json_encode(\DB::getQueryLog())); - } - $index = Request::get('index') ?: 'data'; if ($index == 'none') { @@ -222,7 +212,7 @@ class BaseAPIController extends Controller $data[] = $include; } } - + return $data; } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 1142338b200e..8f7db1f0fabd 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -17,6 +17,7 @@ class Kernel extends HttpKernel { 'Illuminate\View\Middleware\ShareErrorsFromSession', 'App\Http\Middleware\VerifyCsrfToken', 'App\Http\Middleware\DuplicateSubmissionCheck', + 'App\Http\Middleware\QueryLogging', 'App\Http\Middleware\StartupCheck', ]; diff --git a/app/Http/Middleware/QueryLogging.php b/app/Http/Middleware/QueryLogging.php new file mode 100644 index 000000000000..da01a52b05fd --- /dev/null +++ b/app/Http/Middleware/QueryLogging.php @@ -0,0 +1,38 @@ +url(), '_debugbar') === false) { + $queries = DB::getQueryLog(); + $count = count($queries); + Log::info($request->method() . ' - ' . $request->url() . ": $count queries"); + //Log::info(json_encode($queries)); + } + } + + return $response; + } +} From f873998e22a259825d875532bd8d0d6d260cd563 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 12:23:13 +0300 Subject: [PATCH 069/386] Simplified reseller setup --- app/Ninja/Repositories/AccountRepository.php | 74 ++++++++++---------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index 38753dde9b9a..42e04cefe7fc 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -28,7 +28,7 @@ class AccountRepository { $company = new Company(); $company->save(); - + $account = new Account(); $account->ip = Request::getClientIp(); $account->account_key = str_random(RANDOM_KEY_LENGTH); @@ -87,7 +87,7 @@ class AccountRepository private function getAccountSearchData($user) { $account = $user->account; - + $data = [ 'clients' => [], 'contacts' => [], @@ -102,7 +102,7 @@ class AccountRepository if ($account->custom_client_label2) { $data[$account->custom_client_label2] = []; } - + if ($user->hasPermission('view_all')) { $clients = Client::scope() ->with('contacts', 'invoices') @@ -114,7 +114,7 @@ class AccountRepository $query->where('user_id', '=', $user->id); }])->get(); } - + foreach ($clients as $client) { if ($client->name) { $data['clients'][] = [ @@ -122,20 +122,20 @@ class AccountRepository 'tokens' => $client->name, 'url' => $client->present()->url, ]; - } + } if ($client->custom_value1) { $data[$account->custom_client_label1][] = [ 'value' => "{$client->custom_value1}: " . $client->getDisplayName(), 'tokens' => $client->custom_value1, - 'url' => $client->present()->url, + 'url' => $client->present()->url, ]; - } + } if ($client->custom_value2) { $data[$account->custom_client_label2][] = [ 'value' => "{$client->custom_value2}: " . $client->getDisplayName(), 'tokens' => $client->custom_value2, - 'url' => $client->present()->url, + 'url' => $client->present()->url, ]; } @@ -242,9 +242,9 @@ class AccountRepository if ($credit < 0) { $credit = 0; } - + $plan_cost = Account::$plan_prices[$plan][$term]; - + $account = $this->getNinjaAccount(); $lastInvoice = Invoice::withTrashed()->whereAccountId($account->id)->orderBy('public_id', 'DESC')->first(); $publicId = $lastInvoice ? ($lastInvoice->public_id + 1) : 1; @@ -266,28 +266,28 @@ class AccountRepository $credit_item->product_key = trans('texts.plan_credit_product'); $invoice->invoice_items()->save($credit_item); } - + $item = InvoiceItem::createNew($invoice); $item->qty = 1; $item->cost = $plan_cost; $item->notes = trans("texts.{$plan}_plan_{$term}_description"); - + // Don't change this without updating the regex in PaymentService->createPayment() $item->product_key = 'Plan - '.ucfirst($plan).' ('.ucfirst($term).')'; $invoice->invoice_items()->save($item); - + if ($pending_monthly) { $term_end = $term == PLAN_MONTHLY ? date_create('+1 month') : date_create('+1 year'); $pending_monthly_item = InvoiceItem::createNew($invoice); $item->qty = 1; $pending_monthly_item->cost = 0; $pending_monthly_item->notes = trans("texts.plan_pending_monthly", array('date', Utils::dateToString($term_end))); - + // Don't change this without updating the text in PaymentService->createPayment() $pending_monthly_item->product_key = 'Pending Monthly'; $invoice->invoice_items()->save($pending_monthly_item); } - + $invitation = new Invitation(); $invitation->account_id = $account->id; @@ -328,12 +328,14 @@ class AccountRepository $user->notify_paid = true; $account->users()->save($user); - $accountGateway = new AccountGateway(); - $accountGateway->user_id = $user->id; - $accountGateway->gateway_id = NINJA_GATEWAY_ID; - $accountGateway->public_id = 1; - $accountGateway->setConfig(json_decode(env(NINJA_GATEWAY_CONFIG))); - $account->account_gateways()->save($accountGateway); + if ($config = env(NINJA_GATEWAY_CONFIG)) { + $accountGateway = new AccountGateway(); + $accountGateway->user_id = $user->id; + $accountGateway->gateway_id = NINJA_GATEWAY_ID; + $accountGateway->public_id = 1; + $accountGateway->setConfig(json_decode($config)); + $account->account_gateways()->save($accountGateway); + } } return $account; @@ -356,11 +358,11 @@ class AccountRepository $client->user_id = $ninjaUser->id; $client->currency_id = 1; } - + foreach (['name', 'address1', 'address2', 'city', 'state', 'postal_code', 'country_id', 'work_phone', 'language_id', 'vat_number'] as $field) { $client->$field = $account->$field; } - + $client->save(); if ($clientExists) { @@ -372,7 +374,7 @@ class AccountRepository $contact->public_id = $account->id; $contact->is_primary = true; } - + $user = $account->getPrimaryUser(); foreach (['first_name', 'last_name', 'email', 'phone'] as $field) { $contact->$field = $user->$field; @@ -513,7 +515,7 @@ class AccountRepository if ($with) { $users->with($with); } - + return $users->get(); } @@ -565,7 +567,7 @@ class AccountRepository $record->save(); $users = $this->getUserAccounts($record); - + // Pick the primary user foreach ($users as $user) { if (!$user->public_id) { @@ -573,16 +575,16 @@ class AccountRepository if(empty($primaryUser)) { $useAsPrimary = true; } - + $planDetails = $user->account->getPlanDetails(false, false); $planLevel = 0; - + if ($planDetails) { $planLevel = 1; if ($planDetails['plan'] == PLAN_ENTERPRISE) { $planLevel = 2; } - + if (!$useAsPrimary && ( $planLevel > $primaryUserPlanLevel || ($planLevel == $primaryUserPlanLevel && $planDetails['expires'] > $primaryUserPlanExpires) @@ -590,7 +592,7 @@ class AccountRepository $useAsPrimary = true; } } - + if ($useAsPrimary) { $primaryUser = $user; $primaryUserPlanLevel = $planLevel; @@ -600,14 +602,14 @@ class AccountRepository } } } - + // Merge other companies into the primary user's company if (!empty($primaryUser)) { foreach ($users as $user) { if ($user == $primaryUser || $user->public_id) { continue; } - + if ($user->account->company_id != $primaryUser->account->company_id) { foreach ($user->account->company->accounts as $account) { $account->company_id = $primaryUser->account->company_id; @@ -636,9 +638,9 @@ class AccountRepository $userAccount->removeUserId($userId); $userAccount->save(); } - + $user = User::whereId($userId)->first(); - + if (!$user->public_id && $user->account->company->accounts->count() > 1) { $company = Company::create(); $company->save(); @@ -660,7 +662,7 @@ class AccountRepository ->withTrashed() ->first(); } while ($match); - + return $code; } @@ -668,7 +670,7 @@ class AccountRepository { $name = trim($name) ?: 'TOKEN'; $users = $this->findUsers($user); - + foreach ($users as $user) { if ($token = AccountToken::whereUserId($user->id)->whereName($name)->first()) { continue; From df3a103825dcead9d68a31d490b5d41159fc097a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 15:26:22 +0300 Subject: [PATCH 070/386] Added missing report field --- resources/lang/en/texts.php | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 0aebc8b87903..31847fdd09b5 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1054,14 +1054,14 @@ $LANG = array( 'enable_portal_password_help'=>'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.', 'send_portal_password'=>'Generate password automatically', 'send_portal_password_help'=>'If no password is set, one will be generated and sent with the first invoice.', - + 'expired' => 'Expired', 'invalid_card_number' => 'The credit card number is not valid.', 'invalid_expiry' => 'The expiration date is not valid.', 'invalid_cvv' => 'The CVV is not valid.', 'cost' => 'Cost', 'create_invoice_for_sample' => 'Note: create your first invoice to see a preview here.', - + // User Permissions 'owner' => 'Owner', 'administrator' => 'Administrator', @@ -1079,8 +1079,8 @@ $LANG = array( 'create_all_help' => 'Allow user to create and modify records', 'view_all_help' => 'Allow user to view records they didn\'t create', 'edit_all_help' => 'Allow user to modify records they didn\'t create', - 'view_payment' => 'View Payment', - + 'view_payment' => 'View Payment', + 'january' => 'January', 'february' => 'February', 'march' => 'March', @@ -1093,7 +1093,7 @@ $LANG = array( 'october' => 'October', 'november' => 'November', 'december' => 'December', - + // Documents 'documents_header' => 'Documents:', 'email_documents_header' => 'Documents:', @@ -1125,11 +1125,11 @@ $LANG = array( 'enable_client_portal_help' => 'Show/hide the client portal.', 'enable_client_portal_dashboard' => 'Dashboard', 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', - + // Plans 'account_management' => 'Account Management', 'plan_status' => 'Plan Status', - + 'plan_upgrade' => 'Upgrade', 'plan_change' => 'Change Plan', 'pending_change_to' => 'Changes To', @@ -1159,9 +1159,9 @@ $LANG = array( 'plan_paid' => 'Term Started', 'plan_started' => 'Plan Started', 'plan_expires' => 'Plan Expires', - + 'white_label_button' => 'White Label', - + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', 'enterprise_plan_product' => 'Enterprise Plan', @@ -1181,8 +1181,8 @@ $LANG = array( 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', 'return_to_app' => 'Return to app', - - + + // Payment updates 'refund_payment' => 'Refund Payment', 'refund_max' => 'Max:', @@ -1199,7 +1199,7 @@ $LANG = array( 'activity_39' => ':user cancelled a :payment_amount payment (:payment)', 'activity_40' => ':user refunded :adjustment of a :payment_amount payment (:payment)', 'card_expiration' => 'Exp: :expires', - + 'card_creditcardother' => 'Unknown', 'card_americanexpress' => 'American Express', 'card_carteblanche' => 'Carte Blanche', @@ -1251,7 +1251,7 @@ $LANG = array( '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. + '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.', @@ -1293,8 +1293,10 @@ $LANG = array( 'no_payment_method_specified' => 'No payment method specified', + 'chart_type' => 'Chart Type', + ); return $LANG; -?>. \ No newline at end of file +?>. From b111adaf173dd83e1b1103344559ccc27aae5f7c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 15:35:59 +0300 Subject: [PATCH 071/386] Added and removed extra lables from activity messages --- .../Controllers/PublicClientController.php | 89 ++++++++++--------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index 405de63493b0..95f350dc04f0 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -60,7 +60,7 @@ class PublicClientController extends BaseController ]); } - if (!Input::has('phantomjs') && !Input::has('silent') && !Session::has($invitationKey) + if (!Input::has('phantomjs') && !Input::has('silent') && !Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) { if ($invoice->is_quote) { event(new QuoteInvitationWasViewed($invoice, $invitation)); @@ -73,7 +73,7 @@ class PublicClientController extends BaseController Session::put('invitation_key', $invitationKey); // track current invitation $account->loadLocalizationSettings($client); - + $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date); $invoice->features = [ @@ -82,7 +82,7 @@ class PublicClientController extends BaseController 'invoice_settings' => $account->hasFeature(FEATURE_INVOICE_SETTINGS), ]; $invoice->invoice_fonts = $account->getFontsData(); - + if ($invoice->invoice_design_id == CUSTOM_DESIGN) { $invoice->invoice_design->javascript = $account->custom_design; } else { @@ -149,10 +149,10 @@ class PublicClientController extends BaseController 'checkoutComDebug' => $checkoutComDebug, 'phantomjs' => Input::has('phantomjs'), ); - + if($account->hasFeature(FEATURE_DOCUMENTS) && $this->canCreateZip()){ $zipDocs = $this->getInvoiceZipDocuments($invoice, $size); - + if(count($zipDocs) > 1){ $data['documentsZipURL'] = URL::to("client/documents/{$invitation->invitation_key}"); $data['documentsZipSize'] = $size; @@ -173,6 +173,7 @@ class PublicClientController extends BaseController foreach ($paymentMethods as $paymentMethod) { if ($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED) { $code = htmlentities(str_replace(' ', '', strtolower($paymentMethod->payment_type->name))); + $html = ''; if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) { if($paymentMethod->bank_data) { @@ -301,7 +302,7 @@ class PublicClientController extends BaseController $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); } } - + return response()->view('invited.dashboard', $data); } @@ -321,9 +322,9 @@ class PublicClientController extends BaseController $data = [ 'client' => Utils::getClientDisplayName($model), 'user' => $model->is_system ? ('' . trans('texts.system') . '') : ($model->user_first_name . ' ' . $model->user_last_name), - 'invoice' => trans('texts.invoice') . ' ' . $model->invoice, + 'invoice' => $model->invoice, 'contact' => Utils::getClientDisplayName($model), - 'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''), + '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, @@ -349,7 +350,7 @@ class PublicClientController extends BaseController } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; - + $data = [ 'color' => $color, 'account' => $account, @@ -420,7 +421,7 @@ class PublicClientController extends BaseController return $this->returnError(); } - $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, 'account' => $account, @@ -469,7 +470,7 @@ class PublicClientController extends BaseController ->orderColumns( 'invoice_number', 'transaction_reference', 'payment_type', 'amount', 'payment_date') ->make(); } - + private function getPaymentStatusLabel($model) { $label = trans("texts.status_" . strtolower($model->payment_status_name)); @@ -544,7 +545,7 @@ class PublicClientController extends BaseController return $this->returnError(); } - $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, 'account' => $account, @@ -597,55 +598,55 @@ class PublicClientController extends BaseController return $invitation; } - + public function getDocumentVFSJS($publicId, $name){ if (!$invitation = $this->getInvitation()) { return $this->returnError(); } - + $clientId = $invitation->invoice->client_id; $document = Document::scope($publicId, $invitation->account_id)->first(); - - + + if(!$document->isPDFEmbeddable()){ return Response::view('error', array('error'=>'Image does not exist!'), 404); } - + $authorized = false; if($document->expense && $document->expense->client_id == $invitation->invoice->client_id){ $authorized = true; } else if($document->invoice && $document->invoice->client_id == $invitation->invoice->client_id){ $authorized = true; } - + if(!$authorized){ return Response::view('error', array('error'=>'Not authorized'), 403); - } - + } + if(substr($name, -3)=='.js'){ $name = substr($name, 0, -3); } - + $content = $document->preview?$document->getRawPreview():$document->getRaw(); $content = 'ninjaAddVFSDoc('.json_encode(intval($publicId).'/'.strval($name)).',"'.base64_encode($content).'")'; $response = Response::make($content, 200); $response->header('content-type', 'text/javascript'); $response->header('cache-control', 'max-age=31536000'); - + return $response; } - + protected function canCreateZip(){ return function_exists('gmp_init'); } - + protected function getInvoiceZipDocuments($invoice, &$size=0){ $documents = $invoice->documents; - + foreach($invoice->expenses as $expense){ $documents = $documents->merge($expense->documents); } - + $documents = $documents->sortBy('size'); $size = 0; @@ -653,16 +654,16 @@ class PublicClientController extends BaseController $toZip = array(); foreach($documents as $document){ if($size + $document->size > $maxSize)break; - + if(!empty($toZip[$document->name])){ // This name is taken if($toZip[$document->name]->hash != $document->hash){ // 2 different files with the same name $nameInfo = pathinfo($document->name); - + for($i = 1;; $i++){ $name = $nameInfo['filename'].' ('.$i.').'.$nameInfo['extension']; - + if(empty($toZip[$name])){ $toZip[$name] = $document; $size += $document->size; @@ -672,7 +673,7 @@ class PublicClientController extends BaseController break; } } - + } } else{ @@ -680,25 +681,25 @@ class PublicClientController extends BaseController $size += $document->size; } } - + return $toZip; } - + public function getInvoiceDocumentsZip($invitationKey){ if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { return $this->returnError(); } - + Session::put('invitation_key', $invitationKey); // track current invitation - + $invoice = $invitation->invoice; - + $toZip = $this->getInvoiceZipDocuments($invoice); - + if(!count($toZip)){ return Response::view('error', array('error'=>'No documents small enough'), 404); } - + $zip = new ZipArchive($invitation->account->name.' Invoice '.$invoice->invoice_number.'.zip'); return Response::stream(function() use ($toZip, $zip) { foreach($toZip as $name=>$document){ @@ -716,28 +717,28 @@ class PublicClientController extends BaseController $zip->finish(); }, 200); } - + public function getDocument($invitationKey, $publicId){ if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { return $this->returnError(); } - + Session::put('invitation_key', $invitationKey); // track current invitation - + $clientId = $invitation->invoice->client_id; $document = Document::scope($publicId, $invitation->account_id)->firstOrFail(); - + $authorized = false; if($document->expense && $document->expense->client_id == $invitation->invoice->client_id){ $authorized = true; } else if($document->invoice && $document->invoice->client_id == $invitation->invoice->client_id){ $authorized = true; } - + if(!$authorized){ return Response::view('error', array('error'=>'Not authorized'), 403); - } - + } + return DocumentController::getDownloadResponse($document); } From 32ab340b74ce9dd608e9244d8230bf88830c8273 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 13 May 2016 16:00:07 +0300 Subject: [PATCH 072/386] Added fake card details for test payment --- resources/views/payments/add_paymentmethod.blade.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/resources/views/payments/add_paymentmethod.blade.php b/resources/views/payments/add_paymentmethod.blade.php index 7c5ca2b956cf..376230315322 100644 --- a/resources/views/payments/add_paymentmethod.blade.php +++ b/resources/views/payments/add_paymentmethod.blade.php @@ -299,6 +299,15 @@ {{ Former::populateField('state', 'NY') }} {{ Former::populateField('postal_code', '10118') }} {{ Former::populateField('country_id', 840) }} + + @endif @@ -694,4 +703,4 @@ @endif -@stop \ No newline at end of file +@stop From ce04c994dda4cca616f5dd729236774ed2fbc508 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Fri, 13 May 2016 09:30:22 -0400 Subject: [PATCH 073/386] Support payments and storing cards via WePay. --- .../Controllers/AccountGatewayController.php | 2 +- app/Http/Controllers/PaymentController.php | 73 +++--- .../Controllers/PublicClientController.php | 7 +- app/Http/routes.php | 6 +- app/Libraries/Utils.php | 2 +- app/Models/Account.php | 2 + app/Services/AccountGatewayService.php | 4 +- app/Services/PaymentService.php | 210 +++++++++++----- .../payments/add_paymentmethod.blade.php | 234 +----------------- .../payments/paymentmethods_list.blade.php | 1 + .../payments/tokenization_braintree.blade.php | 61 +++++ .../payments/tokenization_stripe.blade.php | 154 ++++++++++++ .../payments/tokenization_wepay.blade.php | 52 ++++ 13 files changed, 480 insertions(+), 328 deletions(-) create mode 100644 resources/views/payments/tokenization_braintree.blade.php create mode 100644 resources/views/payments/tokenization_stripe.blade.php create mode 100644 resources/views/payments/tokenization_wepay.blade.php diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index a192538480e9..27d3d54464d2 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -449,7 +449,7 @@ class AccountGatewayController extends BaseController 'tokenType' => $wepayUser->token_type, 'tokenExpires' => $accessTokenExpires, 'accountId' => $wepayAccount->account_id, - 'testMode' => WEPAY_ENVIRONMENT == WEPAY_STAGING, + 'testMode' => WEPAY_ENVIRONMENT == WEPAY_STAGE, )); $account->account_gateways()->save($accountGateway); diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 8f09c2980a59..e14a9cb9cd69 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -152,6 +152,8 @@ class PaymentController extends BaseController $data = array(); + Session::put($invitation->id.'payment_ref', $invoice->id.'_'.uniqid()); + if ($paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) { if ($paymentType == PAYMENT_TYPE_TOKEN) { $useToken = true; @@ -198,6 +200,10 @@ class PaymentController extends BaseController $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); } + if(!empty($data['braintreeClientToken']) || $accountGateway->getPublishableStripeKey()|| $accountGateway->gateway_id == GATEWAY_WEPAY) { + $data['tokenize'] = true; + } + } else { if ($deviceData = Input::get('details')) { Session::put($invitation->id . 'device_data', $deviceData); @@ -405,7 +411,7 @@ class PaymentController extends BaseController 'last_name' => 'required', ]; - if ( ! Input::get('stripeToken') && ! Input::get('payment_method_nonce') && !(Input::get('plaidPublicToken') && Input::get('plaidAccountId'))) { + if ( ! Input::get('sourceToken') && !(Input::get('plaidPublicToken') && Input::get('plaidAccountId'))) { $rules = array_merge( $rules, [ @@ -459,8 +465,6 @@ class PaymentController extends BaseController $paymentType = Session::get($invitation->id . 'payment_type'); $accountGateway = $account->getGatewayByType($paymentType); $paymentMethod = null; - - if ($useToken) { if(!$sourceId) { @@ -492,21 +496,46 @@ class PaymentController extends BaseController $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway, $data); // check if we're creating/using a billing token + $tokenBillingSupported = false; if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + $tokenBillingSupported = true; + $customerReferenceParam = 'cardReference'; + if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && !Input::get('authorize_ach')) { Session::flash('error', trans('texts.ach_authorization_required')); return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); } + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { + $tokenBillingSupported = true; + $customerReferenceParam = 'paymentMethodToken'; + $deviceData = Input::get('device_data'); + if (!$deviceData) { + $deviceData = Session::get($invitation->id . 'device_data'); + } + + if($deviceData) { + $details['device_data'] = $deviceData; + } + } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { + $tokenBillingSupported = true; + $customerReferenceParam = false; + } + + if ($tokenBillingSupported) { if ($useToken) { - $details['customerReference'] = $customerReference; - unset($details['token']); - $details['cardReference'] = $sourceReference; + if ($customerReferenceParam) { + $details[$customerReferenceParam] = $customerReference; + } + $details['token'] = $sourceReference; + unset($details['card']); } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing') || $paymentType == PAYMENT_TYPE_STRIPE_ACH) { $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */); if ($token) { $details['token'] = $token; - $details['customerReference'] = $customerReference; + if ($customerReferenceParam) { + $details[$customerReferenceParam] = $customerReference; + } if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty(Input::get('plaidPublicToken')) ) { // The user needs to complete verification @@ -518,36 +547,6 @@ class PaymentController extends BaseController return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); } } - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $deviceData = Input::get('device_data'); - if (!$deviceData) { - $deviceData = Session::get($invitation->id . 'device_data'); - } - - if ($token = Input::get('payment_method_nonce')) { - $details['token'] = $token; - unset($details['card']); - } - - if ($useToken) { - $details['customerId'] = $customerReference; - $details['paymentMethodToken'] = $sourceReference; - unset($details['token']); - } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) { - $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */); - if ($token) { - $details['paymentMethodToken'] = $token; - $details['customerId'] = $customerReference; - unset($details['token']); - } else { - $this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway); - return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); - } - } - - if($deviceData) { - $details['deviceData'] = $deviceData; - } } $response = $gateway->purchase($details)->send(); diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index 405de63493b0..360f71e90fbf 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -859,7 +859,6 @@ class PublicClientController extends BaseController ]; if ($paymentType == PAYMENT_TYPE_STRIPE_ACH) { - $data['currencies'] = Cache::get('currencies'); } @@ -867,6 +866,10 @@ class PublicClientController extends BaseController $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); } + if(!empty($data['braintreeClientToken']) || $accountGateway->getPublishableStripeKey()|| $accountGateway->gateway_id == GATEWAY_WEPAY) { + $data['tokenize'] = true; + } + return View::make('payments.add_paymentmethod', $data); } @@ -882,7 +885,7 @@ class PublicClientController extends BaseController $account = $client->account; $accountGateway = $account->getGatewayByType($paymentType); - $sourceToken = $accountGateway->gateway_id == GATEWAY_STRIPE ? Input::get('stripeToken'):Input::get('payment_method_nonce'); + $sourceToken = Input::get('sourceToken'); if (!PaymentController::processPaymentClientDetails($client, $accountGateway, $paymentType)) { return Redirect::to('client/paymentmethods/add/' . $typeLink)->withInput(Request::except('cvv')); diff --git a/app/Http/routes.php b/app/Http/routes.php index 2c12c215cfd3..3a02960d8005 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -562,6 +562,10 @@ if (!defined('CONTACT_EMAIL')) { define('GATEWAY_WEPAY', 60); define('GATEWAY_BRAINTREE', 61); + // The customer exists, but only as a local concept + // The remote gateway doesn't understand the concept of customers + define('CUSTOMER_REFERENCE_LOCAL', 'local'); + define('EVENT_CREATE_CLIENT', 1); define('EVENT_CREATE_INVOICE', 2); define('EVENT_CREATE_QUOTE', 3); @@ -749,7 +753,7 @@ if (!defined('CONTACT_EMAIL')) { // WePay define('WEPAY_PRODUCTION', 'production'); - define('WEPAY_STAGING', 'staging'); + define('WEPAY_STAGE', 'stage'); define('WEPAY_CLIENT_ID', env('WEPAY_CLIENT_ID')); define('WEPAY_CLIENT_SECRET', env('WEPAY_CLIENT_SECRET')); define('WEPAY_ENVIRONMENT', env('WEPAY_ENVIRONMENT', WEPAY_PRODUCTION)); diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 8e54926e8045..dc92f6e65125 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -995,7 +995,7 @@ class Utils public static function setupWePay($accountGateway = null) { if (WePay::getEnvironment() == 'none') { - if (WEPAY_ENVIRONMENT == WEPAY_STAGING) { + if (WEPAY_ENVIRONMENT == WEPAY_STAGE) { WePay::useStaging(WEPAY_CLIENT_ID, WEPAY_CLIENT_SECRET); } else { WePay::useProduction(WEPAY_CLIENT_ID, WEPAY_CLIENT_SECRET); diff --git a/app/Models/Account.php b/app/Models/Account.php index 29a9abbd09be..fb2c39b951db 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -1245,6 +1245,8 @@ class Account extends Eloquent return GATEWAY_STRIPE; } elseif ($this->isGatewayConfigured(GATEWAY_BRAINTREE)) { return GATEWAY_BRAINTREE; + } elseif ($this->isGatewayConfigured(GATEWAY_WEPAY)) { + return GATEWAY_WEPAY; } else { return false; } diff --git a/app/Services/AccountGatewayService.php b/app/Services/AccountGatewayService.php index c21902c87374..fe5427b966bb 100644 --- a/app/Services/AccountGatewayService.php +++ b/app/Services/AccountGatewayService.php @@ -46,7 +46,7 @@ class AccountGatewayService extends BaseService return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); } else { $accountGateway = AccountGateway::find($model->id); - $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGING ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; + $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; $wepayAccountId = $accountGateway->getConfig()->accountId; $linkText = $model->name; $url = $endpoint.'account/'.$wepayAccountId; @@ -116,7 +116,7 @@ class AccountGatewayService extends BaseService uctrans('texts.manage_wepay_account'), function ($model) { $accountGateway = AccountGateway::find($model->id); - $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGING ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; + $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; return array( 'url' => $endpoint.'account/'.$accountGateway->getConfig()->accountId, 'attributes' => 'target="_blank"' diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index b97bbbd67de4..626366a469cd 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -9,6 +9,7 @@ use Cache; use Omnipay; use Session; use CreditCard; +use WePay; use App\Models\Payment; use App\Models\PaymentMethod; use App\Models\Account; @@ -30,7 +31,8 @@ class PaymentService extends BaseService protected static $refundableGateways = array( GATEWAY_STRIPE, - GATEWAY_BRAINTREE + GATEWAY_BRAINTREE, + GATEWAY_WEPAY, ); public function __construct(PaymentRepository $paymentRepo, AccountRepository $accountRepo, DatatableService $datatableService) @@ -95,9 +97,9 @@ class PaymentService extends BaseService $data['ButtonSource'] = 'InvoiceNinja_SP'; }; - if ($input && $accountGateway->isGateway(GATEWAY_STRIPE)) { - if (!empty($input['stripeToken'])) { - $data['token'] = $input['stripeToken']; + if ($input) { + if (!empty($input['sourceToken'])) { + $data['token'] = $input['sourceToken']; unset($data['card']); } elseif (!empty($input['plaidPublicToken'])) { $data['plaidPublicToken'] = $input['plaidPublicToken']; @@ -106,6 +108,10 @@ class PaymentService extends BaseService } } + if ($accountGateway->isGateway(GATEWAY_WEPAY) && $transactionId = Session::get($invitation->id.'payment_ref')) { + $data['transaction_id'] = $transactionId; + } + return $data; } @@ -266,6 +272,17 @@ class PaymentService extends BaseService if (!$response->isSuccessful()) { return $response->getMessage(); } + } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { + try { + $wepay = Utils::setupWePay($accountGateway); + $wepay->request('/credit_card/delete', [ + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => $paymentMethod->source_reference, + ]); + } catch (\WePayException $ex){ + return $ex->getMessage(); + } } $paymentMethod->delete(); @@ -291,16 +308,16 @@ class PaymentService extends BaseService { $customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return paramenter */); - if ($customerReference) { + if ($customerReference && $customerReference != CUSTOMER_REFERENCE_LOCAL) { $details['customerReference'] = $customerReference; - if ($accountGateway->gateway->id == GATEWAY_STRIPE) { + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { $customerResponse = $gateway->fetchCustomer(array('customerReference' => $customerReference))->send(); if (!$customerResponse->isSuccessful()) { $customerReference = null; // The customer might not exist anymore } - } elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) { + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { $customer = $gateway->findCustomer($customerReference)->send()->getData(); if (!($customer instanceof \Braintree\Customer)) { @@ -309,7 +326,7 @@ class PaymentService extends BaseService } } - if ($accountGateway->gateway->id == GATEWAY_STRIPE) { + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { if (!empty($details['plaidPublicToken'])) { $plaidResult = $this->getPlaidToken($accountGateway, $details['plaidPublicToken'], $details['plaidAccountId']); @@ -355,7 +372,7 @@ class PaymentService extends BaseService return; } } - } elseif ($accountGateway->gateway->id == GATEWAY_BRAINTREE) { + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { if (!$customerReference) { $tokenResponse = $gateway->createCustomer(array('customerData' => array()))->send(); if ($tokenResponse->isSuccessful()) { @@ -377,6 +394,31 @@ class PaymentService extends BaseService return; } } + } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { + $wepay = Utils::setupWePay($accountGateway); + + try { + $wepay->request('credit_card/authorize', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => $details['token'], + )); + + // Get the card details + $tokenResponse = $wepay->request('credit_card', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => $details['token'], + )); + + $customerReference = CUSTOMER_REFERENCE_LOCAL; + $sourceReference = $details['token']; + } catch (\WePayException $ex) { + $this->lastError = $ex->getMessage(); + return; + } + } else { + return null; } if ($customerReference) { @@ -394,7 +436,7 @@ class PaymentService extends BaseService $accountGatewayToken->token = $customerReference; $accountGatewayToken->save(); - $paymentMethod = $this->createPaymentMethodFromGatewayResponse($tokenResponse, $accountGateway, $accountGatewayToken, $contactId); + $paymentMethod = $this->convertPaymentMethodFromGatewayResponse($tokenResponse, $accountGateway, $accountGatewayToken, $contactId); } else { $this->lastError = $tokenResponse->getMessage(); @@ -456,8 +498,24 @@ class PaymentService extends BaseService return $paymentMethod; } + + public function convertPaymentMethodFromWePay($source, $accountGatewayToken = null, $paymentMethod = null) { + // Creating a new one or updating an existing one + if (!$paymentMethod) { + $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + } + + $paymentMethod->payment_type_id = $this->parseCardType($source->credit_card_name); + $paymentMethod->last4 = $source->last_four; + $paymentMethod->expiration = $source->expiration_year . '-' . $source->expiration_month . '-00'; + $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); + + $paymentMethod->source_reference = $source->credit_card_id; + + return $paymentMethod; + } - public function createPaymentMethodFromGatewayResponse($gatewayResponse, $accountGateway, $accountGatewayToken = null, $contactId = null) { + public function convertPaymentMethodFromGatewayResponse($gatewayResponse, $accountGateway, $accountGatewayToken = null, $contactId = null, $existingPaymentMethod = null) { if ($accountGateway->gateway_id == GATEWAY_STRIPE) { $data = $gatewayResponse->getData(); if (!empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) { @@ -470,7 +528,7 @@ class PaymentService extends BaseService } if ($source) { - $paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken); + $paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken, $existingPaymentMethod); } } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { $data = $gatewayResponse->getData(); @@ -478,7 +536,12 @@ class PaymentService extends BaseService if (!empty($data->transaction)) { $transaction = $data->transaction; - $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + if ($existingPaymentMethod) { + $paymentMethod = $existingPaymentMethod; + } else { + $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); + } + if ($transaction->paymentInstrumentType == 'credit_card') { $card = $transaction->creditCardDetails; $paymentMethod->last4 = $card->last4; @@ -490,9 +553,20 @@ class PaymentService extends BaseService } $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); } elseif (!empty($data->paymentMethod)) { - $paymentMethod = $this->convertPaymentMethodFromBraintree($data->paymentMethod, $accountGatewayToken); + $paymentMethod = $this->convertPaymentMethodFromBraintree($data->paymentMethod, $accountGatewayToken, $existingPaymentMethod); } + } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { + if ($gatewayResponse instanceof \Omnipay\WePay\Message\CustomCheckoutResponse) { + $wepay = \Utils::setupWePay($accountGateway); + $gatewayResponse = $wepay->request('credit_card', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => $gatewayResponse->getData()['payment_method']['credit_card']['id'], + )); + + } + $paymentMethod = $this->convertPaymentMethodFromWePay($gatewayResponse, $accountGatewayToken, $existingPaymentMethod); } if (!empty($paymentMethod) && $accountGatewayToken && $contactId) { @@ -566,43 +640,49 @@ class PaymentService extends BaseService $payment->payment_type_id = $this->detectCardType($card->getNumber()); } + $savePaymentMethod = !empty($paymentMethod); + + // This will convert various gateway's formats to a known format + $paymentMethod = $this->convertPaymentMethodFromGatewayResponse($purchaseResponse, $accountGateway, null, null, $paymentMethod); + + // If this is a stored payment method, we'll update it with the latest info + if ($savePaymentMethod) { + $paymentMethod->save(); + } + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { $data = $purchaseResponse->getData(); - $source = !empty($data['source'])?$data['source']:$data['card']; - $payment->payment_status_id = $data['status'] == 'succeeded' ? PAYMENT_STATUS_COMPLETED : PAYMENT_STATUS_PENDING; - - if ($source) { - $payment->last4 = $source['last4']; - - if ($source['object'] == 'bank_account') { - $payment->routing_number = $source['routing_number']; - $payment->payment_type_id = PAYMENT_TYPE_ACH; - } - else{ - $payment->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-00'; - $payment->payment_type_id = $this->parseCardType($source['brand']); - } - } - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $transaction = $purchaseResponse->getData()->transaction; - if ($transaction->paymentInstrumentType == 'credit_card') { - $card = $transaction->creditCardDetails; - $payment->last4 = $card->last4; - $payment->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00'; - $payment->payment_type_id = $this->parseCardType($card->cardType); - } elseif ($transaction->paymentInstrumentType == 'paypal_account') { - $payment->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; - $payment->email = $transaction->paypalDetails->payerEmail; - } - } - - if ($payerId) { - $payment->payer_id = $payerId; } if ($paymentMethod) { - $payment->payment_method_id = $paymentMethod->id; + if ($paymentMethod->last4) { + $payment->last4 = $paymentMethod->last4; + } + + if ($paymentMethod->expiration) { + $payment->expiration = $paymentMethod->expiration; + } + + if ($paymentMethod->routing_number) { + $payment->routing_number = $paymentMethod->routing_number; + } + + if ($paymentMethod->payment_type_id) { + $payment->payment_type_id = $paymentMethod->payment_type_id; + } + + if ($paymentMethod->email) { + $payment->email = $paymentMethod->email; + } + + if ($payerId) { + $payment->payer_id = $payerId; + } + + if ($savePaymentMethod) { + $payment->payment_method_id = $paymentMethod->id; + } } $payment->save(); @@ -665,20 +745,29 @@ class PaymentService extends BaseService private function parseCardType($cardName) { $cardTypes = array( - 'Visa' => PAYMENT_TYPE_VISA, - 'American Express' => PAYMENT_TYPE_AMERICAN_EXPRESS, - 'MasterCard' => PAYMENT_TYPE_MASTERCARD, - 'Discover' => PAYMENT_TYPE_DISCOVER, - 'JCB' => PAYMENT_TYPE_JCB, - 'Diners Club' => PAYMENT_TYPE_DINERS, - 'Carte Blanche' => PAYMENT_TYPE_CARTE_BLANCHE, - 'China UnionPay' => PAYMENT_TYPE_UNIONPAY, - 'Laser' => PAYMENT_TYPE_LASER, - 'Maestro' => PAYMENT_TYPE_MAESTRO, - 'Solo' => PAYMENT_TYPE_SOLO, - 'Switch' => PAYMENT_TYPE_SWITCH + 'visa' => PAYMENT_TYPE_VISA, + 'americanexpress' => PAYMENT_TYPE_AMERICAN_EXPRESS, + 'amex' => PAYMENT_TYPE_AMERICAN_EXPRESS, + 'mastercard' => PAYMENT_TYPE_MASTERCARD, + 'discover' => PAYMENT_TYPE_DISCOVER, + 'jcb' => PAYMENT_TYPE_JCB, + 'dinersclub' => PAYMENT_TYPE_DINERS, + 'carteblanche' => PAYMENT_TYPE_CARTE_BLANCHE, + 'chinaunionpay' => PAYMENT_TYPE_UNIONPAY, + 'unionpay' => PAYMENT_TYPE_UNIONPAY, + 'laser' => PAYMENT_TYPE_LASER, + 'maestro' => PAYMENT_TYPE_MAESTRO, + 'solo' => PAYMENT_TYPE_SOLO, + 'switch' => PAYMENT_TYPE_SWITCH ); + $cardName = strtolower(str_replace(array(' ', '-', '_'), '', $cardName)); + + if (empty($cardTypes[$cardName]) && 1 == preg_match('/^('.implode('|', array_keys($cardTypes)).')/', $cardName, $matches)) { + // Some gateways return extra stuff after the card name + $cardName = $matches[1]; + } + if (!empty($cardTypes[$cardName])) { return $cardTypes[$cardName]; } else { @@ -736,10 +825,9 @@ class PaymentService extends BaseService $details = $this->getPaymentDetails($invitation, $accountGateway); $details['customerReference'] = $token; - if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - $details['cardReference'] = $defaultPaymentMethod->source_reference; - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $details['paymentMethodToken'] = $defaultPaymentMethod->source_reference; + $details['token'] = $defaultPaymentMethod->source_reference; + if ($accountGateway->gateway_id == GATEWAY_WEPAY) { + $details['transaction_id'] = 'autobill_'.$invoice->id; } // submit purchase/get response diff --git a/resources/views/payments/add_paymentmethod.blade.php b/resources/views/payments/add_paymentmethod.blade.php index 7c5ca2b956cf..e4d5d3c0a590 100644 --- a/resources/views/payments/add_paymentmethod.blade.php +++ b/resources/views/payments/add_paymentmethod.blade.php @@ -3,221 +3,11 @@ @section('head') @parent @if (!empty($braintreeClientToken)) - - + @include('payments.tokenization_braintree') @elseif (isset($accountGateway) && $accountGateway->getPublishableStripeKey()) - - + @include('payments.tokenization_stripe') + @elseif (isset($accountGateway) && $accountGateway->gateway_id == GATEWAY_WEPAY) + @include('payments.tokenization_wepay') @else @endif - @stop @section('content') @@ -283,7 +72,6 @@ {{ Former::populateField('email', $contact->email) }} @if (!$client->country_id && $client->account->country_id) {{ Former::populateField('country_id', $client->account->country_id) }} - {{ Former::populateField('country', $client->account->country->iso_3166_2) }} @endif @if (!$client->currency_id && $client->account->currency_id) {{ Former::populateField('currency_id', $client->account->currency_id) }} @@ -442,9 +230,9 @@ ))->inline()->label(trans('texts.account_holder_type')); !!} {!! Former::text('account_holder_name') ->label(trans('texts.account_holder_name')) !!} - {!! Former::select('country') + {!! Former::select('country_id') ->label(trans('texts.country_id')) - ->fromQuery($countries, 'name', 'iso_3166_2') + ->fromQuery($countries, 'name', 'id') ->addGroupClass('country-select') !!} {!! Former::select('currency') ->label(trans('texts.currency_id')) @@ -485,7 +273,7 @@

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

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

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

@@ -20,6 +23,23 @@ {!! Former::text('email') !!} {!! Former::text('company_name')->help('wepay_company_name_help')->maxlength(255) !!} {!! Former::text('description')->help('wepay_description_help') !!} + {!! Former::select('token_billing_type_id') + ->options($tokenBillingOptions) + ->help(trans('texts.token_billing_help')) !!} + {!! Former::checkbox('show_address') + ->label(trans('texts.billing_address')) + ->text(trans('texts.show_address_help')) + ->addGroupClass('gateway-option') !!} + {!! Former::checkbox('update_address') + ->label(' ') + ->text(trans('texts.update_address_help')) + ->addGroupClass('gateway-option') !!} + {!! Former::checkboxes('creditCardTypes[]') + ->label('Accepted Credit Cards') + ->checkboxes($creditCardTypes) + ->class('creditcard-types') + ->addGroupClass('gateway-option') + !!} {!! Former::checkbox('tos_agree')->label(' ')->text(trans('texts.wepay_tos_agree', ['link'=>''.trans('texts.wepay_tos_link_text').''] ))->value('true') !!} From 8bc8b384c5513a741b9036e010f58f682f0e2bd1 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 14 May 2016 22:38:09 -0400 Subject: [PATCH 077/386] Fix auto bill error when the user hasn't entered payment information --- app/Services/PaymentService.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 11d9cda41e9f..4425adeebaa3 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -821,6 +821,11 @@ class PaymentService extends BaseService $invitation = $invoice->invitations->first(); $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); + + if (!$accountGatewayToken) { + return false; + } + $defaultPaymentMethod = $accountGatewayToken->default_payment_method; if (!$invitation || !$token || !$defaultPaymentMethod) { From 65feee34e3ab7a1531f146ae38fc8dbaa18de5f1 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 14 May 2016 22:43:55 -0400 Subject: [PATCH 078/386] Fox expiration off-by-one error --- app/Services/PaymentService.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 4425adeebaa3..4a4a757720de 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -471,7 +471,7 @@ class PaymentService extends BaseService $paymentMethod->setRelation('currency', $currency); } } elseif ($source['object'] == 'card') { - $paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-00'; + $paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-01'; $paymentMethod->payment_type_id = $this->parseCardType($source['brand']); } else { return null; @@ -491,7 +491,7 @@ class PaymentService extends BaseService if ($source instanceof \Braintree\CreditCard) { $paymentMethod->payment_type_id = $this->parseCardType($source->cardType); $paymentMethod->last4 = $source->last4; - $paymentMethod->expiration = $source->expirationYear . '-' . $source->expirationMonth . '-00'; + $paymentMethod->expiration = $source->expirationYear . '-' . $source->expirationMonth . '-01'; } elseif ($source instanceof \Braintree\PayPalAccount) { $paymentMethod->email = $source->email; $paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; @@ -514,7 +514,7 @@ class PaymentService extends BaseService $paymentMethod->payment_type_id = $this->parseCardType($source->credit_card_name); $paymentMethod->last4 = $source->last_four; - $paymentMethod->expiration = $source->expiration_year . '-' . $source->expiration_month . '-00'; + $paymentMethod->expiration = $source->expiration_year . '-' . $source->expiration_month . '-01'; $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); $paymentMethod->source_reference = $source->credit_card_id; @@ -552,7 +552,7 @@ class PaymentService extends BaseService if ($transaction->paymentInstrumentType == 'credit_card') { $card = $transaction->creditCardDetails; $paymentMethod->last4 = $card->last4; - $paymentMethod->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-00'; + $paymentMethod->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-01'; $paymentMethod->payment_type_id = $this->parseCardType($card->cardType); } elseif ($transaction->paymentInstrumentType == 'paypal_account') { $paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; From 919aec2192971cc2d7f143ce00a19d83b756c2e2 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 14 May 2016 23:03:39 -0400 Subject: [PATCH 079/386] Better refund handling --- app/Listeners/ActivityListener.php | 12 ++--- app/Services/PaymentService.php | 81 +++++++++++++++++++++--------- resources/views/list.blade.php | 2 +- 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/app/Listeners/ActivityListener.php b/app/Listeners/ActivityListener.php index 1df8edeb6848..14ba0da50ec9 100644 --- a/app/Listeners/ActivityListener.php +++ b/app/Listeners/ActivityListener.php @@ -307,8 +307,8 @@ class ActivityListener $this->activityRepo->create( $payment, ACTIVITY_TYPE_DELETE_PAYMENT, - $payment->amount, - $payment->amount * -1 + $payment->amount - $payment->refunded, + ($payment->amount - $payment->refunded) * -1 ); } @@ -343,8 +343,8 @@ class ActivityListener $this->activityRepo->create( $payment, ACTIVITY_TYPE_FAILED_PAYMENT, - $payment->amount, - $payment->amount * -1 + ($payment->amount - $payment->refunded), + ($payment->amount - $payment->refunded) * -1 ); } @@ -367,8 +367,8 @@ class ActivityListener $this->activityRepo->create( $payment, ACTIVITY_TYPE_RESTORE_PAYMENT, - $event->fromDeleted ? $payment->amount * -1 : 0, - $event->fromDeleted ? $payment->amount : 0 + $event->fromDeleted ? ($payment->amount - $payment->refunded) * -1 : 0, + $event->fromDeleted ? ($payment->amount - $payment->refunded) : 0 ); } } diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 4a4a757720de..32fafc045c01 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -1048,41 +1048,76 @@ class PaymentService extends BaseService if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) { $gateway = $this->createGateway($accountGateway); - $refund = $gateway->refund(array( - 'transactionReference' => $payment->transaction_reference, - 'amount' => $amount, - )); - $response = $refund->send(); - - if ($response->isSuccessful()) { - $payment->recordRefund($amount); + + if ($accountGateway->gateway_id != GATEWAY_WEPAY) { + $refund = $gateway->refund(array( + 'transactionReference' => $payment->transaction_reference, + 'amount' => $amount, + )); + $response = $refund->send(); + + if ($response->isSuccessful()) { + $payment->recordRefund($amount); + } else { + $data = $response->getData(); + + if ($data instanceof \Braintree\Result\Error) { + $error = $data->errors->deepAll()[0]; + if ($error && $error->code == 91506) { + if ($amount == $payment->amount) { + // This is an unsettled transaction; try to void it + $void = $gateway->void(array( + 'transactionReference' => $payment->transaction_reference, + )); + $response = $void->send(); + + if ($response->isSuccessful()) { + $payment->markVoided(); + } + } else { + $this->error('Unknown', 'Partial refund not allowed for unsettled transactions.', $accountGateway); + return false; + } + } + } + + if (!$response->isSuccessful()) { + $this->error('Unknown', $response->getMessage(), $accountGateway); + return false; + } + } } else { - $data = $response->getData(); + $wepay = \Utils::setupWePay($accountGateway); - if ($data instanceof \Braintree\Result\Error) { - $error = $data->errors->deepAll()[0]; - if ($error && $error->code == 91506) { + try { + $wepay->request('checkout/refund', array( + 'checkout_id' => intval($payment->transaction_reference), + 'refund_reason' => 'Refund issued by merchant.', + 'amount' => $amount, + )); + $payment->recordRefund($amount); + } catch (\WePayException $ex) { + if ($ex->getCode() == 4004) { if ($amount == $payment->amount) { - // This is an unsettled transaction; try to void it - $void = $gateway->void(array( - 'transactionReference' => $payment->transaction_reference, - )); - $response = $void->send(); - - if ($response->isSuccessful()) { + try { + // This is an uncaptured transaction; try to cancel it + $wepay->request('checkout/cancel', array( + 'checkout_id' => intval($payment->transaction_reference), + 'cancel_reason' => 'Refund issued by merchant.', + )); $payment->markVoided(); + } catch (\WePayException $ex) { + $this->error('Unknown', $ex->getMessage(), $accountGateway); } } else { $this->error('Unknown', 'Partial refund not allowed for unsettled transactions.', $accountGateway); return false; } + } else { + $this->error('Unknown', $ex->getMessage(), $accountGateway); } } - if (!$response->isSuccessful()) { - $this->error('Unknown', $response->getMessage(), $accountGateway); - return false; - } } } else { $payment->recordRefund($amount); diff --git a/resources/views/list.blade.php b/resources/views/list.blade.php index 02faad273954..93588fba9806 100644 --- a/resources/views/list.blade.php +++ b/resources/views/list.blade.php @@ -26,7 +26,7 @@  -onli +
Date: Sat, 14 May 2016 23:15:22 -0400 Subject: [PATCH 080/386] Make autobill setting numbers match token settings --- app/Http/routes.php | 8 ++++---- app/Ninja/Repositories/InvoiceRepository.php | 2 +- .../2016_04_23_182223_payments_changes.php | 11 ++++++++++- resources/views/invoices/edit.blade.php | 16 ++++++++-------- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index 4f362a3c6df6..8a69b8d53e38 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -707,10 +707,10 @@ if (!defined('CONTACT_EMAIL')) { define('RESELLER_REVENUE_SHARE', 'A'); define('RESELLER_LIMITED_USERS', 'B'); - define('AUTO_BILL_OFF', 0); - define('AUTO_BILL_OPT_IN', 1); - define('AUTO_BILL_OPT_OUT', 2); - define('AUTO_BILL_ALWAYS', 3); + define('AUTO_BILL_OFF', 1); + define('AUTO_BILL_OPT_IN', 2); + define('AUTO_BILL_OPT_OUT', 3); + define('AUTO_BILL_ALWAYS', 4); // These must be lowercase define('PLAN_FREE', 'free'); diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index fd493ba16d93..979304ed01a0 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -324,7 +324,7 @@ class InvoiceRepository extends BaseRepository $invoice->start_date = Utils::toSqlDate($data['start_date']); $invoice->end_date = Utils::toSqlDate($data['end_date']); $invoice->client_enable_auto_bill = isset($data['client_enable_auto_bill']) && $data['client_enable_auto_bill'] ? true : false; - $invoice->auto_bill = isset($data['auto_bill']) ? intval($data['auto_bill']) : 0; + $invoice->auto_bill = isset($data['auto_bill']) ? intval($data['auto_bill']) : AUTO_BILL_OFF; if ($invoice->auto_bill < AUTO_BILL_OFF || $invoice->auto_bill > AUTO_BILL_ALWAYS ) { $invoice->auto_bill = AUTO_BILL_OFF; diff --git a/database/migrations/2016_04_23_182223_payments_changes.php b/database/migrations/2016_04_23_182223_payments_changes.php index 1c277d8da6b2..35a1b6134e9b 100644 --- a/database/migrations/2016_04_23_182223_payments_changes.php +++ b/database/migrations/2016_04_23_182223_payments_changes.php @@ -78,6 +78,11 @@ class PaymentsChanges extends Migration ->where('auto_bill', '=', 1) ->update(array('client_enable_auto_bill' => 1, 'auto_bill' => AUTO_BILL_OPT_OUT)); + \DB::table('invoices') + ->where('auto_bill', '=', 0) + ->where('is_recurring', '=', 1) + ->update(array('auto_bill' => AUTO_BILL_OFF)); + Schema::table('account_gateway_tokens', function($table) { @@ -113,11 +118,15 @@ class PaymentsChanges extends Migration $table->dropColumn('payment_method_id'); }); + \DB::table('invoices') + ->where('auto_bill', '=', AUTO_BILL_OFF) + ->update(array('auto_bill' => 0)); + \DB::table('invoices') ->where(function($query){ $query->where('auto_bill', '=', AUTO_BILL_ALWAYS); $query->orwhere(function($query){ - $query->where('auto_bill', '!=', AUTO_BILL_OFF); + $query->where('auto_bill', '!=', 0); $query->where('client_enable_auto_bill', '=', 1); }); }) diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 5e503b1d7687..82204e7aca4b 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -166,24 +166,24 @@ @if($account->getTokenGatewayId()) -
+
{!! Former::select('auto_bill') - ->data_bind("value: auto_bill, valueUpdate: 'afterkeydown', event:{change:function(){if(auto_bill()==1)client_enable_auto_bill(0);if(auto_bill()==2)client_enable_auto_bill(1)}}") + ->data_bind("value: auto_bill, valueUpdate: 'afterkeydown', event:{change:function(){if(auto_bill()==".AUTO_BILL_OPT_IN.")client_enable_auto_bill(0);if(auto_bill()==".AUTO_BILL_OPT_OUT.")client_enable_auto_bill(1)}}") ->options([ - 0 => trans('texts.off'), - 1 => trans('texts.opt_in'), - 2 => trans('texts.opt_out'), - 3 => trans('texts.always'), + AUTO_BILL_OFF => trans('texts.off'), + AUTO_BILL_OPT_IN => trans('texts.opt_in'), + AUTO_BILL_OPT_OUT => trans('texts.opt_out'), + AUTO_BILL_ALWAYS => trans('texts.always'), ]) !!}
-
+
{{trans('texts.auto_bill')}}
{{trans('texts.opted_in')}} - ({{trans('texts.disable')}})
-
+
{{trans('texts.auto_bill')}}
{{trans('texts.opted_out')}} - ({{trans('texts.enable')}}) From 52fe1b0decb3443a972b980a940812793f4e8d06 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 15 May 2016 10:06:14 +0300 Subject: [PATCH 081/386] Added missing text value --- resources/lang/en/texts.php | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 31847fdd09b5..5ccec0032495 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1294,6 +1294,7 @@ $LANG = array( 'no_payment_method_specified' => 'No payment method specified', 'chart_type' => 'Chart Type', + 'format' => 'Format', ); From b067697b1ca1e84a031705757af90a31e7779c8a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 15 May 2016 13:58:11 +0300 Subject: [PATCH 082/386] Support manually importing OFX files --- app/Http/Controllers/AccountController.php | 14 +-- .../Controllers/BankAccountController.php | 26 ++++++ app/Http/Controllers/BaseController.php | 2 +- app/Http/routes.php | 26 +++--- app/Libraries/OFX.php | 10 +- app/Models/Expense.php | 15 ++- app/Services/BankAccountService.php | 92 ++++++++++++------- resources/lang/en/texts.php | 4 +- .../views/accounts/bank_account.blade.php | 31 ++++--- resources/views/accounts/banks.blade.php | 40 ++++---- resources/views/accounts/import_ofx.blade.php | 26 ++++++ 11 files changed, 187 insertions(+), 99 deletions(-) create mode 100644 resources/views/accounts/import_ofx.blade.php diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 97226ad29c2a..3abaaadfd78c 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -402,17 +402,9 @@ class AccountController extends BaseController private function showBankAccounts() { - $account = Auth::user()->account; - $account->load('bank_accounts'); - $count = count($account->bank_accounts); - - if ($count == 0) { - return Redirect::to('bank_accounts/create'); - } else { - return View::make('accounts.banks', [ - 'title' => trans('texts.bank_accounts') - ]); - } + return View::make('accounts.banks', [ + 'title' => trans('texts.bank_accounts') + ]); } private function showOnlinePayments() diff --git a/app/Http/Controllers/BankAccountController.php b/app/Http/Controllers/BankAccountController.php index bce86bce42f1..1c003cfd47b0 100644 --- a/app/Http/Controllers/BankAccountController.php +++ b/app/Http/Controllers/BankAccountController.php @@ -13,12 +13,14 @@ use stdClass; use Crypt; use URL; use Utils; +use File; use App\Models\Gateway; use App\Models\Account; use App\Models\BankAccount; use App\Ninja\Repositories\BankAccountRepository; use App\Services\BankAccountService; use App\Http\Requests\CreateBankAccountRequest; +use Illuminate\Http\Request; class BankAccountController extends BaseController { @@ -122,4 +124,28 @@ class BankAccountController extends BaseController return $this->bankAccountService->importExpenses($bankId, Input::all()); } + public function showImportOFX() + { + return view('accounts.import_ofx'); + } + + public function doImportOFX(Request $request) + { + $file = File::get($request->file('ofx_file')); + + try { + $data = $this->bankAccountService->parseOFX($file); + } catch (\Exception $e) { + Session::flash('error', trans('texts.ofx_parse_failed')); + return view('accounts.import_ofx'); + } + + $data = [ + 'banks' => null, + 'bankAccount' => null, + 'transactions' => json_encode([$data]) + ]; + + return View::make('accounts.bank_account', $data); + } } diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 2ce7a633f179..3bb399294c14 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -10,7 +10,7 @@ use Utils; class BaseController extends Controller { use DispatchesJobs, AuthorizesRequests; - + protected $entityType; /** diff --git a/app/Http/routes.php b/app/Http/routes.php index 1d6d6b8fb903..47b1a28b1c4e 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -60,7 +60,7 @@ Route::group(['middleware' => 'auth:client'], function() { Route::get('client/documents/js/{documents}/{filename}', 'PublicClientController@getDocumentVFSJS'); Route::get('client/documents/{invitation_key}/{documents}/{filename?}', 'PublicClientController@getDocument'); Route::get('client/documents/{invitation_key}/{filename?}', 'PublicClientController@getInvoiceDocumentsZip'); - + Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable')); Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable')); Route::get('api/client.recurring_invoices', array('as'=>'api.client.recurring_invoices', 'uses'=>'PublicClientController@recurringInvoiceDatatable')); @@ -123,7 +123,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('hide_message', 'HomeController@hideMessage'); Route::get('force_inline_pdf', 'UserController@forcePDFJS'); Route::get('account/getSearchData', array('as' => 'getSearchData', 'uses' => 'AccountController@getSearchData')); - + Route::get('settings/user_details', 'AccountController@showUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails'); Route::post('users/change_password', 'UserController@changePassword'); @@ -156,7 +156,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS'); Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview'); Route::post('document', 'DocumentController@postUpload'); - + Route::get('quotes/create/{client_id?}', 'QuoteController@create'); Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice'); Route::get('quotes/{invoices}/edit', 'InvoiceController@edit'); @@ -245,6 +245,8 @@ Route::group([ Route::get('api/gateways', array('as'=>'api.gateways', 'uses'=>'AccountGatewayController@getDatatable')); Route::post('account_gateways/bulk', 'AccountGatewayController@bulk'); + Route::get('bank_accounts/import_ofx', 'BankAccountController@showImportOFX'); + Route::post('bank_accounts/import_ofx', 'BankAccountController@doImportOFX'); Route::resource('bank_accounts', 'BankAccountController'); Route::get('api/bank_accounts', array('as'=>'api.bank_accounts', 'uses'=>'BankAccountController@getDatatable')); Route::post('bank_accounts/bulk', 'BankAccountController@bulk'); @@ -487,7 +489,7 @@ 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_VOIDED', 2); define('PAYMENT_STATUS_FAILED', 3); @@ -706,7 +708,7 @@ if (!defined('CONTACT_EMAIL')) { define('AUTO_BILL_OPT_IN', 1); define('AUTO_BILL_OPT_OUT', 2); define('AUTO_BILL_ALWAYS', 3); - + // These must be lowercase define('PLAN_FREE', 'free'); define('PLAN_PRO', 'pro'); @@ -714,7 +716,7 @@ if (!defined('CONTACT_EMAIL')) { define('PLAN_WHITE_LABEL', 'white_label'); define('PLAN_TERM_MONTHLY', 'month'); define('PLAN_TERM_YEARLY', 'year'); - + // Pro define('FEATURE_CUSTOMIZE_INVOICE_DESIGN', 'customize_invoice_design'); define('FEATURE_REMOVE_CREATED_BY', 'remove_created_by'); @@ -729,23 +731,23 @@ if (!defined('CONTACT_EMAIL')) { define('FEATURE_API', 'api'); define('FEATURE_CLIENT_PORTAL_PASSWORD', 'client_portal_password'); define('FEATURE_CUSTOM_URL', 'custom_url'); - + define('FEATURE_MORE_CLIENTS', 'more_clients'); // No trial allowed - + // Whitelabel define('FEATURE_CLIENT_PORTAL_CSS', 'client_portal_css'); define('FEATURE_WHITE_LABEL', 'feature_white_label'); // Enterprise define('FEATURE_DOCUMENTS', 'documents'); - + // No Trial allowed define('FEATURE_USERS', 'users');// Grandfathered for old Pro users define('FEATURE_USER_PERMISSIONS', 'user_permissions'); - + // Pro users who started paying on or before this date will be able to manage users define('PRO_USERS_GRANDFATHER_DEADLINE', '2016-05-15'); - + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], @@ -801,4 +803,4 @@ if (Utils::isNinjaDev()) //ini_set('memory_limit','1024M'); //Auth::loginUsingId(1); } -*/ \ No newline at end of file +*/ diff --git a/app/Libraries/OFX.php b/app/Libraries/OFX.php index 721c9f529f85..b32e308a4247 100644 --- a/app/Libraries/OFX.php +++ b/app/Libraries/OFX.php @@ -27,15 +27,15 @@ class OFX curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-Type: application/x-ofx', 'User-Agent: httpclient')); curl_setopt($c, CURLOPT_POSTFIELDS, $this->request); curl_setopt($c, CURLOPT_RETURNTRANSFER, 1); - + $this->response = curl_exec($c); - + if (Utils::isNinjaDev()) { Log::info(print_r($this->response, true)); } - + curl_close($c); - + $tmp = explode('', $this->response); $this->responseHeader = $tmp[0]; $this->responseBody = ''.$tmp[1]; @@ -48,6 +48,7 @@ class OFX return $x; } + public static function closeTags($x) { $x = preg_replace('/\s+/', '', $x); @@ -233,4 +234,3 @@ class Account } } } - diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 316491a5356b..e626e08549bb 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -90,15 +90,24 @@ class Expense extends EntityModel { return round($this->amount * $this->exchange_rate, 2); } - + public function toArray() { $array = parent::toArray(); - + if(empty($this->visible) || in_array('converted_amount', $this->visible))$array['converted_amount'] = $this->convertedAmount(); - + return $array; } + + public function scopeBankId($query, $bankdId = null) + { + if ($bankdId) { + $query->whereBankId($bankId); + } + + return $query; + } } Expense::creating(function ($expense) { diff --git a/app/Services/BankAccountService.php b/app/Services/BankAccountService.php index 72aada6e3ff9..c04651629856 100644 --- a/app/Services/BankAccountService.php +++ b/app/Services/BankAccountService.php @@ -34,14 +34,10 @@ class BankAccountService extends BaseService return $this->bankAccountRepo; } - public function loadBankAccounts($bankId, $username, $password, $includeTransactions = true) + private function getExpenses($bankId = null) { - if (! $bankId || ! $username || ! $password) { - return false; - } - $expenses = Expense::scope() - ->whereBankId($bankId) + ->bankId($bankId) ->where('transaction_id', '!=', '') ->withTrashed() ->get(['transaction_id']) @@ -50,6 +46,16 @@ class BankAccountService extends BaseService return $val['transaction_id']; }, $expenses)); + return $expenses; + } + + public function loadBankAccounts($bankId, $username, $password, $includeTransactions = true) + { + if (! $bankId || ! $username || ! $password) { + return false; + } + + $expenses = $this->getExpenses(); $vendorMap = $this->createVendorMap(); $bankAccounts = BankSubaccount::scope() ->whereHas('bank_account', function ($query) use ($bankId) { @@ -106,44 +112,60 @@ class BankAccountService extends BaseService $obj->balance = Utils::formatMoney($account->ledgerBalance, CURRENCY_DOLLAR); if ($includeTransactions) { - $ofxParser = new \OfxParser\Parser(); - $ofx = $ofxParser->loadFromString($account->response); - - $obj->start_date = $ofx->BankAccount->Statement->startDate; - $obj->end_date = $ofx->BankAccount->Statement->endDate; - $obj->transactions = []; - - foreach ($ofx->BankAccount->Statement->transactions as $transaction) { - // ensure transactions aren't imported as expenses twice - if (isset($expenses[$transaction->uniqueId])) { - continue; - } - if ($transaction->amount >= 0) { - continue; - } - - // if vendor has already been imported use current name - $vendorName = trim(substr($transaction->name, 0, 20)); - $key = strtolower($vendorName); - $vendor = isset($vendorMap[$key]) ? $vendorMap[$key] : null; - - $transaction->vendor = $vendor ? $vendor->name : $this->prepareValue($vendorName); - $transaction->info = $this->prepareValue(substr($transaction->name, 20)); - $transaction->memo = $this->prepareValue($transaction->memo); - $transaction->date = \Auth::user()->account->formatDate($transaction->date); - $transaction->amount *= -1; - $obj->transactions[] = $transaction; - } + $obj = $this->parseTransactions($obj, $account->response, $expenses, $vendorMap); } return $obj; } + private function parseTransactions($account, $data, $expenses, $vendorMap) + { + $ofxParser = new \OfxParser\Parser(); + $ofx = $ofxParser->loadFromString($data); + + $account->start_date = $ofx->BankAccount->Statement->startDate; + $account->end_date = $ofx->BankAccount->Statement->endDate; + $account->transactions = []; + + foreach ($ofx->BankAccount->Statement->transactions as $transaction) { + // ensure transactions aren't imported as expenses twice + if (isset($expenses[$transaction->uniqueId])) { + continue; + } + if ($transaction->amount >= 0) { + continue; + } + + // if vendor has already been imported use current name + $vendorName = trim(substr($transaction->name, 0, 20)); + $key = strtolower($vendorName); + $vendor = isset($vendorMap[$key]) ? $vendorMap[$key] : null; + + $transaction->vendor = $vendor ? $vendor->name : $this->prepareValue($vendorName); + $transaction->info = $this->prepareValue(substr($transaction->name, 20)); + $transaction->memo = $this->prepareValue($transaction->memo); + $transaction->date = \Auth::user()->account->formatDate($transaction->date); + $transaction->amount *= -1; + $account->transactions[] = $transaction; + } + + return $account; + } + private function prepareValue($value) { return ucwords(strtolower(trim($value))); } + public function parseOFX($data) + { + $account = new stdClass; + $expenses = $this->getExpenses(); + $vendorMap = $this->createVendorMap(); + + return $this->parseTransactions($account, $data, $expenses, $vendorMap); + } + private function createVendorMap() { $vendorMap = []; @@ -158,7 +180,7 @@ class BankAccountService extends BaseService return $vendorMap; } - public function importExpenses($bankId, $input) + public function importExpenses($bankId = 0, $input) { $vendorMap = $this->createVendorMap(); $countVendors = 0; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 5ccec0032495..51f746402a87 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1295,7 +1295,9 @@ $LANG = array( 'no_payment_method_specified' => 'No payment method specified', 'chart_type' => 'Chart Type', 'format' => 'Format', - + 'import_ofx' => 'Import OFX', + 'ofx_file' => 'OFX File', + 'ofx_parse_failed' => 'Failed to parse OFX file', ); return $LANG; diff --git a/resources/views/accounts/bank_account.blade.php b/resources/views/accounts/bank_account.blade.php index ff1a0affef2e..4483bc30252e 100644 --- a/resources/views/accounts/bank_account.blade.php +++ b/resources/views/accounts/bank_account.blade.php @@ -2,7 +2,7 @@ @section('head') @parent - + @include('money_script') + + {!! Former::close() !!} \ No newline at end of file From f532639e3c7d495ed61fa2be4ffa4a885212da70 Mon Sep 17 00:00:00 2001 From: vincentdh Date: Tue, 17 May 2016 12:59:55 -0400 Subject: [PATCH 099/386] use_card_on_file translation ambiguous --- resources/lang/fr_CA/texts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/fr_CA/texts.php b/resources/lang/fr_CA/texts.php index 999e553a96c1..3a1e95f04ea1 100644 --- a/resources/lang/fr_CA/texts.php +++ b/resources/lang/fr_CA/texts.php @@ -498,7 +498,7 @@ return array( 'token_billing_4' => 'Toujours', 'token_billing_checkbox' => 'Mémoriser les informations de carte de crédit', 'view_in_gateway' => 'Visualiser dans :gateway', - 'use_card_on_file' => 'Mémoriser les informations de la carte pour usage ultérieur', + 'use_card_on_file' => 'Utiliser la carte de crédit en mémoire', 'edit_payment_details' => 'Éditer les informations de paiement', 'token_billing' => 'Sauvegarder les informations de carte de crédit', 'token_billing_secure' => 'Les données sont mémorisées de façon sécuritaire avec :link', From a2998a8cfb16f1209271d73e433f3345d674804a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 17 May 2016 20:22:41 +0300 Subject: [PATCH 100/386] Added Croatian --- database/seeds/LanguageSeeder.php | 3 +- resources/lang/hr/auth.php | 19 + resources/lang/hr/pagination.php | 19 + resources/lang/hr/passwords.php | 22 + resources/lang/hr/texts.php | 1309 +++++++++++++++++++++++++++++ resources/lang/hr/validation.php | 116 +++ 6 files changed, 1487 insertions(+), 1 deletion(-) create mode 100644 resources/lang/hr/auth.php create mode 100644 resources/lang/hr/pagination.php create mode 100644 resources/lang/hr/passwords.php create mode 100644 resources/lang/hr/texts.php create mode 100644 resources/lang/hr/validation.php diff --git a/database/seeds/LanguageSeeder.php b/database/seeds/LanguageSeeder.php index 5fc9569d5cef..19fff15c64b2 100644 --- a/database/seeds/LanguageSeeder.php +++ b/database/seeds/LanguageSeeder.php @@ -22,9 +22,10 @@ class LanguageSeeder extends Seeder ['name' => 'Swedish', 'locale' => 'sv'], ['name' => 'Spanish - Spain', 'locale' => 'es_ES'], ['name' => 'French - Canada', 'locale' => 'fr_CA'], - ['name' => 'Lithuanian', 'locale' => 'lt'], + ['name' => 'Lithuanian', 'locale' => 'lt'], ['name' => 'Polish', 'locale' => 'pl'], ['name' => 'Czech', 'locale' => 'cs'], + ['name' => 'Croatian', 'locale' => 'hr'], ]; foreach ($languages as $language) { diff --git a/resources/lang/hr/auth.php b/resources/lang/hr/auth.php new file mode 100644 index 000000000000..2142a42f941d --- /dev/null +++ b/resources/lang/hr/auth.php @@ -0,0 +1,19 @@ + 'Ovi podaci ne odgovaraju našima.', + 'throttle' => 'Previše pokušaja prijave. Molim Vas pokušajte ponovno za :seconds sekundi.', + +]; diff --git a/resources/lang/hr/pagination.php b/resources/lang/hr/pagination.php new file mode 100644 index 000000000000..e0bed173dadf --- /dev/null +++ b/resources/lang/hr/pagination.php @@ -0,0 +1,19 @@ + '« Prethodna', + 'next' => 'Sljedeća »', + +]; diff --git a/resources/lang/hr/passwords.php b/resources/lang/hr/passwords.php new file mode 100644 index 000000000000..59f39af3fe05 --- /dev/null +++ b/resources/lang/hr/passwords.php @@ -0,0 +1,22 @@ + 'Lozinke moraju biti duge barem 6 znakova i moraju odgovarati potvrdi.', + 'reset' => 'Lozinka je postavljena!', + 'sent' => 'Poveznica za ponovono postavljanje lozinke je poslana!', + 'token' => 'Oznaka za ponovno postavljanje lozinke više nije važeća.', + 'user' => 'Korisnik nije pronađen.', + +]; diff --git a/resources/lang/hr/texts.php b/resources/lang/hr/texts.php new file mode 100644 index 000000000000..411f8d26d3ce --- /dev/null +++ b/resources/lang/hr/texts.php @@ -0,0 +1,1309 @@ + 'Organizacija', + 'name' => 'Ime', + 'website' => 'Web mjesto', + 'work_phone' => 'Telefon', + 'address' => 'Adresa', + 'address1' => 'Ulica', + 'address2' => 'Kat/soba', + 'city' => 'Grad', + 'state' => 'Županija', + 'postal_code' => 'Poštanski broj', + 'country_id' => 'Zemlja', + 'contacts' => 'Kontakti', + 'first_name' => 'Ime', + 'last_name' => 'Prezime', + 'phone' => 'Telefon', + 'email' => 'E-pošta', + 'additional_info' => 'Dodatne informacije', + 'payment_terms' => 'Uvjeti plaćanja', + 'currency_id' => 'Valuta', + 'size_id' => 'Veličina poduzeća', + 'industry_id' => 'Djelatnost', + 'private_notes' => 'Privatne bilješke', + 'invoice' => 'Račun', + 'client' => 'Klijent', + 'invoice_date' => 'Datum računa', + 'due_date' => 'Datum dospijeća', + 'invoice_number' => 'Broj računa', + 'invoice_number_short' => 'Račun #', + 'po_number' => 'Broj narudžbe', + 'po_number_short' => 'NN #', + 'frequency_id' => 'Koliko često', + 'discount' => 'Popust', + 'taxes' => 'Porezi', + 'tax' => 'Porez', + 'item' => 'Stavka', + 'description' => 'Opis', + 'unit_cost' => 'Jedinična cijena', + 'quantity' => 'Količina', + 'line_total' => 'Ukupno', + 'subtotal' => 'Sveukupno', + 'paid_to_date' => 'Plaćeno na vrijeme', + 'balance_due' => 'Stanje duga', + 'invoice_design_id' => 'Dizajn', + 'terms' => 'Uvjeti', + 'your_invoice' => 'Vaš račun', + 'remove_contact' => 'Ukloni kontakt', + 'add_contact' => 'Dodaj kontakt', + 'create_new_client' => 'Kreiraj novog klijenta', + 'edit_client_details' => 'Uredi detalje klijenta', + 'enable' => 'Omogući', + 'learn_more' => 'Više informacija', + 'manage_rates' => 'Upravljanje ratam', + 'note_to_client' => 'Bilješka klijentu', + 'invoice_terms' => 'Uvjeti računa', + 'save_as_default_terms' => 'Pohrani kao zadane uvjete', + 'download_pdf' => 'Preuzmi PDF', + 'pay_now' => 'Plati odmah', + 'save_invoice' => 'Pohrani račun', + 'clone_invoice' => 'Kloniraj račun', + 'archive_invoice' => 'Arhiviraj račun', + 'delete_invoice' => 'Obriši račun', + 'email_invoice' => 'Pošalji e-poštom', + 'enter_payment' => 'Unesi uplatu', + 'tax_rates' => 'Porezne stope', + 'rate' => 'Stopa', + 'settings' => 'Postavke', + 'enable_invoice_tax' => 'Omogući specificiranje poreza na računu', + 'enable_line_item_tax' => 'Omogući specifikaciju poreza na stavci', + 'dashboard' => 'Kontrolna ploča', + 'clients' => 'Klijenti', + 'invoices' => 'Računi', + 'payments' => 'Uplate', + 'credits' => 'Krediti', + 'history' => 'Povijest', + 'search' => 'Pretraga', + 'sign_up' => 'Prijava', + 'guest' => 'Gost', + 'company_details' => 'Detalji poduzeća', + 'online_payments' => 'Online uplate', + 'notifications' => 'Obavijesti e-poštom', + 'import_export' => 'Import | Export', + 'done' => 'Dovršeno', + 'save' => 'Pohrani', + 'create' => 'Kreiraj', + 'upload' => 'Otpremi', + 'import' => 'Uvoz', + 'download' => 'Preuzmi', + 'cancel' => 'Odustani', + 'close' => 'Zatvori', + 'provide_email' => 'Molim, osigurajte ispravnu adresu e-pošte', + 'powered_by' => 'Powered by', + 'no_items' => 'Nema stavki', + 'recurring_invoices' => 'Redovni računi', + 'recurring_help' => '

Automatski šalje klijentu istovjetan račun tjedno, dvotjedno, mjesečno, kvartalno ili godišnje.

+

Koristite :MONTH, :QUARTER ili :YEAR za dinamičke datume. Osnovna operacije također rade, npr. :MONTH-1.

+

Primjeri dinamičkih varijabli računa:

+
    +
  • "Članarina za teretanu za mjesec :MONTH" => "Članarina za teratanu za mjesec srpanj"
  • +
  • ":YEAR+1 godišnja pretplata" => "2015 godišnja pretplata"
  • +
  • "Uplata predujma za :QUARTER+1" => "Uplata predujma za Q2"
  • +
', + 'in_total_revenue' => 'ukupni prihod', + 'billed_client' => 'fakturirani klijent', + 'billed_clients' => 'fakturairani klijenti', + 'active_client' => 'aktivni klijent', + 'active_clients' => 'aktivni klijenti', + 'invoices_past_due' => 'Računi van valute', + 'upcoming_invoices' => 'Dolazni računi', + 'average_invoice' => 'Proječni račun', + 'archive' => 'Arhiva', + 'delete' => 'Obriši', + 'archive_client' => 'Arhiviraj klijenta', + 'delete_client' => 'Obriši klijenta', + 'archive_payment' => 'Arhiviraj uplatu', + 'delete_payment' => 'Obriši uplatu', + 'archive_credit' => 'Arhiviraj kredit', + 'delete_credit' => 'Obriši kredit', + 'show_archived_deleted' => 'Prikaži arhivirano/obrisano', + 'filter' => 'Filter', + 'new_client' => 'Novi klijent', + 'new_invoice' => 'Novi račun', + 'new_payment' => 'New Payment', + 'new_credit' => 'New Credit', + 'contact' => 'Kontakt', + 'date_created' => 'Datum kreiranja', + 'last_login' => 'Zadnja prijava', + 'balance' => 'Stanje', + 'action' => 'Akcija', + 'status' => 'Status', + 'invoice_total' => 'Račun sveukupno', + 'frequency' => 'Frekvencija', + 'start_date' => 'Početni datum', + 'end_date' => 'Završni datum', + 'transaction_reference' => 'Referenca transakcije', + 'method' => 'Metoda', + 'payment_amount' => 'Iznos uplate', + 'payment_date' => 'Datum uplate', + 'credit_amount' => 'Iznos kredita', + 'credit_balance' => 'Stanje kredita', + 'credit_date' => 'Datum kredita', + 'empty_table' => 'Nema dostupnih podataka u tablici', + 'select' => 'Odaberi', + 'edit_client' => 'Uredi klijenta', + 'edit_invoice' => 'Uredi račun', + 'create_invoice' => 'Kreiraj račun', + 'enter_credit' => 'Unesi kredit', + 'last_logged_in' => 'Zadnja prijava na', + 'details' => 'Detalji', + 'standing' => 'Stanje', + 'credit' => 'Kredit', + 'activity' => 'Aktivnost', + 'date' => 'Datum', + 'message' => 'Poruka', + 'adjustment' => 'Prilagodba', + 'are_you_sure' => 'Da li ste sigurni?', + 'payment_type_id' => 'Tip uplate', + 'amount' => 'Iznos', + 'work_email' => 'E-pošta', + 'language_id' => 'Jezik', + 'timezone_id' => 'Vremnska zona', + 'date_format_id' => 'Date Format', + 'datetime_format_id' => 'Datum/Vrijeme format', + 'users' => 'Korisnici', + 'localization' => 'Lokalizacija', + 'remove_logo' => 'Ukloni logo', + 'logo_help' => 'Podržano: JPEG, GIF i PNG', + 'payment_gateway' => 'Usmjernik naplate', + 'gateway_id' => 'Usmjernik', + 'email_notifications' => 'Obavijesti e-poštom', + 'email_sent' => 'Obavijesti me e-poštom kada je račun poslan', + 'email_viewed' => 'Obavijesti me e-poštom kada je račun pregledan', + 'email_paid' => 'Obavijest me e-poštom kada je račun plaćen', + 'site_updates' => 'Ažuriranja', + 'custom_messages' => 'Prilagođene poruke', + 'default_email_footer' => 'Postavi zadani potpis e-pošte', + 'select_file' => 'Mollim odaberite datoteku', + 'first_row_headers' => 'Koristi prvi redak kao naslovni', + 'column' => 'Kolona', + 'sample' => 'Uzorak', + 'import_to' => 'Uvezi u', + 'client_will_create' => 'klijent će biti kreiran', + 'clients_will_create' => 'klijenti će biti kreirani', + 'email_settings' => 'Postavke e-pošte', + 'client_view_styling' => 'Stil pregleda klijenta', + 'pdf_email_attachment' => 'Priloži PDF', + 'custom_css' => 'Prilagođeni CSS', + 'import_clients' => 'Uvezi podatke klijenta', + 'csv_file' => 'CSV datoteka', + 'export_clients' => 'Izvezi podatke klijenta', + 'created_client' => 'Klijent je uspješno kreiran', + 'created_clients' => 'Uspješno kreirano :count kijenata', + 'updated_settings' => 'Postavke su uspješno ažurirane', + 'removed_logo' => 'Logo je uspješno uklonjen', + 'sent_message' => 'Poruka je uspješno poslana', + 'invoice_error' => 'Molimo provjerite da odaberete klijenta i korigirate greške', + 'limit_clients' => 'Nažalost, ovime ćete preći limit od :count klijenata', + 'payment_error' => 'Došlo je do greške pri procesuiranju vaše uplate. Molimo pokušajte kasnije.', + 'registration_required' => 'Molimo prijavite se prije slanja računa e-poštom', + 'confirmation_required' => 'Molimo potvrdite svoju adresu e-pošte', + 'updated_client' => 'Uspješno ažuriranje klijenta', + 'created_client' => 'Klijent je uspješno kreiran', + 'archived_client' => 'Uspješno arhiviran klijent', + 'archived_clients' => 'Uspješno arhivirano :count klijenata', + 'deleted_client' => 'Uspješno obrisan klijent', + 'deleted_clients' => 'Uspješno obrisano :count klijenata', + 'updated_invoice' => 'Uspješno ažuriran račun', + 'created_invoice' => 'Uspješno kreiran račun', + 'cloned_invoice' => 'Uspješno kloniran račun', + 'emailed_invoice' => 'Račun uspješno poslan e-poštom', + 'and_created_client' => 'i kreiran račun', + 'archived_invoice' => 'Uspješno arhiviran račun', + 'archived_invoices' => 'Uspješno arhivirano :count računa', + 'deleted_invoice' => 'Uspješno obrisan račun', + 'deleted_invoices' => 'Uspješno obrisano :count računa', + 'created_payment' => 'Uspješno kreirana uplata', + 'created_payments' => 'Uspješno kreirano :count uplata', + 'archived_payment' => 'Uspješno arhivirana uplata', + 'archived_payments' => 'Uspješno arhivirana :count uplata', + 'deleted_payment' => 'Uspješno obrisana uplata', + 'deleted_payments' => 'Uspješno obrisano :count uplata', + 'applied_payment' => 'Uspješno primjenjena uplata', + 'created_credit' => 'Uspješno kreiran kredit', + 'archived_credit' => 'Uspješno arhiviran kredit', + 'archived_credits' => 'Uspješno arhivirano :count kredita', + 'deleted_credit' => 'Uspješno obrisan kredit', + 'deleted_credits' => 'Uspješno obrisano :count kredita', + 'imported_file' => 'Uspješno uvezena datoteka', + 'updated_vendor' => 'Uspješno ažuriran dobavljač', + 'created_vendor' => 'Uspješno kreiran dobavljač', + 'archived_vendor' => 'Uspješno arhiviran dobavljač', + 'archived_vendors' => 'Uspješno arhivirano :count dobavljača', + 'deleted_vendor' => 'Uspješno obrisan dobavljač', + 'deleted_vendors' => 'Uspješno obrisano :count dobavljača', + 'confirmation_subject' => 'Invoice Ninja odobravanje računa', + 'confirmation_header' => 'Odobrenje računa', + 'confirmation_message' => 'Molimo pristupite donjom poveznicom za odobrenje vašeg računa.', + 'invoice_subject' => 'Novi račun :invoice za :account', + 'invoice_message' => 'Za pregled vašeg računa na :amount, kliknite donju poveznicu.', + 'payment_subject' => 'Primljena uplata', + 'payment_message' => 'Hvala vam na vašoj uplati od :amount.', + 'email_salutation' => 'Poštovani/a :name,', + 'email_signature' => 'Srdačno,', + 'email_from' => 'Invoice Ninja tim', + 'invoice_link_message' => 'Za pregled računa kliknite na donju poveznicu:', + 'notification_invoice_paid_subject' => ':client je platio račun :invoice', + 'notification_invoice_sent_subject' => 'Račun :invoice je poslan :client', + 'notification_invoice_viewed_subject' => ':client je pregledao račun :invoice', + 'notification_invoice_paid' => 'Uplata u iznosu :amount je izvršena od strane :client prema računu :invoice.', + 'notification_invoice_sent' => 'Slijedećem klijentu :client je poslan e-poštom račun :invoice na iznos :amount.', + 'notification_invoice_viewed' => 'Slijedeći klijent :client je pregledao račun :invoice na iznos :amount.', + 'reset_password' => 'Možete resetirati zaporku za pristup svom računu klikom na tipku:', + 'secure_payment' => 'Sigurna uplata', + 'card_number' => 'Broj kartice', + 'expiration_month' => 'Mjesec isteka', + 'expiration_year' => 'Godina isteka', + 'cvv' => 'CVV', + 'logout' => 'Odjava', + 'sign_up_to_save' => 'Prijavite se kako bi pohranili učinjeno', + 'agree_to_terms' => 'Slažem se sa Invoice Ninja :terms', + 'terms_of_service' => 'Uvjeti korištenja usluge', + 'email_taken' => 'Ova adresa e-pošte je već registrirana', + 'working' => 'Rad u tijeku', + 'success' => 'Uspjeh', + 'success_message' => 'Uspješno ste se registrirali! Molimo kliknite na poveznicu za potvrdu računa dobivenu e-poštom kako bi verificirali svoju adresu e-pošte.', + 'erase_data' => 'Ovo će trajno obrisate vaše podatke.', + 'password' => 'Zaporka', + 'pro_plan_product' => 'Pro Plan', + 'pro_plan_success' => 'Hvala vam na odabiru Invoice Ninja Pro plana!

 
+ Slijedeći koraci

Račun je poslan na adresu e-pošte povezanu + sa vašim korisničkim računom. Za otključavanje svih naprednih + Pro mogućnosti, molimo pratite upute za plaćanje godišnje pretplate + za Invoice Ninja Pro plan.

+ Ne možete naći račun? Trebate dodatnu pomoć? Sa zadovoljstvom ćemo pomoći + -- pošaljite nam e-poštu na contact@invoiceninja.com', + 'unsaved_changes' => 'Imate nepohranjenih promjena', + 'custom_fields' => 'Prilagođena polja', + 'company_fields' => 'Polja poduzeća', + 'client_fields' => 'Polja klijenta', + 'field_label' => 'Oznaka polja', + 'field_value' => 'Vrijednost polja', + 'edit' => 'Uredi', + 'set_name' => 'Postavite ime poduzeća', + 'view_as_recipient' => 'Pregledajte kao primatelj', + 'product_library' => 'Registar proizvoda', + 'product' => 'Proizvod', + 'products' => 'Registar proizvoda', + 'fill_products' => 'Proizvodi sa samoispunom', + 'fill_products_help' => 'Odabir proizvoda će automatski ispuniti opis i cijenu', + 'update_products' => 'Proizvidi sa autoažuriranjem', + 'update_products_help' => 'Ažuriranje računa automatski ažurirati registar proizvoda', + 'create_product' => 'Dodaj proizvod', + 'edit_product' => 'Uredi proizvod', + 'archive_product' => 'Arhiviraj proizvod', + 'updated_product' => 'Proizvod je uspješno ažuriran', + 'created_product' => 'Proizvod je uspješno kreiran', + 'archived_product' => 'Proizvod je uspješno arhiviran', + 'pro_plan_custom_fields' => ':link za omogućavanje prilagođenih polja aktivacijom Pro plana', + 'advanced_settings' => 'Napredne postavke', + 'pro_plan_advanced_settings' => ':link za omogućavanje naprednih postavki aktivacijom Pro plana', + 'invoice_design' => 'Dizajn računa', + 'specify_colors' => 'Odabir boja', + 'specify_colors_label' => 'Odaberite boje korištenje u računu', + 'chart_builder' => 'Graditelj karte', + 'ninja_email_footer' => 'Koristite :site za slanje računa klijentima i primanje uplata online besplatno!', + 'go_pro' => 'Go Pro', + 'quote' => 'Ponuda', + 'quotes' => 'Ponude', + 'quote_number' => 'Broj ponude', + 'quote_number_short' => 'Ponuda #', + 'quote_date' => 'Datum ponude', + 'quote_total' => 'Ponuda sveukupno', + 'your_quote' => 'Vaša ponuda', + 'total' => 'Sveukupno', + 'clone' => 'Kloniraj', + 'new_quote' => 'Nova ponuda', + 'create_quote' => 'Kreiraj ponudu', + 'edit_quote' => 'Uredi ponudu', + 'archive_quote' => 'Arhiviraj ponudu', + 'delete_quote' => 'Obriši ponudu', + 'save_quote' => 'Pohrani ponudu', + 'email_quote' => 'Šalji ponudu e-poštom', + 'clone_quote' => 'Kloniraj ponudu', + 'convert_to_invoice' => 'Konverzija računa', + 'view_invoice' => 'Pregled računa', + 'view_client' => 'Pregled klijenta', + 'view_quote' => 'Pregled ponude', + 'updated_quote' => 'Ponuda je uspješno ažurirana', + 'created_quote' => 'Ponuda uspješno kreirana', + 'cloned_quote' => 'Ponuda uspješno klonirana', + 'emailed_quote' => 'Ponuda uspješno poslana e-poštom', + 'archived_quote' => 'Ponuda uspješno arhivirana', + 'archived_quotes' => 'Uspješno arhivirano :count ponuda', + 'deleted_quote' => 'Ponuda uspješno obrisana', + 'deleted_quotes' => 'Uspješno obrisano :count ponuda', + 'converted_to_invoice' => 'Ponuda uspješno konvertirana u račun', + 'quote_subject' => 'Nova ponuda $quote sa :account', + 'quote_message' => 'Za pregled vaše ponude na :amount, kliknite na donju poveznicu.', + 'quote_link_message' => 'Za pregled ponude vašeg klijenta kliknite na donju poveznicu:', + 'notification_quote_sent_subject' => 'Ponuda :invoice je poslana :client', + 'notification_quote_viewed_subject' => 'Ponuda :invoice je pregledana od :client', + 'notification_quote_sent' => 'Klijentu :client je e-poštom poslana ponuda :invoice na :amount.', + 'notification_quote_viewed' => 'Klijent :client je pregledao ponudu :invoice na :amount.', + 'session_expired' => 'Vaša sjednica je istekla.', + 'invoice_fields' => 'Polja računa', + 'invoice_options' => 'Opcije računa', + 'hide_quantity' => 'Sakrij količinu', + 'hide_quantity_help' => 'Ukoliko su količine vaših stavaka uvijek 1, tada možete isključiti sa računa podatke o količini.', + 'hide_paid_to_date' => 'Sakrij datum plaćanja', + 'hide_paid_to_date_help' => 'Prikažite "Datum plaćanja" na računima, onda kada je uplata primljena.', + 'charge_taxes' => 'Naplati poreze', + 'user_management' => 'Upravljanje korisnicima', + 'add_user' => 'Dodaj korisnika', + 'send_invite' => 'Pošalji pozivnicu', + 'sent_invite' => 'Uspješno poslana pozivnica', + 'updated_user' => 'Korisnik je uspješno ažuriran', + 'invitation_message' => 'Dobili ste pozivnicu od :invitor.', + 'register_to_add_user' => 'Molimo prijavite se za dodavanje korisnika', + 'user_state' => 'Stanje', + 'edit_user' => 'Uredi korisnika', + 'delete_user' => 'Obriši korisnika', + 'active' => 'Aktivan', + 'pending' => 'Na čekanju', + 'deleted_user' => 'Korisnik je uspješno obrisan', + 'confirm_email_invoice' => 'Da li sigurno želite poslati ovaj račun e-poštom?', + 'confirm_email_quote' => 'Da li sigurno želite poslati ovu ponudu e-poštom?', + 'confirm_recurring_email_invoice' => 'Da li ste sigurni da želite poslati ovaj račun e-poštom?', + 'cancel_account' => 'Delete Account', + 'cancel_account_message' => 'Pozor: Ovo će trajno obrisati sve vaše podatke, nema povratka.', + 'go_back' => 'Idi natrag', + 'data_visualizations' => 'Vizualizacije podataka', + 'sample_data' => 'Prikaz primjernih podataka', + 'hide' => 'Sakrij', + 'new_version_available' => 'Nova verzija :releases_link je dostupna. Vi koristite v:user_version, a najnovija je v:latest_version', + 'invoice_settings' => 'Postavke računa', + 'invoice_number_prefix' => 'Prefiks broja računa', + 'invoice_number_counter' => 'Brojač računa', + 'quote_number_prefix' => 'Prefiks broja ponude', + 'quote_number_counter' => 'Brojač ponuda', + 'share_invoice_counter' => 'Dijeljeni brojač računa', + 'invoice_issued_to' => 'Račun izdan', + 'invalid_counter' => 'Za izbjegavanje mogućih konflikata molimo postavite prefiks na broju računa i ponude', + 'mark_sent' => 'Označi kao poslano', + 'gateway_help_1' => ':link za pristup na Authorize.net.', + 'gateway_help_2' => ':link za pristup na Authorize.net.', + 'gateway_help_17' => ':link za pristup vašem PayPal API potpisu.', + 'gateway_help_27' => ':link za pristup za TwoCheckout.', + 'more_designs' => 'Više dizajna', + 'more_designs_title' => 'Dodatni dizajni računa', + 'more_designs_cloud_header' => 'Nadogradite na Pro za više dizajna računa', + 'more_designs_cloud_text' => '', + 'more_designs_self_host_text' => '', + 'buy' => 'Kupi', + 'bought_designs' => 'Uspješno nadodani dizajni računa', + 'sent' => 'poslano', + 'vat_number' => 'OIB', + 'timesheets' => 'Vremenik', + 'payment_title' => 'Unesite svoju adresu računa i informacije kreditne kartice', + 'payment_cvv' => '*Ovo je 3-4 znamenkasti broj na poleđini vaše kartice', + 'payment_footer1' => '*Adresa računa mora se poklapati s adresom na kreditnoj kartici.', + 'payment_footer2' => '*Molimo kliknite "PLATI ODMAH" samo jednom - transakcija može izvoditi i do 1 minute.', + 'id_number' => 'ID broj', + 'white_label_link' => 'Bijela oznaka', + 'white_label_header' => 'Bijela oznaka', + 'bought_white_label' => 'Uspješno je omogućena licenca za bijele oznake', + 'white_labeled' => 'Označeno bijelom oznakom', + 'restore' => 'Obnovi', + 'restore_invoice' => 'Obnovi račun', + 'restore_quote' => 'Obnovi narudžbu', + 'restore_client' => 'Obnovi klijenta', + 'restore_credit' => 'Obnovi kredit', + 'restore_payment' => 'Obnovi uplatu', + 'restored_invoice' => 'Uspješno obnovljen račun', + 'restored_quote' => 'Uspješno obnovljena ponuda', + 'restored_client' => 'Uspješno obnovljen klijent', + 'restored_payment' => 'Uspješno obnovljena uplata', + 'restored_credit' => 'Uspješno obnovljen kredit', + 'reason_for_canceling' => 'Pomozite nam unaprijediti web mjesto informacijom zašto odlazite.', + 'discount_percent' => 'Postotak', + 'discount_amount' => 'Iznos', + 'invoice_history' => 'Povijest računa', + 'quote_history' => 'Povijest ponude', + 'current_version' => 'Trenutna verzija', + 'select_version' => 'Select version', + 'view_history' => 'Pregled povijesti', + 'edit_payment' => 'Uredi uplatu', + 'updated_payment' => 'Uspješno ažurirana uplata', + 'deleted' => 'Obrisano', + 'restore_user' => 'Obnovi korisnika', + 'restored_user' => 'Uspješno obnovljen korisnik', + 'show_deleted_users' => 'Prikaži obrisane korisnike', + 'email_templates' => 'Predlošci e-pošte', + 'invoice_email' => 'E-pošta računa', + 'payment_email' => 'E-pošta uplate', + 'quote_email' => 'E-pošta ponude', + 'reset_all' => 'Resetiraj sve', + 'approve' => 'Odobri', + 'token_billing_type_id' => 'Token naplata', + 'token_billing_help' => 'Omogućava vam pohranu kreditnih kartica sa vašim usmjernikom i naplatu po njima po nadolazećim datumima.', + 'token_billing_1' => 'Onemogućeno', + 'token_billing_2' => 'Opt-in - odabir je prikazan ali nije selektiran', + 'token_billing_3' => 'Opt-out - odabir je prikazan i selektiran', + 'token_billing_4' => 'Uvijek', + 'token_billing_checkbox' => 'Pohrani detalje kreditne kartice', + 'view_in_gateway' => 'View in :gateway', + 'use_card_on_file' => 'Koristi karticu na datoteci', + 'edit_payment_details' => 'Uredi detalje plaćanja', + 'token_billing' => 'Pohrani detalje kartice', + 'token_billing_secure' => 'The data is stored securely by :link', + 'support' => 'Podrška', + 'contact_information' => 'Kontaktne informacije', + '256_encryption' => '256-bitna enkripcija', + 'amount_due' => 'Dospjeli iznos', + 'billing_address' => 'Adresa računa', + 'billing_method' => 'Metoda naplate', + 'order_overview' => 'Pregled narudžbe', + 'match_address' => '*Adresa se mora poklapati s adresom kreditne kartice.', + 'click_once' => '*Molimo kliknite "PLATI ODMAH" samo jednom - transakcija može izvoditi i do 1 minute.', + 'invoice_footer' => 'Podnožje računa', + 'save_as_default_footer' => 'Pohrani kao zadano podnožje', + 'token_management' => 'Upravljanje tokenima', + 'tokens' => 'Tokeni', + 'add_token' => 'Dodaj token', + 'show_deleted_tokens' => 'Prikaži obrisane tokene', + 'deleted_token' => 'Uspješno obrisan token', + 'created_token' => 'Uspješno kreiran token', + 'updated_token' => 'Uspješno ažuriran token', + 'edit_token' => 'Uredi token', + 'delete_token' => 'Obriši token', + 'token' => 'Token', + 'add_gateway' => 'Dodaj usmjernik', + 'delete_gateway' => 'Obriši usmjernik', + 'edit_gateway' => 'Uredi usmjernik', + 'updated_gateway' => 'Uspješno ažuriran usmjernik', + 'created_gateway' => 'Uspješno kreiran usmjernik', + 'deleted_gateway' => 'Uspješno obrisan usmjernik', + 'pay_with_paypal' => 'PayPal', + 'pay_with_card' => 'Kreditna kartica', + 'change_password' => 'Promijeni zaporku', + 'current_password' => 'Trenutna zaporka', + 'new_password' => 'Nova zaporka', + 'confirm_password' => 'Potvrdi zaporku', + 'password_error_incorrect' => 'Trenutna zaporka nije ispravna.', + 'password_error_invalid' => 'Nova zaporka je neispravna.', + 'updated_password' => 'Uspješno ažurirana zaporka', + 'api_tokens' => 'API tokeni', + 'users_and_tokens' => 'Korisnici & tokeni', + 'account_login' => 'Korisnička prijava', + 'recover_password' => 'Obnovite vašu zaporku', + 'forgot_password' => 'Zaboravili ste zaporku?', + 'email_address' => 'Adresa e-pošte', + 'lets_go' => 'Krenimo', + 'password_recovery' => 'Obnova zaporke', + 'send_email' => 'Slanje e-pošte', + 'set_password' => 'Postava zaporke', + 'converted' => 'Konvertirano', + 'email_approved' => 'Pošalji mi e-poštu kada je ponuda odobrena', + 'notification_quote_approved_subject' => 'Quote :invoice je odobrena od strane :client', + 'notification_quote_approved' => 'Slijedeći klijent :client je odobrio ponudu :invoice iznosa :amount.', + 'resend_confirmation' => 'Ponovite potvrdnu e-poštu', + 'confirmation_resent' => 'E-pošta za potvdu je ponovo poslana', + 'gateway_help_42' => ':link za prijavu na BitPay.
Bilješka: koristite tradicionalni API ključ, umjesto API tokena.', + 'payment_type_credit_card' => 'Kreditna kartica', + 'payment_type_paypal' => 'PayPal', + 'payment_type_bitcoin' => 'Bitcoin', + 'knowledge_base' => 'Baza znanja', + 'partial' => 'Parcijalno', + 'partial_remaining' => ':partial od :balance', + 'more_fields' => 'Više polja', + 'less_fields' => 'Manje polja', + 'client_name' => 'Ime klijenta', + 'pdf_settings' => 'PDF postavke', + 'product_settings' => 'Postavke proizvoda', + 'auto_wrap' => 'Auto formatiranje stavke', + 'duplicate_post' => 'Pozor: prethodna stranica je poslana dvaput. Drugo slanje će biti ignorirano.', + 'view_documentation' => 'Pregled dokumentacije', + 'app_title' => 'Slobodno fakturiranje otvorenim kodom.', + 'app_description' => 'Invoice Ninja je slobodno rješenje otvorenog koda za fakturiranje i upravljanje klijentima. Pomoću Invoice Ninje možete jednostavno izrađivati i slati kreativne račune sa bilo kojeg uređaja koji ima pristup Internetu. Vaši klijenti mogu ispisivati račune, preuzeti ih kao pdf datoteke i čak vam platiti online unutar istog sustava.', + 'rows' => 'redci', + 'www' => 'www', + 'logo' => 'Logo', + 'subdomain' => 'Poddomena', + 'provide_name_or_email' => 'Molimo unesite kontakt ime ili adresu e-pošte', + 'charts_and_reports' => 'Karte & Izvješća', + 'chart' => 'Karte', + 'report' => 'Izvješća', + 'group_by' => 'Grupiraj po', + 'paid' => 'Plaćeno', + 'enable_report' => 'Izvješće', + 'enable_chart' => 'Karta', + 'totals' => 'Zbrojevi', + 'run' => 'Pokreni', + 'export' => 'Izvoz', + 'documentation' => 'Dokumentacija', + 'zapier' => 'Zapier', + 'recurring' => 'Redovni', + 'last_invoice_sent' => 'Zadni račun poslan :date', + 'processed_updates' => 'Uspješno dovršeno ažuriranje', + 'tasks' => 'Zadaci', + 'new_task' => 'Novi zadatak', + 'start_time' => 'Početno vrijeme', + 'created_task' => 'Uspješno kreiran zadatak', + 'updated_task' => 'Uspješno ažuriran zadatak', + 'edit_task' => 'Uredi zadatak', + 'archive_task' => 'Arhiviraj zadatak', + 'restore_task' => 'Obnovi zadatak', + 'delete_task' => 'Obriši zadatak', + 'stop_task' => 'Završi zadatak', + 'time' => 'Vrijeme', + 'start' => 'Početak', + 'stop' => 'Završetak', + 'now' => 'Sada', + 'timer' => 'Štoperica', + 'manual' => 'Ručno', + 'date_and_time' => 'Datum & vrijeme', + 'second' => 'sekunda', + 'seconds' => 'sekunde', + 'minute' => 'minuta', + 'minutes' => 'minute', + 'hour' => 'sat', + 'hours' => 'sati', + 'task_details' => 'Detalji zadatka', + 'duration' => 'Trajanje', + 'end_time' => 'Završno vrijeme', + 'end' => 'Kraj', + 'invoiced' => 'Fakturirano', + 'logged' => 'Logirano', + 'running' => 'Pokrenuto', + 'task_error_multiple_clients' => 'Zadaci ne mogu pripadati različitim klijentima', + 'task_error_running' => 'Molimo najprije dovršite izvođenje zadatka', + 'task_error_invoiced' => 'Zadaci su več fakturirani', + 'restored_task' => 'Uspješno obnovljen zadatak', + 'archived_task' => 'Uspješno arhiviran zadatak', + 'archived_tasks' => 'Uspješno arhivirano :count zadataka', + 'deleted_task' => 'Uspješno obrisan zadatak', + 'deleted_tasks' => 'Uspješno obrisano :count zadataka', + 'create_task' => 'Kreiraj zadatak', + 'stopped_task' => 'Uspješno završen zadatak', + 'invoice_task' => 'Fakturiraj zadatak', + 'invoice_labels' => 'Oznake računa', + 'prefix' => 'Prefiks', + 'counter' => 'Brojač', + 'payment_type_dwolla' => 'Dwolla', + 'gateway_help_43' => ':link za prijavu na Dwolla', + 'partial_value' => 'Mora biti veće od nula i manje od zbroja', + 'more_actions' => 'Više akcija', + 'pro_plan_title' => 'NINJA PRO', + 'pro_plan_call_to_action' => 'Nadogradite odmah!', + 'pro_plan_feature1' => 'Kreirajte neograničeno klijenata', + 'pro_plan_feature2' => 'Pristup na 10 izvrsnih dizajna računa', + 'pro_plan_feature3' => 'Prilagođeni URL - "VašBrand.InvoiceNinja.com"', + 'pro_plan_feature4' => 'Uklonite "Created by Invoice Ninja"', + 'pro_plan_feature5' => 'Višekorisnički pristup & praćenje aktivnosti', + 'pro_plan_feature6' => 'Kreiranje ponuda & predračuna', + 'pro_plan_feature7' => 'Prilagodba naslova polja računa & numeriranje', + 'pro_plan_feature8' => 'Opcija za privitak PDFa na e-poštu klijenta', + 'resume' => 'Nastavi', + 'break_duration' => 'Prekini', + 'edit_details' => 'Uredi detalje', + 'work' => 'Rad', + 'timezone_unset' => 'Molimo :link za postavu vaše vremenske zone', + 'click_here' => 'kliknite ovdje', + 'email_receipt' => 'Pošalji e-poštom račun klijentu', + 'created_payment_emailed_client' => 'Uspješno kreirana uplata i poslana klijentu e-poštom', + 'add_company' => 'Dodaj poduzeće', + 'untitled' => 'Bez naslova', + 'new_company' => 'Novo poduzeće', + 'associated_accounts' => 'Uspješno povezani računi', + 'unlinked_account' => 'Uspješno razdvojeni računi', + 'login' => 'Prijava', + 'or' => 'ili', + 'email_error' => 'Došlo je do problema pri slanju e-pošte', + 'confirm_recurring_timing' => 'Bilješka: e-pošta je poslana na početku sata.', + 'payment_terms_help' => 'Postava zadanog datuma dospijeća', + 'unlink_account' => 'Razdvoji račune', + 'unlink' => 'Razdvoji', + 'show_address' => 'Prikaži adrese', + 'show_address_help' => 'Zahtijevaj od klijenta adresu za fakture', + 'update_address' => 'Ažuriraj adresu', + 'update_address_help' => 'Ažuriraj adresu klijenta uz osigurane detalje', + 'times' => 'Vremena', + 'set_now' => 'Postavi na sada', + 'dark_mode' => 'Tamni prikaz', + 'dark_mode_help' => 'Prikaži bijeli tekst na crnoj pozadini', + 'add_to_invoice' => 'Dodaj računu :invoice', + 'create_new_invoice' => 'Kreiraj novi račun', + 'task_errors' => 'Molimo korigirajte preklopna vremena', + 'from' => 'Šalje', + 'to' => 'Prima', + 'font_size' => 'Veličina fonta', + 'primary_color' => 'Primarna boja', + 'secondary_color' => 'Sekundarna boja', + 'customize_design' => 'Prilagodi dizajn', + 'content' => 'Sadržaj', + 'styles' => 'Stilovi', + 'defaults' => 'Zadano', + 'margins' => 'Margine', + 'header' => 'Zaglavlje', + 'footer' => 'Podnožje', + 'custom' => 'Prilagođeno', + 'invoice_to' => 'Fakturiraj na', + 'invoice_no' => 'Broj računa', + 'recent_payments' => 'Nedavne uplate', + 'outstanding' => 'Izvanredno', + 'manage_companies' => 'Upravljanje poduzećima', + 'total_revenue' => 'Ukupni prihod', + 'current_user' => 'Trenutni korisnik', + 'new_recurring_invoice' => 'Novi redovni račun', + 'recurring_invoice' => 'Redovni račun', + 'recurring_too_soon' => 'Prerano je za kreiranje novog redovnog računa, na rasporedu je za :date', + 'created_by_invoice' => 'Kreiran od :invoice', + 'primary_user' => 'Primarni korisnik', + 'help' => 'Pomoć', + 'customize_help' => '

We use pdfmake to define the invoice designs declaratively. The pdfmake playground provide\'s a great way to see the library in action.

+

To access a child property using dot notation. For example to show the client name you could use $client.name.

+

If you need help figuring something out post a question to our support forum.

', + 'invoice_due_date' => 'Datum valute', + 'quote_due_date' => 'Vrijedi do', + 'valid_until' => 'Vrijedi do', + 'reset_terms' => 'Resetiraj uvjete', + 'reset_footer' => 'Resetiraj podnožje', + 'invoices_sent' => ':count invoice sent|:count invoices sent', + 'status_draft' => 'Nacrt', + 'status_sent' => 'Poslano', + 'status_viewed' => 'Pregledano', + 'status_partial' => 'Parcijalno', + 'status_paid' => 'Plaćeno', + 'show_line_item_tax' => 'Prikaži poreze u liniji stavke', + 'iframe_url' => 'Web mjesto', + 'iframe_url_help1' => 'Kopiraj slijedeći kod na stranicu svog web mjesta.', + 'iframe_url_help2' => 'Možete testirati mogućnost klikom na \'Pogledaj kao primatelj\' za račun.', + 'auto_bill' => 'Auto račun', + 'military_time' => '24 satno vrijeme', + 'last_sent' => 'Zadnje poslano', + 'reminder_emails' => 'E-pošta podsjetnik', + 'templates_and_reminders' => 'Predlošci & podsjetnici', + 'subject' => 'Naslov', + 'body' => 'Tijelo', + 'first_reminder' => 'Prvi podsjetnik', + 'second_reminder' => 'Drugi podsjetnik', + 'third_reminder' => 'Treći podsjetnik', + 'num_days_reminder' => 'Dana nakon isteka valute', + 'reminder_subject' => 'Podsjetnik: Račun :invoice od :account', + 'reset' => 'Resetiraj', + 'invoice_not_found' => 'Traženi račun nije dostupan', + 'referral_program' => 'Referentni progam', + 'referral_code' => 'Referentni URL', + 'last_sent_on' => 'Zadnje poslano :date', + 'page_expire' => 'Ova stranica će uskoro isteći, :click_here za nastavak rada', + 'upcoming_quotes' => 'Nadolazeće ponude', + 'expired_quotes' => 'Istekle ponude', + 'sign_up_using' => 'Prijavi se koristeći', + 'invalid_credentials' => 'Ove vjerodajnice se ne odgovaraju našim zapisima', + 'show_all_options' => 'Prkaži sve opcije', + 'user_details' => 'Detalji korisnika', + 'oneclick_login' => 'Prijava jednim klikom', + 'disable' => 'Onemogući', + 'invoice_quote_number' => 'Brojevi računa i ponuda', + 'invoice_charges' => 'Troškovi računa', + 'notification_invoice_bounced' => 'Nismo mogli dostaviti račun :invoice prema :contact.', + 'notification_invoice_bounced_subject' => 'Nije moguće dostaviti :invoice', + 'notification_quote_bounced' => 'Nismo mogli dostaviti ponudu :invoice prema :contact', + 'notification_quote_bounced_subject' => 'Nije moguće dostaviti ponudu :invoice', + 'custom_invoice_link' => 'Prilagođena poveznica računa', + 'total_invoiced' => 'Ukupno fakturirano', + 'open_balance' => 'Otvoreno stanje', + 'verify_email' => 'Molimo posjetite poveznicu unutar potvrdne e-pošte računa kako bi potvrdili svoju adresu e-pošte.', + 'basic_settings' => 'Osnovne postavke', + 'pro' => 'Pro', + 'gateways' => 'Platni usmjernici', + 'next_send_on' => 'Pošalji slijedeći: :date', + 'no_longer_running' => 'Ovaj račun nije zakazan za slanje', + 'general_settings' => 'Opće postavke', + 'customize' => 'Prilagodi', + 'oneclick_login_help' => 'Spoji se na račun za prijavu bez zaporke', + 'referral_code_help' => 'Zaradite novac dijeleći našu aplikaciju online', + 'enable_with_stripe' => 'Omogući | Zahtijeva Stripe', + 'tax_settings' => 'Postavke poreza', + 'create_tax_rate' => 'Dodaj poreznu stopu', + 'updated_tax_rate' => 'Uspješno ažurirana porezna stopa', + 'created_tax_rate' => 'Uspješno kreirana porezna stopa', + 'edit_tax_rate' => 'Uredi poreznu stopu', + 'archive_tax_rate' => 'Arhiviraj poreznu stopu', + 'archived_tax_rate' => 'Uspješno arhivirana porezna stopa', + 'default_tax_rate_id' => 'Zadana porezna stopa', + 'tax_rate' => 'Porezna stopa', + 'recurring_hour' => 'Ponavljajući sat', + 'pattern' => 'Uzorak', + 'pattern_help_title' => 'Pomoć za uzorke', + 'pattern_help_1' => 'Kreirajte prilagođene brojeve računa i ponuda specifikacijom uzorka', + 'pattern_help_2' => 'Dostupne varijable:', + 'pattern_help_3' => 'Na primjer, :example bi bilo pretvoreno u :value', + 'see_options' => 'Pogledaj opcije', + 'invoice_counter' => 'Brojač računa', + 'quote_counter' => 'Brojač ponuda', + 'type' => 'Tip', + 'activity_1' => ':user kreirao klijenta :client', + 'activity_2' => ':user arhivirao klijenta :client', + 'activity_3' => ':user obrisao klijenta :client', + 'activity_4' => ':user kreirao račun :invoice', + 'activity_5' => ':user ažurirao račun :invoice', + 'activity_6' => ':user poslao e-poštom račun :invoice za :contact', + 'activity_7' => ':contact pregledao račun :invoice', + 'activity_8' => ':user arhivirao račun :invoice', + 'activity_9' => ':user obrisao račun :invoce', + 'activity_10' => ':contact upisao uplatu :payment za :invoice', + 'activity_11' => ':user ažurirao uplatu :payment', + 'activity_12' => ':user ahivirao uplatu :payment', + 'activity_13' => ':user obrisao uplatu :payment', + 'activity_14' => ':user upisao :credit kredit', + 'activity_15' => ':user ažurirao :credit kredit', + 'activity_16' => ':user arhivirao :credit kredit', + 'activity_17' => ':user obrisao :credit kredit', + 'activity_18' => ':user kreirao ponudu :quote', + 'activity_19' => ':user ažurirao ponudu :quote', + 'activity_20' => ':user poslao e-poštom ponudu :quote za :contact', + 'activity_21' => ':contact pregledao ponudu :quote', + 'activity_22' => ':user arhivirao ponudu :quote', + 'activity_23' => ':user obrisao ponudu :quote', + 'activity_24' => ':user obnovio ponudu :quote', + 'activity_25' => ':user obnovio račun :invoice', + 'activity_26' => ':user obnovio klijenta :client', + 'activity_27' => ':user obnovio uplatu :payment', + 'activity_28' => ':user obnovio :credit kredit', + 'activity_29' => ':contact odobrio ponudu :quote', + 'activity_30' => ':user kreirao :vendor', + 'activity_31' => ':user kreirao :vendor', + 'activity_32' => ':user kreirao :vendor:', + 'activity_33' => ':user kreirao :vendor', + 'activity_34' => ':user kreirao trošak :expense', + 'activity_35' => ':user kreirao :vendor', + 'activity_36' => ':user kreirao :vendor', + 'activity_37' => ':user kreirao :vendor', + 'payment' => 'Uplata', + 'system' => 'Sustav', + 'signature' => 'Potpis e-pošte', + 'default_messages' => 'Zadane poruke', + 'quote_terms' => 'Uvjeti ponude', + 'default_quote_terms' => 'Zadani uvjeti ponude', + 'default_invoice_terms' => 'Zadani uvjeti računa', + 'default_invoice_footer' => 'Zadano podnožje računa', + 'quote_footer' => 'Podnožje ponude', + 'free' => 'Slobodan', + 'quote_is_approved' => 'Ova ponuda je odobrena', + 'apply_credit' => 'Primjeni kredit', + 'system_settings' => 'Postavke sustava', + 'archive_token' => 'Arhiviraj token', + 'archived_token' => 'Uspješno arhiviran token', + 'archive_user' => 'Arhiviraj korisnika', + 'archived_user' => 'Uspješno arhiviran korisnik', + 'archive_account_gateway' => 'Arhiviraj usmjernik', + 'archived_account_gateway' => 'Uspješno arhiviran usmjernik', + 'archive_recurring_invoice' => 'Arhiviraj redoviti račun', + 'archived_recurring_invoice' => 'Uspješno arhiviran redoviti račun', + 'delete_recurring_invoice' => 'Obriši redoviti račun', + 'deleted_recurring_invoice' => 'Uspješno obrisan redoviti račun', + 'restore_recurring_invoice' => 'Obnovi redoviti račun', + 'restored_recurring_invoice' => 'Uspješno obnovljen redoviti račun', + 'archived' => 'Arhivirano', + 'untitled_account' => 'Neimenovano poduzeće', + 'before' => 'Prije', + 'after' => 'Poslije', + 'reset_terms_help' => 'Resetiraj na zadane postavke računa', + 'reset_footer_help' => 'Resetiraj na zadane postavke podnožja', + 'export_data' => 'Izvezi podatke', + 'user' => 'Korisnik', + 'country' => 'Zemlja', + 'include' => 'Uključi', + 'logo_too_large' => 'Vaš logo je :size, za bolju PDF izvedbu preporučujemo otpremu slikovne datoteke manje od 200KB', + 'import_freshbooks' => 'Uvezi iz FreshBooks', + 'import_data' => 'Uvezi podatke', + 'source' => 'Izvor', + 'csv' => 'CSV', + 'client_file' => 'Klijentska datoteka', + 'invoice_file' => 'Datoteka računa', + 'task_file' => 'Datoteka zadataka', + 'no_mapper' => 'Nema ispravnog mapiranja za datoteku', + 'invalid_csv_header' => 'Neispravno CSV zaglavlje', + 'client_portal' => 'Klijentski portal', + 'admin' => 'Administracija', + 'disabled' => 'Onemogućeno', + 'show_archived_users' => 'Prikaži arhivirane korisnike', + 'notes' => 'Bilješke', + 'invoice_will_create' => 'klijent će biti kreiran', + 'invoices_will_create' => 'računi će biti kreirani', + 'failed_to_import' => 'The following records failed to import, they either already exist or are missing required fields.', + 'publishable_key' => 'Objavljivi ključ', + 'secret_key' => 'Tajni ključ', + 'missing_publishable_key' => 'Postavite vap Stripe objavljivi ključ za poboljšani proces odjave', + 'email_design' => 'Dizajn e-pošte', + 'due_by' => 'Valuta do :date', + 'enable_email_markup' => 'Omogući markup', + 'enable_email_markup_help' => 'Olakšajte svojim klijentima plaćanje dodavanjem schema.org markupa vašoj e-pošti.', + 'template_help_title' => 'Pomoć za predloške', + 'template_help_1' => 'Dostupne varijable:', + 'email_design_id' => 'Stil e-pošte', + 'email_design_help' => 'Napravite svoju e-poštu profesionalnijom pomoću HTML postave', + 'plain' => 'Obično', + 'light' => 'Svijetlo', + 'dark' => 'Tamno', + 'industry_help' => 'Koristi se za usporedbe između prosjeka poduzeća sličnih veličina i djelatnosti.', + 'subdomain_help' => 'Prilagodite poveznicu poddomene na računu ili prikažite račun na vlastitom web mjestu.', + 'invoice_number_help' => 'Odredite prefiks ili koristite prilagođeni uzorak za dinamično postavljanje brojeva računa.', + 'quote_number_help' => 'Odredite prefiks ili koristite prilagođeni uzorak za dinamično postavljanje brojeva ponuda.', + 'custom_client_fields_helps' => 'Add a field when creating a client and display the label and value on the PDF.', + 'custom_account_fields_helps' => 'Dodajte oznaku i vrijednost na sekciju sa detaljima poduzeža na PDFu.', + 'custom_invoice_fields_helps' => 'Add a field when creating an invoice and display the label and value on the PDF.', + 'custom_invoice_charges_helps' => 'Add a field when creating an invoice and include the charge in the invoice subtotals.', + 'token_expired' => 'Validacijski token je istekao. Molimo pokušajte ponovo.', + 'invoice_link' => 'Poveznica računa', + 'button_confirmation_message' => 'Kliknite za potvrdu vaše adrese e-pošte.', + 'confirm' => 'Odobri', + 'email_preferences' => 'Postavke e-pošte', + 'created_invoices' => 'Uspješno kreirano :count računa', + 'next_invoice_number' => 'Slijedeći broj računa je :number.', + 'next_quote_number' => 'Slijedeći broj ponude je :number.', + 'days_before' => 'dana prije', + 'days_after' => 'dana poslije', + 'field_due_date' => 'datum valute', + 'field_invoice_date' => 'datum računa', + 'schedule' => 'Raspored', + 'email_designs' => 'Dizajn e-pošte', + 'assigned_when_sent' => 'Dodijeljeno pri slanju', + 'white_label_purchase_link' => 'Kupite licencu bijele oznake', + 'expense' => 'Trošak', + 'expenses' => 'Troškovi', + 'new_expense' => 'New Expense', + 'enter_expense' => 'Unesi trošak', + 'vendors' => 'Dobavljači', + 'new_vendor' => 'Novi dobavljač', + 'payment_terms_net' => 'čisto', + 'vendor' => 'Dobavljač', + 'edit_vendor' => 'Uredi dobavljača', + 'archive_vendor' => 'Arhiviraj dobavljača', + 'delete_vendor' => 'Obriši dobavljača', + 'view_vendor' => 'Pregledaj dobavljača', + 'deleted_expense' => 'Uspješno obrisan trošak', + 'archived_expense' => 'Uspješno arhiviran trošak', + 'deleted_expenses' => 'Uspješno obrisan trošak', + 'archived_expenses' => 'Uspješno arhivirani troškovi', + 'expense_amount' => 'Iznos troškova', + 'expense_balance' => 'Stanje troškova', + 'expense_date' => 'Datum troška', + 'expense_should_be_invoiced' => 'Treba li fakturirati ovaj trošak?', + 'public_notes' => 'Javne bilješke', + 'invoice_amount' => 'Iznos računa', + 'exchange_rate' => 'Tečaj', + 'yes' => 'Da', + 'no' => 'Ne', + 'should_be_invoiced' => 'Treba biti fakturiran', + 'view_expense' => 'Pregled troškova # :expense', + 'edit_expense' => 'Uredi trošak', + 'archive_expense' => 'Arhiviraj trošak', + 'delete_expense' => 'Obriši trošak', + 'view_expense_num' => 'Trošak # :expense', + 'updated_expense' => 'Uspješno ažuriran trošak', + 'created_expense' => 'Uspješno kreiran trošak', + 'enter_expense' => 'Unesi trošak', + 'view' => 'Pregled', + 'restore_expense' => 'Obnovi trošak', + 'invoice_expense' => 'Trošak računa', + 'expense_error_multiple_clients' => 'Troškovi ne mogu pripadati različitim klijentima', + 'expense_error_invoiced' => 'Trošak je već fakturiran', + 'convert_currency' => 'Konvertiraj valutu', + 'num_days' => 'Broj dana', + 'create_payment_term' => 'Kreiraj uvjete plaćanja', + 'edit_payment_terms' => 'Uredi uvjet plaćanja', + 'edit_payment_term' => 'Uredi uvjete plaćanja', + 'archive_payment_term' => 'Arhiviraj uvjet plaćanje', + 'recurring_due_dates' => 'Datumi dospjeća redovnih računa', + 'recurring_due_date_help' => '

Automatski postavlja datume dospjeća za račune.

+

Računi unutar mjesečnog ili godišnjeg ciklusa postavljeni da dospijevaju na dan ili prije dana na koji su kreirani biti će dospjeli slijedeći mjesec. Računi postavljeni da dospjevaju na 29. ili 30. u mjesecu a koji nemaju taj dan biti će dospjeli zadnji dan u mjesecu.

+

Računi unutar tjednog ciklusa postavljeni da dospjevaju na dan u tjednu kada su kreirani dospijevati će slijedeći tjedan.

+

Na primjer:

+
    +
  • Danas je 15., datum dospijeća je 1. u mjesecu. Datum dospijeća je izgledno 1. slijedećeg mjeseca.
  • +
  • Danas je 15., datum dospijeća je zadnji dan u mjesecu. Datum dospijeća je zadnji dan u ovom mjesecu. +
  • +
  • Danas je 15, datum dospjeća je 15. u mjesedu. Datum dospijeća će biti 15 dan slijedećeg mjeseca. +
  • +
  • Danas je petak, datum dospijeća je prvi slijedeći petak. Datum dospijeća je prvi slijedeći petak, a ne današnji petak. +
  • +
', + 'due' => 'Dospjelo', + 'next_due_on' => 'Dospijeva slijedeće :date', + 'use_client_terms' => 'Koristi uvjete klijenta', + 'day_of_month' => ':ordinal dan u mjesecu', + 'last_day_of_month' => 'Zadnji dan u mjesecu', + 'day_of_week_after' => ':ordinal :day nakon', + 'sunday' => 'Nedjelja', + 'monday' => 'Ponedjeljak', + 'tuesday' => 'Utorak', + 'wednesday' => 'Srijeda', + 'thursday' => 'Četvrtak', + 'friday' => 'Petak', + 'saturday' => 'Subota', + 'header_font_id' => 'Font zaglavlja', + 'body_font_id' => 'Font tijela', + 'color_font_help' => 'Bilješka: primarna boja i fontovi su također korišteni u klijentskom portalu i prilagođenom dizajnu e-pošte', + 'live_preview' => 'Pretpregled uživo', + 'invalid_mail_config' => 'Nije moguće poslati e-poštu, molim provjerite da li su vaše postavke e-pošte ispravne.', + 'invoice_message_button' => 'Za pregled vašeg račun iznosa :amount, kliknite na donju tipku.', + 'quote_message_button' => 'Za pregled vaše ponude iznosa :amount, kliknite donju tipku.', + 'payment_message_button' => 'Hvala vam na vašoj uplati od :amount.', + 'payment_type_direct_debit' => 'Direktni dug', + 'bank_accounts' => 'Kreditne kartice & banke', + 'add_bank_account' => 'Dodajte bankovni račun', + 'setup_account' => 'Postava računa', + 'import_expenses' => 'Uvezi troškove', + 'bank_id' => 'Bank', + 'integration_type' => 'Tip integracije', + 'updated_bank_account' => 'Uspješno ažuriran bankovni račun', + 'edit_bank_account' => 'Uredi bankovni račun', + 'archive_bank_account' => 'Arhiviraj bankovni račun', + 'archived_bank_account' => 'Uspješno arhiviran bankovni račun', + 'created_bank_account' => 'Uspješno kreiran bankovni račun', + 'validate_bank_account' => 'Provjeri bankovni račun', + 'bank_password_help' => 'Bilješka: vaša zaporka je sigurno odaslana i nikada nije pohranjivana na našim serverima.', + 'bank_password_warning' => 'Pozor: vaša zaporka je mogla biti odaslana kao običan tekst, razmotrite omogućavanje HTTPSa.', + 'username' => 'Korisničko ime', + 'account_number' => 'Broj računa', + 'account_name' => 'Ime računa', + 'bank_account_error' => 'Neuspjelo dohvaćanje detalja računa, molimo provjerite svoje ovlasti.', + 'status_approved' => 'Odobreno', + 'quote_settings' => 'Postavke ponude', + 'auto_convert_quote' => 'Auto konverzija ponude', + 'auto_convert_quote_help' => 'Automatski konvertirajte ponudu u račun nakon što je odobrena od strane klijenta.', + 'validate' => 'Validiraj', + 'info' => 'Info', + 'imported_expenses' => 'Uspješno kreirano :count_vendors dobavljača i :count_expenses troškova', + 'iframe_url_help3' => 'Bilješka: Ukoliko planirate prihvaćati kreditne kartice, snažno preporučujemo omogućavanje HTTPS na vašem web mjestu.', + 'expense_error_multiple_currencies' => 'Troškovi ne mogu imati različite valute.', + 'expense_error_mismatch_currencies' => 'Valute klijenata se ne podudaraju sa valutama troškova.', + 'trello_roadmap' => 'Trello razvojna cesta', + 'header_footer' => 'Zaglavlje/Podnožje', + 'first_page' => 'First page', + 'all_pages' => 'All pages', + 'last_page' => 'Last page', + 'all_pages_header' => 'Prikaži zaglavlje na', + 'all_pages_footer' => 'Prikaži podnožje na', + 'invoice_currency' => 'Valuta računa', + 'enable_https' => 'Snažno preporučujemo korištenje HTTPS za prihvat detalja kreditnih kartica online.', + 'quote_issued_to' => 'Ponuda napravljena za', + 'show_currency_code' => 'Kod valute', + 'trial_message' => 'Vaš račun će primiti besplatni dvotjedni probni rok za naš pro plan.', + 'trial_footer' => 'Vaš besplatni probni rok traje još :count dana, :link za trenutnu nadogradnju.', + 'trial_footer_last_day' => 'Ovo je zadnji dan vašeg probnog roka, :link za trenutnu nadogradnju.', + 'trial_call_to_action' => 'Pokreni besplatni probni rok', + 'trial_success' => 'Uspješno je omogućeno dva tjedna besplatnog probnog pro plan roka', + 'overdue' => 'Van valute', + + + 'white_label_text' => 'Purchase a ONE YEAR white label license for $:price to remove the Invoice Ninja branding from the client portal and help support our project.', + 'user_email_footer' => 'To adjust your email notification settings please visit :link', + 'reset_password_footer' => 'If you did not request this password reset please email our support: :email', + 'limit_users' => 'Sorry, this will exceed the limit of :limit users', + 'more_designs_self_host_header' => 'Get 6 more invoice designs for just $:price', + 'old_browser' => 'Please use a newer browser', + 'white_label_custom_css' => ':link for $:price to enable custom styling and help support our project.', + 'bank_accounts_help' => 'Connect a bank account to automatically import expenses and create vendors. Supports American Express and 400+ US banks.', + + 'pro_plan_remove_logo' => ':link to remove the Invoice Ninja logo by joining the Pro Plan', + 'pro_plan_remove_logo_link' => 'Click here', + 'invitation_status_sent' => 'Email Sent', + 'invitation_status_opened' => 'Email Openend', + 'invitation_status_viewed' => 'Invoice Viewed', + 'email_error_inactive_client' => 'Emails can not be sent to inactive clients', + 'email_error_inactive_contact' => 'Emails can not be sent to inactive contacts', + 'email_error_inactive_invoice' => 'Emails can not be sent to inactive invoices', + 'email_error_user_unregistered' => 'Please register your account to send emails', + 'email_error_user_unconfirmed' => 'Please confirm your account to send emails', + 'email_error_invalid_contact_email' => 'Invalid contact email', + + 'navigation' => 'Navigation', + 'list_invoices' => 'List Invoices', + 'list_clients' => 'List Clients', + 'list_quotes' => 'List Quotes', + 'list_tasks' => 'List Tasks', + 'list_expenses' => 'List Expenses', + 'list_recurring_invoices' => 'List Recurring Invoices', + 'list_payments' => 'List Payments', + 'list_credits' => 'List Credits', + 'tax_name' => 'Tax Name', + 'report_settings' => 'Report Settings', + 'search_hotkey' => 'shortcut is /', + + 'new_user' => 'New User', + 'new_product' => 'New Product', + 'new_tax_rate' => 'New Tax Rate', + 'invoiced_amount' => 'Invoiced Amount', + 'invoice_item_fields' => 'Invoice Item Fields', + 'custom_invoice_item_fields_help' => 'Add a field when creating an invoice item and display the label and value on the PDF.', + 'recurring_invoice_number' => 'Recurring Invoice Number', + 'recurring_invoice_number_prefix_help' => 'Speciy a prefix to be added to the invoice number for recurring invoices. The default value is \'R\'.', + + // Client Passwords + 'enable_portal_password'=>'Password protect invoices', + 'enable_portal_password_help'=>'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.', + 'send_portal_password'=>'Generate password automatically', + 'send_portal_password_help'=>'If no password is set, one will be generated and sent with the first invoice.', + + 'expired' => 'Expired', + 'invalid_card_number' => 'The credit card number is not valid.', + 'invalid_expiry' => 'The expiration date is not valid.', + 'invalid_cvv' => 'The CVV is not valid.', + 'cost' => 'Cost', + 'create_invoice_for_sample' => 'Note: create your first invoice to see a preview here.', + + // User Permissions + 'owner' => 'Owner', + 'administrator' => 'Administrator', + 'administrator_help' => 'Allow user to manage users, change settings and modify all records', + 'user_create_all' => 'Create clients, invoices, etc.', + 'user_view_all' => 'View all clients, invoices, etc.', + 'user_edit_all' => 'Edit all clients, invoices, etc.', + 'gateway_help_20' => ':link to sign up for Sage Pay.', + 'gateway_help_21' => ':link to sign up for Sage Pay.', + 'partial_due' => 'Partial Due', + 'restore_vendor' => 'Restore Vendor', + 'restored_vendor' => 'Successfully restored vendor', + 'restored_expense' => 'Successfully restored expense', + 'permissions' => 'Permissions', + 'create_all_help' => 'Allow user to create and modify records', + 'view_all_help' => 'Allow user to view records they didn\'t create', + 'edit_all_help' => 'Allow user to modify records they didn\'t create', + 'view_payment' => 'View Payment', + + 'january' => 'January', + 'february' => 'February', + 'march' => 'March', + 'april' => 'April', + 'may' => 'May', + 'june' => 'June', + 'july' => 'July', + 'august' => 'August', + 'september' => 'September', + 'october' => 'October', + 'november' => 'November', + 'december' => 'December', + + // Documents + 'documents_header' => 'Documents:', + 'email_documents_header' => 'Documents:', + 'email_documents_example_1' => 'Widgets Receipt.pdf', + 'email_documents_example_2' => 'Final Deliverable.zip', + 'invoice_documents' => 'Documents', + 'expense_documents' => 'Attached Documents', + 'invoice_embed_documents' => 'Embed Documents', + 'invoice_embed_documents_help' => 'Include attached images in the invoice.', + 'document_email_attachment' => 'Attach Documents', + 'download_documents' => 'Download Documents (:size)', + 'documents_from_expenses' => 'From Expenses:', + 'dropzone_default_message' => 'Drop files or click to upload', + 'dropzone_fallback_message' => 'Your browser does not support drag\'n\'drop file uploads.', + 'dropzone_fallback_text' => 'Please use the fallback form below to upload your files like in the olden days.', + 'dropzone_file_too_big' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'dropzone_invalid_file_type' => 'You can\'t upload files of this type.', + 'dropzone_response_error' => 'Server responded with {{statusCode}} code.', + 'dropzone_cancel_upload' => 'Cancel upload', + 'dropzone_cancel_upload_confirmation' => 'Are you sure you want to cancel this upload?', + 'dropzone_remove_file' => 'Remove file', + 'documents' => 'Documents', + 'document_date' => 'Document Date', + 'document_size' => 'Size', + + 'enable_client_portal' => 'Client Portal', + 'enable_client_portal_help' => 'Show/hide the client portal.', + 'enable_client_portal_dashboard' => 'Dashboard', + 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans + 'account_management' => 'Account Management', + 'plan_status' => 'Plan Status', + + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Changes To', + 'plan_changes_to' => ':plan on :date', + 'plan_term_changes_to' => ':plan (:term) on :date', + 'cancel_plan_change' => 'Cancel Change', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'renews' => 'Renews', + 'plan_expired' => ':plan Plan Expired', + 'trial_expired' => ':plan Plan Trial Ended', + 'never' => 'Never', + 'plan_free' => 'Free', + 'plan_pro' => 'Pro', + 'plan_enterprise' => 'Enterprise', + 'plan_white_label' => 'Self Hosted (White labeled)', + 'plan_free_self_hosted' => 'Self Hosted (Free)', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + 'plan_paid' => 'Term Started', + 'plan_started' => 'Plan Started', + 'plan_expires' => 'Plan Expires', + + 'white_label_button' => 'White Label', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.', + + 'live_preview' => 'Pretpregled uživo', + 'page_size' => 'Page Size', + 'live_preview_disabled' => 'Live preview has been disabled to support selected font', + 'invoice_number_padding' => 'Padding', + 'preview' => 'Preview', + 'list_vendors' => 'List Vendors', + 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'return_to_app' => 'Return to app', + + + // 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', + 'status_voided' => 'Cancelled', + 'refunded_payment' => 'Refunded Payment', + 'activity_39' => ':user cancelled a :payment_amount payment (:payment)', + 'activity_40' => ':user refunded :adjustment of a :payment_amount payment (:payment)', + 'card_expiration' => 'Exp: :expires', + + 'card_creditcardother' => 'Unknown', + 'card_americanexpress' => 'American Express', + 'card_carteblanche' => 'Carte Blanche', + 'card_unionpay' => 'UnionPay', + 'card_diners' => 'Diners Club', + 'card_discover' => 'Discover', + 'card_jcb' => 'JCB', + 'card_laser' => 'Laser', + 'card_maestro' => 'Maestro', + 'card_mastercard' => 'MasterCard', + 'card_solo' => 'Solo', + 'card_switch' => 'Switch', + 'card_visacard' => 'Visa', + 'card_ach' => 'ACH', + + 'payment_type_stripe' => 'Stripe', + '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.', + + 'plaid' => 'Plaid', + 'client_id' => 'Client Id', + 'secret' => 'Secret', + '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.', + 'activity_41' => ':payment_amount payment (:payment) failed', + 'webhook_url' => 'Webhook URL', + 'stripe_webhook_help' => 'You must :link.', + 'stripe_webhook_help_link_text' => 'add this URL as an endpoint at Stripe', + 'payment_method_error' => 'There was an error adding your payment methd. Please try again later.', + 'notification_invoice_payment_failed_subject' => 'Payment failed for Invoice :invoice', + 'notification_invoice_payment_failed' => 'A payment made by client :client towards Invoice :invoice failed. The payment has been marked as failed and :amount has been added to the client\'s balance.', + 'link_with_plaid' => 'Link Account Instantly with Plaid', + 'link_manually' => 'Link Manually', + 'secured_by_plaid' => 'Secured by Plaid', + '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_required' => 'You must consent to ACH transactions.', + 'off' => 'Off', + 'opt_in' => 'Opt-in', + 'opt_out' => 'Opt-out', + 'always' => 'Always', + 'opted_out' => 'Opted out', + 'opted_in' => 'Opted in', + 'manage_auto_bill' => 'Manage Auto-bill', + 'enabled' => 'Enabled', + 'paypal' => 'PayPal', + 'braintree_enable_paypal' => 'Enable PayPal payments through BrainTree', + 'braintree_paypal_disabled_help' => 'The PayPal gateway is processing PayPal payments', + 'braintree_paypal_help' => 'You must also :link.', + 'braintree_paypal_help_link_text' => 'link PayPal to your BrainTree account', + 'token_billing_braintree_paypal' => 'Save payment details', + 'add_paypal_account' => 'Add PayPal Account', + + + 'no_payment_method_specified' => 'No payment method specified', + 'chart_type' => 'Chart Type', + 'format' => 'Format', + 'import_ofx' => 'Import OFX', + 'ofx_file' => 'OFX File', + 'ofx_parse_failed' => 'Failed to parse OFX file', + + // WePay + 'wepay' => 'WePay', + 'sign_up_with_wepay' => 'Sign up with WePay', + 'use_another_provider' => 'Use another provider', + 'company_name' => 'Company Name', + 'wepay_company_name_help' => 'This will appear on client\'s credit card statements.', + 'wepay_description_help' => 'The purpose of this account.', + 'wepay_tos_agree' => 'I agree to the :link.', + 'wepay_tos_link_text' => 'WePay Terms of Service', + 'resend_confirmation_email' => 'Resend Confirmation Email', + 'manage_wepay_account' => 'Manage WePay Account', + 'action_required' => 'Action Required', + 'finish_setup' => 'Finish Setup', + 'created_wepay_confirmation_required' => 'Please check your email and confirm your email address with WePay.', + 'switch_to_wepay' => 'Switch to WePay', + 'switch' => 'Switch', + 'restore_account_gateway' => 'Restore Gateway', + 'restored_account_gateway' => 'Successfully restored gateway', + +); + +return $LANG; + +?>. diff --git a/resources/lang/hr/validation.php b/resources/lang/hr/validation.php new file mode 100644 index 000000000000..8b54e496b08a --- /dev/null +++ b/resources/lang/hr/validation.php @@ -0,0 +1,116 @@ + 'Polje :attribute mora biti prihvaćeno.', + 'active_url' => 'Polje :attribute nije ispravan URL.', + 'after' => 'Polje :attribute mora biti datum nakon :date.', + 'alpha' => 'Polje :attribute smije sadržavati samo slova.', + 'alpha_dash' => 'Polje :attribute smije sadržavati samo slova, brojeve i crtice.', + 'alpha_num' => 'Polje :attribute smije sadržavati samo slova i brojeve.', + 'array' => 'Polje :attribute mora biti niz.', + 'before' => 'Polje :attribute mora biti datum prije :date.', + 'between' => [ + 'numeric' => 'Polje :attribute mora biti između :min - :max.', + 'file' => 'Polje :attribute mora biti između :min - :max kilobajta.', + 'string' => 'Polje :attribute mora biti između :min - :max znakova.', + 'array' => 'Polje :attribute mora imati između :min - :max stavki.', + ], + 'boolean' => 'Polje :attribute mora biti false ili true.', + 'confirmed' => 'Potvrda polja :attribute se ne podudara.', + 'date' => 'Polje :attribute nije ispravan datum.', + 'date_format' => 'Polje :attribute ne podudara s formatom :format.', + 'different' => 'Polja :attribute i :other moraju biti različita.', + 'digits' => 'Polje :attribute mora sadržavati :digits znamenki.', + 'digits_between' => 'Polje :attribute mora imati između :min i :max znamenki.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'Polje :attribute mora biti ispravna e-mail adresa.', + 'exists' => 'Odabrano polje :attribute nije ispravno.', + 'filled' => 'The :attribute field is required.', + 'image' => 'Polje :attribute mora biti slika.', + 'in' => 'Odabrano polje :attribute nije ispravno.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'Polje :attribute mora biti broj.', + 'ip' => 'Polje :attribute mora biti ispravna IP adresa.', + 'json' => 'The :attribute must be a valid JSON string.', + 'max' => [ + 'numeric' => 'Polje :attribute mora biti manje od :max.', + 'file' => 'Polje :attribute mora biti manje od :max kilobajta.', + 'string' => 'Polje :attribute mora sadržavati manje od :max znakova.', + 'array' => 'Polje :attribute ne smije imati više od :max stavki.', + ], + 'mimes' => 'Polje :attribute mora biti datoteka tipa: :values.', + 'min' => [ + 'numeric' => 'Polje :attribute mora biti najmanje :min.', + 'file' => 'Polje :attribute mora biti najmanje :min kilobajta.', + 'string' => 'Polje :attribute mora sadržavati najmanje :min znakova.', + 'array' => 'Polje :attribute mora sadržavati najmanje :min stavki.', + ], + 'not_in' => 'Odabrano polje :attribute nije ispravno.', + 'numeric' => 'Polje :attribute mora biti broj.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'Polje :attribute se ne podudara s formatom.', + 'required' => 'Polje :attribute je obavezno.', + 'required_if' => 'Polje :attribute je obavezno kada polje :other sadrži :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'Polje :attribute je obavezno kada postoji polje :values.', + 'required_with_all' => 'Polje :attribute je obavezno kada postje polja :values.', + 'required_without' => 'Polje :attribute je obavezno kada ne postoji polje :values.', + 'required_without_all' => 'Polje :attribute je obavezno kada nijedno od polja :values ne postoji.', + 'same' => 'Polja :attribute i :other se moraju podudarati.', + 'size' => [ + 'numeric' => 'Polje :attribute mora biti :size.', + 'file' => 'Polje :attribute mora biti :size kilobajta.', + 'string' => 'Polje :attribute mora biti :size znakova.', + 'array' => 'Polje :attribute mora sadržavati :size stavki.', + ], + 'string' => 'The :attribute must be a string.', + 'timezone' => 'Polje :attribute mora biti ispravna vremenska zona.', + 'unique' => 'Polje :attribute već postoji.', + 'url' => 'Polje :attribute nije ispravnog formata.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap attribute place-holders + | with something more reader friendly such as E-Mail Address instead + | of "email". This simply helps us make messages a little cleaner. + | + */ + + 'attributes' => [ + // + ], + +]; From e905b295105c1a326ec375fff54591ee7538411f Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 17 May 2016 14:09:39 -0400 Subject: [PATCH 101/386] 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 5cb8c317fd6422964d7917f4ec528f9350558032 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 17 May 2016 22:05:21 +0300 Subject: [PATCH 102/386] Added initial CONTRIBUTING.md --- readme.md => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename readme.md => README.md (100%) diff --git a/readme.md b/README.md similarity index 100% rename from readme.md rename to README.md From 3835c62b0d50e952e6248fec1be2b26f7758fd35 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 17 May 2016 15:08:08 -0400 Subject: [PATCH 103/386] Fix composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5921bef056c2..60ed4b259e73 100644 --- a/composer.json +++ b/composer.json @@ -75,7 +75,7 @@ "omnipay/braintree": "~2.0@dev", "gatepay/FedACHdir": "dev-master@dev", "websight/l5-google-cloud-storage": "^1.0", - "fzaninotto/faker": "^1.5" + "fzaninotto/faker": "^1.5", "wepay/php-sdk": "^0.2", "collizo4sky/omnipay-wepay": "dev-additional-calls" }, From 35f6ada76219a2a8ba363d7c1ba68434635378c6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 17 May 2016 22:08:10 +0300 Subject: [PATCH 104/386] Added initial CONTRIBUTING.md --- CONTRIBUTING.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000000..1e26492d5d83 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,9 @@ +# Contributing to Invoice Ninja + +We welcome contributions! We'll improve this guide over time... + +*Please note: although our application is open-source we run a for-profit hosted service at [invoiceninja.com](https://www.invoiceninja.com).* + +Guidelines +- Please try to follow [PSR-2 guidlines](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) +- Add translations in our [Transifex](https://www.transifex.com/invoice-ninja/) project From 7cebb4b7fbbe571b393d31f7aab0dd575bc9e18d Mon Sep 17 00:00:00 2001 From: Bartlomiej Szala Date: Tue, 17 May 2016 21:33:13 +0200 Subject: [PATCH 105/386] Swap symbol for Danish currency --- database/seeds/CurrenciesSeeder.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/database/seeds/CurrenciesSeeder.php b/database/seeds/CurrenciesSeeder.php index f88e1b12cc66..e5ea6b654b47 100644 --- a/database/seeds/CurrenciesSeeder.php +++ b/database/seeds/CurrenciesSeeder.php @@ -14,7 +14,7 @@ class CurrenciesSeeder extends Seeder ['name' => 'Pound Sterling', 'code' => 'GBP', 'symbol' => '£', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['name' => 'Euro', 'code' => 'EUR', 'symbol' => '€', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','], ['name' => 'South African Rand', 'code' => 'ZAR', 'symbol' => 'R', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','], - ['name' => 'Danish Krone', 'code' => 'DKK', 'symbol' => 'kr ', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','], + ['name' => 'Danish Krone', 'code' => 'DKK', 'symbol' => 'kr ', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ',', 'swap_currency_symbol' => true], ['name' => 'Israeli Shekel', 'code' => 'ILS', 'symbol' => 'NIS ', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], ['name' => 'Swedish Krona', 'code' => 'SEK', 'symbol' => 'kr ', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','], ['name' => 'Kenyan Shilling', 'code' => 'KES', 'symbol' => 'KSh ', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'], @@ -68,6 +68,9 @@ class CurrenciesSeeder extends Seeder $record->symbol = $currency['symbol']; $record->thousand_separator = $currency['thousand_separator']; $record->decimal_separator = $currency['decimal_separator']; + if(isset($currency['swap_currency_symbol'])){ + $record->swap_currency_symbol = $currency['swap_currency_symbol']; + } $record->save(); } else { Currency::create($currency); From fa9370d4fbc875c71e8cfc421b828f512fd236fd Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 17 May 2016 22:45:10 +0300 Subject: [PATCH 106/386] Fix for symbol placement --- CONTRIBUTING.md | 3 ++- app/Models/Currency.php | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e26492d5d83..ddac065b56b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,5 +5,6 @@ We welcome contributions! We'll improve this guide over time... *Please note: although our application is open-source we run a for-profit hosted service at [invoiceninja.com](https://www.invoiceninja.com).* Guidelines -- Please try to follow [PSR-2 guidlines](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) +- Try to follow [PSR-2 guidlines](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) +- Create pull requests against the develop branch - Add translations in our [Transifex](https://www.transifex.com/invoice-ninja/) project diff --git a/app/Models/Currency.php b/app/Models/Currency.php index 944f9f2d8eaf..eb058aa67497 100644 --- a/app/Models/Currency.php +++ b/app/Models/Currency.php @@ -6,8 +6,12 @@ class Currency extends Eloquent { public $timestamps = false; - public function getName() + protected $casts = [ + 'swap_currency_symbol' => 'boolean', + ]; + + public function getName() { return $this->name; - } + } } From 8e3fa428c09cd68583c7caa0ffc6317c07f5ba2a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 17 May 2016 23:44:50 +0300 Subject: [PATCH 107/386] Updated readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6fb935ea4ada..a784f230aaa7 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ [![Build Status](https://travis-ci.org/invoiceninja/invoiceninja.svg?branch=develop)](https://travis-ci.org/invoiceninja/invoiceninja) [![Join the chat at https://gitter.im/hillelcoren/invoice-ninja](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hillelcoren/invoice-ninja?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +**To update the translations please use [Transifex](https://www.transifex.com/invoice-ninja/invoice-ninja/dashboard/)** + ### Affiliates Programs * Referral program (we pay you): $100 per signup paid over 3 years - [Learn more](https://www.invoiceninja.com/referral-program/) * White-label reseller (you pay us): 10% of revenue with a $100 sign up fee From 8be1771f47b7cff952835031a7cc9f56c612100e Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 18 May 2016 10:12:33 +0300 Subject: [PATCH 108/386] Bumped version number --- app/Http/routes.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index 7a2fae197e59..34dd41872cf7 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -123,7 +123,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('hide_message', 'HomeController@hideMessage'); Route::get('force_inline_pdf', 'UserController@forcePDFJS'); Route::get('account/getSearchData', array('as' => 'getSearchData', 'uses' => 'AccountController@getSearchData')); - + Route::get('settings/user_details', 'AccountController@showUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails'); Route::post('users/change_password', 'UserController@changePassword'); @@ -156,7 +156,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS'); Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview'); Route::post('document', 'DocumentController@postUpload'); - + Route::get('quotes/create/{client_id?}', 'QuoteController@create'); Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice'); Route::get('quotes/{invoices}/edit', 'InvoiceController@edit'); @@ -489,7 +489,7 @@ 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_VOIDED', 2); define('PAYMENT_STATUS_FAILED', 3); @@ -582,7 +582,7 @@ if (!defined('CONTACT_EMAIL')) { define('NINJA_WEB_URL', env('NINJA_WEB_URL', 'https://www.invoiceninja.com')); define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com')); define('NINJA_DATE', '2000-01-01'); - define('NINJA_VERSION', '2.5.2' . env('NINJA_VERSION_SUFFIX')); + define('NINJA_VERSION', '2.5.2.1' . env('NINJA_VERSION_SUFFIX')); define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja')); define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja')); @@ -711,7 +711,7 @@ if (!defined('CONTACT_EMAIL')) { define('AUTO_BILL_OPT_IN', 2); define('AUTO_BILL_OPT_OUT', 3); define('AUTO_BILL_ALWAYS', 4); - + // These must be lowercase define('PLAN_FREE', 'free'); define('PLAN_PRO', 'pro'); @@ -719,7 +719,7 @@ if (!defined('CONTACT_EMAIL')) { define('PLAN_WHITE_LABEL', 'white_label'); define('PLAN_TERM_MONTHLY', 'month'); define('PLAN_TERM_YEARLY', 'year'); - + // Pro define('FEATURE_CUSTOMIZE_INVOICE_DESIGN', 'customize_invoice_design'); define('FEATURE_REMOVE_CREATED_BY', 'remove_created_by'); @@ -764,7 +764,7 @@ if (!defined('CONTACT_EMAIL')) { define('WEPAY_FEE_PAYER', env('WEPAY_FEE_PAYER', 'payee')); define('WEPAY_APP_FEE_MULTIPLIER', env('WEPAY_APP_FEE_MULTIPLIER', 0.002)); define('WEPAY_APP_FEE_FIXED', env('WEPAY_APP_FEE_MULTIPLIER', 0.00)); - + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], @@ -820,4 +820,4 @@ if (Utils::isNinjaDev()) //ini_set('memory_limit','1024M'); //Auth::loginUsingId(1); } -*/ \ No newline at end of file +*/ From e45ef02b7563009492f44f8dfd1e258ac757556c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 18 May 2016 10:20:51 +0300 Subject: [PATCH 109/386] readme changes --- CONTRIBUTING.md | 6 +++--- README.md | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ddac065b56b3..60ac1176aa10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,6 +5,6 @@ We welcome contributions! We'll improve this guide over time... *Please note: although our application is open-source we run a for-profit hosted service at [invoiceninja.com](https://www.invoiceninja.com).* Guidelines -- Try to follow [PSR-2 guidlines](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) -- Create pull requests against the develop branch -- Add translations in our [Transifex](https://www.transifex.com/invoice-ninja/) project +* Try to follow [PSR-2 guidlines](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) +* Create pull requests against the develop branch +* Submit translations through [Transifex](https://www.transifex.com/invoice-ninja/) diff --git a/README.md b/README.md index a784f230aaa7..9e85db49a05c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,8 @@ * [Feature Roadmap](https://trello.com/b/63BbiVVe/) ### Pull Requests -We're using the [Git-Flow](http://nvie.com/posts/a-successful-git-branching-model/) model of branching and releasing, **please create pull requests against the develop branch**. +* Please create pull requests against the develop branch +* Submit translations through [Transifex](https://www.transifex.com/invoice-ninja/) ### Contributors * [Troels Liebe Bentsen](https://github.com/tlbdk) From 43defa270ae4bed136f86b08bbf0b53cad1eb93d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 18 May 2016 10:21:14 +0300 Subject: [PATCH 110/386] readme changes --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 9e85db49a05c..03362f3a6e6f 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ [![Build Status](https://travis-ci.org/invoiceninja/invoiceninja.svg?branch=develop)](https://travis-ci.org/invoiceninja/invoiceninja) [![Join the chat at https://gitter.im/hillelcoren/invoice-ninja](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hillelcoren/invoice-ninja?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -**To update the translations please use [Transifex](https://www.transifex.com/invoice-ninja/invoice-ninja/dashboard/)** - ### Affiliates Programs * Referral program (we pay you): $100 per signup paid over 3 years - [Learn more](https://www.invoiceninja.com/referral-program/) * White-label reseller (you pay us): 10% of revenue with a $100 sign up fee From 510f5cffac177021b4e6565fc5349696c1c287f5 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 18 May 2016 11:05:00 +0300 Subject: [PATCH 111/386] Auto populate Stripe keys for testing --- resources/views/accounts/account_gateway.blade.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index 155ef6579f69..cfad9f21a6b6 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -44,6 +44,11 @@ {!! Former::populateField('gateway_id', GATEWAY_STRIPE) !!} {!! Former::populateField('show_address', 1) !!} {!! Former::populateField('update_address', 1) !!} + + @if (Utils::isNinjaDev()) + {!! Former::populateField('23_apiKey', env('STRIPE_TEST_SECRET_KEY')) !!} + {!! Former::populateField('publishable_key', env('STRIPE_TEST_PUBLISHABLE_KEY')) !!} + @endif @endif {!! Former::select('payment_type_id') @@ -268,4 +273,4 @@ -@stop \ No newline at end of file +@stop From f0d276be4c7448d75ae04f34181e27057b0c62bc Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Wed, 18 May 2016 09:51:20 -0400 Subject: [PATCH 112/386] Listen for WePay IPNs --- .../Controllers/AccountGatewayController.php | 3 +- app/Http/Controllers/PaymentController.php | 49 +++++++++++++++++++ app/Models/AccountGateway.php | 6 +++ app/Services/AccountGatewayService.php | 9 ++-- app/Services/PaymentService.php | 3 +- 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 9b02bfe56a24..765fbc78a9d6 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -403,7 +403,6 @@ class AccountGatewayController extends BaseController 'original_device' => \Request::server('HTTP_USER_AGENT'), 'tos_acceptance_time' => time(), 'redirect_uri' => URL::to('gateways'), - 'callback_uri' => URL::to(env('WEBHOOK_PREFIX','').'paymenthook/'.$account->account_key.'/'.GATEWAY_WEPAY), 'scope' => 'manage_accounts,collect_payments,view_user,preapprove_payments,send_money', ); @@ -418,6 +417,7 @@ class AccountGatewayController extends BaseController 'name' => Input::get('company_name'), 'description' => Input::get('description'), 'theme_object' => json_decode(WEPAY_THEME), + 'callback_uri' => $accountGateway->getWebhookUrl(), ); if (WEPAY_ENABLE_CANADA) { @@ -453,6 +453,7 @@ class AccountGatewayController extends BaseController 'tokenType' => $wepayUser->token_type, 'tokenExpires' => $accessTokenExpires, 'accountId' => $wepayAccount->account_id, + 'state' => $wepayAccount->state, 'testMode' => WEPAY_ENVIRONMENT == WEPAY_STAGE, 'country' => WEPAY_ENABLE_CANADA ? Input::get('country') : 'US', )); diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 3e89f3091910..6a5742fabe2b 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -839,6 +839,53 @@ class PaymentController extends BaseController $this->paymentService->convertPaymentMethodFromWePay($source, null, $paymentMethod)->save(); } + return array('message' => 'Processed successfully'); + } elseif ($objectType == 'account') { + $config = $accountGateway->getConfig(); + if ($config->accountId != $objectId) { + return array('message' => 'Unknown account'); + } + + $wepay = \Utils::setupWePay($accountGateway); + $wepayAccount = $wepay->request('account', array( + 'account_id' => intval($objectId), + )); + + if ($wepayAccount->state == 'deleted') { + $accountGateway->delete(); + } else { + $config->state = $wepayAccount->state; + $accountGateway->setConfig($config); + $accountGateway->save(); + } + + return array('message' => 'Processed successfully'); + } elseif ($objectType == 'checkout') { + $payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $objectId)->first(); + + if (!$payment) { + return array('message' => 'Unknown payment'); + } + + $wepay = \Utils::setupWePay($accountGateway); + $checkout = $wepay->request('checkout', array( + 'checkout_id' => intval($objectId), + )); + + if ($checkout->state == 'refunded') { + $payment->recordRefund(); + } elseif (!empty($checkout->refund) && !empty($checkout->refund->amount_refunded) && ($checkout->refund->amount_refunded - $payment->refunded) > 0) { + $payment->recordRefund($checkout->refund->amount_refunded - $payment->refunded); + } + + if ($checkout->state == 'captured') { + $payment->markComplete(); + } elseif ($checkout->state == 'cancelled') { + $payment->markCancelled(); + } elseif ($checkout->state == 'failed') { + $payment->markFailed(); + } + return array('message' => 'Processed successfully'); } else { return array('message' => 'Ignoring event'); @@ -904,6 +951,8 @@ class PaymentController extends BaseController } } elseif ($eventType == 'charge.succeeded') { $payment->markComplete(); + } elseif ($eventType == 'charge.refunded') { + $payment->recordRefund($charge['amount_refunded'] / 100 - $payment->refunded); } } elseif($eventType == 'customer.source.updated' || $eventType == 'customer.source.deleted') { $source = $eventDetails['data']['object']; diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php index 0c281856be66..563fe16720fb 100644 --- a/app/Models/AccountGateway.php +++ b/app/Models/AccountGateway.php @@ -124,5 +124,11 @@ class AccountGateway extends EntityModel return substr(trim($stripe_key), 0, 8) == 'pk_test_' ? 'tartan' : 'production'; } + + public function getWebhookUrl() + { + $account = $this->account ? $this->account : Account::find($this->account_id); + return \URL::to(env('WEBHOOK_PREFIX','').'paymenthook/'.$account->account_key.'/'.$this->gateway_id.env('WEBHOOK_SUFFIX','')); + } } diff --git a/app/Services/AccountGatewayService.php b/app/Services/AccountGatewayService.php index a992da5c82c9..88ee177321ec 100644 --- a/app/Services/AccountGatewayService.php +++ b/app/Services/AccountGatewayService.php @@ -48,16 +48,17 @@ class AccountGatewayService extends BaseService return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); } else { $accountGateway = AccountGateway::find($model->id); + $config = $accountGateway->getConfig(); $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; - $wepayAccountId = $accountGateway->getConfig()->accountId; + $wepayAccountId = $config->accountId; + $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 { - $wepayAccount = $wepay->request('/account', array('account_id' => $wepayAccountId)); - if ($wepayAccount->state == 'action_required') { + if ($wepayState == 'action_required') { $updateUri = $wepay->request('/account/get_update_uri', array( 'account_id' => $wepayAccountId, 'redirect_uri' => URL::to('gateways'), @@ -67,7 +68,7 @@ class AccountGatewayService extends BaseService $url = $updateUri->uri; $html = "{$linkText}"; $model->setupUrl = $url; - } elseif ($wepayAccount->state == 'pending') { + } elseif ($wepayState == 'pending') { $linkText .= ' ('.trans('texts.resend_confirmation_email').')'; $model->resendConfirmationUrl = $url = URL::to("gateways/{$accountGateway->public_id}/resend_confirmation"); $html = link_to($url, $linkText)->toHtml(); diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index ae014577a603..51cbccc57177 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -413,7 +413,7 @@ class PaymentService extends BaseService 'client_secret' => WEPAY_CLIENT_SECRET, 'credit_card_id' => intval($details['token']), 'auto_update' => WEPAY_AUTO_UPDATE, - 'callback_uri' => URL::to(env('WEBHOOK_PREFIX','').'paymenthook/'.$client->account->account_key.'/'.GATEWAY_WEPAY), + 'callback_uri' => $accountGateway->getWebhookUrl(), )); $tokenResponse = $wepay->request('credit_card', array( 'client_id' => WEPAY_CLIENT_ID, @@ -1199,6 +1199,7 @@ class PaymentService extends BaseService if ($accountGateway->gateway_id == GATEWAY_WEPAY) { $details['applicationFee'] = $this->calculateApplicationFee($accountGateway, $details['amount']); $details['feePayer'] = WEPAY_FEE_PAYER; + $details['callbackUri'] = $accountGateway->getWebhookUrl(); } $response = $gateway->purchase($details)->send(); From e05bbd18ef50e0a3a398b26df9077e33eb08c963 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Wed, 18 May 2016 10:00:33 -0400 Subject: [PATCH 113/386] Fix JS error --- .../views/accounts/partials/account_gateway_wepay.blade.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/views/accounts/partials/account_gateway_wepay.blade.php b/resources/views/accounts/partials/account_gateway_wepay.blade.php index a5ccd7f01abc..bdbe974aa5e9 100644 --- a/resources/views/accounts/partials/account_gateway_wepay.blade.php +++ b/resources/views/accounts/partials/account_gateway_wepay.blade.php @@ -77,8 +77,10 @@ $(function(){ $('#wepay-country input').change(handleCountryChange) function handleCountryChange(){ var country = $('#wepay-country input:checked').val(); - $('#wepay-accept-debit').toggle(country == 'CA'); - $('#wepay-tos-link').attr('href', 'https://go.wepay.com/terms-of-service-' + country.toLowerCase()); + if(country) { + $('#wepay-accept-debit').toggle(country == 'CA'); + $('#wepay-tos-link').attr('href', 'https://go.wepay.com/terms-of-service-' + country.toLowerCase()); + } } handleCountryChange(); }) From 9bbd4c99b26ed7a44f43eca6314c0743e1caaf0f Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Wed, 18 May 2016 15:27:51 -0400 Subject: [PATCH 114/386] Update dependencies --- composer.lock | 696 ++++++++++++++++++++++++++------------------------ 1 file changed, 356 insertions(+), 340 deletions(-) diff --git a/composer.lock b/composer.lock index c218f45b890b..d5513297fd3f 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": "392a09a61498eddc166665b7cdda1dde", - "content-hash": "1f86d83e1ef0bce86debfb45120e6db8", + "hash": "40e0d67083e4b9f756e5dbf423a8b17b", + "content-hash": "30b45b93abb93c4ee38f7ab987c4b6b1", "packages": [ { "name": "agmscode/omnipay-agms", @@ -123,12 +123,12 @@ "source": { "type": "git", "url": "https://github.com/formers/former.git", - "reference": "d97f907741323b390f43954a90a227921ecc6b96" + "reference": "37f6876a5d211427b5c445cd64f0eb637f42f685" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/formers/former/zipball/37f6876a5d211427b5c445cd64f0eb637f42f685", - "reference": "d97f907741323b390f43954a90a227921ecc6b96", + "reference": "37f6876a5d211427b5c445cd64f0eb637f42f685", "shasum": "" }, "require": { @@ -174,7 +174,7 @@ "foundation", "laravel" ], - "time": "2016-03-16 01:43:45" + "time": "2016-04-18 15:33:06" }, { "name": "anahkiasen/html-object", @@ -323,16 +323,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.17.1", + "version": "3.18.9", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "f8c0cc9357e10896a5c57104f2c79d1b727d97d0" + "reference": "965caffc3f6712cdc5ba88dadaad055b125dd95b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f8c0cc9357e10896a5c57104f2c79d1b727d97d0", - "reference": "f8c0cc9357e10896a5c57104f2c79d1b727d97d0", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/965caffc3f6712cdc5ba88dadaad055b125dd95b", + "reference": "965caffc3f6712cdc5ba88dadaad055b125dd95b", "shasum": "" }, "require": { @@ -399,7 +399,7 @@ "s3", "sdk" ], - "time": "2016-03-22 19:19:22" + "time": "2016-05-17 22:39:16" }, { "name": "barracudanetworks/archivestream-php", @@ -443,16 +443,16 @@ }, { "name": "barryvdh/laravel-debugbar", - "version": "v2.2.0", + "version": "v2.2.2", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "13b7058d2120c8d5af7f1ada21b7c44dd87b666a" + "reference": "c291e58d0a13953e0f68d99182ee77ebc693edc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/13b7058d2120c8d5af7f1ada21b7c44dd87b666a", - "reference": "13b7058d2120c8d5af7f1ada21b7c44dd87b666a", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/c291e58d0a13953e0f68d99182ee77ebc693edc0", + "reference": "c291e58d0a13953e0f68d99182ee77ebc693edc0", "shasum": "" }, "require": { @@ -493,7 +493,7 @@ "profiler", "webprofiler" ], - "time": "2016-02-17 08:32:21" + "time": "2016-05-11 13:54:43" }, { "name": "barryvdh/laravel-ide-helper", @@ -501,18 +501,18 @@ "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "e97ed532f09e290b91ff7713b785ed7ab11d0812" + "reference": "35ebf3a2ba9443e11fbdb9066cc363ec7b2245e4" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/35ebf3a2ba9443e11fbdb9066cc363ec7b2245e4", - "reference": "e97ed532f09e290b91ff7713b785ed7ab11d0812", + "reference": "35ebf3a2ba9443e11fbdb9066cc363ec7b2245e4", "shasum": "" }, "require": { - "illuminate/console": "5.0.x|5.1.x|5.2.x", - "illuminate/filesystem": "5.0.x|5.1.x|5.2.x", - "illuminate/support": "5.0.x|5.1.x|5.2.x", + "illuminate/console": "5.0.x|5.1.x|5.2.x|5.3.x", + "illuminate/filesystem": "5.0.x|5.1.x|5.2.x|5.3.x", + "illuminate/support": "5.0.x|5.1.x|5.2.x|5.3.x", "php": ">=5.4.0", "phpdocumentor/reflection-docblock": "^2.0.4", "symfony/class-loader": "~2.3|~3.0" @@ -556,20 +556,20 @@ "phpstorm", "sublime" ], - "time": "2016-03-03 14:38:04" + "time": "2016-05-12 05:32:54" }, { "name": "braintree/braintree_php", - "version": "3.11.0", + "version": "3.12.0", "source": { "type": "git", "url": "https://github.com/braintree/braintree_php.git", - "reference": "2ab7e41d8d31286fa64bae7279a8e785a40b1be4" + "reference": "5e16bbf8924a6da51356393fbf9381a333a401eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/braintree/braintree_php/zipball/2ab7e41d8d31286fa64bae7279a8e785a40b1be4", - "reference": "2ab7e41d8d31286fa64bae7279a8e785a40b1be4", + "url": "https://api.github.com/repos/braintree/braintree_php/zipball/5e16bbf8924a6da51356393fbf9381a333a401eb", + "reference": "5e16bbf8924a6da51356393fbf9381a333a401eb", "shasum": "" }, "require": { @@ -603,7 +603,7 @@ } ], "description": "Braintree PHP Client Library", - "time": "2016-04-12 20:39:36" + "time": "2016-05-13 18:44:46" }, { "name": "cardgate/omnipay-cardgate", @@ -973,16 +973,16 @@ }, { "name": "collizo4sky/omnipay-wepay", - "version": "1.2.1", + "version": "dev-additional-calls", "source": { "type": "git", - "url": "https://github.com/collizo4sky/omnipay-wepay.git", - "reference": "6a74cc5ea56cb63c74483b1799628ca34c90a6de" + "url": "https://github.com/sometechie/omnipay-wepay.git", + "reference": "2964730018e9fccf0bb0e449065940cad3ca6719" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/collizo4sky/omnipay-wepay/zipball/6a74cc5ea56cb63c74483b1799628ca34c90a6de", - "reference": "6a74cc5ea56cb63c74483b1799628ca34c90a6de", + "url": "https://api.github.com/repos/sometechie/omnipay-wepay/zipball/2964730018e9fccf0bb0e449065940cad3ca6719", + "reference": "2964730018e9fccf0bb0e449065940cad3ca6719", "shasum": "" }, "require": { @@ -990,7 +990,7 @@ }, "require-dev": { "omnipay/tests": "~2.0", - "satooshi/php-coveralls": "dev-master" + "satooshi/php-coveralls": "1.*" }, "type": "library", "autoload": { @@ -998,7 +998,6 @@ "Omnipay\\WePay\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -1012,7 +1011,10 @@ "payment", "wepay" ], - "time": "2015-12-15 20:31:39" + "support": { + "source": "https://github.com/sometechie/omnipay-wepay/tree/additional-calls" + }, + "time": "2016-05-18 18:12:17" }, { "name": "container-interop/container-interop", @@ -2068,7 +2070,7 @@ "reference": "origin/master" }, "type": "library", - "time": "2016-05-09 12:00:35" + "time": "2016-05-18 12:00:45" }, { "name": "google/apiclient", @@ -2115,22 +2117,22 @@ }, { "name": "guzzle/guzzle", - "version": "v3.8.1", + "version": "v3.9.3", "source": { "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba" + "url": "https://github.com/guzzle/guzzle3.git", + "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba", - "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", + "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", "shasum": "" }, "require": { "ext-curl": "*", "php": ">=5.3.3", - "symfony/event-dispatcher": ">=2.1" + "symfony/event-dispatcher": "~2.1" }, "replace": { "guzzle/batch": "self.version", @@ -2157,18 +2159,21 @@ "guzzle/stream": "self.version" }, "require-dev": { - "doctrine/cache": "*", - "monolog/monolog": "1.*", + "doctrine/cache": "~1.3", + "monolog/monolog": "~1.0", "phpunit/phpunit": "3.7.*", - "psr/log": "1.0.*", - "symfony/class-loader": "*", - "zendframework/zend-cache": "<2.3", - "zendframework/zend-log": "<2.3" + "psr/log": "~1.0", + "symfony/class-loader": "~2.1", + "zendframework/zend-cache": "2.*,<2.3", + "zendframework/zend-log": "2.*,<2.3" + }, + "suggest": { + "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.8-dev" + "dev-master": "3.9-dev" } }, "autoload": { @@ -2192,7 +2197,7 @@ "homepage": "https://github.com/guzzle/guzzle/contributors" } ], - "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", "homepage": "http://guzzlephp.org/", "keywords": [ "client", @@ -2203,7 +2208,7 @@ "rest", "web service" ], - "time": "2014-01-28 22:29:15" + "time": "2015-03-18 18:23:50" }, { "name": "guzzlehttp/guzzle", @@ -2269,16 +2274,16 @@ }, { "name": "guzzlehttp/promises", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "bb9024c526b22f3fe6ae55a561fd70653d470aa8" + "reference": "c10d860e2a9595f8883527fa0021c7da9e65f579" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/bb9024c526b22f3fe6ae55a561fd70653d470aa8", - "reference": "bb9024c526b22f3fe6ae55a561fd70653d470aa8", + "url": "https://api.github.com/repos/guzzle/promises/zipball/c10d860e2a9595f8883527fa0021c7da9e65f579", + "reference": "c10d860e2a9595f8883527fa0021c7da9e65f579", "shasum": "" }, "require": { @@ -2316,20 +2321,20 @@ "keywords": [ "promise" ], - "time": "2016-03-08 01:15:46" + "time": "2016-05-18 16:56:05" }, { "name": "guzzlehttp/psr7", - "version": "1.2.3", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "2e89629ff057ebb49492ba08e6995d3a6a80021b" + "reference": "31382fef2889136415751badebbd1cb022a4ed72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/2e89629ff057ebb49492ba08e6995d3a6a80021b", - "reference": "2e89629ff057ebb49492ba08e6995d3a6a80021b", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/31382fef2889136415751badebbd1cb022a4ed72", + "reference": "31382fef2889136415751badebbd1cb022a4ed72", "shasum": "" }, "require": { @@ -2374,7 +2379,7 @@ "stream", "uri" ], - "time": "2016-02-18 21:54:00" + "time": "2016-04-13 19:56:01" }, { "name": "illuminate/html", @@ -2483,12 +2488,12 @@ "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "e368d262887dbb2fdfaf710880571ede51e9c0e6" + "reference": "22088b04728a039bd1fc32f7e79a89a118b78698" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/Intervention/image/zipball/22088b04728a039bd1fc32f7e79a89a118b78698", - "reference": "e368d262887dbb2fdfaf710880571ede51e9c0e6", + "reference": "22088b04728a039bd1fc32f7e79a89a118b78698", "shasum": "" }, "require": { @@ -2537,7 +2542,7 @@ "thumbnail", "watermark" ], - "time": "2016-02-26 18:18:19" + "time": "2016-04-26 14:08:40" }, { "name": "ircmaxell/password-compat", @@ -2872,12 +2877,12 @@ "source": { "type": "git", "url": "https://github.com/labs7in0/omnipay-wechat.git", - "reference": "40c9f86df6573ad98ae1dd0d29712ccbc789a74e" + "reference": "c8d80c3b48bae2bab071f283f75b1cd8624ed3c7" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/labs7in0/omnipay-wechat/zipball/c8d80c3b48bae2bab071f283f75b1cd8624ed3c7", - "reference": "40c9f86df6573ad98ae1dd0d29712ccbc789a74e", + "reference": "c8d80c3b48bae2bab071f283f75b1cd8624ed3c7", "shasum": "" }, "require": { @@ -2913,7 +2918,7 @@ "purchase", "wechat" ], - "time": "2016-03-18 09:59:11" + "time": "2016-05-10 08:43:41" }, { "name": "laracasts/presenter", @@ -2963,16 +2968,16 @@ }, { "name": "laravel/framework", - "version": "v5.2.24", + "version": "v5.2.32", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "396297a5fd3c70c2fc1af68f09ee574a2380175c" + "reference": "f688217113f70b01d0e127da9035195415812bef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/396297a5fd3c70c2fc1af68f09ee574a2380175c", - "reference": "396297a5fd3c70c2fc1af68f09ee574a2380175c", + "url": "https://api.github.com/repos/laravel/framework/zipball/f688217113f70b01d0e127da9035195415812bef", + "reference": "f688217113f70b01d0e127da9035195415812bef", "shasum": "" }, "require": { @@ -3033,7 +3038,7 @@ }, "require-dev": { "aws/aws-sdk-php": "~3.0", - "mockery/mockery": "~0.9.2", + "mockery/mockery": "~0.9.4", "pda/pheanstalk": "~3.0", "phpunit/phpunit": "~4.1", "predis/predis": "~1.0", @@ -3051,7 +3056,8 @@ "predis/predis": "Required to use the redis cache and queue drivers (~1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (~2.0).", "symfony/css-selector": "Required to use some of the crawler integration testing tools (2.8.*|3.0.*).", - "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (2.8.*|3.0.*)." + "symfony/dom-crawler": "Required to use most of the crawler integration testing tools (2.8.*|3.0.*).", + "symfony/psr-http-message-bridge": "Required to psr7 bridging features (0.2.*)." }, "type": "library", "extra": { @@ -3087,20 +3093,20 @@ "framework", "laravel" ], - "time": "2016-03-22 13:45:19" + "time": "2016-05-17 13:24:40" }, { "name": "laravel/socialite", - "version": "v2.0.14", + "version": "v2.0.15", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "b15f4be0ac739405120d74b837af423aa71502d9" + "reference": "edd00ab96933e3ef053533cce81e958fb26921af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/b15f4be0ac739405120d74b837af423aa71502d9", - "reference": "b15f4be0ac739405120d74b837af423aa71502d9", + "url": "https://api.github.com/repos/laravel/socialite/zipball/edd00ab96933e3ef053533cce81e958fb26921af", + "reference": "edd00ab96933e3ef053533cce81e958fb26921af", "shasum": "" }, "require": { @@ -3141,7 +3147,7 @@ "laravel", "oauth" ], - "time": "2015-10-16 15:39:46" + "time": "2016-03-21 14:30:30" }, { "name": "laravelcollective/bus", @@ -3244,16 +3250,16 @@ }, { "name": "league/flysystem", - "version": "1.0.20", + "version": "1.0.22", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "e87a786e3ae12a25cf78a71bb07b4b384bfaa83a" + "reference": "bd73a91703969a2d20ab4bfbf971d6c2cbe36612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/e87a786e3ae12a25cf78a71bb07b4b384bfaa83a", - "reference": "e87a786e3ae12a25cf78a71bb07b4b384bfaa83a", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/bd73a91703969a2d20ab4bfbf971d6c2cbe36612", + "reference": "bd73a91703969a2d20ab4bfbf971d6c2cbe36612", "shasum": "" }, "require": { @@ -3323,20 +3329,20 @@ "sftp", "storage" ], - "time": "2016-03-14 21:54:11" + "time": "2016-04-28 06:53:12" }, { "name": "league/flysystem-aws-s3-v3", - "version": "1.0.9", + "version": "1.0.11", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6" + "reference": "1f7ae4e3cc178686c49a9d23cab43ed1e955368c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/595e24678bf78f8107ebc9355d8376ae0eb712c6", - "reference": "595e24678bf78f8107ebc9355d8376ae0eb712c6", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/1f7ae4e3cc178686c49a9d23cab43ed1e955368c", + "reference": "1f7ae4e3cc178686c49a9d23cab43ed1e955368c", "shasum": "" }, "require": { @@ -3370,7 +3376,7 @@ } ], "description": "Flysystem adapter for the AWS S3 SDK v3.x", - "time": "2015-11-19 08:44:16" + "time": "2016-05-03 19:35:35" }, { "name": "league/flysystem-rackspace", @@ -3977,16 +3983,16 @@ }, { "name": "monolog/monolog", - "version": "1.18.1", + "version": "1.19.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "a5f2734e8c16f3aa21b3da09715d10e15b4d2d45" + "reference": "5f56ed5212dc509c8dc8caeba2715732abb32dbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/a5f2734e8c16f3aa21b3da09715d10e15b4d2d45", - "reference": "a5f2734e8c16f3aa21b3da09715d10e15b4d2d45", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/5f56ed5212dc509c8dc8caeba2715732abb32dbf", + "reference": "5f56ed5212dc509c8dc8caeba2715732abb32dbf", "shasum": "" }, "require": { @@ -4001,13 +4007,13 @@ "doctrine/couchdb": "~1.0@dev", "graylog2/gelf-php": "~1.0", "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", "php-console/php-console": "^3.1.3", "phpunit/phpunit": "~4.5", "phpunit/phpunit-mock-objects": "2.3.0", "raven/raven": "^0.13", "ruflin/elastica": ">=0.90 <3.0", - "swiftmailer/swiftmailer": "~5.3", - "videlalvaro/php-amqplib": "~2.4" + "swiftmailer/swiftmailer": "~5.3" }, "suggest": { "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", @@ -4016,11 +4022,11 @@ "ext-mongo": "Allow sending log messages to a MongoDB server", "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", "php-console/php-console": "Allow sending log messages to Google Chrome", "raven/raven": "Allow sending log messages to a Sentry server", "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server", - "videlalvaro/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib" + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "type": "library", "extra": { @@ -4051,7 +4057,7 @@ "logging", "psr-3" ], - "time": "2016-03-13 16:08:35" + "time": "2016-04-12 18:29:35" }, { "name": "mtdowling/cron-expression", @@ -4201,16 +4207,16 @@ }, { "name": "nikic/php-parser", - "version": "v2.0.1", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ce5be709d59b32dd8a88c80259028759991a4206" + "reference": "47b254ea51f1d6d5dc04b9b299e88346bf2369e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ce5be709d59b32dd8a88c80259028759991a4206", - "reference": "ce5be709d59b32dd8a88c80259028759991a4206", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/47b254ea51f1d6d5dc04b9b299e88346bf2369e3", + "reference": "47b254ea51f1d6d5dc04b9b299e88346bf2369e3", "shasum": "" }, "require": { @@ -4226,7 +4232,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -4248,7 +4254,7 @@ "parser", "php" ], - "time": "2016-02-28 19:48:28" + "time": "2016-04-19 13:41:41" }, { "name": "omnipay/2checkout", @@ -4311,20 +4317,20 @@ }, { "name": "omnipay/authorizenet", - "version": "2.3.1", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-authorizenet.git", - "reference": "e2e813b0b6306ef97b8763037f05476456546b3e" + "reference": "bd9523cbc2bbe74e1caf297ffb2a5effbb583c75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-authorizenet/zipball/e2e813b0b6306ef97b8763037f05476456546b3e", - "reference": "e2e813b0b6306ef97b8763037f05476456546b3e", + "url": "https://api.github.com/repos/thephpleague/omnipay-authorizenet/zipball/bd9523cbc2bbe74e1caf297ffb2a5effbb583c75", + "reference": "bd9523cbc2bbe74e1caf297ffb2a5effbb583c75", "shasum": "" }, "require": { - "omnipay/common": "~2.0" + "omnipay/common": "~2.2" }, "require-dev": { "omnipay/tests": "~2.0" @@ -4366,7 +4372,7 @@ "pay", "payment" ], - "time": "2016-03-10 11:35:24" + "time": "2016-04-30 11:06:33" }, { "name": "omnipay/bitpay", @@ -4374,12 +4380,12 @@ "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-bitpay.git", - "reference": "cf813f1d5436a1d2f942d3df6666695d1e2b5280" + "reference": "9cadfb7955bd361d1a00ac8f0570aee4c05c6bb4" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/thephpleague/omnipay-bitpay/zipball/9cadfb7955bd361d1a00ac8f0570aee4c05c6bb4", - "reference": "cf813f1d5436a1d2f942d3df6666695d1e2b5280", + "reference": "9cadfb7955bd361d1a00ac8f0570aee4c05c6bb4", "shasum": "" }, "require": { @@ -4424,7 +4430,7 @@ "pay", "payment" ], - "time": "2016-03-10 03:16:04" + "time": "2016-04-07 02:53:36" }, { "name": "omnipay/braintree", @@ -4432,12 +4438,12 @@ "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-braintree.git", - "reference": "e4b4027c6a9e6443833490d0d51fd530f0a19f62" + "reference": "a0c8b2152a8a5b7e14572b71d860f2ec26f20a87" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/thephpleague/omnipay-braintree/zipball/a0c8b2152a8a5b7e14572b71d860f2ec26f20a87", - "reference": "e4b4027c6a9e6443833490d0d51fd530f0a19f62", + "reference": "a0c8b2152a8a5b7e14572b71d860f2ec26f20a87", "shasum": "" }, "require": { @@ -4487,7 +4493,7 @@ "payment", "purchase" ], - "time": "2016-02-25 20:54:09" + "time": "2016-05-16 07:59:03" }, { "name": "omnipay/buckaroo", @@ -4663,20 +4669,20 @@ }, { "name": "omnipay/common", - "version": "v2.3.3", + "version": "v2.3.4", "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-common.git", - "reference": "e4c54a314a2529c1008ad3f77e9eef26ed1f311b" + "reference": "fcd5a606713d11536c89315a5ae02d965a737c21" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-common/zipball/e4c54a314a2529c1008ad3f77e9eef26ed1f311b", - "reference": "e4c54a314a2529c1008ad3f77e9eef26ed1f311b", + "url": "https://api.github.com/repos/thephpleague/omnipay-common/zipball/fcd5a606713d11536c89315a5ae02d965a737c21", + "reference": "fcd5a606713d11536c89315a5ae02d965a737c21", "shasum": "" }, "require": { - "guzzle/http": "~3.1", + "guzzle/guzzle": "~3.9", "php": ">=5.3.2", "symfony/http-foundation": "~2.1" }, @@ -4775,7 +4781,7 @@ "payment", "purchase" ], - "time": "2015-01-11 04:54:29" + "time": "2015-03-30 14:34:46" }, { "name": "omnipay/dummy", @@ -5009,16 +5015,16 @@ }, { "name": "omnipay/manual", - "version": "v2.1.1", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-manual.git", - "reference": "ddbe7e8cfdb03b102219185aeb7dd91823275c71" + "reference": "db31b81dc3a9ccbc61a805dd9f922b7bfd0eb0e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-manual/zipball/ddbe7e8cfdb03b102219185aeb7dd91823275c71", - "reference": "ddbe7e8cfdb03b102219185aeb7dd91823275c71", + "url": "https://api.github.com/repos/thephpleague/omnipay-manual/zipball/db31b81dc3a9ccbc61a805dd9f922b7bfd0eb0e9", + "reference": "db31b81dc3a9ccbc61a805dd9f922b7bfd0eb0e9", "shasum": "" }, "require": { @@ -5062,7 +5068,7 @@ "pay", "payment" ], - "time": "2014-09-17 00:37:01" + "time": "2016-03-29 17:52:49" }, { "name": "omnipay/migs", @@ -5181,16 +5187,16 @@ }, { "name": "omnipay/multisafepay", - "version": "v2.3.0", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-multisafepay.git", - "reference": "342d0a3ba1a5ef0d788f20d23d0c70ce04ec3de1" + "reference": "01cfa7115ab7b3c79633e8137f802891c02640f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-multisafepay/zipball/342d0a3ba1a5ef0d788f20d23d0c70ce04ec3de1", - "reference": "342d0a3ba1a5ef0d788f20d23d0c70ce04ec3de1", + "url": "https://api.github.com/repos/thephpleague/omnipay-multisafepay/zipball/01cfa7115ab7b3c79633e8137f802891c02640f2", + "reference": "01cfa7115ab7b3c79633e8137f802891c02640f2", "shasum": "" }, "require": { @@ -5235,7 +5241,7 @@ "pay", "payment" ], - "time": "2016-02-18 00:06:08" + "time": "2016-03-21 02:10:38" }, { "name": "omnipay/netaxept", @@ -5579,16 +5585,16 @@ }, { "name": "omnipay/paymentexpress", - "version": "v2.1.2", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-paymentexpress.git", - "reference": "bd417f02bacb2128c168956739cd3a902d3ee48c" + "reference": "5707f016cd2a6fa735f325c40ff3b5fbe29452ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-paymentexpress/zipball/bd417f02bacb2128c168956739cd3a902d3ee48c", - "reference": "bd417f02bacb2128c168956739cd3a902d3ee48c", + "url": "https://api.github.com/repos/thephpleague/omnipay-paymentexpress/zipball/5707f016cd2a6fa735f325c40ff3b5fbe29452ea", + "reference": "5707f016cd2a6fa735f325c40ff3b5fbe29452ea", "shasum": "" }, "require": { @@ -5638,20 +5644,20 @@ "pxpay", "pxpost" ], - "time": "2015-04-03 00:20:28" + "time": "2016-05-02 13:46:54" }, { "name": "omnipay/paypal", - "version": "v2.5.3", + "version": "v2.5.4", "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-paypal.git", - "reference": "97fc3b1ff43e130ee911b35e139dcc853488d07a" + "reference": "3d39ab63f9c1e31893ec61bbab9eda98df5f1a6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-paypal/zipball/97fc3b1ff43e130ee911b35e139dcc853488d07a", - "reference": "97fc3b1ff43e130ee911b35e139dcc853488d07a", + "url": "https://api.github.com/repos/thephpleague/omnipay-paypal/zipball/3d39ab63f9c1e31893ec61bbab9eda98df5f1a6f", + "reference": "3d39ab63f9c1e31893ec61bbab9eda98df5f1a6f", "shasum": "" }, "require": { @@ -5696,20 +5702,20 @@ "paypal", "purchase" ], - "time": "2016-02-29 00:06:43" + "time": "2016-03-25 10:40:17" }, { "name": "omnipay/pin", - "version": "v2.2.1", + "version": "v2.2.2", "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-pin.git", - "reference": "c2252e41f3674267b2bbe79eaeec73b6b1e4ee58" + "reference": "8c859bc71de8def70623eacd1b3ec6da1829a445" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/omnipay-pin/zipball/c2252e41f3674267b2bbe79eaeec73b6b1e4ee58", - "reference": "c2252e41f3674267b2bbe79eaeec73b6b1e4ee58", + "url": "https://api.github.com/repos/thephpleague/omnipay-pin/zipball/8c859bc71de8def70623eacd1b3ec6da1829a445", + "reference": "8c859bc71de8def70623eacd1b3ec6da1829a445", "shasum": "" }, "require": { @@ -5753,7 +5759,7 @@ "payment", "pin" ], - "time": "2016-01-13 07:00:17" + "time": "2016-03-25 18:06:33" }, { "name": "omnipay/sagepay", @@ -5877,12 +5883,12 @@ "source": { "type": "git", "url": "https://github.com/thephpleague/omnipay-stripe.git", - "reference": "0ea7a647ee01e29c152814e11c2ea6307e5b0db9" + "reference": "88badbda83f1c16e2d94c41a869be37d9d9fef5a" }, "dist": { "type": "zip", "url": "https://api.github.com/repos/thephpleague/omnipay-stripe/zipball/88badbda83f1c16e2d94c41a869be37d9d9fef5a", - "reference": "0ea7a647ee01e29c152814e11c2ea6307e5b0db9", + "reference": "88badbda83f1c16e2d94c41a869be37d9d9fef5a", "shasum": "" }, "require": { @@ -5926,7 +5932,7 @@ "payment", "stripe" ], - "time": "2016-04-26 08:34:50" + "time": "2016-05-16 07:49:56" }, { "name": "omnipay/targetpay", @@ -6589,35 +6595,37 @@ }, { "name": "sly/notification-pusher", - "version": "v2.2.2", + "version": "v2.2.12", "source": { "type": "git", "url": "https://github.com/Ph3nol/NotificationPusher.git", - "reference": "6112841c4b679bc4f6cf01f6cf655e24794bc670" + "reference": "f9a99edb4e26254baf1f7fb1354aa5a3f2c3b0ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Ph3nol/NotificationPusher/zipball/6112841c4b679bc4f6cf01f6cf655e24794bc670", - "reference": "6112841c4b679bc4f6cf01f6cf655e24794bc670", + "url": "https://api.github.com/repos/Ph3nol/NotificationPusher/zipball/f9a99edb4e26254baf1f7fb1354aa5a3f2c3b0ae", + "reference": "f9a99edb4e26254baf1f7fb1354aa5a3f2c3b0ae", "shasum": "" }, "require": { - "php": ">=5.3.2", - "symfony/console": ">=2.3", - "symfony/options-resolver": ">=2.3", - "symfony/process": ">=2.3", - "zendframework/zendservice-apple-apns": "1.*", + "doctrine/inflector": "~1.0", + "php": ">=5.5", + "symfony/console": "~2.3", + "symfony/options-resolver": "~2.3", + "symfony/process": "~2.3", + "zendframework/zendservice-apple-apns": "^1.1.0", "zendframework/zendservice-google-gcm": "1.*" }, "require-dev": { "atoum/atoum": "dev-master" }, + "bin": [ + "np" + ], "type": "standalone", "autoload": { "psr-0": { - "Sly": [ - "src/" - ] + "Sly": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -6647,7 +6655,7 @@ "push", "pusher" ], - "time": "2013-12-08 20:35:03" + "time": "2015-10-01 07:56:41" }, { "name": "softcommerce/omnipay-paytrace", @@ -6750,16 +6758,16 @@ }, { "name": "swiftmailer/swiftmailer", - "version": "v5.4.1", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "0697e6aa65c83edf97bb0f23d8763f94e3f11421" + "reference": "d8db871a54619458a805229a057ea2af33c753e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/0697e6aa65c83edf97bb0f23d8763f94e3f11421", - "reference": "0697e6aa65c83edf97bb0f23d8763f94e3f11421", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/d8db871a54619458a805229a057ea2af33c753e8", + "reference": "d8db871a54619458a805229a057ea2af33c753e8", "shasum": "" }, "require": { @@ -6799,20 +6807,20 @@ "mail", "mailer" ], - "time": "2015-06-06 14:19:39" + "time": "2016-05-01 08:45:47" }, { "name": "symfony/class-loader", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "92e7cf1af2bc1695daabb4ac972db169606e9030" + "reference": "cbb7e6a9c0213a0cffa5d9065ee8214ca4e83877" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/92e7cf1af2bc1695daabb4ac972db169606e9030", - "reference": "92e7cf1af2bc1695daabb4ac972db169606e9030", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/cbb7e6a9c0213a0cffa5d9065ee8214ca4e83877", + "reference": "cbb7e6a9c0213a0cffa5d9065ee8214ca4e83877", "shasum": "" }, "require": { @@ -6855,30 +6863,30 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2016-02-03 09:33:23" + "time": "2016-03-30 10:41:14" }, { "name": "symfony/console", - "version": "v3.0.3", + "version": "v2.8.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2ed5e2706ce92313d120b8fe50d1063bcfd12e04" + "reference": "48221d3de4dc22d2cd57c97e8b9361821da86609" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2ed5e2706ce92313d120b8fe50d1063bcfd12e04", - "reference": "2ed5e2706ce92313d120b8fe50d1063bcfd12e04", + "url": "https://api.github.com/repos/symfony/console/zipball/48221d3de4dc22d2cd57c97e8b9361821da86609", + "reference": "48221d3de4dc22d2cd57c97e8b9361821da86609", "shasum": "" }, "require": { - "php": ">=5.5.9", + "php": ">=5.3.9", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/process": "~2.8|~3.0" + "symfony/event-dispatcher": "~2.1|~3.0.0", + "symfony/process": "~2.1|~3.0.0" }, "suggest": { "psr/log": "For using the console logger", @@ -6888,7 +6896,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -6915,20 +6923,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-02-28 16:24:34" + "time": "2016-04-26 12:00:47" }, { "name": "symfony/css-selector", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "6605602690578496091ac20ec7a5cbd160d4dff4" + "reference": "65e764f404685f2dc20c057e889b3ad04b2e2db0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/6605602690578496091ac20ec7a5cbd160d4dff4", - "reference": "6605602690578496091ac20ec7a5cbd160d4dff4", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/65e764f404685f2dc20c057e889b3ad04b2e2db0", + "reference": "65e764f404685f2dc20c057e889b3ad04b2e2db0", "shasum": "" }, "require": { @@ -6968,20 +6976,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2016-01-27 05:14:46" + "time": "2016-03-04 07:55:57" }, { "name": "symfony/debug", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "29606049ced1ec715475f88d1bbe587252a3476e" + "reference": "a06d10888a45afd97534506afb058ec38d9ba35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/29606049ced1ec715475f88d1bbe587252a3476e", - "reference": "29606049ced1ec715475f88d1bbe587252a3476e", + "url": "https://api.github.com/repos/symfony/debug/zipball/a06d10888a45afd97534506afb058ec38d9ba35b", + "reference": "a06d10888a45afd97534506afb058ec38d9ba35b", "shasum": "" }, "require": { @@ -7025,31 +7033,31 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2016-01-27 05:14:46" + "time": "2016-03-30 10:41:14" }, { "name": "symfony/event-dispatcher", - "version": "v3.0.3", + "version": "v2.8.6", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "4dd5df31a28c0f82b41cb1e1599b74b5dcdbdafa" + "reference": "a158f13992a3147d466af7a23b564ac719a4ddd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4dd5df31a28c0f82b41cb1e1599b74b5dcdbdafa", - "reference": "4dd5df31a28c0f82b41cb1e1599b74b5dcdbdafa", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a158f13992a3147d466af7a23b564ac719a4ddd8", + "reference": "a158f13992a3147d466af7a23b564ac719a4ddd8", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": ">=5.3.9" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~2.8|~3.0", - "symfony/dependency-injection": "~2.8|~3.0", - "symfony/expression-language": "~2.8|~3.0", - "symfony/stopwatch": "~2.8|~3.0" + "symfony/config": "~2.0,>=2.0.5|~3.0.0", + "symfony/dependency-injection": "~2.6|~3.0.0", + "symfony/expression-language": "~2.6|~3.0.0", + "symfony/stopwatch": "~2.3|~3.0.0" }, "suggest": { "symfony/dependency-injection": "", @@ -7058,7 +7066,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -7085,20 +7093,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-01-27 05:14:46" + "time": "2016-05-03 18:59:18" }, { "name": "symfony/finder", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "623bda0abd9aa29e529c8e9c08b3b84171914723" + "reference": "c54e407b35bc098916704e9fd090da21da4c4f52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/623bda0abd9aa29e529c8e9c08b3b84171914723", - "reference": "623bda0abd9aa29e529c8e9c08b3b84171914723", + "url": "https://api.github.com/repos/symfony/finder/zipball/c54e407b35bc098916704e9fd090da21da4c4f52", + "reference": "c54e407b35bc098916704e9fd090da21da4c4f52", "shasum": "" }, "require": { @@ -7134,24 +7142,25 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2016-01-27 05:14:46" + "time": "2016-03-10 11:13:05" }, { "name": "symfony/http-foundation", - "version": "v2.8.3", + "version": "v2.8.6", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6f4e41c41e7d352ed9adf71ff6f2ec1756490a1b" + "reference": "ed9357bf86f041bd69a197742ee22c97989467d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6f4e41c41e7d352ed9adf71ff6f2ec1756490a1b", - "reference": "6f4e41c41e7d352ed9adf71ff6f2ec1756490a1b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ed9357bf86f041bd69a197742ee22c97989467d7", + "reference": "ed9357bf86f041bd69a197742ee22c97989467d7", "shasum": "" }, "require": { "php": ">=5.3.9", + "symfony/polyfill-mbstring": "~1.1", "symfony/polyfill-php54": "~1.0", "symfony/polyfill-php55": "~1.0" }, @@ -7188,20 +7197,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2016-02-28 16:20:50" + "time": "2016-04-05 16:36:54" }, { "name": "symfony/http-kernel", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "59c0a1972e9aad87b7a56bbe1ccee26b7535a0db" + "reference": "6a5010978edf0a9646342232531e53bfc7abbcd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/59c0a1972e9aad87b7a56bbe1ccee26b7535a0db", - "reference": "59c0a1972e9aad87b7a56bbe1ccee26b7535a0db", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6a5010978edf0a9646342232531e53bfc7abbcd3", + "reference": "6a5010978edf0a9646342232531e53bfc7abbcd3", "shasum": "" }, "require": { @@ -7270,29 +7279,29 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2016-02-28 21:33:13" + "time": "2016-05-09 22:13:13" }, { "name": "symfony/options-resolver", - "version": "v3.0.3", + "version": "v2.8.6", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "bc7ee3ef3905f7c21fbed4e921cb39d6518b1300" + "reference": "5e4a8ee6e823428257f2002f6daf52de854d8384" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/bc7ee3ef3905f7c21fbed4e921cb39d6518b1300", - "reference": "bc7ee3ef3905f7c21fbed4e921cb39d6518b1300", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/5e4a8ee6e823428257f2002f6daf52de854d8384", + "reference": "5e4a8ee6e823428257f2002f6daf52de854d8384", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": ">=5.3.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -7324,20 +7333,20 @@ "configuration", "options" ], - "time": "2016-01-21 09:38:31" + "time": "2016-05-09 18:12:35" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "1289d16209491b584839022f29257ad859b8532d" + "reference": "dff51f72b0706335131b00a7f49606168c582594" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/1289d16209491b584839022f29257ad859b8532d", - "reference": "1289d16209491b584839022f29257ad859b8532d", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/dff51f72b0706335131b00a7f49606168c582594", + "reference": "dff51f72b0706335131b00a7f49606168c582594", "shasum": "" }, "require": { @@ -7349,7 +7358,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -7383,20 +7392,20 @@ "portable", "shim" ], - "time": "2016-01-20 09:13:37" + "time": "2016-05-18 14:26:46" }, { "name": "symfony/polyfill-php54", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php54.git", - "reference": "9ba741ca01c77282ecf5796c2c1d667f03454ffb" + "reference": "34d761992f6f2cc6092cc0e5e93f38b53ba5e4f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/9ba741ca01c77282ecf5796c2c1d667f03454ffb", - "reference": "9ba741ca01c77282ecf5796c2c1d667f03454ffb", + "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/34d761992f6f2cc6092cc0e5e93f38b53ba5e4f1", + "reference": "34d761992f6f2cc6092cc0e5e93f38b53ba5e4f1", "shasum": "" }, "require": { @@ -7405,7 +7414,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -7441,20 +7450,20 @@ "portable", "shim" ], - "time": "2016-01-25 19:13:00" + "time": "2016-05-18 14:26:46" }, { "name": "symfony/polyfill-php55", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php55.git", - "reference": "b4f3f07d91702f8f926339fc4fcf81671d8c27e6" + "reference": "bf2ff9ad6be1a4772cb873e4eea94d70daa95c6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/b4f3f07d91702f8f926339fc4fcf81671d8c27e6", - "reference": "b4f3f07d91702f8f926339fc4fcf81671d8c27e6", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/bf2ff9ad6be1a4772cb873e4eea94d70daa95c6d", + "reference": "bf2ff9ad6be1a4772cb873e4eea94d70daa95c6d", "shasum": "" }, "require": { @@ -7464,7 +7473,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -7497,20 +7506,20 @@ "portable", "shim" ], - "time": "2016-01-20 09:13:37" + "time": "2016-05-18 14:26:46" }, { "name": "symfony/polyfill-php56", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "4d891fff050101a53a4caabb03277284942d1ad9" + "reference": "3edf57a8fbf9a927533344cef65ad7e1cf31030a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/4d891fff050101a53a4caabb03277284942d1ad9", - "reference": "4d891fff050101a53a4caabb03277284942d1ad9", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/3edf57a8fbf9a927533344cef65ad7e1cf31030a", + "reference": "3edf57a8fbf9a927533344cef65ad7e1cf31030a", "shasum": "" }, "require": { @@ -7520,7 +7529,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -7553,20 +7562,20 @@ "portable", "shim" ], - "time": "2016-01-20 09:13:37" + "time": "2016-05-18 14:26:46" }, { "name": "symfony/polyfill-util", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-util.git", - "reference": "8de62801aa12bc4dfcf85eef5d21981ae7bb3cc4" + "reference": "ef830ce3d218e622b221d6bfad42c751d974bf99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/8de62801aa12bc4dfcf85eef5d21981ae7bb3cc4", - "reference": "8de62801aa12bc4dfcf85eef5d21981ae7bb3cc4", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/ef830ce3d218e622b221d6bfad42c751d974bf99", + "reference": "ef830ce3d218e622b221d6bfad42c751d974bf99", "shasum": "" }, "require": { @@ -7575,7 +7584,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -7605,29 +7614,29 @@ "polyfill", "shim" ], - "time": "2016-01-20 09:13:37" + "time": "2016-05-18 14:26:46" }, { "name": "symfony/process", - "version": "v3.0.3", + "version": "v2.8.6", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "dfecef47506179db2501430e732adbf3793099c8" + "reference": "1276bd9be89be039748cf753a2137f4ef149cd74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/dfecef47506179db2501430e732adbf3793099c8", - "reference": "dfecef47506179db2501430e732adbf3793099c8", + "url": "https://api.github.com/repos/symfony/process/zipball/1276bd9be89be039748cf753a2137f4ef149cd74", + "reference": "1276bd9be89be039748cf753a2137f4ef149cd74", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": ">=5.3.9" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.8-dev" } }, "autoload": { @@ -7654,20 +7663,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2016-02-02 13:44:19" + "time": "2016-04-14 15:22:22" }, { "name": "symfony/routing", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "fa1e9a8173cf0077dd995205da453eacd758fdf6" + "reference": "a6cd168310066176599442aa21f5da86c3f8e0b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/fa1e9a8173cf0077dd995205da453eacd758fdf6", - "reference": "fa1e9a8173cf0077dd995205da453eacd758fdf6", + "url": "https://api.github.com/repos/symfony/routing/zipball/a6cd168310066176599442aa21f5da86c3f8e0b3", + "reference": "a6cd168310066176599442aa21f5da86c3f8e0b3", "shasum": "" }, "require": { @@ -7729,20 +7738,20 @@ "uri", "url" ], - "time": "2016-02-04 13:53:13" + "time": "2016-05-03 12:23:49" }, { "name": "symfony/translation", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "2de0b6f7ebe43cffd8a06996ebec6aab79ea9e91" + "reference": "f7a07af51ea067745a521dab1e3152044a2fb1f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/2de0b6f7ebe43cffd8a06996ebec6aab79ea9e91", - "reference": "2de0b6f7ebe43cffd8a06996ebec6aab79ea9e91", + "url": "https://api.github.com/repos/symfony/translation/zipball/f7a07af51ea067745a521dab1e3152044a2fb1f2", + "reference": "f7a07af51ea067745a521dab1e3152044a2fb1f2", "shasum": "" }, "require": { @@ -7793,20 +7802,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2016-02-02 13:44:19" + "time": "2016-03-25 01:41:20" }, { "name": "symfony/var-dumper", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "9a6a883c48acb215d4825ce9de61dccf93d62074" + "reference": "0e918c269093ba4c77fca14e9424fa74ed16f1a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9a6a883c48acb215d4825ce9de61dccf93d62074", - "reference": "9a6a883c48acb215d4825ce9de61dccf93d62074", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e918c269093ba4c77fca14e9424fa74ed16f1a6", + "reference": "0e918c269093ba4c77fca14e9424fa74ed16f1a6", "shasum": "" }, "require": { @@ -7856,7 +7865,7 @@ "debug", "dump" ], - "time": "2016-02-13 09:23:44" + "time": "2016-04-25 11:17:47" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -8094,23 +8103,23 @@ }, { "name": "vlucas/phpdotenv", - "version": "v2.2.0", + "version": "v2.2.1", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "9caf304153dc2288e4970caec6f1f3b3bc205412" + "reference": "63f37b9395e8041cd4313129c08ece896d06ca8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/9caf304153dc2288e4970caec6f1f3b3bc205412", - "reference": "9caf304153dc2288e4970caec6f1f3b3bc205412", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/63f37b9395e8041cd4313129c08ece896d06ca8e", + "reference": "63f37b9395e8041cd4313129c08ece896d06ca8e", "shasum": "" }, "require": { "php": ">=5.3.9" }, "require-dev": { - "phpunit/phpunit": "^4.8|^5.0" + "phpunit/phpunit": "^4.8 || ^5.0" }, "type": "library", "extra": { @@ -8125,7 +8134,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD" + "BSD-3-Clause-Attribution" ], "authors": [ { @@ -8135,13 +8144,12 @@ } ], "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", - "homepage": "http://github.com/vlucas/phpdotenv", "keywords": [ "dotenv", "env", "environment" ], - "time": "2015-12-29 15:10:30" + "time": "2016-04-15 10:48:49" }, { "name": "webpatser/laravel-countries", @@ -8318,16 +8326,16 @@ }, { "name": "wildbit/swiftmailer-postmark", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/wildbit/swiftmailer-postmark.git", - "reference": "3673ace0473ec72de31bc7e62d78e8b8f95d15f0" + "reference": "4032d3336ff97761edad3d0a88769820fb84e56f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wildbit/swiftmailer-postmark/zipball/3673ace0473ec72de31bc7e62d78e8b8f95d15f0", - "reference": "3673ace0473ec72de31bc7e62d78e8b8f95d15f0", + "url": "https://api.github.com/repos/wildbit/swiftmailer-postmark/zipball/4032d3336ff97761edad3d0a88769820fb84e56f", + "reference": "4032d3336ff97761edad3d0a88769820fb84e56f", "shasum": "" }, "require": { @@ -8357,7 +8365,7 @@ } ], "description": "A Swiftmailer Transport for Postmark.", - "time": "2015-11-30 18:23:03" + "time": "2016-04-18 14:38:52" }, { "name": "zendframework/zend-escaper", @@ -8554,16 +8562,16 @@ }, { "name": "zendframework/zend-stdlib", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-stdlib.git", - "reference": "22eb098958980fbbe6b9a06f209f5a4b496cc0c1" + "reference": "8bafa58574204bdff03c275d1d618aaa601588ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/22eb098958980fbbe6b9a06f209f5a4b496cc0c1", - "reference": "22eb098958980fbbe6b9a06f209f5a4b496cc0c1", + "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/8bafa58574204bdff03c275d1d618aaa601588ae", + "reference": "8bafa58574204bdff03c275d1d618aaa601588ae", "shasum": "" }, "require": { @@ -8595,7 +8603,7 @@ "stdlib", "zf2" ], - "time": "2016-02-03 16:53:37" + "time": "2016-04-12 21:19:36" }, { "name": "zendframework/zend-uri", @@ -8646,16 +8654,16 @@ }, { "name": "zendframework/zend-validator", - "version": "2.6.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-validator.git", - "reference": "1315fead53358054e3f5fcf440c1a4cd5f0724db" + "reference": "f956581bc5fa4cf3f2933fe24e77deded8d1937b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/1315fead53358054e3f5fcf440c1a4cd5f0724db", - "reference": "1315fead53358054e3f5fcf440c1a4cd5f0724db", + "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/f956581bc5fa4cf3f2933fe24e77deded8d1937b", + "reference": "f956581bc5fa4cf3f2933fe24e77deded8d1937b", "shasum": "" }, "require": { @@ -8668,13 +8676,13 @@ "phpunit/phpunit": "^4.0", "zendframework/zend-cache": "^2.6.1", "zendframework/zend-config": "^2.6", - "zendframework/zend-db": "^2.5", + "zendframework/zend-db": "^2.7", "zendframework/zend-filter": "^2.6", "zendframework/zend-http": "^2.5.4", "zendframework/zend-i18n": "^2.6", "zendframework/zend-math": "^2.6", "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3", - "zendframework/zend-session": "^2.5", + "zendframework/zend-session": "^2.6.2", "zendframework/zend-uri": "^2.5" }, "suggest": { @@ -8690,8 +8698,12 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.6-dev", - "dev-develop": "2.7-dev" + "dev-master": "2.8-dev", + "dev-develop": "2.9-dev" + }, + "zf": { + "component": "Zend\\Validator", + "config-provider": "Zend\\Validator\\ConfigProvider" } }, "autoload": { @@ -8709,7 +8721,7 @@ "validator", "zf2" ], - "time": "2016-02-17 17:59:34" + "time": "2016-05-16 13:39:40" }, { "name": "zendframework/zendservice-apple-apns", @@ -8913,16 +8925,16 @@ }, { "name": "codeception/codeception", - "version": "2.1.7", + "version": "2.1.8", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "65971b0dee4972710365b6102154cd412a9bf7b1" + "reference": "f3daa61f0f11c531b33eb3623ab0daa599d88a79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/65971b0dee4972710365b6102154cd412a9bf7b1", - "reference": "65971b0dee4972710365b6102154cd412a9bf7b1", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/f3daa61f0f11c531b33eb3623ab0daa599d88a79", + "reference": "f3daa61f0f11c531b33eb3623ab0daa599d88a79", "shasum": "" }, "require": { @@ -8990,7 +9002,7 @@ "functional testing", "unit testing" ], - "time": "2016-03-12 01:15:25" + "time": "2016-04-15 02:56:43" }, { "name": "doctrine/instantiator", @@ -9415,21 +9427,24 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.7", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", - "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, "type": "library", "autoload": { "classmap": [ @@ -9452,7 +9467,7 @@ "keywords": [ "timer" ], - "time": "2015-06-21 08:01:12" + "time": "2016-05-12 18:03:57" }, { "name": "phpunit/php-token-stream", @@ -9505,16 +9520,16 @@ }, { "name": "phpunit/phpunit", - "version": "4.8.24", + "version": "4.8.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a1066c562c52900a142a0e2bbf0582994671385e" + "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1066c562c52900a142a0e2bbf0582994671385e", - "reference": "a1066c562c52900a142a0e2bbf0582994671385e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc1d8cd5b5de11625979125c5639347896ac2c74", + "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74", "shasum": "" }, "require": { @@ -9528,7 +9543,7 @@ "phpunit/php-code-coverage": "~2.1", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": ">=1.0.6", + "phpunit/php-timer": "^1.0.6", "phpunit/phpunit-mock-objects": "~2.3", "sebastian/comparator": "~1.1", "sebastian/diff": "~1.2", @@ -9573,7 +9588,7 @@ "testing", "xunit" ], - "time": "2016-03-14 06:16:08" + "time": "2016-05-17 03:09:28" }, { "name": "phpunit/phpunit-mock-objects", @@ -9749,16 +9764,16 @@ }, { "name": "sebastian/environment", - "version": "1.3.5", + "version": "1.3.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf" + "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf", - "reference": "dc7a29032cf72b54f36dac15a1ca5b3a1b6029bf", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4e8f0da10ac5802913afc151413bc8c53b6c2716", + "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716", "shasum": "" }, "require": { @@ -9795,7 +9810,7 @@ "environment", "hhvm" ], - "time": "2016-02-26 18:40:46" + "time": "2016-05-17 03:18:57" }, { "name": "sebastian/exporter", @@ -10004,16 +10019,16 @@ }, { "name": "symfony/browser-kit", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "dde849a0485b70a24b36f826ed3fb95b904d80c3" + "reference": "e07127ac31230b30887c2dddf3708d883d239b14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/dde849a0485b70a24b36f826ed3fb95b904d80c3", - "reference": "dde849a0485b70a24b36f826ed3fb95b904d80c3", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/e07127ac31230b30887c2dddf3708d883d239b14", + "reference": "e07127ac31230b30887c2dddf3708d883d239b14", "shasum": "" }, "require": { @@ -10057,20 +10072,20 @@ ], "description": "Symfony BrowserKit Component", "homepage": "https://symfony.com", - "time": "2016-01-27 11:34:55" + "time": "2016-03-04 07:55:57" }, { "name": "symfony/dom-crawler", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "981c8edb4538f88ba976ed44bdcaa683fce3d6c6" + "reference": "49b588841225b205700e5122fa01911cabada857" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/981c8edb4538f88ba976ed44bdcaa683fce3d6c6", - "reference": "981c8edb4538f88ba976ed44bdcaa683fce3d6c6", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/49b588841225b205700e5122fa01911cabada857", + "reference": "49b588841225b205700e5122fa01911cabada857", "shasum": "" }, "require": { @@ -10113,20 +10128,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2016-02-28 16:24:34" + "time": "2016-04-12 18:09:53" }, { "name": "symfony/yaml", - "version": "v3.0.3", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "b5ba64cd67ecd6887f63868fa781ca094bd1377c" + "reference": "0047c8366744a16de7516622c5b7355336afae96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/b5ba64cd67ecd6887f63868fa781ca094bd1377c", - "reference": "b5ba64cd67ecd6887f63868fa781ca094bd1377c", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0047c8366744a16de7516622c5b7355336afae96", + "reference": "0047c8366744a16de7516622c5b7355336afae96", "shasum": "" }, "require": { @@ -10162,7 +10177,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-02-23 15:16:06" + "time": "2016-03-04 07:55:57" } ], "aliases": [], @@ -10194,7 +10209,8 @@ "laracasts/presenter": 20, "jlapp/swaggervel": 20, "omnipay/braintree": 20, - "gatepay/fedachdir": 20 + "gatepay/fedachdir": 20, + "collizo4sky/omnipay-wepay": 20 }, "prefer-stable": false, "prefer-lowest": false, From 2034c11795282de1e7191a44fb07dbc0bbe40028 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 19 May 2016 09:43:03 +1000 Subject: [PATCH 115/386] Include Documents in Invoice transformer --- app/Ninja/Transformers/InvoiceTransformer.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/Ninja/Transformers/InvoiceTransformer.php b/app/Ninja/Transformers/InvoiceTransformer.php index c866f1213540..fc17127719a2 100644 --- a/app/Ninja/Transformers/InvoiceTransformer.php +++ b/app/Ninja/Transformers/InvoiceTransformer.php @@ -28,7 +28,7 @@ class InvoiceTransformer extends EntityTransformer 'invitations', 'payments', 'client', - //'expenses', + 'documents', ]; public function __construct($account = null, $serializer = null, $client = null) @@ -68,6 +68,12 @@ class InvoiceTransformer extends EntityTransformer return $this->includeCollection($invoice->expenses, $transformer, ENTITY_EXPENSE); } + public function includeDocuments(Invoice $invoice) + { + $transformer = new DocumentTransformer($this->account, $this->serializer); + return $this->includeCollection($invoice->documents, $transformer, ENTITY_DOCUMENT); + } + public function transform(Invoice $invoice) { From 6caf1ff30d72fd2fdea264392abdc01cf431cb58 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 19 May 2016 09:47:57 +0300 Subject: [PATCH 116/386] Minor fixes --- app/Http/routes.php | 2 + app/Ninja/Repositories/ExpenseRepository.php | 21 ++++--- app/Services/BankAccountService.php | 2 +- resources/lang/en/texts.php | 58 ++++++++++---------- 4 files changed, 45 insertions(+), 38 deletions(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index 34dd41872cf7..18d6e8640cfe 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -247,6 +247,8 @@ Route::group([ Route::get('api/gateways', array('as'=>'api.gateways', 'uses'=>'AccountGatewayController@getDatatable')); Route::post('account_gateways/bulk', 'AccountGatewayController@bulk'); + Route::get('bank_accounts/import_ofx', 'BankAccountController@showImportOFX'); + Route::post('bank_accounts/import_ofx', 'BankAccountController@doImportOFX'); Route::resource('bank_accounts', 'BankAccountController'); Route::get('api/bank_accounts', array('as'=>'api.bank_accounts', 'uses'=>'BankAccountController@getDatatable')); Route::post('bank_accounts/bulk', 'BankAccountController@bulk'); diff --git a/app/Ninja/Repositories/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php index 442af74bd32b..0ce0e155cf50 100644 --- a/app/Ninja/Repositories/ExpenseRepository.php +++ b/app/Ninja/Repositories/ExpenseRepository.php @@ -12,7 +12,7 @@ use Session; class ExpenseRepository extends BaseRepository { protected $documentRepo; - + // Expenses public function getClassName() { @@ -23,7 +23,7 @@ class ExpenseRepository extends BaseRepository { $this->documentRepo = $documentRepo; } - + public function all() { return Expense::scope() @@ -156,7 +156,7 @@ class ExpenseRepository extends BaseRepository $rate = isset($input['exchange_rate']) ? Utils::parseFloat($input['exchange_rate']) : 1; $expense->exchange_rate = round($rate, 4); $expense->amount = round(Utils::parseFloat($input['amount']), 2); - + $expense->save(); // Documents @@ -169,7 +169,7 @@ class ExpenseRepository extends BaseRepository $document->save(); } } - + if(!empty($input['documents']) && Auth::user()->can('create', ENTITY_DOCUMENT)){ // Fallback upload $doc_errors = array(); @@ -188,11 +188,14 @@ class ExpenseRepository extends BaseRepository Session::flash('error', implode('
',array_map('htmlentities',$doc_errors))); } } - - foreach ($expense->documents as $document){ - if(!in_array($document->public_id, $document_ids)){ - // Not checking permissions; deleting a document is just editing the invoice - $document->delete(); + + // prevent loading all of the documents if we don't have to + if ( ! $expense->wasRecentlyCreated) { + foreach ($expense->documents as $document){ + if ( ! in_array($document->public_id, $document_ids)){ + // Not checking permissions; deleting a document is just editing the invoice + $document->delete(); + } } } diff --git a/app/Services/BankAccountService.php b/app/Services/BankAccountService.php index cd8776c8b111..688bd866d3ad 100644 --- a/app/Services/BankAccountService.php +++ b/app/Services/BankAccountService.php @@ -206,7 +206,7 @@ class BankAccountService extends BaseService $vendorMap[$transaction['vendor_orig']] = $vendor; $countVendors++; } - + // create the expense record $this->expenseRepo->save([ 'vendor_id' => $vendor->id, diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 871c89e78611..f73e936f5fb5 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -978,8 +978,8 @@ $LANG = array( 'first_page' => 'First page', 'all_pages' => 'All pages', 'last_page' => 'Last page', - 'all_pages_header' => 'Show header on', - 'all_pages_footer' => 'Show footer on', + 'all_pages_header' => 'Show Header on', + 'all_pages_footer' => 'Show Footer on', 'invoice_currency' => 'Invoice Currency', 'enable_https' => 'We strongly recommend using HTTPS to accept credit card details online.', 'quote_issued_to' => 'Quote issued to', @@ -1040,14 +1040,14 @@ $LANG = array( 'enable_portal_password_help'=>'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.', 'send_portal_password'=>'Generate password automatically', 'send_portal_password_help'=>'If no password is set, one will be generated and sent with the first invoice.', - + 'expired' => 'Expired', 'invalid_card_number' => 'The credit card number is not valid.', 'invalid_expiry' => 'The expiration date is not valid.', 'invalid_cvv' => 'The CVV is not valid.', 'cost' => 'Cost', 'create_invoice_for_sample' => 'Note: create your first invoice to see a preview here.', - + // User Permissions 'owner' => 'Owner', 'administrator' => 'Administrator', @@ -1065,8 +1065,8 @@ $LANG = array( 'create_all_help' => 'Allow user to create and modify records', 'view_all_help' => 'Allow user to view records they didn\'t create', 'edit_all_help' => 'Allow user to modify records they didn\'t create', - 'view_payment' => 'View Payment', - + 'view_payment' => 'View Payment', + 'january' => 'January', 'february' => 'February', 'march' => 'March', @@ -1079,7 +1079,7 @@ $LANG = array( 'october' => 'October', 'november' => 'November', 'december' => 'December', - + // Documents 'documents_header' => 'Documents:', 'email_documents_header' => 'Documents:', @@ -1092,17 +1092,15 @@ $LANG = array( 'document_email_attachment' => 'Attach Documents', 'download_documents' => 'Download Documents (:size)', 'documents_from_expenses' => 'From Expenses:', - 'dropzone' => array(// See http://www.dropzonejs.com/#config-dictDefaultMessage - 'DefaultMessage' => 'Drop files or click to upload', - 'FallbackMessage' => 'Your browser does not support drag\'n\'drop file uploads.', - 'FallbackText' => 'Please use the fallback form below to upload your files like in the olden days.', - 'FileTooBig' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', - 'InvalidFileType' => 'You can\'t upload files of this type.', - 'ResponseError' => 'Server responded with {{statusCode}} code.', - 'CancelUpload' => 'Cancel upload', - 'CancelUploadConfirmation' => 'Are you sure you want to cancel this upload?', - 'RemoveFile' => 'Remove file', - ), + 'dropzone_default_message' => 'Drop files or click to upload', + 'dropzone_fallback_message' => 'Your browser does not support drag\'n\'drop file uploads.', + 'dropzone_fallback_text' => 'Please use the fallback form below to upload your files like in the olden days.', + 'dropzone_file_too_big' => 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.', + 'dropzone_invalid_file_type' => 'You can\'t upload files of this type.', + 'dropzone_response_error' => 'Server responded with {{statusCode}} code.', + 'dropzone_cancel_upload' => 'Cancel upload', + 'dropzone_cancel_upload_confirmation' => 'Are you sure you want to cancel this upload?', + 'dropzone_remove_file' => 'Remove file', 'documents' => 'Documents', 'document_date' => 'Document Date', 'document_size' => 'Size', @@ -1111,11 +1109,11 @@ $LANG = array( 'enable_client_portal_help' => 'Show/hide the client portal.', 'enable_client_portal_dashboard' => 'Dashboard', 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', - + // Plans 'account_management' => 'Account Management', 'plan_status' => 'Plan Status', - + 'plan_upgrade' => 'Upgrade', 'plan_change' => 'Change Plan', 'pending_change_to' => 'Changes To', @@ -1145,9 +1143,9 @@ $LANG = array( 'plan_paid' => 'Term Started', 'plan_started' => 'Plan Started', 'plan_expires' => 'Plan Expires', - + 'white_label_button' => 'White Label', - + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', 'enterprise_plan_product' => 'Enterprise Plan', @@ -1167,8 +1165,8 @@ $LANG = array( 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', 'return_to_app' => 'Return to app', - - + + // Payment updates 'refund_payment' => 'Refund Payment', 'refund_max' => 'Max:', @@ -1185,7 +1183,7 @@ $LANG = array( 'activity_39' => ':user cancelled a :payment_amount payment (:payment)', 'activity_40' => ':user refunded :adjustment of a :payment_amount payment (:payment)', 'card_expiration' => 'Exp: :expires', - + 'card_creditcardother' => 'Unknown', 'card_americanexpress' => 'American Express', 'card_carteblanche' => 'Carte Blanche', @@ -1237,7 +1235,7 @@ $LANG = array( '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. + '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.', @@ -1279,7 +1277,11 @@ $LANG = array( 'no_payment_method_specified' => 'No payment method specified', - + 'chart_type' => 'Chart Type', + 'format' => 'Format', + 'import_ofx' => 'Import OFX', + 'ofx_file' => 'OFX File', + 'ofx_parse_failed' => 'Failed to parse OFX file', // WePay 'wepay' => 'WePay', @@ -1307,4 +1309,4 @@ $LANG = array( return $LANG; -?>. \ No newline at end of file +?> From eb1d5be47c80644ba821f02ffd7c773b7363ae2a Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 21 May 2016 09:17:53 +1000 Subject: [PATCH 117/386] bugs in dashboardapicontroller --- app/Http/Controllers/DashboardApiController.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/DashboardApiController.php b/app/Http/Controllers/DashboardApiController.php index 06393e3ddc3c..dc837adb9a94 100644 --- a/app/Http/Controllers/DashboardApiController.php +++ b/app/Http/Controllers/DashboardApiController.php @@ -161,12 +161,12 @@ class DashboardApiController extends BaseAPIController $data = [ 'id' => 1, - 'paidToDate' => $paidToDate[0]->value, - 'paidToDateCurrency' => $paidToDate[0]->currency_id, - 'balances' => $balances[0]->value, - 'balancesCurrency' => $balances[0]->currency_id, - 'averageInvoice' => $averageInvoice[0]->invoice_avg, - 'averageInvoiceCurrency' => $averageInvoice[0]->currency_id, + 'paidToDate' => $paidToDate[0]->value ? $paidToDate[0]->value : 0, + 'paidToDateCurrency' => $paidToDate[0]->currency_id ? $paidToDate[0]->currency_id : 0, + 'balances' => $balances[0]->value ? $balances[0]->value : 0, + 'balancesCurrency' => $balances[0]->currency_id ? $balances[0]->currency_id : 0, + 'averageInvoice' => $averageInvoice[0]->invoice_avg ? $averageInvoice[0]->invoice_avg : 0, + 'averageInvoiceCurrency' => $averageInvoice[0]->currency_id ? $averageInvoice[0]->currency_id : 0, 'invoicesSent' => $metrics ? $metrics->invoices_sent : 0, 'activeClients' => $metrics ? $metrics->active_clients : 0, ]; From 1e370d2cce3d6848e63b76e0232362adee061953 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 21 May 2016 18:44:53 +1000 Subject: [PATCH 118/386] Implement DocumentAPIController --- .../Controllers/DocumentAPIController.php | 40 +++++++++++++++++++ app/Http/routes.php | 1 + 2 files changed, 41 insertions(+) create mode 100644 app/Http/Controllers/DocumentAPIController.php diff --git a/app/Http/Controllers/DocumentAPIController.php b/app/Http/Controllers/DocumentAPIController.php new file mode 100644 index 000000000000..561a5c174a01 --- /dev/null +++ b/app/Http/Controllers/DocumentAPIController.php @@ -0,0 +1,40 @@ +firstOrFail(); + + return DocumentController::getDownloadResponse($document); + } + + public function store() + { + //stub + } + + public function update() + { + //stub + } + + public function destroy($publicId) + { + //stub + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index 18d6e8640cfe..e4e06f76617e 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -284,6 +284,7 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() Route::post('add_token', 'AccountApiController@addDeviceToken'); Route::post('update_notifications', 'AccountApiController@updatePushNotifications'); Route::get('dashboard', 'DashboardApiController@index'); + Route::resource('documents', 'DocumentAPIController'); // Vendor Route::resource('vendors', 'VendorApiController'); From 99da41669bfeb1b38095858e1d70cc2e53aaa691 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 13:10:58 +0300 Subject: [PATCH 119/386] Fixes for #442 and #702 --- app/Models/DateFormat.php | 7 ++ app/Models/DatetimeFormat.php | 7 ++ database/seeds/DateFormatsSeeder.php | 96 ++++++------------- .../views/accounts/localization.blade.php | 14 +-- 4 files changed, 51 insertions(+), 73 deletions(-) diff --git a/app/Models/DateFormat.php b/app/Models/DateFormat.php index b343fd422657..47afafb4cc76 100644 --- a/app/Models/DateFormat.php +++ b/app/Models/DateFormat.php @@ -5,4 +5,11 @@ use Eloquent; class DateFormat extends Eloquent { public $timestamps = false; + + public function __toString() + { + $date = mktime(0, 0, 0, 12, 31, date('Y')); + + return date($this->format, $date); + } } diff --git a/app/Models/DatetimeFormat.php b/app/Models/DatetimeFormat.php index 1d7ba8b93657..6b7edf85ec63 100644 --- a/app/Models/DatetimeFormat.php +++ b/app/Models/DatetimeFormat.php @@ -5,4 +5,11 @@ use Eloquent; class DatetimeFormat extends Eloquent { public $timestamps = false; + + public function __toString() + { + $date = mktime(0, 0, 0, 12, 31, date('Y')); + + return date($this->format, $date); + } } diff --git a/database/seeds/DateFormatsSeeder.php b/database/seeds/DateFormatsSeeder.php index 40568ca3000a..208b213912a9 100644 --- a/database/seeds/DateFormatsSeeder.php +++ b/database/seeds/DateFormatsSeeder.php @@ -11,22 +11,24 @@ class DateFormatsSeeder extends Seeder // Date formats $formats = [ - ['format' => 'd/M/Y', 'picker_format' => 'dd/M/yyyy', 'label' => '10/Mar/2013'], - ['format' => 'd-M-Y', 'picker_format' => 'dd-M-yyyy', 'label' => '10-Mar-2013'], - ['format' => 'd/F/Y', 'picker_format' => 'dd/MM/yyyy', 'label' => '10/March/2013'], - ['format' => 'd-F-Y', 'picker_format' => 'dd-MM-yyyy', 'label' => '10-March-2013'], - ['format' => 'M j, Y', 'picker_format' => 'M d, yyyy', 'label' => 'Mar 10, 2013'], - ['format' => 'F j, Y', 'picker_format' => 'MM d, yyyy', 'label' => 'March 10, 2013'], - ['format' => 'D M j, Y', 'picker_format' => 'D MM d, yyyy', 'label' => 'Mon March 10, 2013'], - ['format' => 'Y-m-d', 'picker_format' => 'yyyy-mm-dd', 'label' => '2013-03-10'], - ['format' => 'd-m-Y', 'picker_format' => 'dd-mm-yyyy', 'label' => '20-03-2013'], - ['format' => 'm/d/Y', 'picker_format' => 'mm/dd/yyyy', 'label' => '03/20/2013'] + ['format' => 'd/M/Y', 'picker_format' => 'dd/M/yyyy'], + ['format' => 'd-M-Y', 'picker_format' => 'dd-M-yyyy'], + ['format' => 'd/F/Y', 'picker_format' => 'dd/MM/yyyy'], + ['format' => 'd-F-Y', 'picker_format' => 'dd-MM-yyyy'], + ['format' => 'M j, Y', 'picker_format' => 'M d, yyyy'], + ['format' => 'F j, Y', 'picker_format' => 'MM d, yyyy'], + ['format' => 'D M j, Y', 'picker_format' => 'D MM d, yyyy'], + ['format' => 'Y-m-d', 'picker_format' => 'yyyy-mm-dd'], + ['format' => 'd-m-Y', 'picker_format' => 'dd-mm-yyyy'], + ['format' => 'm/d/Y', 'picker_format' => 'mm/dd/yyyy'], + ['format' => 'd.m.Y', 'picker_format' => 'dd.mm.yyyy'], + ['format' => 'j. M. Y', 'picker_format' => 'd. M. yyyy'], + ['format' => 'j. F. Y', 'picker_format' => 'd. MM. yyyy'] ]; - + foreach ($formats as $format) { - $record = DateFormat::whereLabel($format['label'])->first(); + $record = DateFormat::whereFormat($format['format'])->first(); if ($record) { - $record->format = $format['format']; $record->picker_format = $format['picker_format']; $record->save(); } else { @@ -36,62 +38,24 @@ class DateFormatsSeeder extends Seeder // Date/time formats $formats = [ - [ - 'format' => 'd/M/Y g:i a', - 'format_moment' => 'DD/MMM/YYYY h:mm:ss a', - 'label' => '10/Mar/2013' - ], - [ - 'format' => 'd-M-Y g:i a', - 'format_moment' => 'DD-MMM-YYYY h:mm:ss a', - 'label' => '10-Mar-2013' - ], - [ - 'format' => 'd/F/Y g:i a', - 'format_moment' => 'DD/MMMM/YYYY h:mm:ss a', - 'label' => '10/March/2013' - ], - [ - 'format' => 'd-F-Y g:i a', - 'format_moment' => 'DD-MMMM-YYYY h:mm:ss a', - 'label' => '10-March-2013' - ], - [ - 'format' => 'M j, Y g:i a', - 'format_moment' => 'MMM D, YYYY h:mm:ss a', - 'label' => 'Mar 10, 2013 6:15 pm' - ], - [ - 'format' => 'F j, Y g:i a', - 'format_moment' => 'MMMM D, YYYY h:mm:ss a', - 'label' => 'March 10, 2013 6:15 pm' - ], - [ - 'format' => 'D M jS, Y g:i a', - 'format_moment' => 'ddd MMM Do, YYYY h:mm:ss a', - 'label' => 'Mon March 10th, 2013 6:15 pm' - ], - [ - 'format' => 'Y-m-d g:i a', - 'format_moment' => 'YYYY-MMM-DD h:mm:ss a', - 'label' => '2013-03-10 6:15 pm' - ], - [ - 'format' => 'd-m-Y g:i a', - 'format_moment' => 'DD-MM-YYYY h:mm:ss a', - 'label' => '20-03-2013 6:15 pm' - ], - [ - 'format' => 'm/d/Y g:i a', - 'format_moment' => 'MM/DD/YYYY h:mm:ss a', - 'label' => '03/20/2013 6:15 pm' - ] + ['format' => 'd/M/Y g:i a', 'format_moment' => 'DD/MMM/YYYY h:mm:ss a'], + ['format' => 'd-M-Y g:i a', 'format_moment' => 'DD-MMM-YYYY h:mm:ss a'], + ['format' => 'd/F/Y g:i a', 'format_moment' => 'DD/MMMM/YYYY h:mm:ss a'], + ['format' => 'd-F-Y g:i a', 'format_moment' => 'DD-MMMM-YYYY h:mm:ss a'], + ['format' => 'M j, Y g:i a', 'format_moment' => 'MMM D, YYYY h:mm:ss a'], + ['format' => 'F j, Y g:i a', 'format_moment' => 'MMMM D, YYYY h:mm:ss a'], + ['format' => 'D M j, Y g:i a', 'format_moment' => 'ddd MMM D, YYYY h:mm:ss a'], + ['format' => 'Y-m-d g:i a', 'format_moment' => 'YYYY-MMM-DD h:mm:ss a'], + ['format' => 'd-m-Y g:i a', 'format_moment' => 'DD-MM-YYYY h:mm:ss a'], + ['format' => 'm/d/Y g:i a', 'format_moment' => 'MM/DD/YYYY h:mm:ss a'], + ['format' => 'd.m.Y g:i a', 'format_moment' => 'D.MM.YYYY h:mm:ss a'], + ['format' => 'j. M. Y g:i a', 'format_moment' => 'DD. MMM. YYYY h:mm:ss a'], + ['format' => 'j. F. Y g:i a', 'format_moment' => 'DD. MMMM. YYYY h:mm:ss a'] ]; - + foreach ($formats as $format) { - $record = DatetimeFormat::whereLabel($format['label'])->first(); + $record = DatetimeFormat::whereFormat($format['format'])->first(); if ($record) { - $record->format = $format['format']; $record->format_moment = $format['format_moment']; $record->save(); } else { diff --git a/resources/views/accounts/localization.blade.php b/resources/views/accounts/localization.blade.php index b921c8710567..ccca9f3c4e20 100644 --- a/resources/views/accounts/localization.blade.php +++ b/resources/views/accounts/localization.blade.php @@ -1,6 +1,6 @@ @extends('header') -@section('content') +@section('content') @parent {!! Former::open_for_files()->addClass('warn-on-exit') !!} @@ -11,7 +11,7 @@ @include('accounts.nav', ['selected' => ACCOUNT_LOCALIZATION])
- +
@@ -21,15 +21,15 @@
{!! Former::select('currency_id')->addOption('','') - ->fromQuery($currencies, 'name', 'id') !!} + ->fromQuery($currencies, 'name', 'id') !!} {!! Former::select('language_id')->addOption('','') - ->fromQuery($languages, 'name', 'id') !!} + ->fromQuery($languages, 'name', 'id') !!} {!! Former::select('timezone_id')->addOption('','') ->fromQuery($timezones, 'location', 'id') !!} {!! Former::select('date_format_id')->addOption('','') - ->fromQuery($dateFormats, 'label', 'id') !!} + ->fromQuery($dateFormats) !!} {!! Former::select('datetime_format_id')->addOption('','') - ->fromQuery($datetimeFormats, 'label', 'id') !!} + ->fromQuery($datetimeFormats) !!} {!! Former::checkbox('military_time')->text(trans('texts.enable')) !!} {{-- Former::checkbox('show_currency_code')->text(trans('texts.enable')) --}} @@ -48,4 +48,4 @@ @section('onReady') $('#currency_id').focus(); -@stop \ No newline at end of file +@stop From 614044364b6d6b5b897205000593eab2a26909dd Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 13:13:24 +0300 Subject: [PATCH 120/386] Fixes for #442 and #702 --- database/seeds/DateFormatsSeeder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/seeds/DateFormatsSeeder.php b/database/seeds/DateFormatsSeeder.php index 208b213912a9..487d4b83619e 100644 --- a/database/seeds/DateFormatsSeeder.php +++ b/database/seeds/DateFormatsSeeder.php @@ -23,7 +23,7 @@ class DateFormatsSeeder extends Seeder ['format' => 'm/d/Y', 'picker_format' => 'mm/dd/yyyy'], ['format' => 'd.m.Y', 'picker_format' => 'dd.mm.yyyy'], ['format' => 'j. M. Y', 'picker_format' => 'd. M. yyyy'], - ['format' => 'j. F. Y', 'picker_format' => 'd. MM. yyyy'] + ['format' => 'j. F Y', 'picker_format' => 'd. MM yyyy'] ]; foreach ($formats as $format) { @@ -50,7 +50,7 @@ class DateFormatsSeeder extends Seeder ['format' => 'm/d/Y g:i a', 'format_moment' => 'MM/DD/YYYY h:mm:ss a'], ['format' => 'd.m.Y g:i a', 'format_moment' => 'D.MM.YYYY h:mm:ss a'], ['format' => 'j. M. Y g:i a', 'format_moment' => 'DD. MMM. YYYY h:mm:ss a'], - ['format' => 'j. F. Y g:i a', 'format_moment' => 'DD. MMMM. YYYY h:mm:ss a'] + ['format' => 'j. F Y g:i a', 'format_moment' => 'DD. MMMM YYYY h:mm:ss a'] ]; foreach ($formats as $format) { From d0232e00f6f23755e4d7c14831d4b47237c3f660 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 15:31:41 +0300 Subject: [PATCH 121/386] Hide outstanding on dashboard from non-admins --- .../Controllers/DashboardApiController.php | 9 +++- app/Http/Controllers/DashboardController.php | 41 +++++++++++-------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/app/Http/Controllers/DashboardApiController.php b/app/Http/Controllers/DashboardApiController.php index 06393e3ddc3c..4d73d0ae2778 100644 --- a/app/Http/Controllers/DashboardApiController.php +++ b/app/Http/Controllers/DashboardApiController.php @@ -80,8 +80,13 @@ class DashboardApiController extends BaseAPIController ->where('accounts.id', '=', Auth::user()->account_id) ->where('clients.is_deleted', '=', false) ->groupBy('accounts.id') - ->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END')) - ->get(); + ->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END')); + + if (!$view_all) { + $balances->where('clients.user_id', '=', $user_id); + } + + $balances = $balances->get(); $pastDue = DB::table('invoices') ->leftJoin('clients', 'clients.id', '=', 'invoices.client_id') diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 5daa8827883a..718d73c67d42 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -13,7 +13,7 @@ class DashboardController extends BaseController { $view_all = Auth::user()->hasPermission('view_all'); $user_id = Auth::user()->id; - + // total_income, billed_clients, invoice_sent and active_clients $select = DB::raw('COUNT(DISTINCT CASE WHEN invoices.id IS NOT NULL THEN clients.id ELSE null END) billed_clients, SUM(CASE WHEN invoices.invoice_status_id >= '.INVOICE_STATUS_SENT.' THEN 1 ELSE 0 END) invoices_sent, @@ -27,17 +27,17 @@ class DashboardController extends BaseController ->where('invoices.is_deleted', '=', false) ->where('invoices.is_recurring', '=', false) ->where('invoices.is_quote', '=', false); - + if(!$view_all){ $metrics = $metrics->where(function($query) use($user_id){ $query->where('invoices.user_id', '=', $user_id); $query->orwhere(function($query) use($user_id){ - $query->where('invoices.user_id', '=', null); + $query->where('invoices.user_id', '=', null); $query->where('clients.user_id', '=', $user_id); }); }); } - + $metrics = $metrics->groupBy('accounts.id') ->first(); @@ -47,11 +47,11 @@ class DashboardController extends BaseController ->leftJoin('clients', 'accounts.id', '=', 'clients.account_id') ->where('accounts.id', '=', Auth::user()->account_id) ->where('clients.is_deleted', '=', false); - + if(!$view_all){ $paidToDate = $paidToDate->where('clients.user_id', '=', $user_id); } - + $paidToDate = $paidToDate->groupBy('accounts.id') ->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END')) ->get(); @@ -66,11 +66,11 @@ class DashboardController extends BaseController ->where('invoices.is_deleted', '=', false) ->where('invoices.is_quote', '=', false) ->where('invoices.is_recurring', '=', false); - + if(!$view_all){ $averageInvoice = $averageInvoice->where('invoices.user_id', '=', $user_id); } - + $averageInvoice = $averageInvoice->groupBy('accounts.id') ->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END')) ->get(); @@ -82,16 +82,21 @@ class DashboardController extends BaseController ->where('accounts.id', '=', Auth::user()->account_id) ->where('clients.is_deleted', '=', false) ->groupBy('accounts.id') - ->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END')) - ->get(); + ->groupBy(DB::raw('CASE WHEN clients.currency_id IS NULL THEN CASE WHEN accounts.currency_id IS NULL THEN 1 ELSE accounts.currency_id END ELSE clients.currency_id END')); + + if (!$view_all) { + $balances->where('clients.user_id', '=', $user_id); + } + + $balances = $balances->get(); $activities = Activity::where('activities.account_id', '=', Auth::user()->account_id) ->where('activities.activity_type_id', '>', 0); - + if(!$view_all){ $activities = $activities->where('activities.user_id', '=', $user_id); } - + $activities = $activities->orderBy('activities.created_at', 'desc') ->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account') ->take(50) @@ -111,11 +116,11 @@ class DashboardController extends BaseController ->where('invoices.deleted_at', '=', null) ->where('contacts.is_primary', '=', true) ->where('invoices.due_date', '<', date('Y-m-d')); - + if(!$view_all){ $pastDue = $pastDue->where('invoices.user_id', '=', $user_id); } - + $pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote']) ->orderBy('invoices.due_date', 'asc') ->take(50) @@ -136,11 +141,11 @@ class DashboardController extends BaseController ->where('contacts.is_primary', '=', true) ->where('invoices.due_date', '>=', date('Y-m-d')) ->orderBy('invoices.due_date', 'asc'); - + if(!$view_all){ $upcoming = $upcoming->where('invoices.user_id', '=', $user_id); } - + $upcoming = $upcoming->take(50) ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote']) ->get(); @@ -155,11 +160,11 @@ class DashboardController extends BaseController ->where('clients.is_deleted', '=', false) ->where('contacts.deleted_at', '=', null) ->where('contacts.is_primary', '=', true); - + if(!$view_all){ $payments = $payments->where('payments.user_id', '=', $user_id); } - + $payments = $payments->select(['payments.payment_date', 'payments.amount', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id']) ->orderBy('payments.payment_date', 'desc') ->take(50) From 0769ae99166751c70a5855d47a861905e706cb86 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 16:55:25 +0300 Subject: [PATCH 122/386] Fix for random invoice design in automated tests --- database/seeds/UserTableSeeder.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/database/seeds/UserTableSeeder.php b/database/seeds/UserTableSeeder.php index dbaba660b75c..00a081cb5412 100644 --- a/database/seeds/UserTableSeeder.php +++ b/database/seeds/UserTableSeeder.php @@ -20,7 +20,7 @@ class UserTableSeeder extends Seeder $faker = Faker\Factory::create(); $company = Company::create(); - + $account = Account::create([ 'name' => $faker->name, 'address1' => $faker->streetAddress, @@ -28,12 +28,12 @@ class UserTableSeeder extends Seeder 'city' => $faker->city, 'state' => $faker->state, 'postal_code' => $faker->postcode, - 'country_id' => Country::all()->random()->id, + 'country_id' => Country::all()->random()->id, 'account_key' => str_random(RANDOM_KEY_LENGTH), 'invoice_terms' => $faker->text($faker->numberBetween(50, 300)), 'work_phone' => $faker->phoneNumber, 'work_email' => $faker->safeEmail, - 'invoice_design_id' => min(InvoiceDesign::all()->random()->id, 10), + 'invoice_design_id' => InvoiceDesign::where('id', '<', CUSTOM_DESIGN)->get()->random()->id, 'header_font_id' => min(Font::all()->random()->id, 17), 'body_font_id' => min(Font::all()->random()->id, 17), 'primary_color' => $faker->hexcolor, @@ -55,7 +55,7 @@ class UserTableSeeder extends Seeder Affiliate::create([ 'affiliate_key' => SELF_HOST_AFFILIATE_KEY ]); - + } -} \ No newline at end of file +} From 142082b4470a5c2cfce6aefc01bbe3f1e08afec5 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 18:11:24 +0300 Subject: [PATCH 123/386] Remove download PDF from recurring invoices --- resources/views/invoices/edit.blade.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 9deb5322452a..8fb079d079a0 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -527,8 +527,10 @@ {!! Former::select('invoice_design_id')->style('display:inline;width:150px;background-color:white !important')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id") !!} @endif - {!! Button::primary(trans('texts.download_pdf'))->withAttributes(array('onclick' => 'onDownloadClick()'))->appendIcon(Icon::create('download-alt')) !!} - + @if ( ! $invoice->is_recurring) + {!! Button::primary(trans('texts.download_pdf'))->withAttributes(array('onclick' => 'onDownloadClick()'))->appendIcon(Icon::create('download-alt')) !!} + @endif + @if ($invoice->isClientTrashed()) @elseif ($invoice->trashed()) From bb2fa9a18ec4eb8cdca369337c5e1961ad68b8d5 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 19:50:45 +0300 Subject: [PATCH 124/386] Reverted date/time format change --- database/seeds/DateFormatsSeeder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/seeds/DateFormatsSeeder.php b/database/seeds/DateFormatsSeeder.php index 487d4b83619e..c55028b6840c 100644 --- a/database/seeds/DateFormatsSeeder.php +++ b/database/seeds/DateFormatsSeeder.php @@ -44,7 +44,7 @@ class DateFormatsSeeder extends Seeder ['format' => 'd-F-Y g:i a', 'format_moment' => 'DD-MMMM-YYYY h:mm:ss a'], ['format' => 'M j, Y g:i a', 'format_moment' => 'MMM D, YYYY h:mm:ss a'], ['format' => 'F j, Y g:i a', 'format_moment' => 'MMMM D, YYYY h:mm:ss a'], - ['format' => 'D M j, Y g:i a', 'format_moment' => 'ddd MMM D, YYYY h:mm:ss a'], + ['format' => 'D M jS, Y g:i a', 'format_moment' => 'ddd MMM Do, YYYY h:mm:ss a'], ['format' => 'Y-m-d g:i a', 'format_moment' => 'YYYY-MMM-DD h:mm:ss a'], ['format' => 'd-m-Y g:i a', 'format_moment' => 'DD-MM-YYYY h:mm:ss a'], ['format' => 'm/d/Y g:i a', 'format_moment' => 'MM/DD/YYYY h:mm:ss a'], From ac09d86f689bb4c4ecf2c9056ea54bf81802b9ae Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 19:51:09 +0300 Subject: [PATCH 125/386] Show warning when changing recurring invoice start date --- resources/lang/en/texts.php | 5 +++ resources/views/invoices/edit.blade.php | 31 ++++++++++++----- resources/views/invoices/knockout.blade.php | 37 +++++++++++---------- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index f73e936f5fb5..aac277bb583c 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1305,6 +1305,11 @@ $LANG = array( 'canada' => 'Canada', 'accept_debit_cards' => 'Accept Debit Cards', 'debit_cards' => 'Debit Cards', + + 'warn_start_date_changed' => 'The next invoice will be sent on the new start date.', + 'original_start_date' => 'Original start date', + 'new_start_date' => 'New start date', + ); return $LANG; diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 8fb079d079a0..a39690f613e7 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -530,7 +530,7 @@ @if ( ! $invoice->is_recurring) {!! Button::primary(trans('texts.download_pdf'))->withAttributes(array('onclick' => 'onDownloadClick()'))->appendIcon(Icon::create('download-alt')) !!} @endif - + @if ($invoice->isClientTrashed()) @elseif ($invoice->trashed()) @@ -809,6 +809,7 @@ var invoice = {!! $invoice !!}; ko.mapping.fromJS(invoice, model.invoice().mapping, model.invoice); model.invoice().is_recurring({{ $invoice->is_recurring ? '1' : '0' }}); + model.invoice().start_date_orig(model.invoice().start_date()); @if ($invoice->id) var invitationContactIds = {!! json_encode($invitationContactIds) !!}; @@ -1192,14 +1193,26 @@ } function onSaveClick() { - if (model.invoice().is_recurring() && {{ $invoice ? 'false' : 'true' }}) { - if (confirm("{!! trans("texts.confirm_recurring_email_$entityType") !!}" + '\n\n' + getSendToEmails() + '\n' + "{!! trans("texts.confirm_recurring_timing") !!}")) { - submitAction(''); - } - } else { - preparePdfData(''); - } - } + if (model.invoice().is_recurring()) { + // warn invoice will be emailed when saving new recurring invoice + if ({{ $invoice->exists() ? 'false' : 'true' }}) { + if (confirm("{!! trans("texts.confirm_recurring_email_$entityType") !!}" + '\n\n' + getSendToEmails() + '\n' + "{!! trans("texts.confirm_recurring_timing") !!}")) { + submitAction(''); + } + return; + // warn invoice will be emailed again if start date is changed + } else if (model.invoice().start_date() != model.invoice().start_date_orig()) { + if (confirm("{!! trans("texts.warn_start_date_changed") !!}" + '\n\n' + + "{!! trans("texts.original_start_date") !!}: " + model.invoice().start_date_orig() + '\n' + + "{!! trans("texts.new_start_date") !!}: " + model.invoice().start_date())) { + submitAction(''); + } + return; + } + } + + preparePdfData(''); + } function getSendToEmails() { var client = model.invoice().client(); diff --git a/resources/views/invoices/knockout.blade.php b/resources/views/invoices/knockout.blade.php index f92055b5df34..ab2e892c25f2 100644 --- a/resources/views/invoices/knockout.blade.php +++ b/resources/views/invoices/knockout.blade.php @@ -55,7 +55,7 @@ function ViewModel(data) { if (self.invoice().tax_name1() || self.invoice().tax_name2()) { return true; } - + return self.invoice_taxes() && {{ count($taxRateOptions) ? 'true' : 'false' }}; }); @@ -100,11 +100,11 @@ function ViewModel(data) { $('input.client-email').each(function(item, value) { var $email = $(value); var email = $(value).val(); - + // Trim whitespace email = (email || '').trim(); $email.val(email); - + if (!firstName && (!email || !isValidEmailAddress(email))) { isValid = false; } @@ -180,6 +180,7 @@ function InvoiceModel(data) { self.due_date = ko.observable(''); self.recurring_due_date = ko.observable(''); self.start_date = ko.observable(''); + self.start_date_orig = ko.observable(''); self.end_date = ko.observable(''); self.last_sent_date = ko.observable(''); self.tax_name1 = ko.observable(); @@ -240,13 +241,13 @@ function InvoiceModel(data) { applyComboboxListeners(); return itemModel; } - + self.addDocument = function() { var documentModel = new DocumentModel(); self.documents.push(documentModel); return documentModel; } - + self.removeDocument = function(doc) { var public_id = doc.public_id?doc.public_id():doc; self.documents.remove(function(document) { @@ -291,7 +292,7 @@ function InvoiceModel(data) { self.tax_rate2(rate); } }) - + self.wrapped_terms = ko.computed({ read: function() { return this.terms(); @@ -386,7 +387,7 @@ function InvoiceModel(data) { var taxRate2 = parseFloat(self.tax_rate2()); var tax2 = roundToTwo(total * (taxRate2/100)); - + return self.formatMoney(tax1 + tax2); }); @@ -403,7 +404,7 @@ function InvoiceModel(data) { lineTotal -= roundToTwo(lineTotal * (self.discount()/100)); } } - + var taxAmount = roundToTwo(lineTotal * item.tax_rate1() / 100); if (taxAmount) { var key = item.tax_name1() + item.tax_rate1(); @@ -664,12 +665,12 @@ function ContactModel(data) { return str; }); - + self.info_color = ko.computed(function() { if (self.invitation_viewed()) { return '#57D172'; } else if (self.invitation_openend()) { - return '#FFCC00'; + return '#FFCC00'; } else { return '#B1B5BA'; } @@ -780,7 +781,7 @@ function ItemModel(data) { this.onSelect = function() {} } - + function DocumentModel(data) { var self = this; self.public_id = ko.observable(0); @@ -788,16 +789,16 @@ function DocumentModel(data) { self.name = ko.observable(''); self.type = ko.observable(''); self.url = ko.observable(''); - + self.update = function(data){ ko.mapping.fromJS(data, {}, this); } - + if (data) { self.update(data); - } + } } - + var ExpenseModel = function(data) { var self = this; @@ -808,7 +809,7 @@ var ExpenseModel = function(data) { } } } - + self.description = ko.observable(''); self.qty = ko.observable(0); self.public_id = ko.observable(0); @@ -825,7 +826,7 @@ ko.bindingHandlers.typeahead = { init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { var $element = $(element); var allBindings = allBindingsAccessor(); - + $element.typeahead({ highlight: true, minLength: 0, @@ -875,4 +876,4 @@ ko.bindingHandlers.typeahead = { } }; - \ No newline at end of file + From 371f224c24aefdfc071ae42fee8aa2f235eaf095 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 20:01:37 +0300 Subject: [PATCH 126/386] Minor fixes for invoice export --- app/Http/Controllers/ExportController.php | 23 ++++++++++++----------- app/Ninja/Presenters/InvoicePresenter.php | 4 +++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index ebaaa1f6c306..c7f30b03be29 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -84,13 +84,14 @@ class ExportController extends BaseController if ($key === 'account' || $key === 'title' || $key === 'multiUser') { continue; } + if ($key === 'recurringInvoices') { + $key = 'recurring_invoices'; + } $label = trans("texts.{$key}"); $excel->sheet($label, function($sheet) use ($key, $data) { if ($key === 'quotes') { $key = 'invoices'; $data['entityType'] = ENTITY_QUOTE; - } elseif ($key === 'recurringInvoices') { - $key = 'recurring_invoices'; } $sheet->loadView("export.{$key}", $data); }); @@ -107,7 +108,7 @@ class ExportController extends BaseController 'title' => 'Invoice Ninja v' . NINJA_VERSION . ' - ' . $account->formatDateTime($account->getDateTime()), 'multiUser' => $account->users->count() > 1 ]; - + if ($request->input(ENTITY_CLIENT)) { $data['clients'] = Client::scope() ->with('user', 'contacts', 'country') @@ -123,14 +124,14 @@ class ExportController extends BaseController ->with('user', 'client.contacts') ->get(); } - + if ($request->input(ENTITY_TASK)) { $data['tasks'] = Task::scope() ->with('user', 'client.contacts') ->withArchived() ->get(); } - + if ($request->input(ENTITY_INVOICE)) { $data['invoices'] = Invoice::scope() ->with('user', 'client.contacts', 'invoice_status') @@ -138,7 +139,7 @@ class ExportController extends BaseController ->where('is_quote', '=', false) ->where('is_recurring', '=', false) ->get(); - + $data['quotes'] = Invoice::scope() ->with('user', 'client.contacts', 'invoice_status') ->withArchived() @@ -153,7 +154,7 @@ class ExportController extends BaseController ->where('is_recurring', '=', true) ->get(); } - + if ($request->input(ENTITY_PAYMENT)) { $data['payments'] = Payment::scope() ->withArchived() @@ -161,7 +162,7 @@ class ExportController extends BaseController ->get(); } - + if ($request->input(ENTITY_VENDOR)) { $data['clients'] = Vendor::scope() ->with('user', 'vendor_contacts', 'country') @@ -172,14 +173,14 @@ class ExportController extends BaseController ->with('user', 'vendor.vendor_contacts') ->withTrashed() ->get(); - + /* $data['expenses'] = Credit::scope() ->with('user', 'client.contacts') ->get(); */ } - + return $data; } -} \ No newline at end of file +} diff --git a/app/Ninja/Presenters/InvoicePresenter.php b/app/Ninja/Presenters/InvoicePresenter.php index 36a2ce2acbd6..7c67fc98110b 100644 --- a/app/Ninja/Presenters/InvoicePresenter.php +++ b/app/Ninja/Presenters/InvoicePresenter.php @@ -45,6 +45,8 @@ class InvoicePresenter extends Presenter { return trans('texts.deleted'); } elseif ($this->entity->trashed()) { return trans('texts.archived'); + } elseif ($this->entity->is_recurring) { + return trans('texts.active'); } else { $status = $this->entity->invoice_status ? $this->entity->invoice_status->name : 'draft'; $status = strtolower($status); @@ -82,4 +84,4 @@ class InvoicePresenter extends Presenter { $client = $this->entity->client; return count($client->contacts) ? $client->contacts[0]->email : ''; } -} \ No newline at end of file +} From 3b0feef87fca3ea0eba0ccb3ed366d0995129c7a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 20:26:19 +0300 Subject: [PATCH 127/386] Fix for attached PDF being in the wrong language --- resources/views/invoices/edit.blade.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index a39690f613e7..699205f8cfb0 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1185,10 +1185,17 @@ if (!isEmailValid()) { alert("{!! trans('texts.provide_email') !!}"); return; - } +8 } if (confirm('{!! trans("texts.confirm_email_$entityType") !!}' + '\n\n' + getSendToEmails())) { - preparePdfData('email'); + var accountLanguageId = parseInt({{ $account->language_id ?: '0' }}); + var clientLanguageId = parseInt(model.invoice().client().language_id()) || 0; + // if the client's language is different then we can't use the browser version of the PDF + if (clientLanguageId && clientLanguageId != accountLanguageId) { + submitAction('email'); + } else { + preparePdfData('email'); + } } } From cdfaae2700922f3ed7fc94a653338ee54f0d8c17 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 20:31:26 +0300 Subject: [PATCH 128/386] Minor change to client portal settings page --- resources/lang/en/texts.php | 1 + .../views/accounts/client_portal.blade.php | 20 ++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index aac277bb583c..28b310160e89 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1309,6 +1309,7 @@ $LANG = array( 'warn_start_date_changed' => 'The next invoice will be sent on the new start date.', 'original_start_date' => 'Original start date', 'new_start_date' => 'New start date', + 'security' => 'Security', ); diff --git a/resources/views/accounts/client_portal.blade.php b/resources/views/accounts/client_portal.blade.php index 1df146123fa3..14883901831c 100644 --- a/resources/views/accounts/client_portal.blade.php +++ b/resources/views/accounts/client_portal.blade.php @@ -30,9 +30,10 @@
+
-

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

+

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

@@ -45,20 +46,29 @@ ->text(trans('texts.enable')) ->help(trans('texts.enable_client_portal_dashboard_help')) !!}
+
+
+ +
+
+

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

+
+
{!! Former::checkbox('enable_portal_password') - ->text(trans('texts.enable_portal_password')) + ->text(trans('texts.enable')) ->help(trans('texts.enable_portal_password_help')) - ->label(' ') !!} + ->label(trans('texts.enable_portal_password')) !!}
{!! Former::checkbox('send_portal_password') - ->text(trans('texts.send_portal_password')) + ->text(trans('texts.enable')) ->help(trans('texts.send_portal_password_help')) - ->label(' ') !!} + ->label(trans('texts.send_portal_password')) !!}
+ @if (Utils::hasFeature(FEATURE_CLIENT_PORTAL_CSS))
From 4eab4b60ded94e60ee46fefdc738681fc93f6372 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 22:01:30 +0300 Subject: [PATCH 129/386] Updated the Transifex link --- CONTRIBUTING.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60ac1176aa10..c271968668be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,4 +7,4 @@ We welcome contributions! We'll improve this guide over time... Guidelines * Try to follow [PSR-2 guidlines](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) * Create pull requests against the develop branch -* Submit translations through [Transifex](https://www.transifex.com/invoice-ninja/) +* Submit translations through [Transifex](https://www.transifex.com/invoice-ninja/invoice-ninja/) diff --git a/README.md b/README.md index 03362f3a6e6f..a3133c4c1010 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ ### Pull Requests * Please create pull requests against the develop branch -* Submit translations through [Transifex](https://www.transifex.com/invoice-ninja/) +* Submit translations through [Transifex](https://www.transifex.com/invoice-ninja/invoice-ninja/) ### Contributors * [Troels Liebe Bentsen](https://github.com/tlbdk) From 6cfee6ec70f726c168805c277363a2be2c5669cc Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 22 May 2016 22:14:47 +0300 Subject: [PATCH 130/386] Open document when clicked --- resources/views/expenses/edit.blade.php | 6 ++++++ resources/views/invoices/edit.blade.php | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/resources/views/expenses/edit.blade.php b/resources/views/expenses/edit.blade.php index b6eb65f763c1..41af3f1e801e 100644 --- a/resources/views/expenses/edit.blade.php +++ b/resources/views/expenses/edit.blade.php @@ -364,6 +364,12 @@ @if (Auth::user()->account->hasFeature(FEATURE_DOCUMENTS)) function handleDocumentAdded(file){ + // open document when clicked + if (file.url) { + file.previewElement.addEventListener("click", function() { + window.open(file.url, '_blank'); + }); + } if(file.mock)return; file.index = model.documents().length; model.addDocument({name:file.name, size:file.size, type:file.type}); diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 699205f8cfb0..3ca6acafba59 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1425,6 +1425,12 @@ @if ($account->hasFeature(FEATURE_DOCUMENTS)) function handleDocumentAdded(file){ + // open document when clicked + if (file.url) { + file.previewElement.addEventListener("click", function() { + window.open(file.url, '_blank'); + }); + } if(file.mock)return; file.index = model.invoice().documents().length; model.invoice().addDocument({name:file.name, size:file.size, type:file.type}); From 86240affab293bc1a1280dccd08aca812564107c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 23 May 2016 09:02:31 +0300 Subject: [PATCH 131/386] Show company name in client portal dashboard activities --- app/Http/Controllers/PublicClientController.php | 2 +- app/Ninja/Repositories/ActivityRepository.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index 79e236d70fc6..9d005bcc41e7 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -323,7 +323,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->account_name), 'invoice' => $model->invoice, 'contact' => Utils::getClientDisplayName($model), 'payment' => $model->payment ? ' ' . $model->payment : '', diff --git a/app/Ninja/Repositories/ActivityRepository.php b/app/Ninja/Repositories/ActivityRepository.php index 60474dc9580d..d0ea33981117 100644 --- a/app/Ninja/Repositories/ActivityRepository.php +++ b/app/Ninja/Repositories/ActivityRepository.php @@ -91,6 +91,7 @@ class ActivityRepository 'invoices.public_id as invoice_public_id', 'invoices.is_recurring', 'clients.name as client_name', + 'accounts.name as account_name', 'clients.public_id as client_public_id', 'contacts.id as contact', 'contacts.first_name as first_name', @@ -102,4 +103,4 @@ class ActivityRepository ); } -} \ No newline at end of file +} From f19058e35ec7a7ddfbaa215a6a12732064222ed1 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 23 May 2016 09:18:39 +0300 Subject: [PATCH 132/386] Improved message after update --- app/Http/Controllers/AppController.php | 7 ++++++- resources/lang/en/texts.php | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/AppController.php b/app/Http/Controllers/AppController.php index a602ae7ecb85..b6f779b9379f 100644 --- a/app/Http/Controllers/AppController.php +++ b/app/Http/Controllers/AppController.php @@ -268,7 +268,12 @@ class AppController extends BaseController Artisan::call('migrate', array('--force' => true)); Artisan::call('db:seed', array('--force' => true, '--class' => "UpdateSeeder")); Event::fire(new UserSettingsChanged()); - Session::flash('message', trans('texts.processed_updates')); + + // show message with link to Trello board + $message = trans('texts.see_whats_new', ['version' => NINJA_VERSION]); + $message = link_to(RELEASES_URL, $message, ['target' => '_blank']); + $message = sprintf('%s - %s', trans('texts.processed_updates'), $message); + Session::flash('warning', $message); } catch (Exception $e) { Utils::logError($e); return Response::make($e->getMessage(), 500); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 28b310160e89..2b9f2e5f4aec 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1310,7 +1310,7 @@ $LANG = array( 'original_start_date' => 'Original start date', 'new_start_date' => 'New start date', 'security' => 'Security', - + 'see_whats_new' => 'See what\'s new in v:version', ); return $LANG; From 260b9212b93ad0729200f61f38885b958f8984ba Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 23 May 2016 11:31:49 +0300 Subject: [PATCH 133/386] Renamed PublicClientController to ClientPortalController --- ...troller.php => ClientPortalController.php} | 2 +- app/Http/routes.php | 48 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) rename app/Http/Controllers/{PublicClientController.php => ClientPortalController.php} (99%) diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/ClientPortalController.php similarity index 99% rename from app/Http/Controllers/PublicClientController.php rename to app/Http/Controllers/ClientPortalController.php index 9d005bcc41e7..56dd183b19fe 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -26,7 +26,7 @@ use App\Events\QuoteInvitationWasViewed; use App\Services\PaymentService; use Barracuda\ArchiveStream\ZipArchive; -class PublicClientController extends BaseController +class ClientPortalController extends BaseController { private $invoiceRepo; private $paymentRepo; diff --git a/app/Http/routes.php b/app/Http/routes.php index 18d6e8640cfe..9f97eea1e9d1 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -37,36 +37,36 @@ Route::post('/get_started', 'AccountController@getStarted'); // Client visible pages Route::group(['middleware' => 'auth:client'], function() { - Route::get('view/{invitation_key}', 'PublicClientController@view'); - Route::get('download/{invitation_key}', 'PublicClientController@download'); + Route::get('view/{invitation_key}', 'ClientPortalController@view'); + Route::get('download/{invitation_key}', 'ClientPortalController@download'); Route::get('view', 'HomeController@viewLogo'); Route::get('approve/{invitation_key}', 'QuoteController@approve'); 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}/{source_id?}', '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/invoices/recurring', 'PublicClientController@recurringInvoiceIndex'); - Route::post('client/invoices/auto_bill', 'PublicClientController@setAutoBill'); - Route::get('client/documents', 'PublicClientController@documentIndex'); - Route::get('client/payments', 'PublicClientController@paymentIndex'); - Route::get('client/dashboard', 'PublicClientController@dashboard'); - Route::get('client/documents/js/{documents}/{filename}', 'PublicClientController@getDocumentVFSJS'); - Route::get('client/documents/{invitation_key}/{documents}/{filename?}', 'PublicClientController@getDocument'); - Route::get('client/documents/{invitation_key}/{filename?}', 'PublicClientController@getInvoiceDocumentsZip'); + Route::get('client/paymentmethods', 'ClientPortalController@paymentMethods'); + Route::post('client/paymentmethods/verify', 'ClientPortalController@verifyPaymentMethod'); + Route::get('client/paymentmethods/add/{payment_type}/{source_id?}', 'ClientPortalController@addPaymentMethod'); + Route::post('client/paymentmethods/add/{payment_type}', 'ClientPortalController@postAddPaymentMethod'); + Route::post('client/paymentmethods/default', 'ClientPortalController@setDefaultPaymentMethod'); + Route::post('client/paymentmethods/{source_id}/remove', 'ClientPortalController@removePaymentMethod'); + Route::get('client/quotes', 'ClientPortalController@quoteIndex'); + Route::get('client/invoices', 'ClientPortalController@invoiceIndex'); + Route::get('client/invoices/recurring', 'ClientPortalController@recurringInvoiceIndex'); + Route::post('client/invoices/auto_bill', 'ClientPortalController@setAutoBill'); + Route::get('client/documents', 'ClientPortalController@documentIndex'); + Route::get('client/payments', 'ClientPortalController@paymentIndex'); + Route::get('client/dashboard', 'ClientPortalController@dashboard'); + 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'); - Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'PublicClientController@quoteDatatable')); - Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'PublicClientController@invoiceDatatable')); - Route::get('api/client.recurring_invoices', array('as'=>'api.client.recurring_invoices', 'uses'=>'PublicClientController@recurringInvoiceDatatable')); - Route::get('api/client.documents', array('as'=>'api.client.documents', 'uses'=>'PublicClientController@documentDatatable')); - Route::get('api/client.payments', array('as'=>'api.client.payments', 'uses'=>'PublicClientController@paymentDatatable')); - Route::get('api/client.activity', array('as'=>'api.client.activity', 'uses'=>'PublicClientController@activityDatatable')); + Route::get('api/client.quotes', array('as'=>'api.client.quotes', 'uses'=>'ClientPortalController@quoteDatatable')); + Route::get('api/client.invoices', array('as'=>'api.client.invoices', 'uses'=>'ClientPortalController@invoiceDatatable')); + Route::get('api/client.recurring_invoices', array('as'=>'api.client.recurring_invoices', 'uses'=>'ClientPortalController@recurringInvoiceDatatable')); + Route::get('api/client.documents', array('as'=>'api.client.documents', 'uses'=>'ClientPortalController@documentDatatable')); + Route::get('api/client.payments', array('as'=>'api.client.payments', 'uses'=>'ClientPortalController@paymentDatatable')); + Route::get('api/client.activity', array('as'=>'api.client.activity', 'uses'=>'ClientPortalController@activityDatatable')); }); From 4b1b80886d4484d91c6c7e44294851e7a1cb3ae6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 23 May 2016 12:26:08 +0300 Subject: [PATCH 134/386] Implemented base entity presenter --- app/Ninja/Presenters/ClientPresenter.php | 15 ++------------ app/Ninja/Presenters/CreditPresenter.php | 5 ++--- app/Ninja/Presenters/EntityPresenter.php | 25 +++++++++++++++++++++++ app/Ninja/Presenters/ExpensePresenter.php | 11 +++------- app/Ninja/Presenters/InvoicePresenter.php | 13 +----------- app/Ninja/Presenters/PaymentPresenter.php | 15 ++------------ app/Ninja/Presenters/TaskPresenter.php | 7 ++----- app/Ninja/Presenters/VendorPresenter.php | 13 +++--------- 8 files changed, 40 insertions(+), 64 deletions(-) create mode 100644 app/Ninja/Presenters/EntityPresenter.php diff --git a/app/Ninja/Presenters/ClientPresenter.php b/app/Ninja/Presenters/ClientPresenter.php index bb32a8b62fb0..22ed376be6c6 100644 --- a/app/Ninja/Presenters/ClientPresenter.php +++ b/app/Ninja/Presenters/ClientPresenter.php @@ -2,9 +2,8 @@ use URL; use Utils; -use Laracasts\Presenter\Presenter; -class ClientPresenter extends Presenter { +class ClientPresenter extends EntityPresenter { public function country() { @@ -28,14 +27,4 @@ class ClientPresenter extends Presenter { return "{$text}"; } - - public function url() - { - return URL::to('/clients/' . $this->entity->public_id); - } - - public function link() - { - return link_to('/clients/' . $this->entity->public_id, $this->entity->getDisplayName()); - } -} \ No newline at end of file +} diff --git a/app/Ninja/Presenters/CreditPresenter.php b/app/Ninja/Presenters/CreditPresenter.php index 7e38205b1067..96c0e5b6f4d0 100644 --- a/app/Ninja/Presenters/CreditPresenter.php +++ b/app/Ninja/Presenters/CreditPresenter.php @@ -1,9 +1,8 @@ entity->credit_date); } -} \ No newline at end of file +} diff --git a/app/Ninja/Presenters/EntityPresenter.php b/app/Ninja/Presenters/EntityPresenter.php new file mode 100644 index 000000000000..b1e16acbd620 --- /dev/null +++ b/app/Ninja/Presenters/EntityPresenter.php @@ -0,0 +1,25 @@ +entity->getEntityType(); + $id = $this->entity->public_id; + $link = sprintf('/%ss/%s', $type, $id); + + return URL::to($link); + } + + public function link() + { + $name = $this->entity->getDisplayName(); + $link = $this->url(); + + return link_to($link, $name)->toHtml(); + } + +} diff --git a/app/Ninja/Presenters/ExpensePresenter.php b/app/Ninja/Presenters/ExpensePresenter.php index 1980480a2f53..275d4e657b09 100644 --- a/app/Ninja/Presenters/ExpensePresenter.php +++ b/app/Ninja/Presenters/ExpensePresenter.php @@ -1,9 +1,8 @@ entity->invoice_id ? $this->entity->convertedAmount() : 0; } - - public function link() - { - return link_to('/expenses/' . $this->entity->public_id, $this->entity->name); - } -} \ No newline at end of file + +} diff --git a/app/Ninja/Presenters/InvoicePresenter.php b/app/Ninja/Presenters/InvoicePresenter.php index 7c67fc98110b..827b171f4208 100644 --- a/app/Ninja/Presenters/InvoicePresenter.php +++ b/app/Ninja/Presenters/InvoicePresenter.php @@ -2,9 +2,8 @@ use URL; use Utils; -use Laracasts\Presenter\Presenter; -class InvoicePresenter extends Presenter { +class InvoicePresenter extends EntityPresenter { public function client() { @@ -69,16 +68,6 @@ class InvoicePresenter extends Presenter { return $this->entity->frequency ? $this->entity->frequency->name : ''; } - public function url() - { - return URL::to('/invoices/' . $this->entity->public_id); - } - - public function link() - { - return link_to('/invoices/' . $this->entity->public_id, $this->entity->invoice_number); - } - public function email() { $client = $this->entity->client; diff --git a/app/Ninja/Presenters/PaymentPresenter.php b/app/Ninja/Presenters/PaymentPresenter.php index a1c3692991fe..9b464b4c8a2a 100644 --- a/app/Ninja/Presenters/PaymentPresenter.php +++ b/app/Ninja/Presenters/PaymentPresenter.php @@ -2,9 +2,8 @@ use URL; use Utils; -use Laracasts\Presenter\Presenter; -class PaymentPresenter extends Presenter { +class PaymentPresenter extends EntityPresenter { public function client() { @@ -25,14 +24,4 @@ class PaymentPresenter extends Presenter { } } - public function url() - { - return URL::to('/payments/' . $this->entity->public_id . '/edit'); - } - - public function link() - { - return link_to('/payments/' . $this->entity->public_id . '/edit', $this->entity->getDisplayName()); - } - -} \ No newline at end of file +} diff --git a/app/Ninja/Presenters/TaskPresenter.php b/app/Ninja/Presenters/TaskPresenter.php index 367e849ca797..5e8eee222c95 100644 --- a/app/Ninja/Presenters/TaskPresenter.php +++ b/app/Ninja/Presenters/TaskPresenter.php @@ -1,9 +1,6 @@ {$text}"; } -} \ No newline at end of file +} diff --git a/app/Ninja/Presenters/VendorPresenter.php b/app/Ninja/Presenters/VendorPresenter.php index d0bef4e0c828..2dd535cac6cd 100644 --- a/app/Ninja/Presenters/VendorPresenter.php +++ b/app/Ninja/Presenters/VendorPresenter.php @@ -1,17 +1,10 @@ entity->country ? $this->entity->country->name : ''; } - - public function link() - { - return link_to('/vendors/' . $this->entity->public_id, $this->entity->name); - } -} \ No newline at end of file + +} From e7bf0599dbbad7e32df1cbe5c2c0d185acb5d0ab Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 23 May 2016 19:52:20 +0300 Subject: [PATCH 135/386] Separated out entity datatable classes --- app/Http/Controllers/ClientController.php | 19 +- app/Models/User.php | 34 +-- .../Datatables/AccountGatewayDatatable.php | 110 ++++++++++ app/Ninja/Datatables/ActivityDatatable.php | 52 +++++ app/Ninja/Datatables/BankAccountDatatable.php | 42 ++++ app/Ninja/Datatables/ClientDatatable.php | 136 ++++++++++++ app/Ninja/Datatables/CreditDatatable.php | 66 ++++++ app/Ninja/Datatables/EntityDatatable.php | 24 +++ app/Ninja/Datatables/ExpenseDatatable.php | 132 ++++++++++++ app/Ninja/Datatables/InvoiceDatatable.php | 193 ++++++++++++++++++ app/Ninja/Datatables/PaymentDatatable.php | 152 ++++++++++++++ app/Ninja/Datatables/ProductDatatable.php | 55 +++++ .../Datatables/RecurringInvoiceDatatable.php | 63 ++++++ app/Ninja/Datatables/TaskDatatable.php | 113 ++++++++++ app/Ninja/Datatables/TaxRateDatatable.php | 41 ++++ app/Ninja/Datatables/TokenDatatable.php | 41 ++++ app/Ninja/Datatables/UserDatatable.php | 96 +++++++++ app/Ninja/Datatables/VendorDatatable.php | 79 +++++++ app/Ninja/Repositories/ClientRepository.php | 14 +- app/Services/AccountGatewayService.php | 110 +--------- app/Services/ActivityService.php | 46 +---- app/Services/BankAccountService.php | 35 +--- app/Services/BaseService.php | 17 -- app/Services/ClientService.php | 135 +----------- app/Services/CreditService.php | 66 +----- app/Services/DatatableService.php | 43 ++-- app/Services/ExpenseService.php | 122 +---------- app/Services/InvoiceService.php | 185 +---------------- app/Services/PaymentService.php | 173 ++-------------- app/Services/PaymentTermService.php | 8 +- app/Services/ProductService.php | 50 +---- app/Services/RecurringInvoiceService.php | 59 +----- app/Services/TaskService.php | 105 +--------- app/Services/TaxRateService.php | 45 +--- app/Services/TokenService.php | 43 +--- app/Services/UserService.php | 97 +-------- app/Services/VendorService.php | 70 +------ 37 files changed, 1536 insertions(+), 1335 deletions(-) create mode 100644 app/Ninja/Datatables/AccountGatewayDatatable.php create mode 100644 app/Ninja/Datatables/ActivityDatatable.php create mode 100644 app/Ninja/Datatables/BankAccountDatatable.php create mode 100644 app/Ninja/Datatables/ClientDatatable.php create mode 100644 app/Ninja/Datatables/CreditDatatable.php create mode 100644 app/Ninja/Datatables/EntityDatatable.php create mode 100644 app/Ninja/Datatables/ExpenseDatatable.php create mode 100644 app/Ninja/Datatables/InvoiceDatatable.php create mode 100644 app/Ninja/Datatables/PaymentDatatable.php create mode 100644 app/Ninja/Datatables/ProductDatatable.php create mode 100644 app/Ninja/Datatables/RecurringInvoiceDatatable.php create mode 100644 app/Ninja/Datatables/TaskDatatable.php create mode 100644 app/Ninja/Datatables/TaxRateDatatable.php create mode 100644 app/Ninja/Datatables/TokenDatatable.php create mode 100644 app/Ninja/Datatables/UserDatatable.php create mode 100644 app/Ninja/Datatables/VendorDatatable.php diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index e5b6fd432b1a..d2a0633e5c73 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -72,7 +72,10 @@ class ClientController extends BaseController public function getDatatable() { - return $this->clientService->getDatatable(Input::get('sSearch')); + $search = Input::get('sSearch'); + $userId = Auth::user()->filterId(); + + return $this->clientService->getDatatable($search, $userId); } /** @@ -97,8 +100,8 @@ class ClientController extends BaseController */ public function show(ClientRequest $request) { - $client = $request->entity(); - + $client = $request->entity(); + $user = Auth::user(); Utils::trackViewed($client->getDisplayName(), ENTITY_CLIENT); @@ -109,19 +112,19 @@ class ClientController extends BaseController if (Utils::hasFeature(FEATURE_QUOTES) && $user->can('create', ENTITY_INVOICE)) { $actionLinks[] = ['label' => trans('texts.new_quote'), 'url' => URL::to('/quotes/create/'.$client->public_id)]; } - + if(!empty($actionLinks)){ $actionLinks[] = \DropdownButton::DIVIDER; } - + if($user->can('create', ENTITY_PAYMENT)){ $actionLinks[] = ['label' => trans('texts.enter_payment'), 'url' => URL::to('/payments/create/'.$client->public_id)]; } - + if($user->can('create', ENTITY_CREDIT)){ $actionLinks[] = ['label' => trans('texts.enter_credit'), 'url' => URL::to('/credits/create/'.$client->public_id)]; } - + if($user->can('create', ENTITY_EXPENSE)){ $actionLinks[] = ['label' => trans('texts.enter_expense'), 'url' => URL::to('/expenses/create/0/'.$client->public_id)]; } @@ -174,7 +177,7 @@ class ClientController extends BaseController public function edit(ClientRequest $request) { $client = $request->entity(); - + $data = [ 'client' => $client, 'method' => 'PUT', diff --git a/app/Models/User.php b/app/Models/User.php index 71069d25821c..9dab5e759a15 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -20,8 +20,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'create_all' => 0b0001, 'view_all' => 0b0010, 'edit_all' => 0b0100, - ); - + ); + use Authenticatable, Authorizable, CanResetPassword; /** @@ -168,7 +168,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac { return Session::get(SESSION_COUNTER, 0); } - + public function afterSave($success = true, $forced = false) { if ($this->email) { @@ -199,8 +199,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return MAX_NUM_VENDORS; } - - + + public function getRememberToken() { return $this->remember_token; @@ -265,9 +265,9 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac && $this->email != $this->getOriginal('email') && $this->getOriginal('confirmed'); } - - - + + + /** * Set the permissions attribute on the model. * @@ -277,7 +277,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac protected function setPermissionsAttribute($value){ if(empty($value)) { $this->attributes['permissions'] = 0; - } else { + } else { $bitmask = 0; foreach($value as $permission){ $bitmask = $bitmask | static::$all_permissions[$permission]; @@ -285,10 +285,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac $this->attributes['permissions'] = $bitmask; } - + return $this; } - + /** * Expands the value of the permissions attribute * @@ -302,10 +302,10 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac $permissions[$permission] = $permission; } } - + return $permissions; } - + /** * Checks to see if the user has the required permission * @@ -325,13 +325,17 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return count(array_intersect($permission, $this->permissions)) > 0; } } - + return false; } - + public function owns($entity) { return !empty($entity->user_id) && $entity->user_id == $this->id; } + + public function filterId() { + return $this->hasPermission('view_all') ? false : $this->id; + } } User::updating(function ($user) { diff --git a/app/Ninja/Datatables/AccountGatewayDatatable.php b/app/Ninja/Datatables/AccountGatewayDatatable.php new file mode 100644 index 000000000000..ad16fc39878a --- /dev/null +++ b/app/Ninja/Datatables/AccountGatewayDatatable.php @@ -0,0 +1,110 @@ +deleted_at) { + return $model->name; + } elseif ($model->gateway_id != GATEWAY_WEPAY) { + return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); + } else { + $accountGateway = AccountGateway::find($model->id); + $config = $accountGateway->getConfig(); + $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; + $wepayAccountId = $config->accountId; + $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'), + )); + + $linkText .= ' ('.trans('texts.action_required').')'; + $url = $updateUri->uri; + $html = "{$linkText}"; + $model->setupUrl = $url; + } elseif ($wepayState == 'pending') { + $linkText .= ' ('.trans('texts.resend_confirmation_email').')'; + $model->resendConfirmationUrl = $url = URL::to("gateways/{$accountGateway->public_id}/resend_confirmation"); + $html = link_to($url, $linkText)->toHtml(); + } + } catch(\WePayException $ex){} + + return $html; + } + } + ], + [ + 'payment_type', + function ($model) { + return Gateway::getPrettyPaymentType($model->gateway_id); + } + ], + ]; + } + + public function actions() + { + return [ + [ + uctrans('texts.resend_confirmation_email'), + function ($model) { + return $model->resendConfirmationUrl; + }, + function($model) { + return !$model->deleted_at && $model->gateway_id == GATEWAY_WEPAY && !empty($model->resendConfirmationUrl); + } + ], [ + uctrans('texts.finish_setup'), + function ($model) { + return $model->setupUrl; + }, + function($model) { + return !$model->deleted_at && $model->gateway_id == GATEWAY_WEPAY && !empty($model->setupUrl); + } + ] , [ + uctrans('texts.edit_gateway'), + function ($model) { + return URL::to("gateways/{$model->public_id}/edit"); + }, + function($model) { + return !$model->deleted_at; + } + ], [ + uctrans('texts.manage_wepay_account'), + function ($model) { + $accountGateway = AccountGateway::find($model->id); + $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; + return array( + 'url' => $endpoint.'account/'.$accountGateway->getConfig()->accountId, + 'attributes' => 'target="_blank"' + ); + }, + function($model) { + return !$model->deleted_at && $model->gateway_id == GATEWAY_WEPAY; + } + ] + ]; + } + +} diff --git a/app/Ninja/Datatables/ActivityDatatable.php b/app/Ninja/Datatables/ActivityDatatable.php new file mode 100644 index 000000000000..5f74bbbe54fb --- /dev/null +++ b/app/Ninja/Datatables/ActivityDatatable.php @@ -0,0 +1,52 @@ +created_at)); + } + ], + [ + 'activity_type_id', + function ($model) { + $data = [ + 'client' => link_to('/clients/' . $model->client_public_id, Utils::getClientDisplayName($model))->toHtml(), + 'user' => $model->is_system ? '' . trans('texts.system') . '' : Utils::getPersonDisplayName($model->user_first_name, $model->user_last_name, $model->user_email), + 'invoice' => $model->invoice ? link_to('/invoices/' . $model->invoice_public_id, $model->is_recurring ? trans('texts.recurring_invoice') : $model->invoice)->toHtml() : null, + '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' => $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); + } + ], + [ + 'balance', + function ($model) { + return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); + } + ], + [ + 'adjustment', + function ($model) { + return $model->adjustment != 0 ? Utils::wrapAdjustment($model->adjustment, $model->currency_id, $model->country_id) : ''; + } + ] + ]; + } +} diff --git a/app/Ninja/Datatables/BankAccountDatatable.php b/app/Ninja/Datatables/BankAccountDatatable.php new file mode 100644 index 000000000000..48bc2672f1c9 --- /dev/null +++ b/app/Ninja/Datatables/BankAccountDatatable.php @@ -0,0 +1,42 @@ +public_id}/edit", $model->bank_name)->toHtml(); + }, + ], + [ + 'bank_library_id', + function ($model) { + return 'OFX'; + } + ], + ]; + } + + public function actions() + { + return [ + [ + uctrans('texts.edit_bank_account'), + function ($model) { + return URL::to("bank_accounts/{$model->public_id}/edit"); + }, + ] + ]; + } + + +} diff --git a/app/Ninja/Datatables/ClientDatatable.php b/app/Ninja/Datatables/ClientDatatable.php new file mode 100644 index 000000000000..4b0ca68b0544 --- /dev/null +++ b/app/Ninja/Datatables/ClientDatatable.php @@ -0,0 +1,136 @@ +public_id}", $model->name ?: '')->toHtml(); + } + ], + [ + 'first_name', + function ($model) { + return link_to("clients/{$model->public_id}", $model->first_name.' '.$model->last_name)->toHtml(); + } + ], + [ + 'email', + function ($model) { + return link_to("clients/{$model->public_id}", $model->email ?: '')->toHtml(); + } + ], + [ + 'clients.created_at', + function ($model) { + return Utils::timestampToDateString(strtotime($model->created_at)); + } + ], + [ + 'last_login', + function ($model) { + return Utils::timestampToDateString(strtotime($model->last_login)); + } + ], + [ + 'balance', + function ($model) { + return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); + } + ] + ]; + } + + public function actions() + { + return [ + [ + trans('texts.edit_client'), + function ($model) { + return URL::to("clients/{$model->public_id}/edit"); + }, + function ($model) { + return Auth::user()->can('editByOwner', [ENTITY_CLIENT, $model->user_id]); + } + ], + [ + '--divider--', function(){return false;}, + function ($model) { + $user = Auth::user(); + return $user->can('editByOwner', [ENTITY_CLIENT, $model->user_id]) && ($user->can('create', ENTITY_TASK) || $user->can('create', ENTITY_INVOICE)); + } + ], + [ + trans('texts.new_task'), + function ($model) { + return URL::to("tasks/create/{$model->public_id}"); + }, + function ($model) { + return Auth::user()->can('create', ENTITY_TASK); + } + ], + [ + trans('texts.new_invoice'), + function ($model) { + return URL::to("invoices/create/{$model->public_id}"); + }, + function ($model) { + return Auth::user()->can('create', ENTITY_INVOICE); + } + ], + [ + trans('texts.new_quote'), + function ($model) { + return URL::to("quotes/create/{$model->public_id}"); + }, + function ($model) { + return Auth::user()->hasFeature(FEATURE_QUOTES) && Auth::user()->can('create', ENTITY_INVOICE); + } + ], + [ + '--divider--', function(){return false;}, + function ($model) { + $user = Auth::user(); + return ($user->can('create', ENTITY_TASK) || $user->can('create', ENTITY_INVOICE)) && ($user->can('create', ENTITY_PAYMENT) || $user->can('create', ENTITY_CREDIT) || $user->can('create', ENTITY_EXPENSE)); + } + ], + [ + trans('texts.enter_payment'), + function ($model) { + return URL::to("payments/create/{$model->public_id}"); + }, + function ($model) { + return Auth::user()->can('create', ENTITY_PAYMENT); + } + ], + [ + trans('texts.enter_credit'), + function ($model) { + return URL::to("credits/create/{$model->public_id}"); + }, + function ($model) { + return Auth::user()->can('create', ENTITY_CREDIT); + } + ], + [ + trans('texts.enter_expense'), + function ($model) { + return URL::to("expenses/create/0/{$model->public_id}"); + }, + function ($model) { + return Auth::user()->can('create', ENTITY_EXPENSE); + } + ] + ]; + } + +} diff --git a/app/Ninja/Datatables/CreditDatatable.php b/app/Ninja/Datatables/CreditDatatable.php new file mode 100644 index 000000000000..7f258e377d1d --- /dev/null +++ b/app/Ninja/Datatables/CreditDatatable.php @@ -0,0 +1,66 @@ +can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ + return Utils::getClientDisplayName($model); + } + + return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : ''; + }, + ! $this->hideClient + ], + [ + 'amount', + function ($model) { + return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id) . ''; + } + ], + [ + 'balance', + function ($model) { + return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); + } + ], + [ + 'credit_date', + function ($model) { + return Utils::fromSqlDate($model->credit_date); + } + ], + [ + 'private_notes', + function ($model) { + return $model->private_notes; + } + ] + ]; + } + + public function actions() + { + return [ + [ + trans('texts.apply_credit'), + function ($model) { + return URL::to("payments/create/{$model->client_public_id}") . '?paymentTypeId=1'; + }, + function ($model) { + return Auth::user()->can('create', ENTITY_PAYMENT); + } + ] + ]; + } +} diff --git a/app/Ninja/Datatables/EntityDatatable.php b/app/Ninja/Datatables/EntityDatatable.php new file mode 100644 index 000000000000..085645ff2069 --- /dev/null +++ b/app/Ninja/Datatables/EntityDatatable.php @@ -0,0 +1,24 @@ +isBulkEdit = $isBulkEdit; + $this->hideClient = $hideClient; + } + + public function columns() + { + return []; + } + + public function actions() + { + return []; + } +} diff --git a/app/Ninja/Datatables/ExpenseDatatable.php b/app/Ninja/Datatables/ExpenseDatatable.php new file mode 100644 index 000000000000..70cc771afe2b --- /dev/null +++ b/app/Ninja/Datatables/ExpenseDatatable.php @@ -0,0 +1,132 @@ +vendor_public_id) { + if(!Auth::user()->can('viewByOwner', [ENTITY_VENDOR, $model->vendor_user_id])){ + return $model->vendor_name; + } + + return link_to("vendors/{$model->vendor_public_id}", $model->vendor_name)->toHtml(); + } else { + return ''; + } + } + ], + [ + 'client_name', + function ($model) + { + if ($model->client_public_id) { + if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ + return Utils::getClientDisplayName($model); + } + + return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml(); + } else { + return ''; + } + } + ], + [ + 'expense_date', + function ($model) { + if(!Auth::user()->can('editByOwner', [ENTITY_EXPENSE, $model->user_id])){ + return Utils::fromSqlDate($model->expense_date); + } + + return link_to("expenses/{$model->public_id}/edit", Utils::fromSqlDate($model->expense_date))->toHtml(); + } + ], + [ + 'amount', + function ($model) { + // show both the amount and the converted amount + if ($model->exchange_rate != 1) { + $converted = round($model->amount * $model->exchange_rate, 2); + return Utils::formatMoney($model->amount, $model->expense_currency_id) . ' | ' . + Utils::formatMoney($converted, $model->invoice_currency_id); + } else { + return Utils::formatMoney($model->amount, $model->expense_currency_id); + } + } + ], + [ + 'public_notes', + function ($model) { + return $model->public_notes != null ? substr($model->public_notes, 0, 100) : ''; + } + ], + [ + 'expense_status_id', + function ($model) { + return self::getStatusLabel($model->invoice_id, $model->should_be_invoiced); + } + ], + ]; + } + + public function actions() + { + return [ + [ + trans('texts.edit_expense'), + function ($model) { + return URL::to("expenses/{$model->public_id}/edit") ; + }, + function ($model) { + return Auth::user()->can('editByOwner', [ENTITY_EXPENSE, $model->user_id]); + } + ], + [ + trans('texts.view_invoice'), + function ($model) { + return URL::to("/invoices/{$model->invoice_public_id}/edit"); + }, + function ($model) { + return $model->invoice_public_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id]); + } + ], + [ + trans('texts.invoice_expense'), + function ($model) { + return "javascript:invoiceEntity({$model->public_id})"; + }, + function ($model) { + return ! $model->invoice_id && (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('create', ENTITY_INVOICE); + } + ], + ]; + } + + + private function getStatusLabel($invoiceId, $shouldBeInvoiced) + { + if ($invoiceId) { + $label = trans('texts.invoiced'); + $class = 'success'; + } elseif ($shouldBeInvoiced) { + $label = trans('texts.pending'); + $class = 'warning'; + } else { + $label = trans('texts.logged'); + $class = 'primary'; + } + + return "

$label

"; + } + +} diff --git a/app/Ninja/Datatables/InvoiceDatatable.php b/app/Ninja/Datatables/InvoiceDatatable.php new file mode 100644 index 000000000000..27b3343561fd --- /dev/null +++ b/app/Ninja/Datatables/InvoiceDatatable.php @@ -0,0 +1,193 @@ +entityType; + + return [ + [ + 'invoice_number', + function ($model) use ($entityType) { + if(!Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id])){ + return $model->invoice_number; + } + + return link_to("{$entityType}s/{$model->public_id}/edit", $model->invoice_number, ['class' => Utils::getEntityRowClass($model)])->toHtml(); + } + ], + [ + 'client_name', + function ($model) { + if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ + return Utils::getClientDisplayName($model); + } + return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml(); + }, + ! $this->hideClient + ], + [ + 'invoice_date', + function ($model) { + return Utils::fromSqlDate($model->invoice_date); + } + ], + [ + 'amount', + function ($model) { + return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); + } + ], + [ + 'balance', + function ($model) { + return $model->partial > 0 ? + trans('texts.partial_remaining', [ + 'partial' => Utils::formatMoney($model->partial, $model->currency_id, $model->country_id), + 'balance' => Utils::formatMoney($model->balance, $model->currency_id, $model->country_id)] + ) : + Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); + }, + $entityType == ENTITY_INVOICE + ], + [ + 'due_date', + function ($model) { + return Utils::fromSqlDate($model->due_date); + }, + ], + [ + 'invoice_status_name', + function ($model) use ($entityType) { + return $model->quote_invoice_id ? link_to("invoices/{$model->quote_invoice_id}/edit", trans('texts.converted'))->toHtml() : self::getStatusLabel($model); + } + ] + ]; + } + + public function actions() + { + $entityType = $this->entityType; + + return [ + [ + trans("texts.edit_{$entityType}"), + function ($model) use ($entityType) { + return URL::to("{$entityType}s/{$model->public_id}/edit"); + }, + function ($model) { + return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); + } + ], + [ + trans("texts.clone_{$entityType}"), + function ($model) use ($entityType) { + return URL::to("{$entityType}s/{$model->public_id}/clone"); + }, + function ($model) { + return Auth::user()->can('create', ENTITY_INVOICE); + } + ], + [ + trans("texts.view_history"), + function ($model) use ($entityType) { + return URL::to("{$entityType}s/{$entityType}_history/{$model->public_id}"); + } + ], + [ + '--divider--', function(){return false;}, + function ($model) { + return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]) || Auth::user()->can('create', ENTITY_PAYMENT); + } + ], + [ + trans("texts.mark_sent"), + function ($model) { + return "javascript:markEntity({$model->public_id})"; + }, + function ($model) { + return $model->invoice_status_id < INVOICE_STATUS_SENT && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); + } + ], + [ + trans('texts.enter_payment'), + function ($model) { + return URL::to("payments/create/{$model->client_public_id}/{$model->public_id}"); + }, + function ($model) use ($entityType) { + return $entityType == ENTITY_INVOICE && $model->balance > 0 && Auth::user()->can('create', ENTITY_PAYMENT); + } + ], + [ + trans("texts.view_quote"), + function ($model) { + return URL::to("quotes/{$model->quote_id}/edit"); + }, + function ($model) use ($entityType) { + return $entityType == ENTITY_INVOICE && $model->quote_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); + } + ], + [ + trans("texts.view_invoice"), + function ($model) { + return URL::to("invoices/{$model->quote_invoice_id}/edit"); + }, + function ($model) use ($entityType) { + return $entityType == ENTITY_QUOTE && $model->quote_invoice_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); + } + ], + [ + trans("texts.convert_to_invoice"), + function ($model) { + return "javascript:convertEntity({$model->public_id})"; + }, + function ($model) use ($entityType) { + return $entityType == ENTITY_QUOTE && ! $model->quote_invoice_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); + } + ] + ]; + } + + private function getStatusLabel($model) + { + $entityType = $this->entityType; + + // check if invoice is overdue + if (Utils::parseFloat($model->balance) && $model->due_date && $model->due_date != '0000-00-00') { + if (\DateTime::createFromFormat('Y-m-d', $model->due_date) < new \DateTime("now")) { + $label = $entityType == ENTITY_INVOICE ? trans('texts.overdue') : trans('texts.expired'); + return "

" . $label . "

"; + } + } + + $label = trans("texts.status_" . strtolower($model->invoice_status_name)); + $class = 'default'; + switch ($model->invoice_status_id) { + case INVOICE_STATUS_SENT: + $class = 'info'; + break; + case INVOICE_STATUS_VIEWED: + $class = 'warning'; + break; + case INVOICE_STATUS_APPROVED: + $class = 'success'; + break; + case INVOICE_STATUS_PARTIAL: + $class = 'primary'; + break; + case INVOICE_STATUS_PAID: + $class = 'success'; + break; + } + + return "

$label

"; + } + +} diff --git a/app/Ninja/Datatables/PaymentDatatable.php b/app/Ninja/Datatables/PaymentDatatable.php new file mode 100644 index 000000000000..4ca1d2839fc8 --- /dev/null +++ b/app/Ninja/Datatables/PaymentDatatable.php @@ -0,0 +1,152 @@ +can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id])){ + return $model->invoice_number; + } + + return link_to("invoices/{$model->invoice_public_id}/edit", $model->invoice_number, ['class' => Utils::getEntityRowClass($model)])->toHtml(); + } + ], + [ + 'client_name', + function ($model) { + if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ + return Utils::getClientDisplayName($model); + } + + return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : ''; + }, + ! $this->hideClient + ], + [ + 'transaction_reference', + function ($model) { + return $model->transaction_reference ? $model->transaction_reference : 'Manual entry'; + } + ], + [ + 'payment_type', + function ($model) { + return ($model->payment_type && !$model->last4) ? $model->payment_type : ($model->account_gateway_id ? $model->gateway_name : ''); + } + ], + [ + 'source', + function ($model) { + $code = str_replace(' ', '', strtolower($model->payment_type)); + $card_type = trans("texts.card_" . $code); + if ($model->payment_type_id != PAYMENT_TYPE_ACH) { + if($model->last4) { + $expiration = trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($model->expiration, false)->format('m/y'))); + return '' . htmlentities($card_type) . '  •••' . $model->last4 . ' ' . $expiration; + } elseif ($model->email) { + return $model->email; + } + } elseif ($model->last4) { + $bankData = PaymentMethod::lookupBankData($model->routing_number); + if (is_object($bankData)) { + return $bankData->name.'  •••' . $model->last4; + } elseif($model->last4) { + return '' . htmlentities($card_type) . '  •••' . $model->last4; + } + } + } + ], + [ + 'amount', + function ($model) { + return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); + } + ], + [ + 'payment_date', + function ($model) { + return Utils::dateToString($model->payment_date); + } + ], + [ + 'payment_status_name', + function ($model) { + return self::getStatusLabel($model); + } + ] + ]; + } + + + public function actions() + { + return [ + [ + trans('texts.edit_payment'), + function ($model) { + return URL::to("payments/{$model->public_id}/edit"); + }, + function ($model) { + return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]); + } + ], + [ + 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 ? $model->currency_id : 1, 'currencies')->symbol ; + return "javascript:showRefundModal({$model->public_id}, '{$max_refund}', '{$formatted}', '{$symbol}')"; + }, + function ($model) { + return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]) && $model->payment_status_id >= PAYMENT_STATUS_COMPLETED && + $model->refunded < $model->amount && + ( + ($model->transaction_reference && in_array($model->gateway_id , static::$refundableGateways)) + || $model->payment_type_id == PAYMENT_TYPE_CREDIT + ); + } + ] + ]; + } + + private function getStatusLabel($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_VOIDED: + case PAYMENT_STATUS_REFUNDED: + $class = 'default'; + break; + } + return "

$label

"; + } +} diff --git a/app/Ninja/Datatables/ProductDatatable.php b/app/Ninja/Datatables/ProductDatatable.php new file mode 100644 index 000000000000..a5b3cbfc218d --- /dev/null +++ b/app/Ninja/Datatables/ProductDatatable.php @@ -0,0 +1,55 @@ +public_id.'/edit', $model->product_key)->toHtml(); + } + ], + [ + 'notes', + function ($model) { + return nl2br(Str::limit($model->notes, 100)); + } + ], + [ + 'cost', + function ($model) { + return Utils::formatMoney($model->cost); + } + ], + [ + 'tax_rate', + function ($model) { + return $model->tax_rate ? ($model->tax_name . ' ' . $model->tax_rate . '%') : ''; + }, + Auth::user()->account->invoice_item_taxes + ] + ]; + } + + public function actions() + { + return [ + [ + uctrans('texts.edit_product'), + function ($model) { + return URL::to("products/{$model->public_id}/edit"); + } + ] + ]; + } + +} diff --git a/app/Ninja/Datatables/RecurringInvoiceDatatable.php b/app/Ninja/Datatables/RecurringInvoiceDatatable.php new file mode 100644 index 000000000000..7b5365814eca --- /dev/null +++ b/app/Ninja/Datatables/RecurringInvoiceDatatable.php @@ -0,0 +1,63 @@ +public_id}", $model->frequency)->toHtml(); + } + ], + [ + 'client_name', + function ($model) { + return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml(); + }, + ! $this->hideClient + ], + [ + 'start_date', + function ($model) { + return Utils::fromSqlDate($model->start_date); + } + ], + [ + 'end_date', + function ($model) { + return Utils::fromSqlDate($model->end_date); + } + ], + [ + 'amount', + function ($model) { + return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); + } + ] + ]; + } + + public function actions() + { + return [ + [ + trans('texts.edit_invoice'), + function ($model) { + return URL::to("invoices/{$model->public_id}/edit"); + }, + function ($model) { + return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); + } + ] + ]; + } + +} diff --git a/app/Ninja/Datatables/TaskDatatable.php b/app/Ninja/Datatables/TaskDatatable.php new file mode 100644 index 000000000000..6f460b418e86 --- /dev/null +++ b/app/Ninja/Datatables/TaskDatatable.php @@ -0,0 +1,113 @@ +can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ + return Utils::getClientDisplayName($model); + } + + return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : ''; + }, + ! $this->hideClient + ], + [ + 'created_at', + function ($model) { + return link_to("tasks/{$model->public_id}/edit", Task::calcStartTime($model))->toHtml(); + } + ], + [ + 'time_log', + function($model) { + return Utils::formatTime(Task::calcDuration($model)); + } + ], + [ + 'description', + function ($model) { + return $model->description; + } + ], + [ + 'invoice_number', + function ($model) { + return self::getStatusLabel($model); + } + ] + ]; + } + + public function actions() + { + return [ + [ + trans('texts.edit_task'), + function ($model) { + return URL::to('tasks/'.$model->public_id.'/edit'); + }, + function ($model) { + return (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('editByOwner', [ENTITY_TASK, $model->user_id]); + } + ], + [ + trans('texts.view_invoice'), + function ($model) { + return URL::to("/invoices/{$model->invoice_public_id}/edit"); + }, + function ($model) { + return $model->invoice_number && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id]); + } + ], + [ + trans('texts.stop_task'), + function ($model) { + return "javascript:stopTask({$model->public_id})"; + }, + function ($model) { + return $model->is_running && Auth::user()->can('editByOwner', [ENTITY_TASK, $model->user_id]); + } + ], + [ + trans('texts.invoice_task'), + function ($model) { + return "javascript:invoiceEntity({$model->public_id})"; + }, + function ($model) { + return ! $model->invoice_number && (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('create', ENTITY_INVOICE); + } + ] + ]; + } + + private function getStatusLabel($model) + { + if ($model->invoice_number) { + $class = 'success'; + $label = trans('texts.invoiced'); + } elseif ($model->is_running) { + $class = 'primary'; + $label = trans('texts.running'); + } else { + $class = 'default'; + $label = trans('texts.logged'); + } + + return "

$label

"; + } + + +} diff --git a/app/Ninja/Datatables/TaxRateDatatable.php b/app/Ninja/Datatables/TaxRateDatatable.php new file mode 100644 index 000000000000..d6cb0cb54f03 --- /dev/null +++ b/app/Ninja/Datatables/TaxRateDatatable.php @@ -0,0 +1,41 @@ +public_id}/edit", $model->name)->toHtml(); + } + ], + [ + 'rate', + function ($model) { + return $model->rate . '%'; + } + ] + ]; + } + + public function actions() + { + return [ + [ + uctrans('texts.edit_tax_rate'), + function ($model) { + return URL::to("tax_rates/{$model->public_id}/edit"); + } + ] + ]; + } + +} diff --git a/app/Ninja/Datatables/TokenDatatable.php b/app/Ninja/Datatables/TokenDatatable.php new file mode 100644 index 000000000000..e0e248f15cee --- /dev/null +++ b/app/Ninja/Datatables/TokenDatatable.php @@ -0,0 +1,41 @@ +public_id}/edit", $model->name)->toHtml(); + } + ], + [ + 'token', + function ($model) { + return $model->token; + } + ] + ]; + } + + public function actions() + { + return [ + [ + uctrans('texts.edit_token'), + function ($model) { + return URL::to("tokens/{$model->public_id}/edit"); + } + ] + ]; + } + +} diff --git a/app/Ninja/Datatables/UserDatatable.php b/app/Ninja/Datatables/UserDatatable.php new file mode 100644 index 000000000000..a6f267fdbd00 --- /dev/null +++ b/app/Ninja/Datatables/UserDatatable.php @@ -0,0 +1,96 @@ +public_id ? link_to('users/'.$model->public_id.'/edit', $model->first_name.' '.$model->last_name)->toHtml() : ($model->first_name.' '.$model->last_name); + } + ], + [ + 'email', + function ($model) { + return $model->email; + } + ], + [ + 'confirmed', + function ($model) { + if (!$model->public_id) { + return self::getStatusLabel(USER_STATE_OWNER); + } elseif ($model->deleted_at) { + return self::getStatusLabel(USER_STATE_DISABLED); + } elseif ($model->confirmed) { + if($model->is_admin){ + return self::getStatusLabel(USER_STATE_ADMIN); + } else { + return self::getStatusLabel(USER_STATE_ACTIVE); + } + } else { + return self::getStatusLabel(USER_STATE_PENDING); + } + } + ], + ]; + } + + public function actions() + { + return [ + [ + uctrans('texts.edit_user'), + function ($model) { + return URL::to("users/{$model->public_id}/edit"); + }, + function ($model) { + return $model->public_id; + } + ], + [ + uctrans('texts.send_invite'), + function ($model) { + return URL::to("send_confirmation/{$model->public_id}"); + }, + function ($model) { + return $model->public_id && ! $model->confirmed; + } + ] + ]; + } + + private function getStatusLabel($state) + { + $label = trans("texts.{$state}"); + $class = 'default'; + switch ($state) { + case USER_STATE_PENDING: + $class = 'default'; + break; + case USER_STATE_ACTIVE: + $class = 'info'; + break; + case USER_STATE_DISABLED: + $class = 'warning'; + break; + case USER_STATE_OWNER: + $class = 'success'; + break; + case USER_STATE_ADMIN: + $class = 'primary'; + break; + } + return "

$label

"; + } + + +} diff --git a/app/Ninja/Datatables/VendorDatatable.php b/app/Ninja/Datatables/VendorDatatable.php new file mode 100644 index 000000000000..fda61fe457d8 --- /dev/null +++ b/app/Ninja/Datatables/VendorDatatable.php @@ -0,0 +1,79 @@ +public_id}", $model->name ?: '')->toHtml(); + } + ], + [ + 'city', + function ($model) { + return $model->city; + } + ], + [ + 'work_phone', + function ($model) { + return $model->work_phone; + } + ], + [ + 'email', + function ($model) { + return link_to("vendors/{$model->public_id}", $model->email ?: '')->toHtml(); + } + ], + [ + 'vendors.created_at', + function ($model) { + return Utils::timestampToDateString(strtotime($model->created_at)); + } + ], + ]; + } + + public function actions() + { + return [ + [ + trans('texts.edit_vendor'), + function ($model) { + return URL::to("vendors/{$model->public_id}/edit"); + }, + function ($model) { + return Auth::user()->can('editByOwner', [ENTITY_VENDOR, $model->user_id]); + } + ], + [ + '--divider--', function(){return false;}, + function ($model) { + return Auth::user()->can('editByOwner', [ENTITY_VENDOR, $model->user_id]) && Auth::user()->can('create', ENTITY_EXPENSE); + } + + ], + [ + trans('texts.enter_expense'), + function ($model) { + return URL::to("expenses/create/{$model->public_id}"); + }, + function ($model) { + return Auth::user()->can('create', ENTITY_EXPENSE); + } + ] + ]; + } + + +} diff --git a/app/Ninja/Repositories/ClientRepository.php b/app/Ninja/Repositories/ClientRepository.php index 83f08cd97a59..bb56d944e6fc 100644 --- a/app/Ninja/Repositories/ClientRepository.php +++ b/app/Ninja/Repositories/ClientRepository.php @@ -25,7 +25,7 @@ class ClientRepository extends BaseRepository ->get(); } - public function find($filter = null) + public function find($filter = null, $userId = false) { $query = DB::table('clients') ->join('accounts', 'accounts.id', '=', 'clients.account_id') @@ -63,9 +63,13 @@ class ClientRepository extends BaseRepository }); } + if ($userId) { + $query->where('clients.user_id', '=', $userId); + } + return $query; } - + public function save($data, $client = null) { $publicId = isset($data['public_id']) ? $data['public_id'] : false; @@ -78,7 +82,7 @@ class ClientRepository extends BaseRepository $client = Client::scope($publicId)->with('contacts')->firstOrFail(); \Log::warning('Entity not set in client repo save'); } - + // convert currency code to id if (isset($data['currency_code'])) { $currencyCode = strtolower($data['currency_code']); @@ -98,7 +102,7 @@ class ClientRepository extends BaseRepository return $client; } */ - + $first = true; $contacts = isset($data['contact']) ? [$data['contact']] : $data['contacts']; $contactIds = []; @@ -107,7 +111,7 @@ class ClientRepository extends BaseRepository usort($contacts, function ($left, $right) { return (isset($right['is_primary']) ? $right['is_primary'] : 1) - (isset($left['is_primary']) ? $left['is_primary'] : 0); }); - + foreach ($contacts as $contact) { $contact = $client->addContact($contact, $first); $contactIds[] = $contact->public_id; diff --git a/app/Services/AccountGatewayService.php b/app/Services/AccountGatewayService.php index 88ee177321ec..00e3f672e602 100644 --- a/app/Services/AccountGatewayService.php +++ b/app/Services/AccountGatewayService.php @@ -1,10 +1,9 @@ accountGatewayRepo; } - /* - public function save() - { - return null; - } - */ - public function getDatatable($accountId) { $query = $this->accountGatewayRepo->find($accountId); - return $this->createDatatable(ENTITY_ACCOUNT_GATEWAY, $query, false); + return $this->datatableService->createDatatable(new AccountGatewayDatatable(false), $query); } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'name', - function ($model) { - if ($model->deleted_at) { - return $model->name; - } elseif ($model->gateway_id != GATEWAY_WEPAY) { - return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); - } else { - $accountGateway = AccountGateway::find($model->id); - $config = $accountGateway->getConfig(); - $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; - $wepayAccountId = $config->accountId; - $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'), - )); - - $linkText .= ' ('.trans('texts.action_required').')'; - $url = $updateUri->uri; - $html = "{$linkText}"; - $model->setupUrl = $url; - } elseif ($wepayState == 'pending') { - $linkText .= ' ('.trans('texts.resend_confirmation_email').')'; - $model->resendConfirmationUrl = $url = URL::to("gateways/{$accountGateway->public_id}/resend_confirmation"); - $html = link_to($url, $linkText)->toHtml(); - } - } catch(\WePayException $ex){} - - return $html; - } - } - ], - [ - 'payment_type', - function ($model) { - return Gateway::getPrettyPaymentType($model->gateway_id); - } - ], - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - uctrans('texts.resend_confirmation_email'), - function ($model) { - return $model->resendConfirmationUrl; - }, - function($model) { - return !$model->deleted_at && $model->gateway_id == GATEWAY_WEPAY && !empty($model->resendConfirmationUrl); - } - ], [ - uctrans('texts.finish_setup'), - function ($model) { - return $model->setupUrl; - }, - function($model) { - return !$model->deleted_at && $model->gateway_id == GATEWAY_WEPAY && !empty($model->setupUrl); - } - ] , [ - uctrans('texts.edit_gateway'), - function ($model) { - return URL::to("gateways/{$model->public_id}/edit"); - }, - function($model) { - return !$model->deleted_at; - } - ], [ - uctrans('texts.manage_wepay_account'), - function ($model) { - $accountGateway = AccountGateway::find($model->id); - $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; - return array( - 'url' => $endpoint.'account/'.$accountGateway->getConfig()->accountId, - 'attributes' => 'target="_blank"' - ); - }, - function($model) { - return !$model->deleted_at && $model->gateway_id == GATEWAY_WEPAY; - } - ] - ]; - } - -} \ No newline at end of file +} diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index 6f6ef4b35956..6b06fce73a8b 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -4,6 +4,7 @@ use Utils; use App\Models\Client; use App\Services\BaseService; use App\Ninja\Repositories\ActivityRepository; +use App\Ninja\Datatables\ActivityDatatable; class ActivityService extends BaseService { @@ -22,48 +23,7 @@ class ActivityService extends BaseService $query = $this->activityRepo->findByClientId($clientId); - return $this->createDatatable(ENTITY_ACTIVITY, $query); + return $this->datatableService->createDatatable(new ActivityDatatable(false), $query); } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'activities.id', - function ($model) { - return Utils::timestampToDateTimeString(strtotime($model->created_at)); - } - ], - [ - 'activity_type_id', - function ($model) { - $data = [ - 'client' => link_to('/clients/' . $model->client_public_id, Utils::getClientDisplayName($model))->toHtml(), - 'user' => $model->is_system ? '' . trans('texts.system') . '' : Utils::getPersonDisplayName($model->user_first_name, $model->user_last_name, $model->user_email), - 'invoice' => $model->invoice ? link_to('/invoices/' . $model->invoice_public_id, $model->is_recurring ? trans('texts.recurring_invoice') : $model->invoice)->toHtml() : null, - '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' => $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); - } - ], - [ - 'balance', - function ($model) { - return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); - } - ], - [ - 'adjustment', - function ($model) { - return $model->adjustment != 0 ? Utils::wrapAdjustment($model->adjustment, $model->currency_id, $model->country_id) : ''; - } - ] - ]; - } -} \ No newline at end of file +} diff --git a/app/Services/BankAccountService.php b/app/Services/BankAccountService.php index 688bd866d3ad..9291a36342c3 100644 --- a/app/Services/BankAccountService.php +++ b/app/Services/BankAccountService.php @@ -11,6 +11,7 @@ use App\Services\BaseService; use App\Ninja\Repositories\BankAccountRepository; use App\Ninja\Repositories\ExpenseRepository; use App\Ninja\Repositories\VendorRepository; +use App\Ninja\Datatables\BankAccountDatatable; use App\Libraries\Finance; use App\Libraries\Login; @@ -206,7 +207,7 @@ class BankAccountService extends BaseService $vendorMap[$transaction['vendor_orig']] = $vendor; $countVendors++; } - + // create the expense record $this->expenseRepo->save([ 'vendor_id' => $vendor->id, @@ -241,36 +242,6 @@ class BankAccountService extends BaseService { $query = $this->bankAccountRepo->find($accountId); - return $this->createDatatable(ENTITY_BANK_ACCOUNT, $query, false); - } - - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'bank_name', - function ($model) { - return link_to("bank_accounts/{$model->public_id}/edit", $model->bank_name)->toHtml(); - }, - ], - [ - 'bank_library_id', - function ($model) { - return 'OFX'; - } - ], - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - uctrans('texts.edit_bank_account'), - function ($model) { - return URL::to("bank_accounts/{$model->public_id}/edit"); - }, - ] - ]; + return $this->datatableService->createDatatable(new BankAccountDatatable(false), $query); } } diff --git a/app/Services/BaseService.php b/app/Services/BaseService.php index afd6ed1ea1eb..e6dad06334bd 100644 --- a/app/Services/BaseService.php +++ b/app/Services/BaseService.php @@ -30,21 +30,4 @@ class BaseService return count($entities); } - public function createDatatable($entityType, $query, $showCheckbox = true, $hideClient = false, $orderColumns = []) - { - $columns = $this->getDatatableColumns($entityType, !$showCheckbox); - $actions = $this->getDatatableActions($entityType); - - return $this->datatableService->createDatatable($entityType, $query, $columns, $actions, $showCheckbox, $orderColumns); - } - - protected function getDatatableColumns($entityType, $hideClient) - { - return []; - } - - protected function getDatatableActions($entityType) - { - return []; - } } diff --git a/app/Services/ClientService.php b/app/Services/ClientService.php index 96357991c519..065258d05abb 100644 --- a/app/Services/ClientService.php +++ b/app/Services/ClientService.php @@ -12,6 +12,7 @@ use App\Models\Payment; use App\Models\Task; use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\NinjaRepository; +use App\Ninja\Datatables\ClientDatatable; class ClientService extends BaseService { @@ -39,139 +40,13 @@ class ClientService extends BaseService return $this->clientRepo->save($data, $client); } - public function getDatatable($search) + public function getDatatable($search, $userId) { - $query = $this->clientRepo->find($search); + $datatable = new ClientDatatable(); - if(!Utils::hasPermission('view_all')){ - $query->where('clients.user_id', '=', Auth::user()->id); - } + $query = $this->clientRepo->find($search, $userId); - return $this->createDatatable(ENTITY_CLIENT, $query); + return $this->datatableService->createDatatable($datatable, $query); } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'name', - function ($model) { - return link_to("clients/{$model->public_id}", $model->name ?: '')->toHtml(); - } - ], - [ - 'first_name', - function ($model) { - return link_to("clients/{$model->public_id}", $model->first_name.' '.$model->last_name)->toHtml(); - } - ], - [ - 'email', - function ($model) { - return link_to("clients/{$model->public_id}", $model->email ?: '')->toHtml(); - } - ], - [ - 'clients.created_at', - function ($model) { - return Utils::timestampToDateString(strtotime($model->created_at)); - } - ], - [ - 'last_login', - function ($model) { - return Utils::timestampToDateString(strtotime($model->last_login)); - } - ], - [ - 'balance', - function ($model) { - return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); - } - ] - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - trans('texts.edit_client'), - function ($model) { - return URL::to("clients/{$model->public_id}/edit"); - }, - function ($model) { - return Auth::user()->can('editByOwner', [ENTITY_CLIENT, $model->user_id]); - } - ], - [ - '--divider--', function(){return false;}, - function ($model) { - $user = Auth::user(); - return $user->can('editByOwner', [ENTITY_CLIENT, $model->user_id]) && ($user->can('create', ENTITY_TASK) || $user->can('create', ENTITY_INVOICE)); - } - ], - [ - trans('texts.new_task'), - function ($model) { - return URL::to("tasks/create/{$model->public_id}"); - }, - function ($model) { - return Auth::user()->can('create', ENTITY_TASK); - } - ], - [ - trans('texts.new_invoice'), - function ($model) { - return URL::to("invoices/create/{$model->public_id}"); - }, - function ($model) { - return Auth::user()->can('create', ENTITY_INVOICE); - } - ], - [ - trans('texts.new_quote'), - function ($model) { - return URL::to("quotes/create/{$model->public_id}"); - }, - function ($model) { - return Auth::user()->hasFeature(FEATURE_QUOTES) && Auth::user()->can('create', ENTITY_INVOICE); - } - ], - [ - '--divider--', function(){return false;}, - function ($model) { - $user = Auth::user(); - return ($user->can('create', ENTITY_TASK) || $user->can('create', ENTITY_INVOICE)) && ($user->can('create', ENTITY_PAYMENT) || $user->can('create', ENTITY_CREDIT) || $user->can('create', ENTITY_EXPENSE)); - } - ], - [ - trans('texts.enter_payment'), - function ($model) { - return URL::to("payments/create/{$model->public_id}"); - }, - function ($model) { - return Auth::user()->can('create', ENTITY_PAYMENT); - } - ], - [ - trans('texts.enter_credit'), - function ($model) { - return URL::to("credits/create/{$model->public_id}"); - }, - function ($model) { - return Auth::user()->can('create', ENTITY_CREDIT); - } - ], - [ - trans('texts.enter_expense'), - function ($model) { - return URL::to("expenses/create/0/{$model->public_id}"); - }, - function ($model) { - return Auth::user()->can('create', ENTITY_EXPENSE); - } - ] - ]; - } } diff --git a/app/Services/CreditService.php b/app/Services/CreditService.php index 54ef659f05f9..a1e1a5db40ab 100644 --- a/app/Services/CreditService.php +++ b/app/Services/CreditService.php @@ -7,7 +7,7 @@ use App\Services\BaseService; use App\Models\Client; use App\Models\Payment; use App\Ninja\Repositories\CreditRepository; - +use App\Ninja\Datatables\CreditDatatable; class CreditService extends BaseService { @@ -32,68 +32,14 @@ class CreditService extends BaseService public function getDatatable($clientPublicId, $search) { + // we don't support bulk edit and hide the client on the individual client page + $datatable = new CreditDatatable( ! $clientPublicId, $clientPublicId); $query = $this->creditRepo->find($clientPublicId, $search); - + if(!Utils::hasPermission('view_all')){ $query->where('credits.user_id', '=', Auth::user()->id); } - return $this->createDatatable(ENTITY_CREDIT, $query, !$clientPublicId); + return $this->datatableService->createDatatable($datatable, $query); } - - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'client_name', - function ($model) { - if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ - return Utils::getClientDisplayName($model); - } - - return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : ''; - }, - ! $hideClient - ], - [ - 'amount', - function ($model) { - return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id) . ''; - } - ], - [ - 'balance', - function ($model) { - return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); - } - ], - [ - 'credit_date', - function ($model) { - return Utils::fromSqlDate($model->credit_date); - } - ], - [ - 'private_notes', - function ($model) { - return $model->private_notes; - } - ] - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - trans('texts.apply_credit'), - function ($model) { - return URL::to("payments/create/{$model->client_public_id}") . '?paymentTypeId=1'; - }, - function ($model) { - return Auth::user()->can('create', ENTITY_PAYMENT); - } - ] - ]; - } -} \ No newline at end of file +} diff --git a/app/Services/DatatableService.php b/app/Services/DatatableService.php index df4c9d93d55d..453f4d0f25b0 100644 --- a/app/Services/DatatableService.php +++ b/app/Services/DatatableService.php @@ -4,24 +4,26 @@ use HtmlString; use Utils; use Datatable; use Auth; +use App\Ninja\Datatables\EntityDatatable; class DatatableService { - public function createDatatable($entityType, $query, $columns, $actions = null, $showCheckbox = true, $orderColumns = []) + //public function createDatatable($entityType, $query, $columns, $actions = null, $showCheckbox = true, $orderColumns = []) + public function createDatatable(EntityDatatable $datatable, $query) { $table = Datatable::query($query); - $calculateOrderColumns = empty($orderColumns); - - if ($actions && $showCheckbox) { + //$calculateOrderColumns = empty($orderColumns); + + if ($datatable->isBulkEdit) { $table->addColumn('checkbox', function ($model) { $can_edit = Auth::user()->hasPermission('edit_all') || (isset($model->user_id) && Auth::user()->id == $model->user_id); - + return !$can_edit?'':''; }); } - foreach ($columns as $column) { + foreach ($datatable->columns() as $column) { // set visible to true by default if (count($column) == 2) { $column[] = true; @@ -31,27 +33,31 @@ class DatatableService if ($visible) { $table->addColumn($field, $value); + $orderColumns[] = $field; + /* if ($calculateOrderColumns) { $orderColumns[] = $field; } + */ } } - if ($actions) { - $this->createDropdown($entityType, $table, $actions); + if (count($datatable->actions())) { + $this->createDropdown($datatable, $table); } return $table->orderColumns($orderColumns)->make(); } - private function createDropdown($entityType, $table, $actions) + //private function createDropdown($entityType, $table, $actions) + private function createDropdown(EntityDatatable $datatable, $table) { - $table->addColumn('dropdown', function ($model) use ($entityType, $actions) { + $table->addColumn('dropdown', function ($model) use ($datatable) { $hasAction = false; $str = '
'; $can_edit = Auth::user()->hasPermission('edit_all') || (isset($model->user_id) && Auth::user()->id == $model->user_id); - + if (property_exists($model, 'is_deleted') && $model->is_deleted) { $str .= ''; } elseif ($model->deleted_at && $model->deleted_at !== '0000-00-00') { @@ -64,7 +70,7 @@ class DatatableService $lastIsDivider = false; if (!$model->deleted_at || $model->deleted_at == '0000-00-00') { - foreach ($actions as $action) { + foreach ($datatable->actions() as $action) { if (count($action)) { if (count($action) == 2) { $action[] = function() { @@ -104,20 +110,20 @@ class DatatableService $dropdown_contents .= "
  • "; } - if (($entityType != ENTITY_USER || $model->public_id) && $can_edit) { + if (($datatable->entityType != ENTITY_USER || $model->public_id) && $can_edit) { $dropdown_contents .= "
  • public_id})\">" - . trans("texts.archive_{$entityType}") . "
  • "; + . trans("texts.archive_{$datatable->entityType}") . ""; } } else if($can_edit) { - if ($entityType != ENTITY_ACCOUNT_GATEWAY || Auth::user()->account->canAddGateway(\App\Models\Gateway::getPaymentType($model->gateway_id))) { + if ($datatable->entityType != ENTITY_ACCOUNT_GATEWAY || Auth::user()->account->canAddGateway(\App\Models\Gateway::getPaymentType($model->gateway_id))) { $dropdown_contents .= "
  • public_id})\">" - . trans("texts.restore_{$entityType}") . "
  • "; + . trans("texts.restore_{$datatable->entityType}") . ""; } } if (property_exists($model, 'is_deleted') && !$model->is_deleted && $can_edit) { $dropdown_contents .= "
  • public_id})\">" - . trans("texts.delete_{$entityType}") . "
  • "; + . trans("texts.delete_{$datatable->entityType}") . ""; } if (!empty($dropdown_contents)) { @@ -132,5 +138,4 @@ class DatatableService return $str.'
    '; }); } - -} \ No newline at end of file +} diff --git a/app/Services/ExpenseService.php b/app/Services/ExpenseService.php index 671648ea32a5..94bfd7d8c87a 100644 --- a/app/Services/ExpenseService.php +++ b/app/Services/ExpenseService.php @@ -10,6 +10,7 @@ use App\Models\Expense; use App\Models\Invoice; use App\Models\Client; use App\Models\Vendor; +use App\Ninja\Datatables\ExpenseDatatable; class ExpenseService extends BaseService { @@ -49,7 +50,7 @@ class ExpenseService extends BaseService $query->where('expenses.user_id', '=', Auth::user()->id); } - return $this->createDatatable(ENTITY_EXPENSE, $query); + return $this->datatableService->createDatatable(new ExpenseDatatable(), $query); } public function getDatatableVendor($vendorPublicId) @@ -62,76 +63,6 @@ class ExpenseService extends BaseService false); } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'vendor_name', - function ($model) - { - if ($model->vendor_public_id) { - if(!Auth::user()->can('viewByOwner', [ENTITY_VENDOR, $model->vendor_user_id])){ - return $model->vendor_name; - } - - return link_to("vendors/{$model->vendor_public_id}", $model->vendor_name)->toHtml(); - } else { - return ''; - } - } - ], - [ - 'client_name', - function ($model) - { - if ($model->client_public_id) { - if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ - return Utils::getClientDisplayName($model); - } - - return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml(); - } else { - return ''; - } - } - ], - [ - 'expense_date', - function ($model) { - if(!Auth::user()->can('editByOwner', [ENTITY_EXPENSE, $model->user_id])){ - return Utils::fromSqlDate($model->expense_date); - } - - return link_to("expenses/{$model->public_id}/edit", Utils::fromSqlDate($model->expense_date))->toHtml(); - } - ], - [ - 'amount', - function ($model) { - // show both the amount and the converted amount - if ($model->exchange_rate != 1) { - $converted = round($model->amount * $model->exchange_rate, 2); - return Utils::formatMoney($model->amount, $model->expense_currency_id) . ' | ' . - Utils::formatMoney($converted, $model->invoice_currency_id); - } else { - return Utils::formatMoney($model->amount, $model->expense_currency_id); - } - } - ], - [ - 'public_notes', - function ($model) { - return $model->public_notes != null ? substr($model->public_notes, 0, 100) : ''; - } - ], - [ - 'expense_status_id', - function ($model) { - return self::getStatusLabel($model->invoice_id, $model->should_be_invoiced); - } - ], - ]; - } protected function getDatatableColumnsVendor($entityType, $hideClient) { @@ -163,58 +94,9 @@ class ExpenseService extends BaseService ]; } - protected function getDatatableActions($entityType) - { - return [ - [ - trans('texts.edit_expense'), - function ($model) { - return URL::to("expenses/{$model->public_id}/edit") ; - }, - function ($model) { - return Auth::user()->can('editByOwner', [ENTITY_EXPENSE, $model->user_id]); - } - ], - [ - trans('texts.view_invoice'), - function ($model) { - return URL::to("/invoices/{$model->invoice_public_id}/edit"); - }, - function ($model) { - return $model->invoice_public_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id]); - } - ], - [ - trans('texts.invoice_expense'), - function ($model) { - return "javascript:invoiceEntity({$model->public_id})"; - }, - function ($model) { - return ! $model->invoice_id && (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('create', ENTITY_INVOICE); - } - ], - ]; - } - protected function getDatatableActionsVendor($entityType) { return []; } - private function getStatusLabel($invoiceId, $shouldBeInvoiced) - { - if ($invoiceId) { - $label = trans('texts.invoiced'); - $class = 'success'; - } elseif ($shouldBeInvoiced) { - $label = trans('texts.pending'); - $class = 'warning'; - } else { - $label = trans('texts.logged'); - $class = 'primary'; - } - - return "

    $label

    "; - } - } diff --git a/app/Services/InvoiceService.php b/app/Services/InvoiceService.php index edbee8caf84c..950c8bb95fcb 100644 --- a/app/Services/InvoiceService.php +++ b/app/Services/InvoiceService.php @@ -11,6 +11,7 @@ use App\Models\Invitation; use App\Models\Invoice; use App\Models\Client; use App\Models\Payment; +use App\Ninja\Datatables\InvoiceDatatable; class InvoiceService extends BaseService { @@ -34,12 +35,12 @@ class InvoiceService extends BaseService { if (isset($data['client'])) { $canSaveClient = false; - $clientPublicId = array_get($data, 'client.public_id') ?: array_get($data, 'client.id'); + $clientPublicId = array_get($data, 'client.public_id') ?: array_get($data, 'client.id'); if (empty($clientPublicId) || $clientPublicId == '-1') { $canSaveClient = Auth::user()->can('create', ENTITY_CLIENT); } else { $canSaveClient = Auth::user()->can('edit', Client::scope($clientPublicId)->first()); - } + } if ($canSaveClient) { $client = $this->clientRepo->save($data['client']); $data['client_id'] = $client->id; @@ -92,7 +93,7 @@ class InvoiceService extends BaseService public function approveQuote($quote, $invitation = null) { $account = $quote->account; - + if (!$quote->is_quote || $quote->quote_invoice_id) { return null; } @@ -118,189 +119,15 @@ class InvoiceService extends BaseService public function getDatatable($accountId, $clientPublicId = null, $entityType, $search) { + $datatable = new InvoiceDatatable( ! $clientPublicId, $clientPublicId); $query = $this->invoiceRepo->getInvoices($accountId, $clientPublicId, $entityType, $search) ->where('invoices.is_quote', '=', $entityType == ENTITY_QUOTE ? true : false); if(!Utils::hasPermission('view_all')){ $query->where('invoices.user_id', '=', Auth::user()->id); } - - return $this->createDatatable($entityType, $query, !$clientPublicId); - } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'invoice_number', - function ($model) use ($entityType) { - if(!Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id])){ - return $model->invoice_number; - } - - return link_to("{$entityType}s/{$model->public_id}/edit", $model->invoice_number, ['class' => Utils::getEntityRowClass($model)])->toHtml(); - } - ], - [ - 'client_name', - function ($model) { - if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ - return Utils::getClientDisplayName($model); - } - return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml(); - }, - ! $hideClient - ], - [ - 'invoice_date', - function ($model) { - return Utils::fromSqlDate($model->invoice_date); - } - ], - [ - 'amount', - function ($model) { - return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); - } - ], - [ - 'balance', - function ($model) { - return $model->partial > 0 ? - trans('texts.partial_remaining', [ - 'partial' => Utils::formatMoney($model->partial, $model->currency_id, $model->country_id), - 'balance' => Utils::formatMoney($model->balance, $model->currency_id, $model->country_id)] - ) : - Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); - }, - $entityType == ENTITY_INVOICE - ], - [ - 'due_date', - function ($model) { - return Utils::fromSqlDate($model->due_date); - }, - ], - [ - 'invoice_status_name', - function ($model) use ($entityType) { - return $model->quote_invoice_id ? link_to("invoices/{$model->quote_invoice_id}/edit", trans('texts.converted'))->toHtml() : self::getStatusLabel($entityType, $model); - } - ] - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - trans("texts.edit_{$entityType}"), - function ($model) use ($entityType) { - return URL::to("{$entityType}s/{$model->public_id}/edit"); - }, - function ($model) { - return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); - } - ], - [ - trans("texts.clone_{$entityType}"), - function ($model) use ($entityType) { - return URL::to("{$entityType}s/{$model->public_id}/clone"); - }, - function ($model) { - return Auth::user()->can('create', ENTITY_INVOICE); - } - ], - [ - trans("texts.view_history"), - function ($model) use ($entityType) { - return URL::to("{$entityType}s/{$entityType}_history/{$model->public_id}"); - } - ], - [ - '--divider--', function(){return false;}, - function ($model) { - return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]) || Auth::user()->can('create', ENTITY_PAYMENT); - } - ], - [ - trans("texts.mark_sent"), - function ($model) { - return "javascript:markEntity({$model->public_id})"; - }, - function ($model) { - return $model->invoice_status_id < INVOICE_STATUS_SENT && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); - } - ], - [ - trans('texts.enter_payment'), - function ($model) { - return URL::to("payments/create/{$model->client_public_id}/{$model->public_id}"); - }, - function ($model) use ($entityType) { - return $entityType == ENTITY_INVOICE && $model->balance > 0 && Auth::user()->can('create', ENTITY_PAYMENT); - } - ], - [ - trans("texts.view_quote"), - function ($model) { - return URL::to("quotes/{$model->quote_id}/edit"); - }, - function ($model) use ($entityType) { - return $entityType == ENTITY_INVOICE && $model->quote_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); - } - ], - [ - trans("texts.view_invoice"), - function ($model) { - return URL::to("invoices/{$model->quote_invoice_id}/edit"); - }, - function ($model) use ($entityType) { - return $entityType == ENTITY_QUOTE && $model->quote_invoice_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); - } - ], - [ - trans("texts.convert_to_invoice"), - function ($model) { - return "javascript:convertEntity({$model->public_id})"; - }, - function ($model) use ($entityType) { - return $entityType == ENTITY_QUOTE && ! $model->quote_invoice_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); - } - ] - ]; - } - - private function getStatusLabel($entityType, $model) - { - // check if invoice is overdue - if (Utils::parseFloat($model->balance) && $model->due_date && $model->due_date != '0000-00-00') { - if (\DateTime::createFromFormat('Y-m-d', $model->due_date) < new \DateTime("now")) { - $label = $entityType == ENTITY_INVOICE ? trans('texts.overdue') : trans('texts.expired'); - return "

    " . $label . "

    "; - } - } - - $label = trans("texts.status_" . strtolower($model->invoice_status_name)); - $class = 'default'; - switch ($model->invoice_status_id) { - case INVOICE_STATUS_SENT: - $class = 'info'; - break; - case INVOICE_STATUS_VIEWED: - $class = 'warning'; - break; - case INVOICE_STATUS_APPROVED: - $class = 'success'; - break; - case INVOICE_STATUS_PARTIAL: - $class = 'primary'; - break; - case INVOICE_STATUS_PAID: - $class = 'success'; - break; - } - return "

    $label

    "; + return $this->datatableService->createDatatable($datatable, $query); } } diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 51cbccc57177..c4dabe5b9a2c 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -23,6 +23,7 @@ use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\AccountRepository; use App\Services\BaseService; use App\Events\PaymentWasCreated; +use App\Ninja\Datatables\PaymentDatatable; class PaymentService extends BaseService { @@ -524,7 +525,7 @@ class PaymentService extends BaseService return $paymentMethod; } - + public function convertPaymentMethodFromGatewayResponse($gatewayResponse, $accountGateway, $accountGatewayToken = null, $contactId = null, $existingPaymentMethod = null) { if ($accountGateway->gateway_id == GATEWAY_STRIPE) { $data = $gatewayResponse->getData(); @@ -642,7 +643,7 @@ class PaymentService extends BaseService $payment->contact_id = $invitation->contact_id; $payment->transaction_reference = $ref; $payment->payment_date = date_create()->format('Y-m-d'); - + if (!empty($paymentDetails['card'])) { $card = $paymentDetails['card']; $payment->last4 = $card->getNumberLastFour(); @@ -707,10 +708,10 @@ class PaymentService extends BaseService $pending_monthly = true; } } - - if (!empty($plan)) { + + if (!empty($plan)) { $account = Account::with('users')->find($invoice->client->public_id); - + if( $account->company->plan != $plan || DateTime::createFromFormat('Y-m-d', $account->company->plan_expires) >= date_create('-7 days') @@ -719,10 +720,10 @@ class PaymentService extends BaseService // Reset any grandfathering $account->company->plan_started = date_create()->format('Y-m-d'); } - + if ( $account->company->plan == $plan - && $account->company->plan_term == $term + && $account->company->plan_term == $term && DateTime::createFromFormat('Y-m-d', $account->company->plan_expires) >= date_create() ) { // This is a renewal; mark it paid as of when this term expires @@ -730,13 +731,13 @@ class PaymentService extends BaseService } else { $account->company->plan_paid = date_create()->format('Y-m-d'); } - + $account->company->payment_id = $payment->id; $account->company->plan = $plan; $account->company->plan_term = $term; $account->company->plan_expires = DateTime::createFromFormat('Y-m-d', $account->company->plan_paid) ->modify($term == PLAN_TERM_MONTHLY ? '+1 month' : '+1 year')->format('Y-m-d'); - + if (!empty($pending_monthly)) { $account->company->pending_plan = $plan; $account->company->pending_term = PLAN_TERM_MONTHLY; @@ -744,7 +745,7 @@ class PaymentService extends BaseService $account->company->pending_plan = null; $account->company->pending_term = null; } - + $account->company->save(); } } @@ -783,7 +784,7 @@ class PaymentService extends BaseService return PAYMENT_TYPE_CREDIT_CARD_OTHER; } } - + private function detectCardType($number) { if (preg_match('/^3[47][0-9]{13}$/',$number)) { @@ -856,127 +857,17 @@ class PaymentService extends BaseService public function getDatatable($clientPublicId, $search) { + $datatable = new PaymentDatatable( ! $clientPublicId, $clientPublicId); $query = $this->paymentRepo->find($clientPublicId, $search); if(!Utils::hasPermission('view_all')){ $query->where('payments.user_id', '=', Auth::user()->id); } - return $this->createDatatable(ENTITY_PAYMENT, $query, !$clientPublicId, false, - ['invoice_number', 'transaction_reference', 'payment_type', 'amount', 'payment_date']); + return $this->datatableService->createDatatable($datatable, $query); } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'invoice_number', - function ($model) { - if(!Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id])){ - return $model->invoice_number; - } - - return link_to("invoices/{$model->invoice_public_id}/edit", $model->invoice_number, ['class' => Utils::getEntityRowClass($model)])->toHtml(); - } - ], - [ - 'client_name', - function ($model) { - if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ - return Utils::getClientDisplayName($model); - } - - return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : ''; - }, - ! $hideClient - ], - [ - 'transaction_reference', - function ($model) { - return $model->transaction_reference ? $model->transaction_reference : 'Manual entry'; - } - ], - [ - 'payment_type', - function ($model) { - return ($model->payment_type && !$model->last4) ? $model->payment_type : ($model->account_gateway_id ? $model->gateway_name : ''); - } - ], - [ - 'source', - function ($model) { - $code = str_replace(' ', '', strtolower($model->payment_type)); - $card_type = trans("texts.card_" . $code); - if ($model->payment_type_id != PAYMENT_TYPE_ACH) { - if($model->last4) { - $expiration = trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($model->expiration, false)->format('m/y'))); - return '' . htmlentities($card_type) . '  •••' . $model->last4 . ' ' . $expiration; - } elseif ($model->email) { - return $model->email; - } - } elseif ($model->last4) { - $bankData = PaymentMethod::lookupBankData($model->routing_number); - if (is_object($bankData)) { - return $bankData->name.'  •••' . $model->last4; - } elseif($model->last4) { - return '' . htmlentities($card_type) . '  •••' . $model->last4; - } - } - } - ], - [ - 'amount', - function ($model) { - return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); - } - ], - [ - 'payment_date', - function ($model) { - return Utils::dateToString($model->payment_date); - } - ], - [ - 'payment_status_name', - function ($model) use ($entityType) { - return self::getStatusLabel($entityType, $model); - } - ] - ]; - } - protected function getDatatableActions($entityType) - { - return [ - [ - trans('texts.edit_payment'), - function ($model) { - return URL::to("payments/{$model->public_id}/edit"); - }, - function ($model) { - return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]); - } - ], - [ - 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 ? $model->currency_id : 1, 'currencies')->symbol ; - return "javascript:showRefundModal({$model->public_id}, '{$max_refund}', '{$formatted}', '{$symbol}')"; - }, - function ($model) { - return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]) && $model->payment_status_id >= PAYMENT_STATUS_COMPLETED && - $model->refunded < $model->amount && - ( - ($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') { @@ -1001,50 +892,22 @@ class PaymentService extends BaseService 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_VOIDED: - case PAYMENT_STATUS_REFUNDED: - $class = 'default'; - break; - } - return "

    $label

    "; - } - + public function refund($payment, $amount = null) { if ($amount) { $amount = min($amount, $payment->amount - $payment->refunded); } $accountGateway = $payment->account_gateway; - + if (!$accountGateway) { $accountGateway = AccountGateway::withTrashed()->find($payment->account_gateway_id); } - + if (!$amount || !$accountGateway) { return; } - + if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) { $gateway = $this->createGateway($accountGateway); diff --git a/app/Services/PaymentTermService.php b/app/Services/PaymentTermService.php index 1371d20c2c43..08e2a84caf4b 100644 --- a/app/Services/PaymentTermService.php +++ b/app/Services/PaymentTermService.php @@ -25,10 +25,10 @@ class PaymentTermService extends BaseService { $query = $this->paymentTermRepo->find(); - return $this->createDatatable(ENTITY_PAYMENT_TERM, $query, false); + return $this->datatableService->createDatatable(ENTITY_PAYMENT_TERM, $query, false); } - protected function getDatatableColumns($entityType, $hideClient) + public function columns($entityType, $hideClient) { return [ [ @@ -46,7 +46,7 @@ class PaymentTermService extends BaseService ]; } - protected function getDatatableActions($entityType) + public function actions($entityType) { return [ [ @@ -57,4 +57,4 @@ class PaymentTermService extends BaseService ] ]; } -} \ No newline at end of file +} diff --git a/app/Services/ProductService.php b/app/Services/ProductService.php index f8ec6e1131d5..fc7fc6cceed4 100644 --- a/app/Services/ProductService.php +++ b/app/Services/ProductService.php @@ -1,12 +1,12 @@ productRepo->find($accountId); - return $this->createDatatable(ENTITY_PRODUCT, $query, false); + return $this->datatableService->createDatatable($datatable, $query); } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'product_key', - function ($model) { - return link_to('products/'.$model->public_id.'/edit', $model->product_key)->toHtml(); - } - ], - [ - 'notes', - function ($model) { - return nl2br(Str::limit($model->notes, 100)); - } - ], - [ - 'cost', - function ($model) { - return Utils::formatMoney($model->cost); - } - ], - [ - 'tax_rate', - function ($model) { - return $model->tax_rate ? ($model->tax_name . ' ' . $model->tax_rate . '%') : ''; - }, - Auth::user()->account->invoice_item_taxes - ] - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - uctrans('texts.edit_product'), - function ($model) { - return URL::to("products/{$model->public_id}/edit"); - } - ] - ]; - } - -} \ No newline at end of file +} diff --git a/app/Services/RecurringInvoiceService.php b/app/Services/RecurringInvoiceService.php index b003455abd6a..bc6daa1f0178 100644 --- a/app/Services/RecurringInvoiceService.php +++ b/app/Services/RecurringInvoiceService.php @@ -5,6 +5,7 @@ use Auth; use Utils; use App\Models\Invoice; use App\Ninja\Repositories\InvoiceRepository; +use App\Ninja\Datatables\RecurringInvoiceDatatable; class RecurringInvoiceService extends BaseService { @@ -19,64 +20,14 @@ class RecurringInvoiceService extends BaseService public function getDatatable($accountId, $clientPublicId = null, $entityType, $search) { + $datatable = new RecurringInvoiceDatatable( ! $clientPublicId, $clientPublicId); $query = $this->invoiceRepo->getRecurringInvoices($accountId, $clientPublicId, $search); if(!Utils::hasPermission('view_all')){ $query->where('invoices.user_id', '=', Auth::user()->id); } - - return $this->createDatatable(ENTITY_RECURRING_INVOICE, $query, !$clientPublicId); + + return $this->datatableService->createDatatable($datatable, $query); } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'frequency', - function ($model) { - return link_to("invoices/{$model->public_id}", $model->frequency)->toHtml(); - } - ], - [ - 'client_name', - function ($model) { - return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml(); - }, - ! $hideClient - ], - [ - 'start_date', - function ($model) { - return Utils::fromSqlDate($model->start_date); - } - ], - [ - 'end_date', - function ($model) { - return Utils::fromSqlDate($model->end_date); - } - ], - [ - 'amount', - function ($model) { - return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); - } - ] - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - trans('texts.edit_invoice'), - function ($model) { - return URL::to("invoices/{$model->public_id}/edit"); - }, - function ($model) { - return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); - } - ] - ]; - } -} \ No newline at end of file +} diff --git a/app/Services/TaskService.php b/app/Services/TaskService.php index e07793b2f85c..bfb25708d0d5 100644 --- a/app/Services/TaskService.php +++ b/app/Services/TaskService.php @@ -8,6 +8,7 @@ use App\Models\Invoice; use App\Models\Client; use App\Ninja\Repositories\TaskRepository; use App\Services\BaseService; +use App\Ninja\Datatables\TaskDatatable; class TaskService extends BaseService { @@ -34,112 +35,14 @@ class TaskService extends BaseService public function getDatatable($clientPublicId, $search) { + $datatable = new TaskDatatable( ! $clientPublicId, $clientPublicId); $query = $this->taskRepo->find($clientPublicId, $search); if(!Utils::hasPermission('view_all')){ $query->where('tasks.user_id', '=', Auth::user()->id); } - return $this->createDatatable(ENTITY_TASK, $query, !$clientPublicId); + return $this->datatableService->createDatatable($datatable, $query); } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'client_name', - function ($model) { - if(!Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])){ - return Utils::getClientDisplayName($model); - } - - return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : ''; - }, - ! $hideClient - ], - [ - 'created_at', - function ($model) { - return link_to("tasks/{$model->public_id}/edit", Task::calcStartTime($model))->toHtml(); - } - ], - [ - 'time_log', - function($model) { - return Utils::formatTime(Task::calcDuration($model)); - } - ], - [ - 'description', - function ($model) { - return $model->description; - } - ], - [ - 'invoice_number', - function ($model) { - return self::getStatusLabel($model); - } - ] - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - trans('texts.edit_task'), - function ($model) { - return URL::to('tasks/'.$model->public_id.'/edit'); - }, - function ($model) { - return (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('editByOwner', [ENTITY_TASK, $model->user_id]); - } - ], - [ - trans('texts.view_invoice'), - function ($model) { - return URL::to("/invoices/{$model->invoice_public_id}/edit"); - }, - function ($model) { - return $model->invoice_number && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id]); - } - ], - [ - trans('texts.stop_task'), - function ($model) { - return "javascript:stopTask({$model->public_id})"; - }, - function ($model) { - return $model->is_running && Auth::user()->can('editByOwner', [ENTITY_TASK, $model->user_id]); - } - ], - [ - trans('texts.invoice_task'), - function ($model) { - return "javascript:invoiceEntity({$model->public_id})"; - }, - function ($model) { - return ! $model->invoice_number && (!$model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('create', ENTITY_INVOICE); - } - ] - ]; - } - - private function getStatusLabel($model) - { - if ($model->invoice_number) { - $class = 'success'; - $label = trans('texts.invoiced'); - } elseif ($model->is_running) { - $class = 'primary'; - $label = trans('texts.running'); - } else { - $class = 'default'; - $label = trans('texts.logged'); - } - - return "

    $label

    "; - } - -} \ No newline at end of file +} diff --git a/app/Services/TaxRateService.php b/app/Services/TaxRateService.php index db5b041395f0..330477149462 100644 --- a/app/Services/TaxRateService.php +++ b/app/Services/TaxRateService.php @@ -4,6 +4,7 @@ use URL; use Auth; use App\Services\BaseService; use App\Ninja\Repositories\TaxRateRepository; +use App\Ninja\Datatables\TaxRateDatatable; class TaxRateService extends BaseService { @@ -21,48 +22,12 @@ class TaxRateService extends BaseService return $this->taxRateRepo; } - /* - public function save() - { - return null; - } - */ - public function getDatatable($accountId) { + $datatable = new TaxRateDatatable(false); $query = $this->taxRateRepo->find($accountId); - return $this->createDatatable(ENTITY_TAX_RATE, $query, false); + return $this->datatableService->createDatatable($datatable, $query); } - - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'name', - function ($model) { - return link_to("tax_rates/{$model->public_id}/edit", $model->name)->toHtml(); - } - ], - [ - 'rate', - function ($model) { - return $model->rate . '%'; - } - ] - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - uctrans('texts.edit_tax_rate'), - function ($model) { - return URL::to("tax_rates/{$model->public_id}/edit"); - } - ] - ]; - } - -} \ No newline at end of file + +} diff --git a/app/Services/TokenService.php b/app/Services/TokenService.php index 092f3995d3d7..5d5b29e3de6c 100644 --- a/app/Services/TokenService.php +++ b/app/Services/TokenService.php @@ -3,6 +3,7 @@ use URL; use App\Services\BaseService; use App\Ninja\Repositories\TokenRepository; +use App\Ninja\Datatables\TokenDatatable; class TokenService extends BaseService { @@ -20,48 +21,12 @@ class TokenService extends BaseService return $this->tokenRepo; } - /* - public function save() - { - return null; - } - */ - public function getDatatable($userId) { + $datatable = new TokenDatatable(false); $query = $this->tokenRepo->find($userId); - return $this->createDatatable(ENTITY_TOKEN, $query, false); + return $this->datatableService->createDatatable($datatable, $query); } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'name', - function ($model) { - return link_to("tokens/{$model->public_id}/edit", $model->name)->toHtml(); - } - ], - [ - 'token', - function ($model) { - return $model->token; - } - ] - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - uctrans('texts.edit_token'), - function ($model) { - return URL::to("tokens/{$model->public_id}/edit"); - } - ] - ]; - } - -} \ No newline at end of file +} diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 8e31b4c30d2d..3aa7a60c8851 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -3,6 +3,7 @@ use URL; use App\Services\BaseService; use App\Ninja\Repositories\UserRepository; +use App\Ninja\Datatables\UserDatatable; class UserService extends BaseService { @@ -20,102 +21,12 @@ class UserService extends BaseService return $this->userRepo; } - /* - public function save() - { - return null; - } - */ - public function getDatatable($accountId) { + $datatable = new UserDatatable(false); $query = $this->userRepo->find($accountId); - return $this->createDatatable(ENTITY_USER, $query, false); + return $this->datatableService->createDatatable($datatable, $query); } - protected function getDatatableColumns($entityType, $hideClient) - { - return [ - [ - 'first_name', - function ($model) { - return $model->public_id ? link_to('users/'.$model->public_id.'/edit', $model->first_name.' '.$model->last_name)->toHtml() : ($model->first_name.' '.$model->last_name); - } - ], - [ - 'email', - function ($model) { - return $model->email; - } - ], - [ - 'confirmed', - function ($model) { - if (!$model->public_id) { - return self::getStatusLabel(USER_STATE_OWNER); - } elseif ($model->deleted_at) { - return self::getStatusLabel(USER_STATE_DISABLED); - } elseif ($model->confirmed) { - if($model->is_admin){ - return self::getStatusLabel(USER_STATE_ADMIN); - } else { - return self::getStatusLabel(USER_STATE_ACTIVE); - } - } else { - return self::getStatusLabel(USER_STATE_PENDING); - } - } - ], - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - uctrans('texts.edit_user'), - function ($model) { - return URL::to("users/{$model->public_id}/edit"); - }, - function ($model) { - return $model->public_id; - } - ], - [ - uctrans('texts.send_invite'), - function ($model) { - return URL::to("send_confirmation/{$model->public_id}"); - }, - function ($model) { - return $model->public_id && ! $model->confirmed; - } - ] - ]; - } - - private function getStatusLabel($state) - { - $label = trans("texts.{$state}"); - $class = 'default'; - switch ($state) { - case USER_STATE_PENDING: - $class = 'default'; - break; - case USER_STATE_ACTIVE: - $class = 'info'; - break; - case USER_STATE_DISABLED: - $class = 'warning'; - break; - case USER_STATE_OWNER: - $class = 'success'; - break; - case USER_STATE_ADMIN: - $class = 'primary'; - break; - } - return "

    $label

    "; - } - -} \ No newline at end of file +} diff --git a/app/Services/VendorService.php b/app/Services/VendorService.php index 41f5fd4664bb..7477300036d7 100644 --- a/app/Services/VendorService.php +++ b/app/Services/VendorService.php @@ -38,78 +38,12 @@ class VendorService extends BaseService public function getDatatable($search) { $query = $this->vendorRepo->find($search); - + if(!Utils::hasPermission('view_all')){ $query->where('vendors.user_id', '=', Auth::user()->id); } - return $this->createDatatable(ENTITY_VENDOR, $query); + return $this->datatableService->createDatatable(ENTITY_VENDOR, $query); } - protected function getDatatableColumns($entityType, $hideVendor) - { - return [ - [ - 'name', - function ($model) { - return link_to("vendors/{$model->public_id}", $model->name ?: '')->toHtml(); - } - ], - [ - 'city', - function ($model) { - return $model->city; - } - ], - [ - 'work_phone', - function ($model) { - return $model->work_phone; - } - ], - [ - 'email', - function ($model) { - return link_to("vendors/{$model->public_id}", $model->email ?: '')->toHtml(); - } - ], - [ - 'vendors.created_at', - function ($model) { - return Utils::timestampToDateString(strtotime($model->created_at)); - } - ], - ]; - } - - protected function getDatatableActions($entityType) - { - return [ - [ - trans('texts.edit_vendor'), - function ($model) { - return URL::to("vendors/{$model->public_id}/edit"); - }, - function ($model) { - return Auth::user()->can('editByOwner', [ENTITY_VENDOR, $model->user_id]); - } - ], - [ - '--divider--', function(){return false;}, - function ($model) { - return Auth::user()->can('editByOwner', [ENTITY_VENDOR, $model->user_id]) && Auth::user()->can('create', ENTITY_EXPENSE); - } - - ], - [ - trans('texts.enter_expense'), - function ($model) { - return URL::to("expenses/create/{$model->public_id}"); - }, - function ($model) { - return Auth::user()->can('create', ENTITY_EXPENSE); - } - ] - ]; - } } From 541b19cd5f0ac213014e3f6a91252afe791344e6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 23 May 2016 21:03:01 +0300 Subject: [PATCH 136/386] Updated vendor expenses to new datatable class --- app/Http/routes.php | 2 +- app/Ninja/Datatables/ExpenseDatatable.php | 6 ++++-- app/Ninja/Repositories/ExpenseRepository.php | 18 ++---------------- app/Services/ExpenseService.php | 13 ++++++++----- resources/views/vendors/show.blade.php | 2 +- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index 9f97eea1e9d1..ae0cec077a2b 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -190,7 +190,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::resource('expenses', 'ExpenseController'); Route::get('expenses/create/{vendor_id?}/{client_id?}', 'ExpenseController@create'); Route::get('api/expense', array('as'=>'api.expenses', 'uses'=>'ExpenseController@getDatatable')); - Route::get('api/expenseVendor/{id}', array('as'=>'api.expense', 'uses'=>'ExpenseController@getDatatableVendor')); + Route::get('api/vendor_expense/{id}', array('as'=>'api.expense', 'uses'=>'ExpenseController@getDatatableVendor')); Route::post('expenses/bulk', 'ExpenseController@bulk'); }); diff --git a/app/Ninja/Datatables/ExpenseDatatable.php b/app/Ninja/Datatables/ExpenseDatatable.php index 70cc771afe2b..29bbbe0c087d 100644 --- a/app/Ninja/Datatables/ExpenseDatatable.php +++ b/app/Ninja/Datatables/ExpenseDatatable.php @@ -24,7 +24,8 @@ class ExpenseDatatable extends EntityDatatable } else { return ''; } - } + }, + ! $this->hideClient ], [ 'client_name', @@ -39,7 +40,8 @@ class ExpenseDatatable extends EntityDatatable } else { return ''; } - } + }, + ! $this->hideClient ], [ 'expense_date', diff --git a/app/Ninja/Repositories/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php index 0ce0e155cf50..2d782298a2e7 100644 --- a/app/Ninja/Repositories/ExpenseRepository.php +++ b/app/Ninja/Repositories/ExpenseRepository.php @@ -36,22 +36,8 @@ class ExpenseRepository extends BaseRepository public function findVendor($vendorPublicId) { $vendorId = Vendor::getPrivateId($vendorPublicId); - $accountid = \Auth::user()->account_id; - $query = DB::table('expenses') - ->join('accounts', 'accounts.id', '=', 'expenses.account_id') - ->where('expenses.account_id', '=', $accountid) - ->where('expenses.vendor_id', '=', $vendorId) - ->select( - 'expenses.id', - 'expenses.expense_date', - 'expenses.amount', - 'expenses.public_notes', - 'expenses.public_id', - 'expenses.deleted_at', - 'expenses.should_be_invoiced', - 'expenses.created_at', - 'expenses.user_id' - ); + + $query = $this->find()->where('expenses.vendor_id', '=', $vendorId); return $query; } diff --git a/app/Services/ExpenseService.php b/app/Services/ExpenseService.php index 94bfd7d8c87a..a16bff7dd3f1 100644 --- a/app/Services/ExpenseService.php +++ b/app/Services/ExpenseService.php @@ -55,12 +55,15 @@ class ExpenseService extends BaseService public function getDatatableVendor($vendorPublicId) { + $datatable = new ExpenseDatatable(false, true); + $query = $this->expenseRepo->findVendor($vendorPublicId); - return $this->datatableService->createDatatable(ENTITY_EXPENSE, - $query, - $this->getDatatableColumnsVendor(ENTITY_EXPENSE,false), - $this->getDatatableActionsVendor(ENTITY_EXPENSE), - false); + + if(!Utils::hasPermission('view_all')){ + $query->where('expenses.user_id', '=', Auth::user()->id); + } + + return $this->datatableService->createDatatable($datatable, $query); } diff --git a/resources/views/vendors/show.blade.php b/resources/views/vendors/show.blade.php index 37572ee4c5bc..eca43f233662 100644 --- a/resources/views/vendors/show.blade.php +++ b/resources/views/vendors/show.blade.php @@ -152,7 +152,7 @@ trans('texts.expense_date'), trans('texts.amount'), trans('texts.public_notes')) - ->setUrl(url('api/expenseVendor/' . $vendor->public_id)) + ->setUrl(url('api/vendor_expense/' . $vendor->public_id)) ->setCustomValues('entityType', 'expenses') ->setOptions('sPaginationType', 'bootstrap') ->setOptions('bFilter', false) From b2beb8fb73e84472b92cab85786e08aed823b9a8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 23 May 2016 21:07:06 +0300 Subject: [PATCH 137/386] Updated vendors to new datatable class --- app/Ninja/Datatables/VendorDatatable.php | 2 +- app/Services/DatatableService.php | 1 - app/Services/VendorService.php | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Ninja/Datatables/VendorDatatable.php b/app/Ninja/Datatables/VendorDatatable.php index fda61fe457d8..93e059b6ae55 100644 --- a/app/Ninja/Datatables/VendorDatatable.php +++ b/app/Ninja/Datatables/VendorDatatable.php @@ -74,6 +74,6 @@ class VendorDatatable extends EntityDatatable ] ]; } - + } diff --git a/app/Services/DatatableService.php b/app/Services/DatatableService.php index 453f4d0f25b0..8fa49df5f7df 100644 --- a/app/Services/DatatableService.php +++ b/app/Services/DatatableService.php @@ -8,7 +8,6 @@ use App\Ninja\Datatables\EntityDatatable; class DatatableService { - //public function createDatatable($entityType, $query, $columns, $actions = null, $showCheckbox = true, $orderColumns = []) public function createDatatable(EntityDatatable $datatable, $query) { $table = Datatable::query($query); diff --git a/app/Services/VendorService.php b/app/Services/VendorService.php index 7477300036d7..0940be420cb4 100644 --- a/app/Services/VendorService.php +++ b/app/Services/VendorService.php @@ -8,6 +8,7 @@ use App\Models\Expense; use App\Services\BaseService; use App\Ninja\Repositories\VendorRepository; use App\Ninja\Repositories\NinjaRepository; +use App\Ninja\Datatables\VendorDatatable; class VendorService extends BaseService { @@ -37,13 +38,14 @@ class VendorService extends BaseService public function getDatatable($search) { + $datatable = new VendorDatatable(); $query = $this->vendorRepo->find($search); if(!Utils::hasPermission('view_all')){ $query->where('vendors.user_id', '=', Auth::user()->id); } - return $this->datatableService->createDatatable(ENTITY_VENDOR, $query); + return $this->datatableService->createDatatable($datatable, $query); } } From 406df6f3d870bdf5191618171cdf0d252db321be Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 23 May 2016 21:10:40 +0300 Subject: [PATCH 138/386] Fix for payment datatable list --- app/Ninja/Datatables/PaymentDatatable.php | 6 ++++++ app/Services/PaymentService.php | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Ninja/Datatables/PaymentDatatable.php b/app/Ninja/Datatables/PaymentDatatable.php index 4ca1d2839fc8..22006bf742e1 100644 --- a/app/Ninja/Datatables/PaymentDatatable.php +++ b/app/Ninja/Datatables/PaymentDatatable.php @@ -10,6 +10,12 @@ class PaymentDatatable extends EntityDatatable { public $entityType = ENTITY_PAYMENT; + protected static $refundableGateways = array( + GATEWAY_STRIPE, + GATEWAY_BRAINTREE, + GATEWAY_WEPAY, + ); + public function columns() { return [ diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index c4dabe5b9a2c..b227b466413c 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -30,12 +30,6 @@ class PaymentService extends BaseService public $lastError; protected $datatableService; - protected static $refundableGateways = array( - GATEWAY_STRIPE, - GATEWAY_BRAINTREE, - GATEWAY_WEPAY, - ); - public function __construct(PaymentRepository $paymentRepo, AccountRepository $accountRepo, DatatableService $datatableService) { $this->datatableService = $datatableService; From 7cc29de8810e5783f80717306141ab35447af8ea Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 24 May 2016 10:18:10 +0300 Subject: [PATCH 139/386] Bumped version --- app/Http/routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index ae0cec077a2b..28b9a4d5a153 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -584,7 +584,7 @@ if (!defined('CONTACT_EMAIL')) { define('NINJA_WEB_URL', env('NINJA_WEB_URL', 'https://www.invoiceninja.com')); define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com')); define('NINJA_DATE', '2000-01-01'); - define('NINJA_VERSION', '2.5.2.1' . env('NINJA_VERSION_SUFFIX')); + define('NINJA_VERSION', '2.5.2.2' . env('NINJA_VERSION_SUFFIX')); define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja')); define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja')); From 2aed1354e9afb5a3f72606a28b23bf8ccb83f80c Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Tue, 24 May 2016 11:47:03 -0400 Subject: [PATCH 140/386] 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 141/386] 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 142/386] 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 143/386] 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 144/386] 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 145/386] 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 146/386] 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 147/386] 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 ee2ef4608e3ddba7264198719f4f7ff49e5f9fc0 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 May 2016 11:59:46 +0300 Subject: [PATCH 148/386] Lazy load invoice documents --- resources/views/invoices/edit.blade.php | 92 ++++++++++++++----------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 3ca6acafba59..26b42a0bd90b 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -998,47 +998,59 @@ }) // Initialize document upload - dropzone = new Dropzone('#document-upload .dropzone', { - url:{!! json_encode(url('document')) !!}, - params:{ - _token:"{{ Session::getToken() }}" - }, - acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!}, - addRemoveLinks:true, - @foreach(['default_message', 'fallback_message', 'fallback_text', 'file_too_big', 'invalid_file_type', 'response_error', 'cancel_upload', 'cancel_upload_confirmation', 'remove_file'] as $key) - "dict{{Utils::toClassCase($key)}}":"{{trans('texts.dropzone_'.$key)}}", - @endforeach - maxFileSize:{{floatval(MAX_DOCUMENT_SIZE/1000)}}, - }); - if(dropzone instanceof Dropzone){ - dropzone.on("addedfile",handleDocumentAdded); - dropzone.on("removedfile",handleDocumentRemoved); - dropzone.on("success",handleDocumentUploaded); - for (var i=0; i Date: Wed, 25 May 2016 12:10:22 +0300 Subject: [PATCH 149/386] Prevent setting pdfstring when saving an invoice --- resources/views/invoices/edit.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 26b42a0bd90b..5099ded5206b 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1230,7 +1230,7 @@ } } - preparePdfData(''); + submitAction(''); } function getSendToEmails() { From ba561c7d0d3b3f3672608344d382735d908f611f Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 May 2016 14:42:20 +0300 Subject: [PATCH 150/386] Prevent clicking save while document is still uploading --- resources/lang/en/texts.php | 2 ++ resources/views/invoices/edit.blade.php | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 2b9f2e5f4aec..481de829b614 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1311,6 +1311,8 @@ $LANG = array( 'new_start_date' => 'New start date', 'security' => 'Security', 'see_whats_new' => 'See what\'s new in v:version', + 'wait_for_upload' => 'Please wait for the document upload to complete.' + ); return $LANG; diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 5099ded5206b..c27057b2b284 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1265,6 +1265,11 @@ } function onFormSubmit(event) { + if (window.countUploadingDocuments > 0) { + alert("{!! trans('texts.wait_for_upload') !!}"); + return false; + } + if (!isSaveValid()) { model.showClientForm(); return false; @@ -1435,6 +1440,7 @@ model.invoice().invoice_number(number); } + window.countUploadingDocuments = 0; @if ($account->hasFeature(FEATURE_DOCUMENTS)) function handleDocumentAdded(file){ // open document when clicked @@ -1446,6 +1452,7 @@ if(file.mock)return; file.index = model.invoice().documents().length; model.invoice().addDocument({name:file.name, size:file.size, type:file.type}); + window.countUploadingDocuments++; } function handleDocumentRemoved(file){ @@ -1456,6 +1463,7 @@ function handleDocumentUploaded(file, response){ file.public_id = response.document.public_id model.invoice().documents()[file.index].update(response.document); + window.countUploadingDocuments--; refreshPDF(true); if(response.document.preview_url){ From df2a6527988ca59718a35b7666c0bee32f937403 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 May 2016 15:18:12 +0300 Subject: [PATCH 151/386] Change document to click to full size --- resources/views/expenses/edit.blade.php | 2 +- resources/views/invoices/edit.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/expenses/edit.blade.php b/resources/views/expenses/edit.blade.php index 41af3f1e801e..873da5451ea0 100644 --- a/resources/views/expenses/edit.blade.php +++ b/resources/views/expenses/edit.blade.php @@ -249,7 +249,7 @@ public_id:document.public_id(), status:Dropzone.SUCCESS, accepted:true, - url:document.preview_url()||document.url(), + url:document.url(), mock:true, index:i }; diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index c27057b2b284..235266c64a54 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1034,7 +1034,7 @@ public_id:document.public_id(), status:Dropzone.SUCCESS, accepted:true, - url:document.preview_url()||document.url(), + url:document.url(), mock:true, index:i }; From 4a4494a2f3b9704bda1290655df5315af8db65d9 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 May 2016 16:16:34 +0300 Subject: [PATCH 152/386] Fix for cancelled accounts --- .../2016_03_22_168362_add_documents.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/database/migrations/2016_03_22_168362_add_documents.php b/database/migrations/2016_03_22_168362_add_documents.php index 233f420c198b..d921deeafaed 100644 --- a/database/migrations/2016_03_22_168362_add_documents.php +++ b/database/migrations/2016_03_22_168362_add_documents.php @@ -18,7 +18,7 @@ class AddDocuments extends Migration { $table->boolean('invoice_embed_documents')->default(1); $table->boolean('document_email_attachment')->default(1); }); - + \DB::table('accounts')->update(array('logo' => '')); Schema::dropIfExists('documents'); Schema::create('documents', function($t) @@ -41,14 +41,14 @@ class AddDocuments extends Migration { $t->timestamps(); - $t->foreign('account_id')->references('id')->on('accounts'); - $t->foreign('user_id')->references('id')->on('users'); - $t->foreign('invoice_id')->references('id')->on('invoices'); - $t->foreign('expense_id')->references('id')->on('expenses'); - - + $t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $t->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade'); + $t->foreign('expense_id')->references('id')->on('expenses')->onDelete('cascade'); + + $t->unique( array('account_id','public_id') ); - }); + }); } /** * Reverse the migrations. @@ -65,7 +65,7 @@ class AddDocuments extends Migration { $table->dropColumn('invoice_embed_documents'); $table->dropColumn('document_email_attachment'); }); - + Schema::dropIfExists('documents'); } } From c701c5563c03cc907f8ba13af28971db97b69c3d Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Wed, 25 May 2016 10:36:40 -0400 Subject: [PATCH 153/386] 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 154/386] 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 bd817a3700a4cf36a5c8a6959ca633d27d2c6cc8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 May 2016 21:28:41 +0300 Subject: [PATCH 155/386] Try running TaxRates test earlier --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1f66f7aa29f7..cbfc3331d574 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,6 +66,7 @@ before_script: script: - php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php - php ./vendor/codeception/codeception/codecept run --debug acceptance APICest.php + - php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance CheckBalanceCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance ClientCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance ExpenseCest.php @@ -76,7 +77,6 @@ script: - php ./vendor/codeception/codeception/codecept run acceptance OnlinePaymentCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance PaymentCest.php - php ./vendor/codeception/codeception/codecept run --debug acceptance TaskCest.php - - php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php #- sed -i 's/NINJA_DEV=true/NINJA_PROD=true/g' .env #- php ./vendor/codeception/codeception/codecept run acceptance GoProCest.php @@ -96,4 +96,4 @@ after_script: notifications: email: on_success: never - on_failure: change \ No newline at end of file + on_failure: change From 85184f9a5b57bb6b0ed3255b6af1a7d2c23cdff7 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 May 2016 21:42:22 +0300 Subject: [PATCH 156/386] check document completed upload before user submitted form --- app/Ninja/Repositories/ExpenseRepository.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/Ninja/Repositories/ExpenseRepository.php b/app/Ninja/Repositories/ExpenseRepository.php index 2d782298a2e7..2007ec6e8613 100644 --- a/app/Ninja/Repositories/ExpenseRepository.php +++ b/app/Ninja/Repositories/ExpenseRepository.php @@ -148,11 +148,14 @@ class ExpenseRepository extends BaseRepository // Documents $document_ids = !empty($input['document_ids'])?array_map('intval', $input['document_ids']):array();; foreach ($document_ids as $document_id){ - $document = Document::scope($document_id)->first(); - if($document && Auth::user()->can('edit', $document)){ - $document->invoice_id = null; - $document->expense_id = $expense->id; - $document->save(); + // check document completed upload before user submitted form + if ($document_id) { + $document = Document::scope($document_id)->first(); + if($document && Auth::user()->can('edit', $document)){ + $document->invoice_id = null; + $document->expense_id = $expense->id; + $document->save(); + } } } From 44fb6682e484b2cf27287cd2548af4987d631ba0 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 25 May 2016 22:01:15 +0300 Subject: [PATCH 157/386] Prevent saving expense before documents have uploaded --- resources/views/expenses/edit.blade.php | 24 ++++++++++++++++++++++-- resources/views/invoices/edit.blade.php | 7 ++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/resources/views/expenses/edit.blade.php b/resources/views/expenses/edit.blade.php index 873da5451ea0..aadfacc52617 100644 --- a/resources/views/expenses/edit.blade.php +++ b/resources/views/expenses/edit.blade.php @@ -15,7 +15,10 @@ @section('content') - {!! Former::open($url)->addClass('warn-on-exit main-form')->method($method) !!} + {!! Former::open($url) + ->addClass('warn-on-exit main-form') + ->onsubmit('return onFormSubmit(event)') + ->method($method) !!}
    {!! Former::text('action') !!}
    @@ -154,6 +157,15 @@ clientMap[client.public_id] = client; } + function onFormSubmit(event) { + if (window.countUploadingDocuments > 0) { + alert("{!! trans('texts.wait_for_upload') !!}"); + return false; + } + + return true; + } + function onClientChange() { var clientId = $('select#client_id').val(); var client = clientMap[clientId]; @@ -240,6 +252,7 @@ dropzone.on("addedfile",handleDocumentAdded); dropzone.on("removedfile",handleDocumentRemoved); dropzone.on("success",handleDocumentUploaded); + dropzone.on("canceled",handleDocumentCanceled); for (var i=0; iaccount->hasFeature(FEATURE_DOCUMENTS)) function handleDocumentAdded(file){ // open document when clicked @@ -373,6 +387,7 @@ if(file.mock)return; file.index = model.documents().length; model.addDocument({name:file.name, size:file.size, type:file.type}); + window.countUploadingDocuments++; } function handleDocumentRemoved(file){ @@ -382,11 +397,16 @@ function handleDocumentUploaded(file, response){ file.public_id = response.document.public_id model.documents()[file.index].update(response.document); - + window.countUploadingDocuments--; if(response.document.preview_url){ dropzone.emit('thumbnail', file, response.document.preview_url); } } + + function handleDocumentCanceled() + { + window.countUploadingDocuments--; + } @endif diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 235266c64a54..d17a3ba25c62 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1025,6 +1025,7 @@ dropzone.on("addedfile",handleDocumentAdded); dropzone.on("removedfile",handleDocumentRemoved); dropzone.on("success",handleDocumentUploaded); + dropzone.on("canceled",handleDocumentCanceled); for (var i=0; i From c9f00274b1d4c164c514cb0eef78b34f51798514 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Wed, 25 May 2016 15:04:58 -0400 Subject: [PATCH 158/386] 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()) - + @endif {!! Former::checkbox('is_admin') ->label(' ') @@ -61,12 +68,11 @@ ->id('permissions_edit_all') ->text(trans('texts.user_edit_all')) ->help(trans('texts.edit_all_help')) !!} - -
    -
    -@endif - {!! Former::actions( +
    +
    + + {!! Former::actions( Button::normal(trans('texts.cancel'))->asLinkTo(URL::to('/settings/user_management'))->appendIcon(Icon::create('remove-circle'))->large(), Button::success(trans($user && $user->confirmed ? 'texts.save' : 'texts.send_invite'))->submit()->large()->appendIcon(Icon::create($user && $user->confirmed ? 'floppy-disk' : 'send')) )!!} @@ -88,4 +94,4 @@ if(!viewChecked)$('#permissions_edit_all').prop('checked',false) } fixCheckboxes(); -@stop \ No newline at end of file +@stop From dd9c95a1a851189772e29a04d850d63c35ddfe4b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 May 2016 18:21:43 +0300 Subject: [PATCH 166/386] Trying to fix zend_mm_heap corrupted --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index dd7541da820e..0518d00953f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ before_install: # set GitHub token and update composer - if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi; - composer self-update && composer -V + - export USE_ZEND_ALLOC=0 install: # install Composer dependencies From 0da3741b567e6492f63e91e13eeeb1dbaf23e6cb Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 May 2016 18:28:13 +0300 Subject: [PATCH 167/386] Fixed date formatting in document list --- app/Ninja/Repositories/DocumentRepository.php | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/app/Ninja/Repositories/DocumentRepository.php b/app/Ninja/Repositories/DocumentRepository.php index 094b4848ebe0..a2278b1843ae 100644 --- a/app/Ninja/Repositories/DocumentRepository.php +++ b/app/Ninja/Repositories/DocumentRepository.php @@ -61,31 +61,31 @@ class DocumentRepository extends BaseRepository { $extension = strtolower($uploaded->getClientOriginalExtension()); if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){ - $documentType = Document::$extraExtensions[$extension]; + $documentType = Document::$extraExtensions[$extension]; } else{ $documentType = $extension; } - + if(empty(Document::$types[$documentType])){ return 'Unsupported file type'; } - + $documentTypeData = Document::$types[$documentType]; - + $filePath = $uploaded->path(); $name = $uploaded->getClientOriginalName(); $size = filesize($filePath); - + if($size/1000 > MAX_DOCUMENT_SIZE){ return 'File too large'; } - - - + + + $hash = sha1_file($filePath); $filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType; - + $document = Document::createNew(); $disk = $document->getDisk(); if(!$disk->exists($filename)){// Have we already stored the same file @@ -93,7 +93,7 @@ class DocumentRepository extends BaseRepository $disk->getDriver()->putStream($filename, $stream, ['mimetype'=>$documentTypeData['mime']]); fclose($stream); } - + // This is an image; check if we need to create a preview if(in_array($documentType, array('jpeg','png','gif','bmp','tiff','psd'))){ $makePreview = false; @@ -105,32 +105,32 @@ class DocumentRepository extends BaseRepository // Needs to be converted $makePreview = true; } else if($width > DOCUMENT_PREVIEW_SIZE || $height > DOCUMENT_PREVIEW_SIZE){ - $makePreview = true; + $makePreview = true; } - + if(in_array($documentType,array('bmp','tiff','psd'))){ if(!class_exists('Imagick')){ // Cant't read this $makePreview = false; } else { $imgManagerConfig['driver'] = 'imagick'; - } + } } - + if($makePreview){ $previewType = 'jpeg'; if(in_array($documentType, array('png','gif','tiff','psd'))){ // Has transparency $previewType = 'png'; } - + $document->preview = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType.'.x'.DOCUMENT_PREVIEW_SIZE.'.'.$previewType; if(!$disk->exists($document->preview)){ // We haven't created a preview yet $imgManager = new ImageManager($imgManagerConfig); - + $img = $imgManager->make($filePath); - + if($width <= DOCUMENT_PREVIEW_SIZE && $height <= DOCUMENT_PREVIEW_SIZE){ $previewWidth = $width; $previewHeight = $height; @@ -141,9 +141,9 @@ class DocumentRepository extends BaseRepository $previewHeight = DOCUMENT_PREVIEW_SIZE; $previewWidth = $width * DOCUMENT_PREVIEW_SIZE / $height; } - + $img->resize($previewWidth, $previewHeight); - + $previewContent = (string) $img->encode($previewType); $disk->put($document->preview, $previewContent); $base64 = base64_encode($previewContent); @@ -153,23 +153,23 @@ class DocumentRepository extends BaseRepository } }else{ $base64 = base64_encode(file_get_contents($filePath)); - } + } } - + $document->path = $filename; $document->type = $documentType; $document->size = $size; $document->hash = $hash; $document->name = substr($name, -255); - + if(!empty($imageSize)){ $document->width = $imageSize[0]; $document->height = $imageSize[1]; } - + $document->save(); $doc_array = $document->toArray(); - + if(!empty($base64)){ $mime = Document::$types[!empty($previewType)?$previewType:$documentType]['mime']; $doc_array['base64'] = 'data:'.$mime.';base64,'.$base64; @@ -177,10 +177,10 @@ class DocumentRepository extends BaseRepository return $document; } - + public function getClientDatatable($contactId, $entityType, $search) { - + $query = DB::table('invitations') ->join('accounts', 'accounts.id', '=', 'invitations.account_id') ->join('invoices', 'invoices.id', '=', 'invitations.invoice_id') @@ -192,7 +192,7 @@ class DocumentRepository extends BaseRepository ->where('clients.deleted_at', '=', null) ->where('invoices.is_recurring', '=', false) // This needs to be a setting to also hide the activity on the dashboard page - //->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT) + //->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT) ->select( 'invitations.invitation_key', 'invoices.invoice_number', @@ -205,22 +205,22 @@ class DocumentRepository extends BaseRepository $table = \Datatable::query($query) ->addColumn('invoice_number', function ($model) { return link_to( - '/view/'.$model->invitation_key, + '/view/'.$model->invitation_key, $model->invoice_number - )->toHtml(); + )->toHtml(); }) ->addColumn('name', function ($model) { return link_to( - '/client/documents/'.$model->invitation_key.'/'.$model->public_id.'/'.$model->name, + '/client/documents/'.$model->invitation_key.'/'.$model->public_id.'/'.$model->name, $model->name, ['target'=>'_blank'] - )->toHtml(); + )->toHtml(); }) ->addColumn('document_date', function ($model) { - return Utils::fromSqlDate($model->created_at); + return Utils::dateToString($model->created_at); }) ->addColumn('document_size', function ($model) { - return Form::human_filesize($model->size); + return Form::human_filesize($model->size); }); return $table->make(); From 2043621ac9105d244dff85f921adfd746ea40d29 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 May 2016 19:02:53 +0300 Subject: [PATCH 168/386] Fix for deleting documents --- app/Http/Controllers/DocumentController.php | 44 ++++++++++++--------- app/Http/Requests/UpdateDocumentRequest.php | 26 ++++++++++++ app/Http/routes.php | 1 + resources/views/expenses/edit.blade.php | 8 ++++ resources/views/invoices/edit.blade.php | 8 ++++ 5 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 app/Http/Requests/UpdateDocumentRequest.php diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php index d597ba004474..e4ea605d57a0 100644 --- a/app/Http/Controllers/DocumentController.php +++ b/app/Http/Controllers/DocumentController.php @@ -14,6 +14,7 @@ use App\Ninja\Repositories\DocumentRepository; use App\Http\Requests\DocumentRequest; use App\Http\Requests\CreateDocumentRequest; +use App\Http\Requests\UpdateDocumentRequest; class DocumentController extends BaseController { @@ -26,20 +27,20 @@ class DocumentController extends BaseController $this->documentRepo = $documentRepo; } - + public function get(DocumentRequest $request) { return static::getDownloadResponse($request->entity()); } - + public static function getDownloadResponse($document){ $direct_url = $document->getDirectUrl(); if($direct_url){ return redirect($direct_url); } - + $stream = $document->getStream(); - + if($stream){ $headers = [ 'Content-Type' => Document::$types[$document->type]['mime'], @@ -54,59 +55,59 @@ class DocumentController extends BaseController $response = Response::make($document->getRaw(), 200); $response->header('content-type', Document::$types[$document->type]['mime']); } - + return $response; } - + public function getPreview(DocumentRequest $request) { $document = $request->entity(); - + if(empty($document->preview)){ return Response::view('error', array('error'=>'Preview does not exist!'), 404); } - + $direct_url = $document->getDirectPreviewUrl(); if($direct_url){ return redirect($direct_url); } - + $previewType = pathinfo($document->preview, PATHINFO_EXTENSION); $response = Response::make($document->getRawPreview(), 200); $response->header('content-type', Document::$types[$previewType]['mime']); - + return $response; } - + public function getVFSJS(DocumentRequest $request, $publicId, $name) { $document = $request->entity(); - + if(substr($name, -3)=='.js'){ $name = substr($name, 0, -3); } - + if(!$document->isPDFEmbeddable()){ return Response::view('error', array('error'=>'Image does not exist!'), 404); } - + $content = $document->preview?$document->getRawPreview():$document->getRaw(); $content = 'ninjaAddVFSDoc('.json_encode(intval($publicId).'/'.strval($name)).',"'.base64_encode($content).'")'; $response = Response::make($content, 200); $response->header('content-type', 'text/javascript'); $response->header('cache-control', 'max-age=31536000'); - + return $response; } - + public function postUpload(CreateDocumentRequest $request) { if (!Utils::hasFeature(FEATURE_DOCUMENTS)) { return; } - + $result = $this->documentRepo->upload(Input::all()['file'], $doc_array); - + if(is_string($result)){ return Response::json([ 'error' => $result, @@ -120,4 +121,11 @@ class DocumentController extends BaseController ], 200); } } + + public function delete(UpdateDocumentRequest $request) + { + $request->entity()->delete(); + + return RESULT_SUCCESS; + } } diff --git a/app/Http/Requests/UpdateDocumentRequest.php b/app/Http/Requests/UpdateDocumentRequest.php new file mode 100644 index 000000000000..3a950c9f3475 --- /dev/null +++ b/app/Http/Requests/UpdateDocumentRequest.php @@ -0,0 +1,26 @@ +user()->can('edit', $this->entity()); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + + ]; + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index 84ec2552bb30..446179ae23d8 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -156,6 +156,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('documents/js/{documents}/{filename}', 'DocumentController@getVFSJS'); Route::get('documents/preview/{documents}/{filename?}', 'DocumentController@getPreview'); Route::post('document', 'DocumentController@postUpload'); + Route::delete('documents/{documents}', 'DocumentController@delete'); Route::get('quotes/create/{client_id?}', 'QuoteController@create'); Route::get('quotes/{invoices}/clone', 'InvoiceController@cloneInvoice'); diff --git a/resources/views/expenses/edit.blade.php b/resources/views/expenses/edit.blade.php index aadfacc52617..d8c5c62d36e1 100644 --- a/resources/views/expenses/edit.blade.php +++ b/resources/views/expenses/edit.blade.php @@ -243,6 +243,7 @@ }, acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!}, addRemoveLinks:true, + dictRemoveFileConfirmation:"{{trans('texts.are_you_sure')}}", @foreach(['default_message', 'fallback_message', 'fallback_text', 'file_too_big', 'invalid_file_type', 'response_error', 'cancel_upload', 'cancel_upload_confirmation', 'remove_file'] as $key) "dict{{strval($key)}}":"{{trans('texts.dropzone_'.Utils::toClassCase($key))}}", @endforeach @@ -392,6 +393,13 @@ function handleDocumentRemoved(file){ model.removeDocument(file.public_id); + $.ajax({ + url: '{{ '/documents/' }}' + file.public_id, + type: 'DELETE', + success: function(result) { + // Do something with the result + } + }); } function handleDocumentUploaded(file, response){ diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index d17a3ba25c62..4f6940645ada 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1016,6 +1016,7 @@ }, acceptedFiles:{!! json_encode(implode(',',\App\Models\Document::$allowedMimes)) !!}, addRemoveLinks:true, + dictRemoveFileConfirmation:"{{trans('texts.are_you_sure')}}", @foreach(['default_message', 'fallback_message', 'fallback_text', 'file_too_big', 'invalid_file_type', 'response_error', 'cancel_upload', 'cancel_upload_confirmation', 'remove_file'] as $key) "dict{{Utils::toClassCase($key)}}":"{{trans('texts.dropzone_'.$key)}}", @endforeach @@ -1459,6 +1460,13 @@ function handleDocumentRemoved(file){ model.invoice().removeDocument(file.public_id); refreshPDF(true); + $.ajax({ + url: '{{ '/documents/' }}' + file.public_id, + type: 'DELETE', + success: function(result) { + // Do something with the result + } + }); } function handleDocumentUploaded(file, response){ From f233582ea8720cf28ae8df7aff44cfaa6bbb5a02 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 May 2016 20:48:27 +0300 Subject: [PATCH 169/386] Ensure all files are deleted with the account --- app/Http/Controllers/AccountController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 49955e184b89..45057914f1e6 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -1245,6 +1245,10 @@ class AccountController extends BaseController $account = Auth::user()->account; \Log::info("Canceled Account: {$account->name} - {$user->email}"); + Document::scope()->each(function($item, $key) { + $item->delete(); + }); + $this->accountRepo->unlinkAccount($account); if ($account->company->accounts->count() == 1) { $account->company->forceDelete(); From e5afb28e4b7e8d2da6beaee93fd787fae4971f56 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 May 2016 20:53:13 +0300 Subject: [PATCH 170/386] Testing travis change --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0518d00953f9..9eef15bf06b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ before_install: # set GitHub token and update composer - if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi; - composer self-update && composer -V - - export USE_ZEND_ALLOC=0 +# - export USE_ZEND_ALLOC=0 install: # install Composer dependencies From 1c119fe35a955fbaa451944ded02c1ee427d7e38 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Thu, 26 May 2016 15:22:09 -0400 Subject: [PATCH 171/386] ACH autobill bug fixes --- app/Console/Commands/SendRecurringInvoices.php | 11 +++++------ app/Models/PaymentMethod.php | 2 +- app/Ninja/Repositories/InvoiceRepository.php | 1 + app/Services/PaymentService.php | 17 ++++++++++------- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index d07645bf9bc2..574d324d3842 100644 --- a/app/Console/Commands/SendRecurringInvoices.php +++ b/app/Console/Commands/SendRecurringInvoices.php @@ -71,14 +71,12 @@ 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)) + ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS FALSE + AND balance > 0 AND due_date = ? AND recurring_invoice_id IS NOT NULL', + array($today->format('Y-m-d'))) ->orderBy('invoices.id', 'asc') ->get(); - $this->info(count($delayedAutoBillInvoices).' due recurring auto bill invoice instance(s) found'); + $this->info(count($delayedAutoBillInvoices).' due recurring invoice instance(s) found'); foreach ($delayedAutoBillInvoices as $invoice) { $autoBill = $invoice->getAutoBillEnabled(); @@ -91,6 +89,7 @@ class SendRecurringInvoices extends Command $this->info('Processing Invoice '.$invoice->id.' - Should bill '.($billNow ? 'YES' : 'NO')); if ($billNow) { + // autoBillInvoice will check for changes to ACH invoices, so we're not checking here $this->paymentService->autoBillInvoice($invoice); } } diff --git a/app/Models/PaymentMethod.php b/app/Models/PaymentMethod.php index c18d9d2b8bae..8c3ea8b2dfd8 100644 --- a/app/Models/PaymentMethod.php +++ b/app/Models/PaymentMethod.php @@ -160,7 +160,7 @@ class PaymentMethod extends EntityModel } public function requiresDelayedAutoBill(){ - return $this->payment_type_id == PAYMENT_TYPE_DIRECT_DEBIT; + return $this->payment_type_id == PAYMENT_TYPE_ACH; } } diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index 6bbd4f9fa639..d007971e777d 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -816,6 +816,7 @@ class InvoiceRepository extends BaseRepository $recurInvoice->save(); if ($recurInvoice->getAutoBillEnabled() && !$recurInvoice->account->auto_bill_on_due_date) { + // autoBillInvoice will check for ACH, so we're not checking here 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 77f489049f68..efa27d945b7a 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -16,6 +16,7 @@ use App\Models\Account; use App\Models\Country; use App\Models\Client; use App\Models\Invoice; +use App\Models\Activity; use App\Models\AccountGateway; use App\Http\Controllers\PaymentController; use App\Models\AccountGatewayToken; @@ -872,16 +873,21 @@ class PaymentService extends BaseService } if ($defaultPaymentMethod->requiresDelayedAutoBill()) { - $invoiceDate = DateTime::createFromFormat('Y-m-d', $invoice_date); + $invoiceDate = \DateTime::createFromFormat('Y-m-d', $invoice->invoice_date); $minDueDate = clone $invoiceDate; $minDueDate->modify('+10 days'); - if (DateTime::create() < $minDueDate) { + if (date_create() < $minDueDate) { // Can't auto bill now return false; } - $firstUpdate = \App\Models\Activities::where('invoice_id', '=', $invoice->id) + if ($invoice->partial > 0) { + // The amount would be different than the amount in the email + return false; + } + + $firstUpdate = Activity::where('invoice_id', '=', $invoice->id) ->where('activity_type_id', '=', ACTIVITY_TYPE_UPDATE_INVOICE) ->first(); @@ -894,10 +900,7 @@ class PaymentService extends BaseService } } - $invoicePayments = \App\Models\Activities::where('invoice_id', '=', $invoice->id) - ->where('activity_type_id', '=', ACTIVITY_TYPE_CREATE_PAYMENT); - - if ($invoicePayments->count()) { + if ($invoice->payments->count()) { // ACH requirements are strict; don't auto bill this return false; } From dadac05532c5d398961df69d70eb864b164deef4 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 26 May 2016 17:56:54 +0300 Subject: [PATCH 172/386] add support to multiple invoice types --- app/Console/Commands/CheckData.php | 12 +++---- app/Http/Controllers/AccountController.php | 2 +- app/Http/Controllers/ClientController.php | 2 +- .../Controllers/ClientPortalController.php | 2 +- .../Controllers/DashboardApiController.php | 10 +++--- app/Http/Controllers/DashboardController.php | 10 +++--- app/Http/Controllers/ExportController.php | 6 ++-- app/Http/Controllers/InvoiceController.php | 6 ++-- app/Http/Controllers/PaymentController.php | 4 +-- app/Http/Controllers/ReportController.php | 6 ++-- app/Http/routes.php | 3 ++ app/Models/Account.php | 34 +++++++++---------- app/Models/Invoice.php | 27 ++++++++++----- app/Ninja/Mailers/ContactMailer.php | 2 +- app/Ninja/Presenters/InvoicePresenter.php | 2 +- app/Ninja/Repositories/InvoiceRepository.php | 16 ++++----- app/Ninja/Transformers/InvoiceTransformer.php | 2 +- app/Services/InvoiceService.php | 4 +-- app/Services/PushService.php | 4 +-- ..._05_18_085739_add_invoice_type_support.php | 31 +++++++++++++++++ resources/views/dashboard.blade.php | 8 ++--- 21 files changed, 118 insertions(+), 75 deletions(-) create mode 100644 database/migrations/2016_05_18_085739_add_invoice_type_support.php diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index 5c644c5b4bf4..795e241e6249 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -154,7 +154,7 @@ class CheckData extends Command { $clients->where('clients.id', '=', $this->option('client_id')); } else { $clients->where('invoices.is_deleted', '=', 0) - ->where('invoices.is_quote', '=', 0) + ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->where('invoices.is_recurring', '=', 0) ->havingRaw('abs(clients.balance - sum(invoices.balance)) > .01 and clients.balance != 999999999.9999'); } @@ -184,7 +184,7 @@ class CheckData extends Command { if ($activity->invoice_id) { $invoice = DB::table('invoices') ->where('id', '=', $activity->invoice_id) - ->first(['invoices.amount', 'invoices.is_recurring', 'invoices.is_quote', 'invoices.deleted_at', 'invoices.id', 'invoices.is_deleted']); + ->first(['invoices.amount', 'invoices.is_recurring', 'invoices.invoice_type_id', 'invoices.deleted_at', 'invoices.id', 'invoices.is_deleted']); // Check if this invoice was once set as recurring invoice if ($invoice && !$invoice->is_recurring && DB::table('invoices') @@ -221,14 +221,14 @@ class CheckData extends Command { && $invoice->amount > 0; // **Fix for allowing converting a recurring invoice to a normal one without updating the balance** - if ($noAdjustment && !$invoice->is_quote && !$invoice->is_recurring) { - $this->info("No adjustment for new invoice:{$activity->invoice_id} amount:{$invoice->amount} isQuote:{$invoice->is_quote} isRecurring:{$invoice->is_recurring}"); + if ($noAdjustment && $invoice->invoice_type_id == INVOICE_TYPE_STANDARD && !$invoice->is_recurring) { + $this->info("No adjustment for new invoice:{$activity->invoice_id} amount:{$invoice->amount} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}"); $foundProblem = true; $clientFix += $invoice->amount; $activityFix = $invoice->amount; // **Fix for updating balance when creating a quote or recurring invoice** - } elseif ($activity->adjustment != 0 && ($invoice->is_quote || $invoice->is_recurring)) { - $this->info("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} isQuote:{$invoice->is_quote} isRecurring:{$invoice->is_recurring}"); + } elseif ($activity->adjustment != 0 && ($invoice->invoice_type_id == INVOICE_TYPE_QUOTE || $invoice->is_recurring)) { + $this->info("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}"); $foundProblem = true; $clientFix -= $activity->adjustment; $activityFix = 0; diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 49955e184b89..6544333a3f03 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -590,8 +590,8 @@ class AccountController extends BaseController // sample invoice to help determine variables $invoice = Invoice::scope() + ->invoiceType(INVOICE_TYPE_STANDARD) ->with('client', 'account') - ->where('is_quote', '=', false) ->where('is_recurring', '=', false) ->first(); diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index d2a0633e5c73..8a451113115e 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -136,7 +136,7 @@ class ClientController extends BaseController 'credit' => $client->getTotalCredit(), 'title' => trans('texts.view_client'), 'hasRecurringInvoices' => Invoice::scope()->where('is_recurring', '=', true)->whereClientId($client->id)->count() > 0, - 'hasQuotes' => Invoice::scope()->where('is_quote', '=', true)->whereClientId($client->id)->count() > 0, + 'hasQuotes' => Invoice::scope()->invoiceType(INVOICE_TYPE_QUOTE)->whereClientId($client->id)->count() > 0, 'hasTasks' => Task::scope()->whereClientId($client->id)->count() > 0, 'gatewayLink' => $client->getGatewayLink($accountGateway), 'gateway' => $accountGateway diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index 56dd183b19fe..ba025e283f1b 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -62,7 +62,7 @@ class ClientPortalController extends BaseController if (!Input::has('phantomjs') && !Input::has('silent') && !Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) { - if ($invoice->is_quote) { + if ($invoice->isType(INVOICE_TYPE_QUOTE)) { event(new QuoteInvitationWasViewed($invoice, $invitation)); } else { event(new InvoiceInvitationWasViewed($invoice, $invitation)); diff --git a/app/Http/Controllers/DashboardApiController.php b/app/Http/Controllers/DashboardApiController.php index 1170812b8356..e7e96a5ad91c 100644 --- a/app/Http/Controllers/DashboardApiController.php +++ b/app/Http/Controllers/DashboardApiController.php @@ -24,7 +24,7 @@ class DashboardApiController extends BaseAPIController ->where('clients.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false) ->where('invoices.is_recurring', '=', false) - ->where('invoices.is_quote', '=', false); + ->where('invoices.invoice_type_id', '=', false); if(!$view_all){ $metrics = $metrics->where(function($query) use($user_id){ @@ -62,7 +62,7 @@ class DashboardApiController extends BaseAPIController ->where('accounts.id', '=', Auth::user()->account_id) ->where('clients.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false) - ->where('invoices.is_quote', '=', false) + ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->where('invoices.is_recurring', '=', false); if(!$view_all){ @@ -106,7 +106,7 @@ class DashboardApiController extends BaseAPIController $pastDue = $pastDue->where('invoices.user_id', '=', $user_id); } - $pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote']) + $pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id']) ->orderBy('invoices.due_date', 'asc') ->take(50) ->get(); @@ -131,7 +131,7 @@ class DashboardApiController extends BaseAPIController } $upcoming = $upcoming->take(50) - ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote']) + ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id']) ->get(); $payments = DB::table('payments') @@ -157,7 +157,7 @@ class DashboardApiController extends BaseAPIController $hasQuotes = false; foreach ([$upcoming, $pastDue] as $data) { foreach ($data as $invoice) { - if ($invoice->is_quote) { + if ($invoice->isType(INVOICE_TYPE_QUOTE)) { $hasQuotes = true; } } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 718d73c67d42..283a0984771c 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -26,7 +26,7 @@ class DashboardController extends BaseController ->where('clients.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false) ->where('invoices.is_recurring', '=', false) - ->where('invoices.is_quote', '=', false); + ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD); if(!$view_all){ $metrics = $metrics->where(function($query) use($user_id){ @@ -64,7 +64,7 @@ class DashboardController extends BaseController ->where('accounts.id', '=', Auth::user()->account_id) ->where('clients.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false) - ->where('invoices.is_quote', '=', false) + ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->where('invoices.is_recurring', '=', false); if(!$view_all){ @@ -121,7 +121,7 @@ class DashboardController extends BaseController $pastDue = $pastDue->where('invoices.user_id', '=', $user_id); } - $pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote']) + $pastDue = $pastDue->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id']) ->orderBy('invoices.due_date', 'asc') ->take(50) ->get(); @@ -147,7 +147,7 @@ class DashboardController extends BaseController } $upcoming = $upcoming->take(50) - ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'is_quote']) + ->select(['invoices.due_date', 'invoices.balance', 'invoices.public_id', 'invoices.invoice_number', 'clients.name as client_name', 'contacts.email', 'contacts.first_name', 'contacts.last_name', 'clients.currency_id', 'clients.public_id as client_public_id', 'clients.user_id as client_user_id', 'invoice_type_id']) ->get(); $payments = DB::table('payments') @@ -173,7 +173,7 @@ class DashboardController extends BaseController $hasQuotes = false; foreach ([$upcoming, $pastDue] as $data) { foreach ($data as $invoice) { - if ($invoice->is_quote) { + if ($invoice->invoice_type_id == INVOICE_TYPE_QUOTE) { $hasQuotes = true; } } diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index c7f30b03be29..244b6e8aee5f 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -134,23 +134,23 @@ class ExportController extends BaseController if ($request->input(ENTITY_INVOICE)) { $data['invoices'] = Invoice::scope() + ->invoiceType(INVOICE_TYPE_STANDARD) ->with('user', 'client.contacts', 'invoice_status') ->withArchived() - ->where('is_quote', '=', false) ->where('is_recurring', '=', false) ->get(); $data['quotes'] = Invoice::scope() + ->invoiceType(INVOICE_TYPE_QUOTE) ->with('user', 'client.contacts', 'invoice_status') ->withArchived() - ->where('is_quote', '=', true) ->where('is_recurring', '=', false) ->get(); $data['recurringInvoices'] = Invoice::scope() + ->invoiceType(INVOICE_TYPE_STANDARD) ->with('user', 'client.contacts', 'invoice_status', 'frequency') ->withArchived() - ->where('is_quote', '=', false) ->where('is_recurring', '=', true) ->get(); } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 9a0dadf494c4..62faaca695b5 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -567,9 +567,9 @@ class InvoiceController extends BaseController 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), ]; - $invoice->is_quote = intval($invoice->is_quote); + $invoice->invoice_type_id = intval($invoice->invoice_type_id); - $activityTypeId = $invoice->is_quote ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE; + $activityTypeId = $invoice->isType(INVOICE_TYPE_QUOTE) ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE; $activities = Activity::scope(false, $invoice->account_id) ->where('activity_type_id', '=', $activityTypeId) ->where('invoice_id', '=', $invoice->id) @@ -589,7 +589,7 @@ class InvoiceController extends BaseController 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), ]; - $backup->is_quote = isset($backup->is_quote) && intval($backup->is_quote); + $backup->invoice_type_id = isset($backup->invoice_type_id) && intval($backup->invoice_type_id) == INVOICE_TYPE_QUOTE; $backup->account = $invoice->account->toArray(); $versionsJson[$activity->id] = $backup; diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 6a5742fabe2b..c0dd4a9f8550 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -77,8 +77,8 @@ class PaymentController extends BaseController public function create(PaymentRequest $request) { $invoices = Invoice::scope() + ->invoiceType(INVOICE_TYPE_STANDARD) ->where('is_recurring', '=', false) - ->where('is_quote', '=', false) ->where('invoices.balance', '>', 0) ->with('client', 'invoice_status') ->orderBy('invoice_number')->get(); @@ -108,7 +108,7 @@ class PaymentController extends BaseController $data = array( 'client' => null, 'invoice' => null, - 'invoices' => Invoice::scope()->where('is_recurring', '=', false)->where('is_quote', '=', false) + 'invoices' => Invoice::scope()->invoiceType(INVOICE_TYPE_STANDARD)->where('is_recurring', '=', false) ->with('client', 'invoice_status')->orderBy('invoice_number')->get(), 'payment' => $payment, 'method' => 'PUT', diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 4f162f260d37..65d379eb3f68 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -168,7 +168,7 @@ class ReportController extends BaseController ->groupBy($groupBy); if ($entityType == ENTITY_INVOICE) { - $records->where('is_quote', '=', false) + $records->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->where('is_recurring', '=', false); } elseif ($entityType == ENTITY_PAYMENT) { $records->join('invoices', 'invoices.id', '=', 'payments.invoice_id') @@ -374,7 +374,7 @@ class ReportController extends BaseController $query->where('invoice_date', '>=', $startDate) ->where('invoice_date', '<=', $endDate) ->where('is_deleted', '=', false) - ->where('is_quote', '=', false) + ->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->where('is_recurring', '=', false) ->with(['payments' => function($query) { $query->withTrashed() @@ -429,7 +429,7 @@ class ReportController extends BaseController ->with(['invoices' => function($query) use ($startDate, $endDate) { $query->where('invoice_date', '>=', $startDate) ->where('invoice_date', '<=', $endDate) - ->where('is_quote', '=', false) + ->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->where('is_recurring', '=', false) ->withArchived(); }]); diff --git a/app/Http/routes.php b/app/Http/routes.php index 84ec2552bb30..4b1a8e10ad95 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -358,6 +358,9 @@ if (!defined('CONTACT_EMAIL')) { define('ENTITY_BANK_ACCOUNT', 'bank_account'); define('ENTITY_BANK_SUBACCOUNT', 'bank_subaccount'); + define('INVOICE_TYPE_STANDARD', 1); + define('INVOICE_TYPE_QUOTE', 2); + define('PERSON_CONTACT', 'contact'); define('PERSON_USER', 'user'); define('PERSON_VENDOR_CONTACT','vendorcontact'); diff --git a/app/Models/Account.php b/app/Models/Account.php index c489d21b180b..208ea19dcdf0 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -524,7 +524,7 @@ class Account extends Eloquent $invoice = Invoice::createNew(); $invoice->is_recurring = false; - $invoice->is_quote = false; + $invoice->invoice_type_id = INVOICE_TYPE_STANDARD; $invoice->invoice_date = Utils::today(); $invoice->start_date = Utils::today(); $invoice->invoice_design_id = $this->invoice_design_id; @@ -535,7 +535,7 @@ class Account extends Eloquent $invoice->is_recurring = true; } else { if ($entityType == ENTITY_QUOTE) { - $invoice->is_quote = true; + $invoice->invoice_type_id = INVOICE_TYPE_QUOTE; } if ($this->hasClientNumberPattern($invoice) && !$clientId) { @@ -553,34 +553,34 @@ class Account extends Eloquent return $invoice; } - public function getNumberPrefix($isQuote) + public function getNumberPrefix($invoice_type_id) { if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { return ''; } - return ($isQuote ? $this->quote_number_prefix : $this->invoice_number_prefix) ?: ''; + return ($invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_prefix : $this->invoice_number_prefix) ?: ''; } - public function hasNumberPattern($isQuote) + public function hasNumberPattern($invoice_type_id) { if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { return false; } - return $isQuote ? ($this->quote_number_pattern ? true : false) : ($this->invoice_number_pattern ? true : false); + return $invoice_type_id == INVOICE_TYPE_QUOTE ? ($this->quote_number_pattern ? true : false) : ($this->invoice_number_pattern ? true : false); } public function hasClientNumberPattern($invoice) { - $pattern = $invoice->is_quote ? $this->quote_number_pattern : $this->invoice_number_pattern; + $pattern = $invoice->invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_pattern : $this->invoice_number_pattern; return strstr($pattern, '$custom'); } public function getNumberPattern($invoice) { - $pattern = $invoice->is_quote ? $this->quote_number_pattern : $this->invoice_number_pattern; + $pattern = $invoice->invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_pattern : $this->invoice_number_pattern; if (!$pattern) { return false; @@ -590,7 +590,7 @@ class Account extends Eloquent $replace = [date('Y')]; $search[] = '{$counter}'; - $replace[] = str_pad($this->getCounter($invoice->is_quote), $this->invoice_number_padding, '0', STR_PAD_LEFT); + $replace[] = str_pad($this->getCounter($invoice->invoice_type_id), $this->invoice_number_padding, '0', STR_PAD_LEFT); if (strstr($pattern, '{$userId}')) { $search[] = '{$userId}'; @@ -633,9 +633,9 @@ class Account extends Eloquent return str_replace($search, $replace, $pattern); } - public function getCounter($isQuote) + public function getCounter($invoice_type_id) { - return $isQuote && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter; + return $invoice_type_id == INVOICE_TYPE_QUOTE && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter; } public function previewNextInvoiceNumber($entityType = ENTITY_INVOICE) @@ -646,12 +646,12 @@ class Account extends Eloquent public function getNextInvoiceNumber($invoice) { - if ($this->hasNumberPattern($invoice->is_quote)) { + if ($this->hasNumberPattern($invoice->invoice_type_id)) { return $this->getNumberPattern($invoice); } - $counter = $this->getCounter($invoice->is_quote); - $prefix = $this->getNumberPrefix($invoice->is_quote); + $counter = $this->getCounter($invoice->invoice_type_id); + $prefix = $this->getNumberPrefix($invoice->invoice_type_id); $counterOffset = 0; // confirm the invoice number isn't already taken @@ -664,7 +664,7 @@ class Account extends Eloquent // update the invoice counter to be caught up if ($counterOffset > 1) { - if ($invoice->is_quote && !$this->share_counter) { + if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) { $this->quote_number_counter += $counterOffset - 1; } else { $this->invoice_number_counter += $counterOffset - 1; @@ -678,7 +678,7 @@ class Account extends Eloquent public function incrementCounter($invoice) { - if ($invoice->is_quote && !$this->share_counter) { + if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) { $this->quote_number_counter += 1; } else { $default = $this->invoice_number_counter; @@ -1102,7 +1102,7 @@ class Account extends Eloquent 'invoice_items', 'created_at', 'is_recurring', - 'is_quote', + 'invoice_type_id', ]); foreach ($invoice->invoice_items as $invoiceItem) { diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 02fdc73f99bd..f3717e9afa65 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -96,7 +96,7 @@ class Invoice extends EntityModel implements BalanceAffecting public function affectsBalance() { - return !$this->is_quote && !$this->is_recurring; + return $this->isType(INVOICE_TYPE_STANDARD) && !$this->is_recurring; } public function getAdjustment() @@ -139,7 +139,7 @@ class Invoice extends EntityModel implements BalanceAffecting public function getAmountPaid($calculate = false) { - if ($this->is_quote || $this->is_recurring) { + if ($this->isType(INVOICE_TYPE_QUOTE) || $this->is_recurring) { return 0; } @@ -230,10 +230,19 @@ class Invoice extends EntityModel implements BalanceAffecting public function scopeInvoices($query) { - return $query->where('is_quote', '=', false) + return $query->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->where('is_recurring', '=', false); } + public function scopeInvoiceType($query, $typeId) + { + return $query->where('invoice_type_id', '=', $typeId); + } + + public function isType($typeId) { + return $this->invoice_type_id == $typeId; + } + public function markInvitationsSent($notify = false) { foreach ($this->invitations as $invitation) { @@ -256,7 +265,7 @@ class Invoice extends EntityModel implements BalanceAffecting return; } - if ($this->is_quote) { + if ($this->isType(INVOICE_TYPE_QUOTE)) { event(new QuoteInvitationWasEmailed($invitation)); } else { event(new InvoiceInvitationWasEmailed($invitation)); @@ -292,7 +301,7 @@ class Invoice extends EntityModel implements BalanceAffecting public function markApproved() { - if ($this->is_quote) { + if ($this->isType(INVOICE_TYPE_QUOTE)) { $this->invoice_status_id = INVOICE_STATUS_APPROVED; $this->save(); } @@ -341,7 +350,7 @@ class Invoice extends EntityModel implements BalanceAffecting public function getEntityType() { - return $this->is_quote ? ENTITY_QUOTE : ENTITY_INVOICE; + return $this->isType(INVOICE_TYPE_QUOTE) ? ENTITY_QUOTE : ENTITY_INVOICE; } public function isSent() @@ -416,7 +425,7 @@ class Invoice extends EntityModel implements BalanceAffecting 'invoice_design_id', 'invoice_fonts', 'features', - 'is_quote', + 'invoice_type_id', 'custom_value1', 'custom_value2', 'custom_taxes1', @@ -943,7 +952,7 @@ Invoice::creating(function ($invoice) { }); Invoice::created(function ($invoice) { - if ($invoice->is_quote) { + if ($invoice->isType(INVOICE_TYPE_QUOTE)) { event(new QuoteWasCreated($invoice)); } else { event(new InvoiceWasCreated($invoice)); @@ -951,7 +960,7 @@ Invoice::created(function ($invoice) { }); Invoice::updating(function ($invoice) { - if ($invoice->is_quote) { + if ($invoice->isType(INVOICE_TYPE_QUOTE)) { event(new QuoteWasUpdated($invoice)); } else { event(new InvoiceWasUpdated($invoice)); diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index 224d48785aea..9d4b3c509111 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -96,7 +96,7 @@ class ContactMailer extends Mailer $account->loadLocalizationSettings(); if ($sent === true) { - if ($invoice->is_quote) { + if ($invoice->isType(INVOICE_TYPE_QUOTE)) { event(new QuoteWasEmailed($invoice)); } else { event(new InvoiceWasEmailed($invoice)); diff --git a/app/Ninja/Presenters/InvoicePresenter.php b/app/Ninja/Presenters/InvoicePresenter.php index 827b171f4208..1c77ef1b09e7 100644 --- a/app/Ninja/Presenters/InvoicePresenter.php +++ b/app/Ninja/Presenters/InvoicePresenter.php @@ -19,7 +19,7 @@ class InvoicePresenter extends EntityPresenter { { if ($this->entity->partial > 0) { return 'partial_due'; - } elseif ($this->entity->is_quote) { + } elseif ($this->entity->isType(INVOICE_TYPE_QUOTE)) { return 'total'; } else { return 'balance_due'; diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index 979304ed01a0..6a48c9a74921 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -32,9 +32,9 @@ class InvoiceRepository extends BaseRepository public function all() { return Invoice::scope() + ->invoiceType(INVOICE_TYPE_STANDARD) ->with('user', 'client.contacts', 'invoice_status') ->withTrashed() - ->where('is_quote', '=', false) ->where('is_recurring', '=', false) ->get(); } @@ -106,7 +106,7 @@ class InvoiceRepository extends BaseRepository ->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id') ->join('contacts', 'contacts.client_id', '=', 'clients.id') ->where('invoices.account_id', '=', $accountId) - ->where('invoices.is_quote', '=', false) + ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->where('contacts.deleted_at', '=', null) ->where('invoices.is_recurring', '=', true) ->where('contacts.is_primary', '=', true) @@ -156,7 +156,7 @@ class InvoiceRepository extends BaseRepository ->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id') ->where('invitations.contact_id', '=', $contactId) ->where('invitations.deleted_at', '=', null) - ->where('invoices.is_quote', '=', false) + ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->where('invoices.is_deleted', '=', false) ->where('clients.deleted_at', '=', null) ->where('invoices.is_recurring', '=', true) @@ -203,7 +203,7 @@ class InvoiceRepository extends BaseRepository ->join('contacts', 'contacts.client_id', '=', 'clients.id') ->where('invitations.contact_id', '=', $contactId) ->where('invitations.deleted_at', '=', null) - ->where('invoices.is_quote', '=', $entityType == ENTITY_QUOTE) + ->where('invoices.invoice_type_id', '=', $entityType == ENTITY_QUOTE ? INVOICE_TYPE_QUOTE : INVOICE_TYPE_STANDARD) ->where('invoices.is_deleted', '=', false) ->where('clients.deleted_at', '=', null) ->where('contacts.deleted_at', '=', null) @@ -642,7 +642,7 @@ class InvoiceRepository extends BaseRepository 'tax_name2', 'tax_rate2', 'amount', - 'is_quote', + 'invoice_type_id', 'custom_value1', 'custom_value2', 'custom_taxes1', @@ -654,7 +654,7 @@ class InvoiceRepository extends BaseRepository } if ($quotePublicId) { - $clone->is_quote = false; + $clone->invoice_type_id = INVOICE_TYPE_STANDARD; $clone->quote_id = $quotePublicId; } @@ -838,9 +838,9 @@ class InvoiceRepository extends BaseRepository } $sql = implode(' OR ', $dates); - $invoices = Invoice::whereAccountId($account->id) + $invoices = Invoice::invoiceType(INVOICE_TYPE_STANDARD) + ->whereAccountId($account->id) ->where('balance', '>', 0) - ->where('is_quote', '=', false) ->where('is_recurring', '=', false) ->whereRaw('('.$sql.')') ->get(); diff --git a/app/Ninja/Transformers/InvoiceTransformer.php b/app/Ninja/Transformers/InvoiceTransformer.php index fc17127719a2..b961dbdfaeba 100644 --- a/app/Ninja/Transformers/InvoiceTransformer.php +++ b/app/Ninja/Transformers/InvoiceTransformer.php @@ -93,7 +93,7 @@ class InvoiceTransformer extends EntityTransformer 'terms' => $invoice->terms, 'public_notes' => $invoice->public_notes, 'is_deleted' => (bool) $invoice->is_deleted, - 'is_quote' => (bool) $invoice->is_quote, + 'invoice_type_id' => (int) $invoice->invoice_type_id, 'is_recurring' => (bool) $invoice->is_recurring, 'frequency_id' => (int) $invoice->frequency_id, 'start_date' => $invoice->start_date, diff --git a/app/Services/InvoiceService.php b/app/Services/InvoiceService.php index 950c8bb95fcb..71a8f91f0f68 100644 --- a/app/Services/InvoiceService.php +++ b/app/Services/InvoiceService.php @@ -94,7 +94,7 @@ class InvoiceService extends BaseService { $account = $quote->account; - if (!$quote->is_quote || $quote->quote_invoice_id) { + if (!$quote->isType(INVOICE_TYPE_QUOTE) || $quote->quote_invoice_id) { return null; } @@ -121,7 +121,7 @@ class InvoiceService extends BaseService { $datatable = new InvoiceDatatable( ! $clientPublicId, $clientPublicId); $query = $this->invoiceRepo->getInvoices($accountId, $clientPublicId, $entityType, $search) - ->where('invoices.is_quote', '=', $entityType == ENTITY_QUOTE ? true : false); + ->where('invoices.invoice_type_id', '=', $entityType == ENTITY_QUOTE ? INVOICE_TYPE_QUOTE : INVOICE_TYPE_STANDARD); if(!Utils::hasPermission('view_all')){ $query->where('invoices.user_id', '=', Auth::user()->id); diff --git a/app/Services/PushService.php b/app/Services/PushService.php index a96042b92419..2edd86af4be8 100644 --- a/app/Services/PushService.php +++ b/app/Services/PushService.php @@ -132,7 +132,7 @@ class PushService */ private function entitySentMessage($invoice) { - if($invoice->is_quote) + if($invoice->isType(INVOICE_TYPE_QUOTE)) return trans("texts.notification_quote_sent_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]); else return trans("texts.notification_invoice_sent_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]); @@ -163,7 +163,7 @@ class PushService */ private function entityViewedMessage($invoice) { - if($invoice->is_quote) + if($invoice->isType(INVOICE_TYPE_QUOTE)) return trans("texts.notification_quote_viewed_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]); else return trans("texts.notification_invoice_viewed_subject", ['invoice' => $invoice->invoice_number, 'client' => $invoice->client->name]); diff --git a/database/migrations/2016_05_18_085739_add_invoice_type_support.php b/database/migrations/2016_05_18_085739_add_invoice_type_support.php new file mode 100644 index 000000000000..7b0b928d811b --- /dev/null +++ b/database/migrations/2016_05_18_085739_add_invoice_type_support.php @@ -0,0 +1,31 @@ +renameColumn('is_quote', 'invoice_type_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('invoices', function ($table) { + $table->renameColumn('invoice_type_id', 'is_quote'); + }); + } +} diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index bc1d2bb6e862..b661af7a40fc 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -150,7 +150,7 @@ @foreach ($upcoming as $invoice) - @if (!$invoice->is_quote) + @if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD) {!! \App\Models\Invoice::calcLink($invoice) !!} @can('viewByOwner', [ENTITY_CLIENT, $invoice->client_user_id]) @@ -185,7 +185,7 @@ @foreach ($pastDue as $invoice) - @if (!$invoice->is_quote) + @if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD) {!! \App\Models\Invoice::calcLink($invoice) !!} @can('viewByOwner', [ENTITY_CLIENT, $invoice->client_user_id]) @@ -224,7 +224,7 @@ @foreach ($upcoming as $invoice) - @if ($invoice->is_quote) + @if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD) {!! \App\Models\Invoice::calcLink($invoice) !!} {!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!} @@ -255,7 +255,7 @@ @foreach ($pastDue as $invoice) - @if ($invoice->is_quote) + @if ($invoice->invoice_type_id == INVOICE_TYPE_STANDARD) {!! \App\Models\Invoice::calcLink($invoice) !!} {!! link_to('/clients/'.$invoice->client_public_id, trim($invoice->client_name) ?: (trim($invoice->first_name . ' ' . $invoice->last_name) ?: $invoice->email)) !!} From 0e75ddb5e66edc502957768d58ea7ca7c613cb08 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 May 2016 12:26:02 +0300 Subject: [PATCH 173/386] Added setting to hide second tax rate --- app/Http/Controllers/AccountController.php | 6 +- app/Http/Controllers/InvoiceApiController.php | 11 +- app/Models/Account.php | 159 +++++++++--------- config/former.php | 4 +- resources/lang/en/texts.php | 3 +- resources/views/accounts/tax_rates.blade.php | 21 ++- resources/views/invoices/edit.blade.php | 22 ++- 7 files changed, 119 insertions(+), 107 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 45057914f1e6..8ad53a7d260c 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -790,11 +790,7 @@ class AccountController extends BaseController private function saveTaxRates() { $account = Auth::user()->account; - - $account->invoice_taxes = Input::get('invoice_taxes') ? true : false; - $account->invoice_item_taxes = Input::get('invoice_item_taxes') ? true : false; - $account->show_item_taxes = Input::get('show_item_taxes') ? true : false; - $account->default_tax_rate_id = Input::get('default_tax_rate_id'); + $account->fill(Input::all()); $account->save(); Session::flash('message', trans('texts.updated_settings')); diff --git a/app/Http/Controllers/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php index 983e8fa14d71..520b5b09acac 100644 --- a/app/Http/Controllers/InvoiceApiController.php +++ b/app/Http/Controllers/InvoiceApiController.php @@ -134,6 +134,7 @@ class InvoiceApiController extends BaseAPIController 'city', 'state', 'postal_code', + 'country_id', 'private_notes', 'currency_code', ] as $field) { @@ -182,7 +183,7 @@ class InvoiceApiController extends BaseAPIController $invoice = Invoice::scope($invoice->public_id) ->with('client', 'invoice_items', 'invitations') ->first(); - + return $this->itemResponse($invoice); } @@ -269,7 +270,7 @@ class InvoiceApiController extends BaseAPIController $item[$key] = $val; } } - + return $item; } @@ -308,7 +309,7 @@ class InvoiceApiController extends BaseAPIController public function update(UpdateInvoiceAPIRequest $request, $publicId) { if ($request->action == ACTION_CONVERT) { - $quote = $request->entity(); + $quote = $request->entity(); $invoice = $this->invoiceRepo->cloneInvoice($quote, $quote->id); return $this->itemResponse($invoice); } elseif ($request->action) { @@ -322,7 +323,7 @@ class InvoiceApiController extends BaseAPIController $invoice = Invoice::scope($publicId) ->with('client', 'invoice_items', 'invitations') ->firstOrFail(); - + return $this->itemResponse($invoice); } @@ -351,7 +352,7 @@ class InvoiceApiController extends BaseAPIController public function destroy(UpdateInvoiceAPIRequest $request) { $invoice = $request->entity(); - + $this->invoiceRepo->delete($invoice); return $this->itemResponse($invoice); diff --git a/app/Models/Account.php b/app/Models/Account.php index c489d21b180b..2aa1b910b5b4 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -18,7 +18,7 @@ class Account extends Eloquent { use PresentableTrait; use SoftDeletes; - + public static $plan_prices = array( PLAN_PRO => array( PLAN_TERM_MONTHLY => PLAN_PRICE_PRO_MONTHLY, @@ -56,6 +56,11 @@ class Account extends Eloquent 'currency_id', 'language_id', 'military_time', + 'invoice_taxes', + 'invoice_item_taxes', + 'show_item_taxes', + 'default_tax_rate_id', + 'enable_second_tax_rate', ]; public static $basicSettings = [ @@ -401,7 +406,7 @@ class Account extends Eloquent return $gateway; } } - + return false; } @@ -421,27 +426,27 @@ class Account extends Eloquent if($this->logo == ''){ $this->calculateLogoDetails(); } - + return !empty($this->logo); } - + public function getLogoDisk(){ return Storage::disk(env('LOGO_FILESYSTEM', 'logos')); } - + protected function calculateLogoDetails(){ $disk = $this->getLogoDisk(); - + if($disk->exists($this->account_key.'.png')){ $this->logo = $this->account_key.'.png'; } else if($disk->exists($this->account_key.'.jpg')) { $this->logo = $this->account_key.'.jpg'; } - + if(!empty($this->logo)){ $image = imagecreatefromstring($disk->get($this->logo)); $this->logo_width = imagesx($image); - $this->logo_height = imagesy($image); + $this->logo_height = imagesy($image); $this->logo_size = $disk->size($this->logo); } else { $this->logo = null; @@ -453,33 +458,33 @@ class Account extends Eloquent if(!$this->hasLogo()){ return null; } - + $disk = $this->getLogoDisk(); return $disk->get($this->logo); } - + public function getLogoURL($cachebuster = false) { if(!$this->hasLogo()){ return null; } - + $disk = $this->getLogoDisk(); $adapter = $disk->getAdapter(); - + if($adapter instanceof \League\Flysystem\Adapter\Local) { // Stored locally $logo_url = str_replace(public_path(), url('/'), $adapter->applyPathPrefix($this->logo), $count); - + if ($cachebuster) { $logo_url .= '?no_cache='.time(); } - + if($count == 1){ return str_replace(DIRECTORY_SEPARATOR, '/', $logo_url); } } - + return Document::getDirectFileUrl($this->logo, $this->getLogoDisk()); } @@ -529,7 +534,7 @@ class Account extends Eloquent $invoice->start_date = Utils::today(); $invoice->invoice_design_id = $this->invoice_design_id; $invoice->client_id = $clientId; - + if ($entityType === ENTITY_RECURRING_INVOICE) { $invoice->invoice_number = microtime(true); $invoice->is_recurring = true; @@ -544,7 +549,7 @@ class Account extends Eloquent $invoice->invoice_number = $this->getNextInvoiceNumber($invoice); } } - + if (!$clientId) { $invoice->client = Client::createNew(); $invoice->client->public_id = 0; @@ -574,7 +579,7 @@ class Account extends Eloquent public function hasClientNumberPattern($invoice) { $pattern = $invoice->is_quote ? $this->quote_number_pattern : $this->invoice_number_pattern; - + return strstr($pattern, '$custom'); } @@ -654,7 +659,7 @@ class Account extends Eloquent $prefix = $this->getNumberPrefix($invoice->is_quote); $counterOffset = 0; - // confirm the invoice number isn't already taken + // confirm the invoice number isn't already taken do { $number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT); $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); @@ -690,7 +695,7 @@ class Account extends Eloquent $this->invoice_number_counter += 1; } } - + $this->save(); } @@ -713,7 +718,7 @@ class Account extends Eloquent $query->where('updated_at', '>=', $updatedAt); } }]); - } + } } public function loadLocalizationSettings($client = false) @@ -726,8 +731,8 @@ class Account extends Eloquent Session::put(SESSION_DATE_FORMAT, $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT); Session::put(SESSION_DATE_PICKER_FORMAT, $this->date_format ? $this->date_format->picker_format : DEFAULT_DATE_PICKER_FORMAT); - $currencyId = ($client && $client->currency_id) ? $client->currency_id : $this->currency_id ?: DEFAULT_CURRENCY; - $locale = ($client && $client->language_id) ? $client->language->locale : ($this->language_id ? $this->Language->locale : DEFAULT_LOCALE); + $currencyId = ($client && $client->currency_id) ? $client->currency_id : $this->currency_id ?: DEFAULT_CURRENCY; + $locale = ($client && $client->language_id) ? $client->language->locale : ($this->language_id ? $this->Language->locale : DEFAULT_LOCALE); Session::put(SESSION_CURRENCY, $currencyId); Session::put(SESSION_LOCALE, $locale); @@ -810,7 +815,7 @@ class Account extends Eloquent if ( ! Utils::isNinja()) { return; } - + $this->company->trial_plan = $plan; $this->company->trial_started = date_create()->format('Y-m-d'); $this->company->save(); @@ -821,18 +826,18 @@ class Account extends Eloquent if (Utils::isNinjaDev()) { return true; } - + $planDetails = $this->getPlanDetails(); $selfHost = !Utils::isNinjaProd(); - + if (!$selfHost && function_exists('ninja_account_features')) { $result = ninja_account_features($this, $feature); - + if ($result != null) { return $result; } } - + switch ($feature) { // Pro case FEATURE_CUSTOMIZE_INVOICE_DESIGN: @@ -849,7 +854,7 @@ class Account extends Eloquent case FEATURE_CLIENT_PORTAL_PASSWORD: case FEATURE_CUSTOM_URL: return $selfHost || !empty($planDetails); - + // Pro; No trial allowed, unless they're trialing enterprise with an active pro plan case FEATURE_MORE_CLIENTS: return $selfHost || !empty($planDetails) && (!$planDetails['trial'] || !empty($this->getPlanDetails(false, false))); @@ -862,26 +867,26 @@ class Account extends Eloquent // Fallthrough case FEATURE_CLIENT_PORTAL_CSS: return !empty($planDetails);// A plan is required even for self-hosted users - + // Enterprise; No Trial allowed; grandfathered for old pro users case FEATURE_USERS:// Grandfathered for old Pro users if($planDetails && $planDetails['trial']) { // Do they have a non-trial plan? $planDetails = $this->getPlanDetails(false, false); } - + return $selfHost || !empty($planDetails) && ($planDetails['plan'] == PLAN_ENTERPRISE || $planDetails['started'] <= date_create(PRO_USERS_GRANDFATHER_DEADLINE)); - + // Enterprise; No Trial allowed case FEATURE_DOCUMENTS: case FEATURE_USER_PERMISSIONS: return $selfHost || !empty($planDetails) && $planDetails['plan'] == PLAN_ENTERPRISE && !$planDetails['trial']; - + default: return false; } } - + public function isPro(&$plan_details = null) { if (!Utils::isNinjaProd()) { @@ -893,7 +898,7 @@ class Account extends Eloquent } $plan_details = $this->getPlanDetails(); - + return !empty($plan_details); } @@ -908,36 +913,36 @@ class Account extends Eloquent } $plan_details = $this->getPlanDetails(); - + return $plan_details && $plan_details['plan'] == PLAN_ENTERPRISE; } - + public function getPlanDetails($include_inactive = false, $include_trial = true) { if (!$this->company) { return null; } - + $plan = $this->company->plan; $trial_plan = $this->company->trial_plan; - + if(!$plan && (!$trial_plan || !$include_trial)) { return null; - } - + } + $trial_active = false; if ($trial_plan && $include_trial) { $trial_started = DateTime::createFromFormat('Y-m-d', $this->company->trial_started); $trial_expires = clone $trial_started; $trial_expires->modify('+2 weeks'); - + if ($trial_expires >= date_create()) { $trial_active = true; } } - + $plan_active = false; - if ($plan) { + if ($plan) { if ($this->company->plan_expires == null) { $plan_active = true; $plan_expires = false; @@ -948,11 +953,11 @@ class Account extends Eloquent } } } - + if (!$include_inactive && !$plan_active && !$trial_active) { return null; } - + // Should we show plan details or trial details? if (($plan && !$trial_plan) || !$include_trial) { $use_plan = true; @@ -979,7 +984,7 @@ class Account extends Eloquent $use_plan = $plan_expires >= $trial_expires; } } - + if ($use_plan) { return array( 'trial' => false, @@ -1006,7 +1011,7 @@ class Account extends Eloquent if (!Utils::isNinjaProd()) { return false; } - + $plan_details = $this->getPlanDetails(); return $plan_details && $plan_details['trial']; @@ -1021,7 +1026,7 @@ class Account extends Eloquent return array(PLAN_PRO, PLAN_ENTERPRISE); } } - + if ($this->company->trial_plan == PLAN_PRO) { if ($plan) { return $plan != PLAN_PRO; @@ -1029,28 +1034,28 @@ class Account extends Eloquent return array(PLAN_ENTERPRISE); } } - + return false; } public function getCountTrialDaysLeft() { $planDetails = $this->getPlanDetails(true); - + if(!$planDetails || !$planDetails['trial']) { return 0; } - + $today = new DateTime('now'); $interval = $today->diff($planDetails['expires']); - + return $interval ? $interval->d : 0; } public function getRenewalDate() { $planDetails = $this->getPlanDetails(); - + if ($planDetails) { $date = $planDetails['expires']; $date = max($date, date_create()); @@ -1180,7 +1185,7 @@ class Account extends Eloquent $field = "email_template_{$entityType}"; $template = $this->$field; } - + if (!$template) { $template = $this->getDefaultEmailTemplate($entityType, $message); } @@ -1270,7 +1275,7 @@ class Account extends Eloquent { $url = SITE_URL; $iframe_url = $this->iframe_url; - + if ($iframe_url) { return "{$iframe_url}/?"; } else if ($this->subdomain) { @@ -1305,10 +1310,10 @@ class Account extends Eloquent if (!$entity) { return false; } - + // convert (for example) 'custom_invoice_label1' to 'invoice.custom_value1' $field = str_replace(['invoice_', 'label'], ['', 'value'], $field); - + return Utils::isEmpty($entity->$field) ? false : true; } @@ -1316,7 +1321,7 @@ class Account extends Eloquent { return $this->hasFeature(FEATURE_PDF_ATTACHMENT) && $this->pdf_email_attachment; } - + public function getEmailDesignId() { return $this->hasFeature(FEATURE_CUSTOM_EMAILS) ? $this->email_design_id : EMAIL_DESIGN_PLAIN; @@ -1324,11 +1329,11 @@ class Account extends Eloquent public function clientViewCSS(){ $css = ''; - + if ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) { $bodyFont = $this->getBodyFontCss(); $headerFont = $this->getHeaderFontCss(); - + $css = 'body{'.$bodyFont.'}'; if ($headerFont != $bodyFont) { $css .= 'h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{'.$headerFont.'}'; @@ -1338,17 +1343,17 @@ class Account extends Eloquent // For self-hosted users, a white-label license is required for custom CSS $css .= $this->client_view_css; } - + return $css; } - + public function getFontsUrl($protocol = ''){ $bodyFont = $this->getHeaderFontId(); $headerFont = $this->getBodyFontId(); $bodyFontSettings = Utils::getFromCache($bodyFont, 'fonts'); $google_fonts = array($bodyFontSettings['google_font']); - + if($headerFont != $bodyFont){ $headerFontSettings = Utils::getFromCache($headerFont, 'fonts'); $google_fonts[] = $headerFontSettings['google_font']; @@ -1356,7 +1361,7 @@ class Account extends Eloquent return ($protocol?$protocol.':':'').'//fonts.googleapis.com/css?family='.implode('|',$google_fonts); } - + public function getHeaderFontId() { return ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) && $this->header_font_id) ? $this->header_font_id : DEFAULT_HEADER_FONT; } @@ -1368,47 +1373,47 @@ class Account extends Eloquent public function getHeaderFontName(){ return Utils::getFromCache($this->getHeaderFontId(), 'fonts')['name']; } - + public function getBodyFontName(){ return Utils::getFromCache($this->getBodyFontId(), 'fonts')['name']; } - + public function getHeaderFontCss($include_weight = true){ $font_data = Utils::getFromCache($this->getHeaderFontId(), 'fonts'); $css = 'font-family:'.$font_data['css_stack'].';'; - + if($include_weight){ $css .= 'font-weight:'.$font_data['css_weight'].';'; } - + return $css; } - + public function getBodyFontCss($include_weight = true){ $font_data = Utils::getFromCache($this->getBodyFontId(), 'fonts'); $css = 'font-family:'.$font_data['css_stack'].';'; - + if($include_weight){ $css .= 'font-weight:'.$font_data['css_weight'].';'; } - + return $css; } - + public function getFonts(){ return array_unique(array($this->getHeaderFontId(), $this->getBodyFontId())); } - + public function getFontsData(){ $data = array(); - + foreach($this->getFonts() as $font){ $data[] = Utils::getFromCache($font, 'fonts'); } - + return $data; } - + public function getFontFolders(){ return array_map(function($item){return $item['folder'];}, $this->getFontsData()); } diff --git a/config/former.php b/config/former.php index b9c729e2f9ae..110fffe8ae2b 100644 --- a/config/former.php +++ b/config/former.php @@ -27,7 +27,7 @@ // Whether checkboxes should always be present in the POST data, // no matter if you checked them or not - 'push_checkboxes' => false, + 'push_checkboxes' => true, // The value a checkbox will have in the POST array if unchecked 'unchecked_value' => 0, @@ -181,4 +181,4 @@ ), -); \ No newline at end of file +); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index e70250130638..9bf85f0202ae 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1312,7 +1312,8 @@ $LANG = array( 'security' => 'Security', 'see_whats_new' => 'See what\'s new in v:version', 'wait_for_upload' => 'Please wait for the document upload to complete.', - 'upgrade_for_permissions' => 'Upgrade to our Enterprise plan to enable permissions.' + 'upgrade_for_permissions' => 'Upgrade to our Enterprise plan to enable permissions.', + 'enable_second_tax_rate' => 'Enable specifying a second tax rate', ); diff --git a/resources/views/accounts/tax_rates.blade.php b/resources/views/accounts/tax_rates.blade.php index 2079d72f2155..1a482dff8cad 100644 --- a/resources/views/accounts/tax_rates.blade.php +++ b/resources/views/accounts/tax_rates.blade.php @@ -1,6 +1,6 @@ @extends('header') -@section('content') +@section('content') @parent @include('accounts.nav', ['selected' => ACCOUNT_TAX_RATES]) @@ -10,12 +10,13 @@ {{ Former::populateField('invoice_taxes', intval($account->invoice_taxes)) }} {{ Former::populateField('invoice_item_taxes', intval($account->invoice_item_taxes)) }} {{ Former::populateField('show_item_taxes', intval($account->show_item_taxes)) }} + {{ Former::populateField('enable_second_tax_rate', intval($account->enable_second_tax_rate)) }}

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

    -
    +
    {!! Former::checkbox('invoice_taxes') @@ -30,6 +31,10 @@ ->text(trans('texts.show_line_item_tax')) ->label(' ') !!} + {!! Former::checkbox('enable_second_tax_rate') + ->text(trans('texts.enable_second_tax_rate')) + ->label(' ') !!} +   {!! Former::select('default_tax_rate_id') @@ -51,22 +56,22 @@ @include('partials.bulk_form', ['entityType' => ENTITY_TAX_RATE]) - {!! Datatable::table() + {!! Datatable::table() ->addColumn( trans('texts.name'), trans('texts.rate'), trans('texts.action')) - ->setUrl(url('api/tax_rates/')) + ->setUrl(url('api/tax_rates/')) ->setOptions('sPaginationType', 'bootstrap') - ->setOptions('bFilter', false) - ->setOptions('bAutoWidth', false) + ->setOptions('bFilter', false) + ->setOptions('bAutoWidth', false) ->setOptions('aoColumns', [[ "sWidth"=> "40%" ], [ "sWidth"=> "40%" ], ["sWidth"=> "20%"]]) ->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[2]]]) ->render('datatable') !!} + -@stop \ No newline at end of file +@stop diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 4f6940645ada..7a744bee4601 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -243,7 +243,7 @@ @endif {{ $invoiceLabels['unit_cost'] }} {{ $invoiceLabels['quantity'] }} - {{ trans('texts.tax') }} + {{ trans('texts.tax') }} {{ trans('texts.line_total') }} @@ -288,16 +288,18 @@ ->addOption('', '') ->options($taxRateOptions) ->data_bind('value: tax1') - ->addClass('tax-select') + ->addClass($account->enable_second_tax_rate ? 'tax-select' : '') ->raw() !!} - {!! Former::select('') - ->addOption('', '') - ->options($taxRateOptions) - ->data_bind('value: tax2') - ->addClass('tax-select') - ->raw() !!} +
    + {!! Former::select('') + ->addOption('', '') + ->options($taxRateOptions) + ->data_bind('value: tax2') + ->addClass('tax-select') + ->raw() !!} +
    @@ -438,17 +440,19 @@ ->id('taxRateSelect1') ->addOption('', '') ->options($taxRateOptions) - ->addClass('tax-select') + ->addClass($account->enable_second_tax_rate ? 'tax-select' : '') ->data_bind('value: tax1') ->raw() !!} +
    {!! Former::select('') ->addOption('', '') ->options($taxRateOptions) ->addClass('tax-select') ->data_bind('value: tax2') ->raw() !!} +
    From c254544b198f8f7fe24220194003ab4c2551d476 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 May 2016 12:56:10 +0300 Subject: [PATCH 174/386] Added setting to hide second tax rate --- .../2014_05_17_175626_add_quotes.php | 6 ++-- ..._05_18_085739_add_invoice_type_support.php | 32 +++++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/database/migrations/2014_05_17_175626_add_quotes.php b/database/migrations/2014_05_17_175626_add_quotes.php index 96ae916cafae..c332013d240d 100644 --- a/database/migrations/2014_05_17_175626_add_quotes.php +++ b/database/migrations/2014_05_17_175626_add_quotes.php @@ -14,9 +14,9 @@ class AddQuotes extends Migration { { Schema::table('invoices', function($table) { - $table->boolean('is_quote')->default(0); + $table->boolean('invoice_type_id')->default(0); $table->unsignedInteger('quote_id')->nullable(); - $table->unsignedInteger('quote_invoice_id')->nullable(); + $table->unsignedInteger('quote_invoice_id')->nullable(); }); } @@ -30,7 +30,7 @@ class AddQuotes extends Migration { Schema::table('invoices', function($table) { $table->dropColumn('is_quote'); - $table->dropColumn('quote_id'); + $table->dropColumn('invoice_type_id'); $table->dropColumn('quote_invoice_id'); }); } diff --git a/database/migrations/2016_05_18_085739_add_invoice_type_support.php b/database/migrations/2016_05_18_085739_add_invoice_type_support.php index 7b0b928d811b..faeb6ae0845d 100644 --- a/database/migrations/2016_05_18_085739_add_invoice_type_support.php +++ b/database/migrations/2016_05_18_085739_add_invoice_type_support.php @@ -12,9 +12,20 @@ class AddInvoiceTypeSupport extends Migration */ public function up() { - Schema::table('invoices', function ($table) { - $table->renameColumn('is_quote', 'invoice_type_id'); - }); + if (Schema::hasColumn('invoices', 'is_quote')) { + DB::update('update invoices set is_quote = is_quote + 1'); + + Schema::table('invoices', function ($table) { + $table->renameColumn('is_quote', 'invoice_type_id'); + }); + } + + Schema::table('accounts', function($table) + { + $table->boolean('enable_second_tax_rate')->default(false); + }); + + } /** @@ -24,8 +35,17 @@ class AddInvoiceTypeSupport extends Migration */ public function down() { - Schema::table('invoices', function ($table) { - $table->renameColumn('invoice_type_id', 'is_quote'); - }); + if (Schema::hasColumn('invoices', 'invoice_type_id')) { + DB::update('update invoices set invoice_type_id = invoice_type_id - 1'); + + Schema::table('invoices', function ($table) { + $table->renameColumn('invoice_type_id', 'is_quote'); + }); + } + + Schema::table('accounts', function($table) + { + $table->dropColumn('enable_second_tax_rate'); + }); } } From 59d0a1cb807cb42c21322723143af9af5efaa909 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 May 2016 14:22:25 +0300 Subject: [PATCH 175/386] Fix to support mobile app --- .travis.yml | 2 +- app/Ninja/Transformers/InvoiceTransformer.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9eef15bf06b8..63de5f8ce3aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ sudo: true php: - 5.5.9 - - 5.6 +# - 5.6 # - 5.6 # - 7.0 # - hhvm diff --git a/app/Ninja/Transformers/InvoiceTransformer.php b/app/Ninja/Transformers/InvoiceTransformer.php index b961dbdfaeba..553fa604daae 100644 --- a/app/Ninja/Transformers/InvoiceTransformer.php +++ b/app/Ninja/Transformers/InvoiceTransformer.php @@ -34,7 +34,7 @@ class InvoiceTransformer extends EntityTransformer public function __construct($account = null, $serializer = null, $client = null) { parent::__construct($account, $serializer); - + $this->client = $client; } @@ -119,6 +119,7 @@ class InvoiceTransformer extends EntityTransformer 'quote_invoice_id' => (int) $invoice->quote_invoice_id, 'custom_text_value1' => $invoice->custom_text_value1, 'custom_text_value2' => $invoice->custom_text_value2, + 'is_quote' => (bool) $invoice->isType(INVOICE_TYPE_QUOTE), // Temp to support mobile app ]); } } From a9302d4d013d0d697541a83acf4422a97c3b8e49 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 29 May 2016 15:34:44 +0300 Subject: [PATCH 176/386] Fixes for is_quote --- app/Models/Invoice.php | 60 +++++++++++--------- app/Ninja/Repositories/InvoiceRepository.php | 42 +++++++------- resources/views/invoices/view.blade.php | 20 +++---- 3 files changed, 63 insertions(+), 59 deletions(-) diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index f3717e9afa65..17b6292739c1 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -29,9 +29,9 @@ class Invoice extends EntityModel implements BalanceAffecting 'tax_name1', 'tax_rate1', 'tax_name2', - 'tax_rate2', + 'tax_rate2', ]; - + protected $casts = [ 'is_recurring' => 'boolean', 'has_tasks' => 'boolean', @@ -243,6 +243,10 @@ class Invoice extends EntityModel implements BalanceAffecting return $this->invoice_type_id == $typeId; } + public function isQuote() { + return $this->isType(INVOICE_TYPE_QUOTE); + } + public function markInvitationsSent($notify = false) { foreach ($this->invitations as $invitation) { @@ -524,12 +528,12 @@ class Invoice extends EntityModel implements BalanceAffecting 'name', ]); } - + foreach ($this->expenses as $expense) { $expense->setVisible([ 'documents', ]); - + foreach ($expense->documents as $document) { $document->setVisible([ 'public_id', @@ -588,12 +592,12 @@ class Invoice extends EntityModel implements BalanceAffecting return $schedule[1]->getStart(); } - + public function getDueDate($invoice_date = null){ if(!$this->is_recurring) { return $this->due_date ? $this->due_date : null; } - else{ + else{ $now = time(); if($invoice_date) { // If $invoice_date is specified, all calculations are based on that date @@ -607,7 +611,7 @@ class Invoice extends EntityModel implements BalanceAffecting $now = $invoice_date->getTimestamp(); } } - + if($this->due_date && $this->due_date != '0000-00-00'){ // This is a recurring invoice; we're using a custom format here. // The year is always 1998; January is 1st, 2nd, last day of the month. @@ -616,7 +620,7 @@ class Invoice extends EntityModel implements BalanceAffecting $monthVal = (int)date('n', $dueDateVal); $dayVal = (int)date('j', $dueDateVal); $dueDate = false; - + if($monthVal == 1) {// January; day of month $currentDay = (int)date('j', $now); $lastDayOfMonth = (int)date('t', $now); @@ -643,7 +647,7 @@ class Invoice extends EntityModel implements BalanceAffecting if($dueDay > $lastDayOfMonth){ // No later than the end of the month $dueDay = $lastDayOfMonth; - } + } } $dueDate = mktime(0, 0, 0, $dueMonth, $dueDay, $dueYear); @@ -672,7 +676,7 @@ class Invoice extends EntityModel implements BalanceAffecting return date('Y-m-d', strtotime('+'.$days.' day', $now)); } } - + // Couldn't calculate one return null; } @@ -690,11 +694,11 @@ class Invoice extends EntityModel implements BalanceAffecting $dateStart = $date->getStart(); $date = $this->account->formatDate($dateStart); $dueDate = $this->getDueDate($dateStart); - + if($dueDate) { $date .= ' (' . trans('texts.due') . ' ' . $this->account->formatDate($dueDate) . ')'; } - + $dates[] = $date; } @@ -808,16 +812,16 @@ class Invoice extends EntityModel implements BalanceAffecting $invitation = $this->invitations[0]; $link = $invitation->getLink('view', true); $key = env('PHANTOMJS_CLOUD_KEY'); - + if (Utils::isNinjaDev()) { $link = env('TEST_LINK'); } $url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D"; - + $pdfString = file_get_contents($url); $pdfString = strip_tags($pdfString); - + if ( ! $pdfString || strlen($pdfString) < 200) { Utils::logError("PhantomJSCloud - failed to create pdf: {$pdfString}"); return false; @@ -870,14 +874,14 @@ class Invoice extends EntityModel implements BalanceAffecting return $total; } - // if $calculatePaid is true we'll loop through each payment to + // if $calculatePaid is true we'll loop through each payment to // determine the sum, otherwise we'll use the cached paid_to_date amount public function getTaxes($calculatePaid = false) { $taxes = []; $taxable = $this->getTaxable(); $paidAmount = $this->getAmountPaid($calculatePaid); - + if ($this->tax_name1) { $invoiceTaxAmount = round($taxable * ($this->tax_rate1 / 100), 2); $invoicePaidAmount = $this->amount && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0; @@ -892,7 +896,7 @@ class Invoice extends EntityModel implements BalanceAffecting foreach ($this->invoice_items as $invoiceItem) { $itemTaxAmount = $this->getItemTaxable($invoiceItem, $taxable); - + if ($invoiceItem->tax_name1) { $itemTaxAmount = round($taxable * ($invoiceItem->tax_rate1 / 100), 2); $itemPaidAmount = $this->amount && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0; @@ -905,20 +909,20 @@ class Invoice extends EntityModel implements BalanceAffecting $this->calculateTax($taxes, $invoiceItem->tax_name2, $invoiceItem->tax_rate2, $itemTaxAmount, $itemPaidAmount); } } - + return $taxes; } - - private function calculateTax(&$taxes, $name, $rate, $amount, $paid) - { + + private function calculateTax(&$taxes, $name, $rate, $amount, $paid) + { if ( ! $amount) { return; - } - + } + $amount = round($amount, 2); $paid = round($paid, 2); $key = $rate . ' ' . $name; - + if ( ! isset($taxes[$key])) { $taxes[$key] = [ 'name' => $name, @@ -929,14 +933,14 @@ class Invoice extends EntityModel implements BalanceAffecting } $taxes[$key]['amount'] += $amount; - $taxes[$key]['paid'] += $paid; + $taxes[$key]['paid'] += $paid; } - + public function hasDocuments(){ if(count($this->documents))return true; return $this->hasExpenseDocuments(); } - + public function hasExpenseDocuments(){ foreach($this->expenses as $expense){ if(count($expense->documents))return true; diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index 6a48c9a74921..290e22ed296c 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -210,7 +210,7 @@ class InvoiceRepository extends BaseRepository ->where('contacts.is_primary', '=', true) ->where('invoices.is_recurring', '=', false) // This needs to be a setting to also hide the activity on the dashboard page - //->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT) + //->where('invoices.invoice_status_id', '>=', INVOICE_STATUS_SENT) ->select( DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), @@ -287,7 +287,7 @@ class InvoiceRepository extends BaseRepository $account->invoice_footer = trim($data['invoice_footer']); } $account->save(); - } + } if (isset($data['invoice_number']) && !$invoice->is_recurring) { $invoice->invoice_number = trim($data['invoice_number']); @@ -329,7 +329,7 @@ class InvoiceRepository extends BaseRepository if ($invoice->auto_bill < AUTO_BILL_OFF || $invoice->auto_bill > AUTO_BILL_ALWAYS ) { $invoice->auto_bill = AUTO_BILL_OFF; } - + if (isset($data['recurring_due_date'])) { $invoice->due_date = $data['recurring_due_date']; } elseif (isset($data['due_date'])) { @@ -351,7 +351,7 @@ class InvoiceRepository extends BaseRepository } else { $invoice->terms = ''; } - + $invoice->invoice_footer = (isset($data['invoice_footer']) && trim($data['invoice_footer'])) ? trim($data['invoice_footer']) : (!$publicId && $account->invoice_footer ? $account->invoice_footer : ''); $invoice->public_notes = isset($data['public_notes']) ? trim($data['public_notes']) : null; @@ -370,8 +370,8 @@ class InvoiceRepository extends BaseRepository // provide backwards compatability if (isset($data['tax_name']) && isset($data['tax_rate'])) { - $data['tax_name1'] = $data['tax_name']; - $data['tax_rate1'] = $data['tax_rate']; + $data['tax_name1'] = $data['tax_name']; + $data['tax_rate1'] = $data['tax_rate']; } $total = 0; @@ -405,11 +405,11 @@ class InvoiceRepository extends BaseRepository } if (isset($item['tax_rate1']) && Utils::parseFloat($item['tax_rate1']) > 0) { - $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate1']); + $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate1']); $itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2); } if (isset($item['tax_rate2']) && Utils::parseFloat($item['tax_rate2']) > 0) { - $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate2']); + $invoiceItemTaxRate = Utils::parseFloat($item['tax_rate2']); $itemTax += round($lineTotal * $invoiceItemTaxRate / 100, 2); } } @@ -453,7 +453,7 @@ class InvoiceRepository extends BaseRepository $taxAmount1 = round($total * $invoice->tax_rate1 / 100, 2); $taxAmount2 = round($total * $invoice->tax_rate2 / 100, 2); - $total = round($total + $taxAmount1 + $taxAmount2, 2); + $total = round($total + $taxAmount1 + $taxAmount2, 2); $total += $itemTax; // custom fields not charged taxes @@ -476,24 +476,24 @@ class InvoiceRepository extends BaseRepository if ($publicId) { $invoice->invoice_items()->forceDelete(); } - + $document_ids = !empty($data['document_ids'])?array_map('intval', $data['document_ids']):array();; foreach ($document_ids as $document_id){ $document = Document::scope($document_id)->first(); if($document && Auth::user()->can('edit', $document)){ - + if($document->invoice_id && $document->invoice_id != $invoice->id){ // From a clone $document = $document->cloneDocument(); $document_ids[] = $document->public_id;// Don't remove this document } - + $document->invoice_id = $invoice->id; $document->expense_id = null; $document->save(); } } - + if(!empty($data['documents']) && Auth::user()->can('create', ENTITY_DOCUMENT)){ // Fallback upload $doc_errors = array(); @@ -512,7 +512,7 @@ class InvoiceRepository extends BaseRepository Session::flash('error', implode('
    ',array_map('htmlentities',$doc_errors))); } } - + foreach ($invoice->documents as $document){ if(!in_array($document->public_id, $document_ids)){ // Removed @@ -586,12 +586,12 @@ class InvoiceRepository extends BaseRepository // provide backwards compatability if (isset($item['tax_name']) && isset($item['tax_rate'])) { - $item['tax_name1'] = $item['tax_name']; - $item['tax_rate1'] = $item['tax_rate']; + $item['tax_name1'] = $item['tax_name']; + $item['tax_rate1'] = $item['tax_rate']; } $invoiceItem->fill($item); - + $invoice->invoice_items()->save($invoiceItem); } @@ -675,9 +675,9 @@ class InvoiceRepository extends BaseRepository 'cost', 'qty', 'tax_name1', - 'tax_rate1', + 'tax_rate1', 'tax_name2', - 'tax_rate2', + 'tax_rate2', ] as $field) { $cloneItem->$field = $item->$field; } @@ -686,7 +686,7 @@ class InvoiceRepository extends BaseRepository } foreach ($invoice->documents as $document) { - $cloneDocument = $document->cloneDocument(); + $cloneDocument = $document->cloneDocument(); $invoice->documents()->save($cloneDocument); } @@ -731,8 +731,8 @@ class InvoiceRepository extends BaseRepository public function findOpenInvoices($clientId) { return Invoice::scope() + ->invoiceType(INVOICE_TYPE_STANDARD) ->whereClientId($clientId) - ->whereIsQuote(false) ->whereIsRecurring(false) ->whereDeletedAt(null) ->whereHasTasks(true) diff --git a/resources/views/invoices/view.blade.php b/resources/views/invoices/view.blade.php index 5c32210f0c80..096b7b74f765 100644 --- a/resources/views/invoices/view.blade.php +++ b/resources/views/invoices/view.blade.php @@ -4,15 +4,15 @@ @parent @include('money_script') - + @foreach ($invoice->client->account->getFontFolders() as $font) @endforeach - + @@ -34,7 +32,7 @@ @foreach (\App\Services\ImportService::$entityTypes as $entityType) {!! Former::file("{$entityType}_file") - ->addGroupClass("{$entityType}-file") !!} + ->addGroupClass("import-file {$entityType}-file") !!} @endforeach {!! Former::actions( Button::info(trans('texts.upload'))->submit()->large()->appendIcon(Icon::create('open'))) !!} @@ -67,13 +65,17 @@ trans('texts.payments') => array('name' => ENTITY_PAYMENT, 'value' => 1), ])->check(ENTITY_CLIENT)->check(ENTITY_TASK)->check(ENTITY_INVOICE)->check(ENTITY_PAYMENT) !!} - {!! Former::actions( Button::primary(trans('texts.download'))->submit()->large()->appendIcon(Icon::create('download-alt'))) !!} + {!! Former::actions( Button::primary(trans('texts.download'))->submit()->large()->appendIcon(Icon::create('download-alt'))) !!}
    {!! Former::close() !!} -@stop \ No newline at end of file +@stop diff --git a/resources/views/accounts/import_map.blade.php b/resources/views/accounts/import_map.blade.php index 12ac6766d693..5c3c76ac7c20 100644 --- a/resources/views/accounts/import_map.blade.php +++ b/resources/views/accounts/import_map.blade.php @@ -7,18 +7,16 @@ {!! Former::open('/import_csv')->addClass('warn-on-exit') !!} - @if (isset($data[ENTITY_CLIENT])) - @include('accounts.partials.map', $data[ENTITY_CLIENT]) - @endif + @foreach (App\Services\ImportService::$entityTypes as $entityType) + @if (isset($data[$entityType])) + @include('accounts.partials.map', $data[$entityType]) + @endif + @endforeach - @if (isset($data[ENTITY_INVOICE])) - @include('accounts.partials.map', $data[ENTITY_INVOICE]) - @endif - - {!! Former::actions( + {!! Former::actions( Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/settings/import_export'))->appendIcon(Icon::create('remove-circle')), Button::success(trans('texts.import'))->submit()->large()->appendIcon(Icon::create('floppy-disk'))) !!} - + {!! Former::close() !!} -@stop \ No newline at end of file +@stop From e6a05509b19a94f0ce5be754a08632a540ed5204 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 31 May 2016 23:16:05 +0300 Subject: [PATCH 202/386] Enabled importing products --- app/Ninja/Import/CSV/ProductTransformer.php | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/Ninja/Import/CSV/ProductTransformer.php diff --git a/app/Ninja/Import/CSV/ProductTransformer.php b/app/Ninja/Import/CSV/ProductTransformer.php new file mode 100644 index 000000000000..248d3ed09cba --- /dev/null +++ b/app/Ninja/Import/CSV/ProductTransformer.php @@ -0,0 +1,22 @@ +product_key) || $this->hasProduct($data->product_key)) { + return false; + } + + return new Item($data, function ($data) { + return [ + 'product_key' => $this->getString($data, 'product_key'), + 'notes' => $this->getString($data, 'notes'), + 'cost' => $this->getNumber($data, 'cost'), + ]; + }); + } +} From 51d9b2b4271ce40559c3603c90996cd4436e26b0 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Jun 2016 10:56:44 +0300 Subject: [PATCH 203/386] Prevent converting a qoute from incrementing the counter --- app/Models/Account.php | 62 ++++++++++---------- app/Ninja/Repositories/InvoiceRepository.php | 2 +- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/app/Models/Account.php b/app/Models/Account.php index 5dd8a12c1851..61526c9095d4 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -652,30 +652,34 @@ class Account extends Eloquent public function getNextInvoiceNumber($invoice) { if ($this->hasNumberPattern($invoice->invoice_type_id)) { - return $this->getNumberPattern($invoice); + $number = $this->getNumberPattern($invoice); + } else { + $counter = $this->getCounter($invoice->invoice_type_id); + $prefix = $this->getNumberPrefix($invoice->invoice_type_id); + $counterOffset = 0; + + // confirm the invoice number isn't already taken + do { + $number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT); + $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); + $counter++; + $counterOffset++; + } while ($check); + + // update the invoice counter to be caught up + if ($counterOffset > 1) { + if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) { + $this->quote_number_counter += $counterOffset - 1; + } else { + $this->invoice_number_counter += $counterOffset - 1; + } + + $this->save(); + } } - $counter = $this->getCounter($invoice->invoice_type_id); - $prefix = $this->getNumberPrefix($invoice->invoice_type_id); - $counterOffset = 0; - - // confirm the invoice number isn't already taken - do { - $number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT); - $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); - $counter++; - $counterOffset++; - } while ($check); - - // update the invoice counter to be caught up - if ($counterOffset > 1) { - if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) { - $this->quote_number_counter += $counterOffset - 1; - } else { - $this->invoice_number_counter += $counterOffset - 1; - } - - $this->save(); + if ($invoice->recurring_invoice_id) { + $number = $this->recurring_invoice_number_prefix . $number; } return $number; @@ -683,17 +687,15 @@ class Account extends Eloquent public function incrementCounter($invoice) { + // if they didn't use the counter don't increment it + if ($invoice->invoice_number != $this->getNextInvoiceNumber($invoice)) { + return; + } + if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) { $this->quote_number_counter += 1; } else { - $default = $this->invoice_number_counter; - $actual = Utils::parseInt($invoice->invoice_number); - - if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $default != $actual) { - $this->invoice_number_counter = $actual + 1; - } else { - $this->invoice_number_counter += 1; - } + $this->invoice_number_counter += 1; } $this->save(); diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index cb687430ebc5..c15e261848d0 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -741,7 +741,7 @@ class InvoiceRepository extends BaseRepository $invoice = Invoice::createNew($recurInvoice); $invoice->client_id = $recurInvoice->client_id; $invoice->recurring_invoice_id = $recurInvoice->id; - $invoice->invoice_number = $recurInvoice->account->recurring_invoice_number_prefix . $recurInvoice->account->getNextInvoiceNumber($recurInvoice); + $invoice->invoice_number = $recurInvoice->account->getNextInvoiceNumber($invoice); $invoice->amount = $recurInvoice->amount; $invoice->balance = $recurInvoice->amount; $invoice->invoice_date = date_create()->format('Y-m-d'); From 17eb2a7a79cd0686cf8ddf99efb7fa5861f96d97 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Jun 2016 12:39:42 +0300 Subject: [PATCH 204/386] Support saving invoice_id and expense_id with the document --- .../Controllers/DocumentAPIController.php | 2 +- app/Http/Controllers/DocumentController.php | 2 +- app/Http/Requests/CreateDocumentRequest.php | 39 ++++----- app/Http/Requests/Request.php | 22 +++++- app/Models/Document.php | 79 ++++++++++--------- app/Ninja/Repositories/DocumentRepository.php | 10 ++- app/Providers/AppServiceProvider.php | 16 ++-- 7 files changed, 100 insertions(+), 70 deletions(-) diff --git a/app/Http/Controllers/DocumentAPIController.php b/app/Http/Controllers/DocumentAPIController.php index e4ca7c7fb5de..194c32e07d81 100644 --- a/app/Http/Controllers/DocumentAPIController.php +++ b/app/Http/Controllers/DocumentAPIController.php @@ -33,7 +33,7 @@ class DocumentAPIController extends BaseAPIController public function store(CreateDocumentRequest $request) { - $document = $this->documentRepo->upload($request->file); + $document = $this->documentRepo->upload($request->all()); return $this->itemResponse($document); } diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php index f0871a2408f2..6f6b0bd0889d 100644 --- a/app/Http/Controllers/DocumentController.php +++ b/app/Http/Controllers/DocumentController.php @@ -102,7 +102,7 @@ class DocumentController extends BaseController public function postUpload(CreateDocumentRequest $request) { - $result = $this->documentRepo->upload($request->file, $doc_array); + $result = $this->documentRepo->upload($request->all(), $doc_array); if(is_string($result)){ return Response::json([ diff --git a/app/Http/Requests/CreateDocumentRequest.php b/app/Http/Requests/CreateDocumentRequest.php index d00576478191..5037ae3beb68 100644 --- a/app/Http/Requests/CreateDocumentRequest.php +++ b/app/Http/Requests/CreateDocumentRequest.php @@ -1,7 +1,15 @@ user()->can('create', ENTITY_DOCUMENT) && $this->user()->hasFeature(FEATURE_DOCUMENTS); + if ( ! $this->user()->hasFeature(FEATURE_DOCUMENTS)) { + return false; + } + + if ($this->invoice && $this->user()->cannot('edit', $this->invoice)) { + return false; + } + + if ($this->expense && $this->user()->cannot('edit', $this->expense)) { + return false; + } + + return $this->user()->can('create', ENTITY_DOCUMENT); } /** @@ -24,21 +44,4 @@ class CreateDocumentRequest extends DocumentRequest ]; } - /** - * Sanitize input before validation. - * - * @return array - */ - /* - public function sanitize() - { - $input = $this->all(); - - $input['phone'] = 'test123'; - - $this->replace($input); - - return $this->all(); - } - */ } diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index b0f5a5d85a95..c704e409f373 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -5,6 +5,9 @@ use Illuminate\Foundation\Http\FormRequest; // https://laracasts.com/discuss/channels/general-discussion/laravel-5-modify-input-before-validation/replies/34366 abstract class Request extends FormRequest { + // populate in subclass to auto load record + protected $autoload = []; + /** * Validate the input. * @@ -25,11 +28,24 @@ abstract class Request extends FormRequest { */ protected function sanitizeInput() { - if (method_exists($this, 'sanitize')) - { - return $this->container->call([$this, 'sanitize']); + if (method_exists($this, 'sanitize')) { + $input = $this->container->call([$this, 'sanitize']); + } else { + $input = $this->all(); } + // autoload referenced entities + foreach ($this->autoload as $entityType) { + if ($id = $this->input("{$entityType}_public_id") ?: $this->input("{$entityType}_id")) { + $class = "App\\Models\\" . ucwords($entityType); + $entity = $class::scope($id)->firstOrFail(); + $input[$entityType] = $entity; + $input[$entityType . '_id'] = $entity->id; + } + } + + $this->replace($input); + return $this->all(); } } diff --git a/app/Models/Document.php b/app/Models/Document.php index 6d9c24857143..cc455fed0d79 100644 --- a/app/Models/Document.php +++ b/app/Models/Document.php @@ -6,20 +6,25 @@ use Auth; class Document extends EntityModel { + protected $fillable = [ + 'invoice_id', + 'expense_id', + ]; + public static $extraExtensions = array( 'jpg' => 'jpeg', 'tif' => 'tiff', ); - + public static $allowedMimes = array(// Used by Dropzone.js; does not affect what the server accepts 'image/png', 'image/jpeg', 'image/tiff', 'application/pdf', 'image/gif', 'image/vnd.adobe.photoshop', 'text/plain', 'application/zip', 'application/msword', - 'application/excel', 'application/vnd.ms-excel', 'application/x-excel', 'application/x-msexcel', + 'application/excel', 'application/vnd.ms-excel', 'application/x-excel', 'application/x-msexcel', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','application/postscript', 'image/svg+xml', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.ms-powerpoint', ); - + public static $types = array( 'png' => array( 'mime' => 'image/png', @@ -70,18 +75,18 @@ class Document extends EntityModel 'mime' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', ), ); - + public function fill(array $attributes) { parent::fill($attributes); - + if(empty($this->attributes['disk'])){ $this->attributes['disk'] = env('DOCUMENT_FILESYSTEM', 'documents'); } - + return $this; } - + public function account() { return $this->belongsTo('App\Models\Account'); @@ -101,7 +106,7 @@ class Document extends EntityModel { return $this->belongsTo('App\Models\Invoice')->withTrashed(); } - + public function getDisk(){ return Storage::disk(!empty($this->disk)?$this->disk:env('DOCUMENT_FILESYSTEM', 'documents')); } @@ -110,19 +115,19 @@ class Document extends EntityModel { $this->attributes['disk'] = $value?$value:env('DOCUMENT_FILESYSTEM', 'documents'); } - + public function getDirectUrl(){ return static::getDirectFileUrl($this->path, $this->getDisk()); } - + public function getDirectPreviewUrl(){ return $this->preview?static::getDirectFileUrl($this->preview, $this->getDisk(), true):null; } - + public static function getDirectFileUrl($path, $disk, $prioritizeSpeed = false){ $adapter = $disk->getAdapter(); $fullPath = $adapter->applyPathPrefix($path); - + if($adapter instanceof \League\Flysystem\AwsS3v3\AwsS3Adapter) { $client = $adapter->getClient(); $command = $client->getCommand('GetObject', [ @@ -136,12 +141,12 @@ class Document extends EntityModel $secret = env('RACKSPACE_TEMP_URL_SECRET'); if($secret){ $object = $adapter->getContainer()->getObject($fullPath); - + if(env('RACKSPACE_TEMP_URL_SECRET_SET')){ // Go ahead and set the secret too $object->getService()->getAccount()->setTempUrlSecret($secret); - } - + } + $url = $object->getUrl(); $expiry = strtotime('+10 minutes'); $urlPath = urldecode($url->getPath()); @@ -150,64 +155,64 @@ class Document extends EntityModel return sprintf('%s?temp_url_sig=%s&temp_url_expires=%d', $url, $hash, $expiry); } } - + return null; } - + public function getRaw(){ $disk = $this->getDisk(); - + return $disk->get($this->path); } - + public function getStream(){ $disk = $this->getDisk(); - + return $disk->readStream($this->path); } - + public function getRawPreview(){ $disk = $this->getDisk(); - + return $disk->get($this->preview); } - + public function getUrl(){ return url('documents/'.$this->public_id.'/'.$this->name); } - + public function getClientUrl($invitation){ return url('client/documents/'.$invitation->invitation_key.'/'.$this->public_id.'/'.$this->name); } - + public function isPDFEmbeddable(){ return $this->type == 'jpeg' || $this->type == 'png' || $this->preview; } - + public function getVFSJSUrl(){ if(!$this->isPDFEmbeddable())return null; return url('documents/js/'.$this->public_id.'/'.$this->name.'.js'); } - + public function getClientVFSJSUrl(){ if(!$this->isPDFEmbeddable())return null; return url('client/documents/js/'.$this->public_id.'/'.$this->name.'.js'); } - + public function getPreviewUrl(){ return $this->preview?url('documents/preview/'.$this->public_id.'/'.$this->name.'.'.pathinfo($this->preview, PATHINFO_EXTENSION)):null; } - + public function toArray() { $array = parent::toArray(); - + if(empty($this->visible) || in_array('url', $this->visible))$array['url'] = $this->getUrl(); if(empty($this->visible) || in_array('preview_url', $this->visible))$array['preview_url'] = $this->getPreviewUrl(); - + return $array; } - + public function cloneDocument(){ $document = Document::createNew($this); $document->path = $this->path; @@ -219,7 +224,7 @@ class Document extends EntityModel $document->size = $this->size; $document->width = $this->width; $document->height = $this->height; - + return $document; } } @@ -230,11 +235,11 @@ Document::deleted(function ($document) { ->where('documents.path', '=', $document->path) ->where('documents.disk', '=', $document->disk) ->count(); - + if(!$same_path_count){ $document->getDisk()->delete($document->path); } - + if($document->preview){ $same_preview_count = DB::table('documents') ->where('documents.account_id', '=', $document->account_id) @@ -245,5 +250,5 @@ Document::deleted(function ($document) { $document->getDisk()->delete($document->preview); } } - -}); \ No newline at end of file + +}); diff --git a/app/Ninja/Repositories/DocumentRepository.php b/app/Ninja/Repositories/DocumentRepository.php index a2278b1843ae..0724144a7f09 100644 --- a/app/Ninja/Repositories/DocumentRepository.php +++ b/app/Ninja/Repositories/DocumentRepository.php @@ -57,8 +57,9 @@ class DocumentRepository extends BaseRepository return $query; } - public function upload($uploaded, &$doc_array=null) + public function upload($data, &$doc_array=null) { + $uploaded = $data['file']; $extension = strtolower($uploaded->getClientOriginalExtension()); if(empty(Document::$types[$extension]) && !empty(Document::$extraExtensions[$extension])){ $documentType = Document::$extraExtensions[$extension]; @@ -81,12 +82,17 @@ class DocumentRepository extends BaseRepository return 'File too large'; } - + // don't allow a document to be linked to both an invoice and an expense + if (array_get($data, 'invoice_id') && array_get($data, 'expense_id')) { + unset($data['expense_id']); + } $hash = sha1_file($filePath); $filename = \Auth::user()->account->account_key.'/'.$hash.'.'.$documentType; $document = Document::createNew(); + $document->fill($data); + $disk = $document->getDisk(); if(!$disk->exists($filename)){// Have we already stored the same file $stream = fopen($filePath, 'r'); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index e7587e48b11a..79e0f5dc032d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -29,8 +29,8 @@ class AppServiceProvider extends ServiceProvider { else{ $contents = $image; } - - return 'data:image/jpeg;base64,' . base64_encode($contents); + + return 'data:image/jpeg;base64,' . base64_encode($contents); }); Form::macro('nav_link', function($url, $text, $url2 = '', $extra = '') { @@ -58,11 +58,11 @@ class AppServiceProvider extends ServiceProvider { $str = '
  • '.trans("texts.new_$type").'
  • '; - + if ($type == ENTITY_INVOICE) { if(!empty($items))$items[] = '
  • '; $items[] = '
  • '.trans("texts.recurring_invoices").'
  • '; @@ -81,7 +81,7 @@ class AppServiceProvider extends ServiceProvider { $items[] = '
  • '.trans("texts.vendors").'
  • '; if($user->can('create', ENTITY_VENDOR))$items[] = '
  • '.trans("texts.new_vendor").'
  • '; } - + if(!empty($items)){ $str.= ''; } @@ -157,14 +157,14 @@ class AppServiceProvider extends ServiceProvider { return $str . ''; }); - + Form::macro('human_filesize', function($bytes, $decimals = 1) { $size = array('B','kB','MB','GB','TB','PB','EB','ZB','YB'); $factor = floor((strlen($bytes) - 1) / 3); if($factor == 0)$decimals=0;// There aren't fractional bytes return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . @$size[$factor]; }); - + Validator::extend('positive', function($attribute, $value, $parameters) { return Utils::parseFloat($value) >= 0; }); From 78bafbbf0971e9eef5814707452548b7203992f3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Jun 2016 12:56:15 +0300 Subject: [PATCH 205/386] Support downloading PDF through the API --- app/Http/Controllers/InvoiceApiController.php | 13 +++++++++++++ app/Http/routes.php | 1 + 2 files changed, 14 insertions(+) diff --git a/app/Http/Controllers/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php index 520b5b09acac..c508b790d4c4 100644 --- a/app/Http/Controllers/InvoiceApiController.php +++ b/app/Http/Controllers/InvoiceApiController.php @@ -358,4 +358,17 @@ class InvoiceApiController extends BaseAPIController return $this->itemResponse($invoice); } + public function download(InvoiceRequest $request) + { + $invoice = $request->entity(); + $pdfString = $invoice->getPDFString(); + + header('Content-Type: application/pdf'); + header('Content-Length: ' . strlen($pdfString)); + header('Content-disposition: attachment; filename="' . $invoice->getFileName() . '"'); + header('Cache-Control: public, must-revalidate, max-age=0'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + + return $pdfString; + } } diff --git a/app/Http/routes.php b/app/Http/routes.php index e96aebb7ea54..f4e65d0af180 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -270,6 +270,7 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() //Route::get('quotes', 'QuoteApiController@index'); //Route::resource('quotes', 'QuoteApiController'); Route::get('invoices', 'InvoiceApiController@index'); + Route::get('download/{invoice_id}', 'InvoiceApiController@download'); Route::resource('invoices', 'InvoiceApiController'); Route::get('payments', 'PaymentApiController@index'); Route::resource('payments', 'PaymentApiController'); From 671b3ba02a2d2cbd4aefe19d3826751684939dc0 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 1 Jun 2016 20:15:31 +1000 Subject: [PATCH 206/386] test --- app/Http/Controllers/DocumentAPIController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Http/Controllers/DocumentAPIController.php b/app/Http/Controllers/DocumentAPIController.php index 1845615cad0f..91ec1a87a165 100644 --- a/app/Http/Controllers/DocumentAPIController.php +++ b/app/Http/Controllers/DocumentAPIController.php @@ -34,6 +34,8 @@ class DocumentAPIController extends BaseAPIController public function store(CreateDocumentRequest $request) { + Log::info($request); + $document = $this->documentRepo->upload($request->all()); return $this->itemResponse($document); From 707334375659c4fcb0a4b5d8963a56008e359291 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 1 Jun 2016 21:35:53 +1000 Subject: [PATCH 207/386] Update DocumentTransformer.php --- app/Ninja/Transformers/DocumentTransformer.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Ninja/Transformers/DocumentTransformer.php b/app/Ninja/Transformers/DocumentTransformer.php index 4cbfa0619193..95e4b281bec4 100644 --- a/app/Ninja/Transformers/DocumentTransformer.php +++ b/app/Ninja/Transformers/DocumentTransformer.php @@ -14,6 +14,7 @@ class DocumentTransformer extends EntityTransformer 'type' => $document->type, 'invoice_id' => isset($document->invoice->public_id) ? (int) $document->invoice->public_id : null, 'expense_id' => isset($document->expense->public_id) ? (int) $document->expense->public_id : null, + 'updated_at' => $this->getTimestamp($account->updated_at), ]); } -} \ No newline at end of file +} From 8b781746fea58b550a1c6f3c41f68ce2605f007d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 1 Jun 2016 21:36:27 +1000 Subject: [PATCH 208/386] Update DocumentTransformer.php --- app/Ninja/Transformers/DocumentTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Ninja/Transformers/DocumentTransformer.php b/app/Ninja/Transformers/DocumentTransformer.php index 95e4b281bec4..4130bbd6c459 100644 --- a/app/Ninja/Transformers/DocumentTransformer.php +++ b/app/Ninja/Transformers/DocumentTransformer.php @@ -14,7 +14,7 @@ class DocumentTransformer extends EntityTransformer 'type' => $document->type, 'invoice_id' => isset($document->invoice->public_id) ? (int) $document->invoice->public_id : null, 'expense_id' => isset($document->expense->public_id) ? (int) $document->expense->public_id : null, - 'updated_at' => $this->getTimestamp($account->updated_at), + 'updated_at' => $this->getTimestamp($document->updated_at), ]); } } From 86a2dec504262d69ffa89d6b1e8add58432b7ebf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 1 Jun 2016 21:38:35 +1000 Subject: [PATCH 209/386] Bug Fixes --- app/Http/Controllers/DashboardApiController.php | 1 - app/Http/Controllers/DocumentAPIController.php | 2 -- 2 files changed, 3 deletions(-) diff --git a/app/Http/Controllers/DashboardApiController.php b/app/Http/Controllers/DashboardApiController.php index 85d24ad7b329..6d69c53f2a01 100644 --- a/app/Http/Controllers/DashboardApiController.php +++ b/app/Http/Controllers/DashboardApiController.php @@ -2,7 +2,6 @@ use Auth; use DB; -use Illuminate\Support\Facades\Log; use View; use App\Models\Activity; diff --git a/app/Http/Controllers/DocumentAPIController.php b/app/Http/Controllers/DocumentAPIController.php index 91ec1a87a165..a1eef82c703d 100644 --- a/app/Http/Controllers/DocumentAPIController.php +++ b/app/Http/Controllers/DocumentAPIController.php @@ -5,7 +5,6 @@ use App\Models\Document; use App\Ninja\Repositories\DocumentRepository; use App\Http\Requests\DocumentRequest; use App\Http\Requests\CreateDocumentRequest; -use Illuminate\Support\Facades\Log; class DocumentAPIController extends BaseAPIController { @@ -34,7 +33,6 @@ class DocumentAPIController extends BaseAPIController public function store(CreateDocumentRequest $request) { - Log::info($request); $document = $this->documentRepo->upload($request->all()); From 901464380e71253f48159cd9bae5a9a8c9ea54ba Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Jun 2016 16:39:07 +0300 Subject: [PATCH 210/386] Prevent downloading PDF if this invoice hasn't been saved --- resources/views/invoices/edit.blade.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index fa4b0178aefc..dd7b00f24126 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -524,8 +524,10 @@ {!! Former::select('invoice_design_id')->style('display:inline;width:150px;background-color:white !important')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id") !!} @endif - @if ( ! $invoice->is_recurring) - {!! Button::primary(trans('texts.download_pdf'))->withAttributes(array('onclick' => 'onDownloadClick()'))->appendIcon(Icon::create('download-alt')) !!} + @if ( $invoice->exists && ! $invoice->is_recurring) + {!! Button::primary(trans('texts.download_pdf')) + ->withAttributes(['onclick' => 'onDownloadClick()', 'id' => 'downloadPdfButton']) + ->appendIcon(Icon::create('download-alt')) !!} @endif @if ($invoice->isClientTrashed()) @@ -923,6 +925,7 @@ } $('#invoice_footer, #terms, #public_notes, #invoice_number, #invoice_date, #due_date, #start_date, #po_number, #discount, #currency_id, #invoice_design_id, #recurring, #is_amount_discount, #partial, #custom_text_value1, #custom_text_value2').change(function() { + $('#downloadPdfButton').attr('disabled', true); setTimeout(function() { refreshPDF(true); }, 1); @@ -1078,6 +1081,7 @@ if ($(event.target).hasClass('handled')) { return; } + $('#downloadPdfButton').attr('disabled', true); onItemChange(); refreshPDF(true); }); @@ -1219,7 +1223,7 @@ function onSaveClick() { if (model.invoice().is_recurring()) { // warn invoice will be emailed when saving new recurring invoice - if ({{ $invoice->exists() ? 'false' : 'true' }}) { + if ({{ $invoice->exists ? 'false' : 'true' }}) { if (confirm("{!! trans("texts.confirm_recurring_email_$entityType") !!}" + '\n\n' + getSendToEmails() + '\n' + "{!! trans("texts.confirm_recurring_timing") !!}")) { submitAction(''); } From 1bed3dbdf5c530940c0971f95f9fad5dbf0bc3ef Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Jun 2016 16:49:33 +0300 Subject: [PATCH 211/386] Display number of documents --- resources/views/invoices/edit.blade.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index dd7b00f24126..dc297bc2bfe5 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -327,7 +327,12 @@
  • {{ trans("texts.terms") }}
  • {{ trans("texts.footer") }}
  • @if ($account->hasFeature(FEATURE_DOCUMENTS)) -
  • {{ trans("texts.invoice_documents") }}
  • +
  • + {{ trans("texts.invoice_documents") }} + @if (count($invoice->documents)) + ({{ count($invoice->documents) }}) + @endif +
  • @endif From e312f00b80c145a1d1b721c93b2054d26c057880 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 1 Jun 2016 17:32:51 +0300 Subject: [PATCH 212/386] Don't refresh PDF after file uploaded --- resources/views/invoices/edit.blade.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index dc297bc2bfe5..f32cdcdc70cf 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1485,7 +1485,9 @@ file.public_id = response.document.public_id model.invoice().documents()[file.index].update(response.document); window.countUploadingDocuments--; - refreshPDF(true); + @if ($account->invoice_embed_documents) + refreshPDF(true); + @endif if(response.document.preview_url){ dropzone.emit('thumbnail', file, response.document.preview_url); } From 0acf4ff1fc7380165d24c26204c494b06ef2b02d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 2 Jun 2016 22:03:59 +0300 Subject: [PATCH 213/386] Support importing JSON file --- app/Http/Controllers/ExportController.php | 6 +- app/Http/Controllers/ImportController.php | 5 +- app/Http/Controllers/InvoiceController.php | 42 +-- app/Http/Requests/CreatePaymentRequest.php | 8 +- app/Http/routes.php | 1 + app/Listeners/SubscriptionListener.php | 7 +- app/Models/Account.php | 25 +- app/Models/EntityModel.php | 19 +- .../Import/FreshBooks/PaymentTransformer.php | 6 +- .../Import/Hiveage/PaymentTransformer.php | 6 +- .../Import/Invoiceable/PaymentTransformer.php | 6 +- .../Import/Nutcache/PaymentTransformer.php | 6 +- app/Ninja/Import/Ronin/PaymentTransformer.php | 6 +- app/Ninja/Import/Wave/PaymentTransformer.php | 8 +- app/Ninja/Import/Zoho/PaymentTransformer.php | 6 +- app/Ninja/Repositories/ClientRepository.php | 8 +- app/Ninja/Repositories/InvoiceRepository.php | 16 +- app/Providers/EventServiceProvider.php | 2 - app/Providers/RouteServiceProvider.php | 2 +- app/Services/ImportService.php | 264 +++++++++++++----- resources/lang/en/texts.php | 2 + .../views/accounts/import_export.blade.php | 8 +- resources/views/master.blade.php | 36 +-- 23 files changed, 323 insertions(+), 172 deletions(-) diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index 244b6e8aee5f..114c52a110ab 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -43,10 +43,12 @@ class ExportController extends BaseController $manager->setSerializer(new ArraySerializer()); $account = Auth::user()->account; - $account->loadAllData(); + $account->load(['clients.contacts', 'clients.invoices.payments', 'clients.invoices.invoice_items']); $resource = new Item($account, new AccountTransformer); - $data = $manager->createData($resource)->toArray(); + $data = $manager->parseIncludes('clients.invoices.payments') + ->createData($resource) + ->toArray(); return response()->json($data); } diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index fe006332ca48..3e53170ced35 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -36,8 +36,11 @@ class ImportController extends BaseController if ($source === IMPORT_CSV) { $data = $this->importService->mapCSV($files); return View::make('accounts.import_map', ['data' => $data]); + } elseif ($source === IMPORT_JSON) { + $results = $this->importService->importJSON($files[IMPORT_JSON]); + return $this->showResult($results); } else { - $results = $this->importService->import($source, $files); + $results = $this->importService->importFiles($source, $files); return $this->showResult($results); } } catch (Exception $exception) { diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 62faaca695b5..da9b90467350 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -95,9 +95,9 @@ class InvoiceController extends BaseController { $account = Auth::user()->account; $invoice = $request->entity()->load('invitations', 'account.country', 'client.contacts', 'client.country', 'invoice_items', 'documents', 'expenses', 'expenses.documents', 'payments'); - + $entityType = $invoice->getEntityType(); - + $contactIds = DB::table('invitations') ->join('contacts', 'contacts.id', '=', 'invitations.contact_id') ->where('invitations.invoice_id', '=', $invoice->id) @@ -158,12 +158,12 @@ class InvoiceController extends BaseController if (!$invoice->is_recurring && $invoice->balance > 0) { $actions[] = ['url' => 'javascript:onPaymentClick()', 'label' => trans('texts.enter_payment')]; } - + foreach ($invoice->payments as $payment) { $label = trans("texts.view_payment"); if (count($invoice->payments) > 1) { - $label .= ' - ' . $account->formatMoney($payment->amount, $invoice->client); - } + $label .= ' - ' . $account->formatMoney($payment->amount, $invoice->client); + } $actions[] = ['url' => $payment->present()->url, 'label' => $label]; } } @@ -180,7 +180,7 @@ class InvoiceController extends BaseController if(!Auth::user()->hasPermission('view_all')){ $clients = $clients->where('clients.user_id', '=', Auth::user()->id); } - + $data = array( 'clients' => $clients->get(), 'entityType' => $entityType, @@ -230,7 +230,7 @@ class InvoiceController extends BaseController public function create(InvoiceRequest $request, $clientPublicId = 0, $isRecurring = false) { $account = Auth::user()->account; - + $entityType = $isRecurring ? ENTITY_RECURRING_INVOICE : ENTITY_INVOICE; $clientId = null; @@ -240,17 +240,17 @@ class InvoiceController extends BaseController $invoice = $account->createInvoice($entityType, $clientId); $invoice->public_id = 0; - + if (Session::get('expenses')) { $invoice->expenses = Expense::scope(Session::get('expenses'))->with('documents')->get(); } - + $clients = Client::scope()->with('contacts', 'country')->orderBy('name'); if (!Auth::user()->hasPermission('view_all')) { $clients = $clients->where('clients.user_id', '=', Auth::user()->id); } - + $data = [ 'clients' => $clients->get(), 'entityType' => $invoice->getEntityType(), @@ -335,26 +335,26 @@ class InvoiceController extends BaseController $rates = TaxRate::scope()->orderBy('name')->get(); $options = []; $defaultTax = false; - + foreach ($rates as $rate) { - $options[$rate->rate . ' ' . $rate->name] = $rate->name . ' ' . ($rate->rate+0) . '%'; - + $options[$rate->rate . ' ' . $rate->name] = $rate->name . ' ' . ($rate->rate+0) . '%'; + // load default invoice tax if ($rate->id == $account->default_tax_rate_id) { $defaultTax = $rate; } - } - + } + // Check for any taxes which have been deleted if ($invoice->exists) { foreach ($invoice->getTaxes() as $key => $rate) { if (isset($options[$key])) { continue; - } + } $options[$key] = $rate['name'] . ' ' . $rate['rate'] . '%'; } } - + return [ 'data' => Input::old('data'), 'account' => Auth::user()->account->load('country'), @@ -396,10 +396,10 @@ class InvoiceController extends BaseController { $data = $request->input(); $data['documents'] = $request->file('documents'); - + $action = Input::get('action'); $entityType = Input::get('entityType'); - + $invoice = $this->invoiceService->save($data); $entityType = $invoice->getEntityType(); $message = trans("texts.created_{$entityType}"); @@ -433,7 +433,7 @@ class InvoiceController extends BaseController { $data = $request->input(); $data['documents'] = $request->file('documents'); - + $action = Input::get('action'); $entityType = Input::get('entityType'); @@ -547,7 +547,7 @@ class InvoiceController extends BaseController $clone = $this->invoiceService->convertQuote($request->entity()); Session::flash('message', trans('texts.converted_to_invoice')); - + return Redirect::to('invoices/' . $clone->public_id); } diff --git a/app/Http/Requests/CreatePaymentRequest.php b/app/Http/Requests/CreatePaymentRequest.php index d14d1ddba616..dbc830271197 100644 --- a/app/Http/Requests/CreatePaymentRequest.php +++ b/app/Http/Requests/CreatePaymentRequest.php @@ -23,14 +23,14 @@ class CreatePaymentRequest extends PaymentRequest { $input = $this->input(); $invoice = Invoice::scope($input['invoice'])->firstOrFail(); - + $rules = array( - 'client' => 'required', - 'invoice' => 'required', + 'client' => 'required', // TODO: change to client_id once views are updated + 'invoice' => 'required', // TODO: change to invoice_id once views are updated 'amount' => "required|less_than:{$invoice->balance}|positive", ); - if ($input['payment_type_id'] == PAYMENT_TYPE_CREDIT) { + if ( ! empty($input['payment_type_id']) && $input['payment_type_id'] == PAYMENT_TYPE_CREDIT) { $rules['payment_type_id'] = 'has_credit:'.$input['client'].','.$input['amount']; } diff --git a/app/Http/routes.php b/app/Http/routes.php index f4e65d0af180..f8fcbca5ffd7 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -472,6 +472,7 @@ if (!defined('CONTACT_EMAIL')) { define('DEFAULT_SEND_RECURRING_HOUR', 8); define('IMPORT_CSV', 'CSV'); + define('IMPORT_JSON', 'JSON'); define('IMPORT_FRESHBOOKS', 'FreshBooks'); define('IMPORT_WAVE', 'Wave'); define('IMPORT_RONIN', 'Ronin'); diff --git a/app/Listeners/SubscriptionListener.php b/app/Listeners/SubscriptionListener.php index 493a53f1110d..50d6f3be9240 100644 --- a/app/Listeners/SubscriptionListener.php +++ b/app/Listeners/SubscriptionListener.php @@ -3,6 +3,7 @@ use Auth; use Utils; +use App\Models\EntityModel; use App\Events\ClientWasCreated; use App\Events\QuoteWasCreated; use App\Events\InvoiceWasCreated; @@ -48,7 +49,7 @@ class SubscriptionListener public function createdCredit(CreditWasCreated $event) { - + } public function createdVendor(VendorWasCreated $event) @@ -63,6 +64,10 @@ class SubscriptionListener private function checkSubscriptions($eventId, $entity, $transformer, $include = '') { + if ( ! EntityModel::$notifySubscriptions) { + return; + } + $subscription = $entity->account->getSubscription($eventId); if ($subscription) { diff --git a/app/Models/Account.php b/app/Models/Account.php index 61526c9095d4..3248bf95814f 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -545,7 +545,7 @@ class Account extends Eloquent if ($this->hasClientNumberPattern($invoice) && !$clientId) { // do nothing, we don't yet know the value - } else { + } elseif ( ! $invoice->invoice_number) { $invoice->invoice_number = $this->getNextInvoiceNumber($invoice); } } @@ -649,7 +649,7 @@ class Account extends Eloquent return $this->getNextInvoiceNumber($invoice); } - public function getNextInvoiceNumber($invoice) + public function getNextInvoiceNumber($invoice, $validateUnique = true) { if ($this->hasNumberPattern($invoice->invoice_type_id)) { $number = $this->getNumberPattern($invoice); @@ -657,13 +657,16 @@ class Account extends Eloquent $counter = $this->getCounter($invoice->invoice_type_id); $prefix = $this->getNumberPrefix($invoice->invoice_type_id); $counterOffset = 0; + $check = false; // confirm the invoice number isn't already taken do { $number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT); - $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); - $counter++; - $counterOffset++; + if ($validateUnique) { + $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); + $counter++; + $counterOffset++; + } } while ($check); // update the invoice counter to be caught up @@ -688,7 +691,7 @@ class Account extends Eloquent public function incrementCounter($invoice) { // if they didn't use the counter don't increment it - if ($invoice->invoice_number != $this->getNextInvoiceNumber($invoice)) { + if ($invoice->invoice_number != $this->getNextInvoiceNumber($invoice, false)) { return; } @@ -1452,6 +1455,14 @@ class Account extends Eloquent } } -Account::updated(function ($account) { +Account::updated(function ($account) +{ + // prevent firing event if the invoice/quote counter was changed + // TODO: remove once counters are moved to separate table + $dirty = $account->getDirty(); + if (isset($dirty['invoice_number_counter']) || isset($dirty['quote_number_counter'])) { + return; + } + Event::fire(new UserSettingsChanged()); }); diff --git a/app/Models/EntityModel.php b/app/Models/EntityModel.php index 4b724d3953ee..4a6bffbc9e6b 100644 --- a/app/Models/EntityModel.php +++ b/app/Models/EntityModel.php @@ -9,22 +9,31 @@ class EntityModel extends Eloquent public $timestamps = true; protected $hidden = ['id']; + public static $notifySubscriptions = true; + public static function createNew($context = null) { $className = get_called_class(); $entity = new $className(); if ($context) { - $entity->user_id = $context instanceof User ? $context->id : $context->user_id; - $entity->account_id = $context->account_id; + $user = $context instanceof User ? $context : $context->user; + $account = $context->account; } elseif (Auth::check()) { - $entity->user_id = Auth::user()->id; - $entity->account_id = Auth::user()->account_id; + $user = Auth::user(); + $account = Auth::user()->account; } else { Utils::fatalError(); } - if(method_exists($className, 'withTrashed')){ + $entity->user_id = $user->id; + $entity->account_id = $account->id; + + // store references to the original user/account to prevent needing to reload them + $entity->setRelation('user', $user); + $entity->setRelation('account', $account); + + if (method_exists($className, 'withTrashed')){ $lastEntity = $className::withTrashed() ->scope(false, $entity->account_id); } else { diff --git a/app/Ninja/Import/FreshBooks/PaymentTransformer.php b/app/Ninja/Import/FreshBooks/PaymentTransformer.php index 1f69fdbacf41..eaf371f91eda 100644 --- a/app/Ninja/Import/FreshBooks/PaymentTransformer.php +++ b/app/Ninja/Import/FreshBooks/PaymentTransformer.php @@ -5,9 +5,9 @@ use League\Fractal\Resource\Item; class PaymentTransformer extends BaseTransformer { - public function transform($data, $maps) + public function transform($data) { - return new Item($data, function ($data) use ($maps) { + return new Item($data, function ($data) { return [ 'amount' => $data->paid, 'payment_date_sql' => $data->create_date, @@ -16,4 +16,4 @@ class PaymentTransformer extends BaseTransformer ]; }); } -} \ No newline at end of file +} diff --git a/app/Ninja/Import/Hiveage/PaymentTransformer.php b/app/Ninja/Import/Hiveage/PaymentTransformer.php index d6232d05bcc9..e7e4befb5714 100644 --- a/app/Ninja/Import/Hiveage/PaymentTransformer.php +++ b/app/Ninja/Import/Hiveage/PaymentTransformer.php @@ -5,9 +5,9 @@ use League\Fractal\Resource\Item; class PaymentTransformer extends BaseTransformer { - public function transform($data, $maps) + public function transform($data) { - return new Item($data, function ($data) use ($maps) { + return new Item($data, function ($data) { return [ 'amount' => $data->paid_total, 'payment_date_sql' => $this->getDate($data->last_paid_on), @@ -16,4 +16,4 @@ class PaymentTransformer extends BaseTransformer ]; }); } -} \ No newline at end of file +} diff --git a/app/Ninja/Import/Invoiceable/PaymentTransformer.php b/app/Ninja/Import/Invoiceable/PaymentTransformer.php index c52494cdc689..ea2310c01ada 100644 --- a/app/Ninja/Import/Invoiceable/PaymentTransformer.php +++ b/app/Ninja/Import/Invoiceable/PaymentTransformer.php @@ -5,9 +5,9 @@ use League\Fractal\Resource\Item; class PaymentTransformer extends BaseTransformer { - public function transform($data, $maps) + public function transform($data) { - return new Item($data, function ($data) use ($maps) { + return new Item($data, function ($data) { return [ 'amount' => $data->paid, 'payment_date_sql' => $data->date_paid, @@ -16,4 +16,4 @@ class PaymentTransformer extends BaseTransformer ]; }); } -} \ No newline at end of file +} diff --git a/app/Ninja/Import/Nutcache/PaymentTransformer.php b/app/Ninja/Import/Nutcache/PaymentTransformer.php index 04e783361f80..9434e274fc05 100644 --- a/app/Ninja/Import/Nutcache/PaymentTransformer.php +++ b/app/Ninja/Import/Nutcache/PaymentTransformer.php @@ -5,9 +5,9 @@ use League\Fractal\Resource\Item; class PaymentTransformer extends BaseTransformer { - public function transform($data, $maps) + public function transform($data) { - return new Item($data, function ($data) use ($maps) { + return new Item($data, function ($data) { return [ 'amount' => (float) $data->paid_to_date, 'payment_date_sql' => $this->getDate($data->date), @@ -16,4 +16,4 @@ class PaymentTransformer extends BaseTransformer ]; }); } -} \ No newline at end of file +} diff --git a/app/Ninja/Import/Ronin/PaymentTransformer.php b/app/Ninja/Import/Ronin/PaymentTransformer.php index c04101456200..b797d3f672f3 100644 --- a/app/Ninja/Import/Ronin/PaymentTransformer.php +++ b/app/Ninja/Import/Ronin/PaymentTransformer.php @@ -5,9 +5,9 @@ use League\Fractal\Resource\Item; class PaymentTransformer extends BaseTransformer { - public function transform($data, $maps) + public function transform($data) { - return new Item($data, function ($data) use ($maps) { + return new Item($data, function ($data) { return [ 'amount' => (float) $data->total - (float) $data->balance, 'payment_date_sql' => $data->date_paid, @@ -16,4 +16,4 @@ class PaymentTransformer extends BaseTransformer ]; }); } -} \ No newline at end of file +} diff --git a/app/Ninja/Import/Wave/PaymentTransformer.php b/app/Ninja/Import/Wave/PaymentTransformer.php index 522fe8ff9238..e809f0d3b408 100644 --- a/app/Ninja/Import/Wave/PaymentTransformer.php +++ b/app/Ninja/Import/Wave/PaymentTransformer.php @@ -5,13 +5,13 @@ use League\Fractal\Resource\Item; class PaymentTransformer extends BaseTransformer { - public function transform($data, $maps) + public function transform($data) { if ( ! $this->getInvoiceClientId($data->invoice_num)) { return false; } - - return new Item($data, function ($data) use ($maps) { + + return new Item($data, function ($data) { return [ 'amount' => (float) $data->amount, 'payment_date_sql' => $this->getDate($data->payment_date), @@ -20,4 +20,4 @@ class PaymentTransformer extends BaseTransformer ]; }); } -} \ No newline at end of file +} diff --git a/app/Ninja/Import/Zoho/PaymentTransformer.php b/app/Ninja/Import/Zoho/PaymentTransformer.php index a8fc74962321..0f9ad8bad891 100644 --- a/app/Ninja/Import/Zoho/PaymentTransformer.php +++ b/app/Ninja/Import/Zoho/PaymentTransformer.php @@ -5,9 +5,9 @@ use League\Fractal\Resource\Item; class PaymentTransformer extends BaseTransformer { - public function transform($data, $maps) + public function transform($data) { - return new Item($data, function ($data) use ($maps) { + return new Item($data, function ($data) { return [ 'amount' => (float) $data->total - (float) $data->balance, 'payment_date_sql' => $data->last_payment_date, @@ -16,4 +16,4 @@ class PaymentTransformer extends BaseTransformer ]; }); } -} \ No newline at end of file +} diff --git a/app/Ninja/Repositories/ClientRepository.php b/app/Ninja/Repositories/ClientRepository.php index bb56d944e6fc..cf2ad12daf39 100644 --- a/app/Ninja/Repositories/ClientRepository.php +++ b/app/Ninja/Repositories/ClientRepository.php @@ -118,9 +118,11 @@ class ClientRepository extends BaseRepository $first = false; } - foreach ($client->contacts as $contact) { - if (!in_array($contact->public_id, $contactIds)) { - $contact->delete(); + if ( ! $client->wasRecentlyCreated) { + foreach ($client->contacts as $contact) { + if (!in_array($contact->public_id, $contactIds)) { + $contact->delete(); + } } } diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index c15e261848d0..d79c15f24c08 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -494,13 +494,15 @@ class InvoiceRepository extends BaseRepository } } - foreach ($invoice->documents as $document){ - if(!in_array($document->public_id, $document_ids)){ - // Removed - // Not checking permissions; deleting a document is just editing the invoice - if($document->invoice_id == $invoice->id){ - // Make sure the document isn't on a clone - $document->delete(); + if ( ! $invoice->wasRecentlyCreated) { + foreach ($invoice->documents as $document){ + if(!in_array($document->public_id, $document_ids)){ + // Removed + // Not checking permissions; deleting a document is just editing the invoice + if($document->invoice_id == $invoice->id){ + // Make sure the document isn't on a clone + $document->delete(); + } } } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index b19594dacb87..74ff7ac78fc3 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -11,7 +11,6 @@ class EventServiceProvider extends ServiceProvider { * @var array */ protected $listen = [ - // Clients 'App\Events\ClientWasCreated' => [ 'App\Listeners\ActivityListener@createdClient', @@ -151,7 +150,6 @@ class EventServiceProvider extends ServiceProvider { 'App\Events\UserSettingsChanged' => [ 'App\Listeners\HandleUserSettingsChanged', ], - ]; /** diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 506179d6d4e3..11d3c8005903 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -24,7 +24,7 @@ class RouteServiceProvider extends ServiceProvider { { parent::boot($router); - + } diff --git a/app/Services/ImportService.php b/app/Services/ImportService.php index 61eb0a1881f9..147a63cdeb08 100644 --- a/app/Services/ImportService.php +++ b/app/Services/ImportService.php @@ -17,6 +17,7 @@ use App\Ninja\Repositories\ProductRepository; use App\Ninja\Serializers\ArraySerializer; use App\Models\Client; use App\Models\Invoice; +use App\Models\EntityModel; class ImportService { @@ -25,9 +26,13 @@ class ImportService protected $clientRepo; protected $contactRepo; protected $productRepo; - protected $processedRows = array(); + protected $processedRows = []; + + private $maps = []; + public $results = []; public static $entityTypes = [ + IMPORT_JSON, ENTITY_CLIENT, ENTITY_CONTACT, ENTITY_INVOICE, @@ -39,6 +44,7 @@ class ImportService public static $sources = [ IMPORT_CSV, + IMPORT_JSON, IMPORT_FRESHBOOKS, //IMPORT_HARVEST, IMPORT_HIVEAGE, @@ -68,10 +74,70 @@ class ImportService $this->productRepo = $productRepo; } - public function import($source, $files) + public function importJSON($file) + { + $this->init(); + + $file = file_get_contents($file); + $json = json_decode($file, true); + $json = $this->removeIdFields($json); + + $this->checkClientCount(count($json['clients'])); + + foreach ($json['clients'] as $jsonClient) { + + if ($this->validate($jsonClient, ENTITY_CLIENT) === true) { + $client = $this->clientRepo->save($jsonClient); + $this->addSuccess($client); + } else { + $this->addFailure(ENTITY_CLIENT, $jsonClient); + continue; + } + + foreach ($jsonClient['invoices'] as $jsonInvoice) { + $jsonInvoice['client_id'] = $client->id; + if ($this->validate($jsonInvoice, ENTITY_INVOICE) === true) { + $invoice = $this->invoiceRepo->save($jsonInvoice); + $this->addSuccess($invoice); + } else { + $this->addFailure(ENTITY_INVOICE, $jsonInvoice); + continue; + } + + foreach ($jsonInvoice['payments'] as $jsonPayment) { + $jsonPayment['client_id'] = $jsonPayment['client'] = $client->id; // TODO: change to client_id once views are updated + $jsonPayment['invoice_id'] = $jsonPayment['invoice'] = $invoice->id; // TODO: change to invoice_id once views are updated + if ($this->validate($jsonPayment, ENTITY_PAYMENT) === true) { + $payment = $this->paymentRepo->save($jsonPayment); + $this->addSuccess($payment); + } else { + $this->addFailure(ENTITY_PAYMENT, $jsonPayment); + continue; + } + } + } + } + + return $this->results; + } + + public function removeIdFields($array) + { + foreach ($array as $key => $val) { + if (is_array($val)) { + $array[$key] = $this->removeIdFields($val); + } elseif ($key === 'id') { + unset($array[$key]); + } + } + return $array; + } + + public function importFiles($source, $files) { $results = []; $imported_files = null; + $this->initMaps(); foreach ($files as $entityType => $file) { $results[$entityType] = $this->execute($source, $entityType, $file); @@ -89,12 +155,12 @@ class ImportService // Convert the data $row_list = array(); - $maps = $this->createMaps(); - Excel::load($file, function ($reader) use ($source, $entityType, $maps, &$row_list, &$results) { + + Excel::load($file, function ($reader) use ($source, $entityType, &$row_list, &$results) { $this->checkData($entityType, count($reader->all())); - $reader->each(function ($row) use ($source, $entityType, $maps, &$row_list, &$results) { - $data_index = $this->transformRow($source, $entityType, $row, $maps); + $reader->each(function ($row) use ($source, $entityType, &$row_list, &$results) { + $data_index = $this->transformRow($source, $entityType, $row); if ($data_index !== false) { if ($data_index !== true) { @@ -109,7 +175,7 @@ class ImportService // Save the data foreach ($row_list as $row_data) { - $result = $this->saveData($source, $entityType, $row_data['row'], $row_data['data_index'], $maps); + $result = $this->saveData($source, $entityType, $row_data['row'], $row_data['data_index']); if ($result) { $results[RESULT_SUCCESS][] = $result; } else { @@ -120,10 +186,10 @@ class ImportService return $results; } - private function transformRow($source, $entityType, $row, $maps) + private function transformRow($source, $entityType, $row) { - $transformer = $this->getTransformer($source, $entityType, $maps); - $resource = $transformer->transform($row, $maps); + $transformer = $this->getTransformer($source, $entityType, $this->maps); + $resource = $transformer->transform($row); if (!$resource) { return false; @@ -138,7 +204,7 @@ class ImportService $data['invoice_number'] = $account->getNextInvoiceNumber($invoice); } - if ($this->validate($source, $data, $entityType) !== true) { + if ($this->validate($data, $entityType) !== true) { return false; } @@ -160,14 +226,18 @@ class ImportService return key($this->processedRows); } - private function saveData($source, $entityType, $row, $data_index, $maps) + private function saveData($source, $entityType, $row, $data_index) { $data = $this->processedRows[$data_index]; $entity = $this->{"{$entityType}Repo"}->save($data); + // update the entity maps + $mapFunction = 'add' . ucwords($entity->getEntityType()) . 'ToMaps'; + $this->$mapFunction($entity); + // if the invoice is paid we'll also create a payment record if ($entityType === ENTITY_INVOICE && isset($data['paid']) && $data['paid'] > 0) { - $this->createPayment($source, $row, $maps, $data['client_id'], $entity->id); + $this->createPayment($source, $row, $data['client_id'], $entity->id); } return $entity; @@ -200,21 +270,22 @@ class ImportService return new $className($maps); } - private function createPayment($source, $data, $maps, $clientId, $invoiceId) + private function createPayment($source, $data, $clientId, $invoiceId) { - $paymentTransformer = $this->getTransformer($source, ENTITY_PAYMENT, $maps); + $paymentTransformer = $this->getTransformer($source, ENTITY_PAYMENT, $this->maps); $data->client_id = $clientId; $data->invoice_id = $invoiceId; - if ($resource = $paymentTransformer->transform($data, $maps)) { + if ($resource = $paymentTransformer->transform($data)) { $data = $this->fractal->createData($resource)->toArray(); $this->paymentRepo->save($data); } } - private function validate($source, $data, $entityType) + private function validate($data, $entityType) { + /* // Harvest's contacts are listed separately if ($entityType === ENTITY_CLIENT && $source != IMPORT_HARVEST) { $rules = [ @@ -234,71 +305,21 @@ class ImportService 'product_key' => 'required', ]; } + */ + $requestClass = 'App\\Http\\Requests\\Create' . ucwords($entityType) . 'Request'; + $request = new $requestClass(); + $request->setUserResolver(function() { return Auth::user(); }); + $request->replace($data); - $validator = Validator::make($data, $rules); + $validator = Validator::make($data, $request->rules()); if ($validator->fails()) { - $messages = $validator->messages(); - - return $messages->first(); + return $validator->messages()->first(); } else { return true; } } - private function createMaps() - { - $clientMap = []; - $clients = $this->clientRepo->all(); - foreach ($clients as $client) { - if ($name = strtolower(trim($client->name))) { - $clientMap[$name] = $client->id; - } - } - - $invoiceMap = []; - $invoiceClientMap = []; - $invoices = $this->invoiceRepo->all(); - foreach ($invoices as $invoice) { - if ($number = strtolower(trim($invoice->invoice_number))) { - $invoiceMap[$number] = $invoice->id; - $invoiceClientMap[$number] = $invoice->client_id; - } - } - - $productMap = []; - $products = $this->productRepo->all(); - foreach ($products as $product) { - if ($key = strtolower(trim($product->product_key))) { - $productMap[$key] = $product->id; - } - } - - $countryMap = []; - $countryMap2 = []; - $countries = Cache::get('countries'); - foreach ($countries as $country) { - $countryMap[strtolower($country->name)] = $country->id; - $countryMap2[strtolower($country->iso_3166_2)] = $country->id; - } - - $currencyMap = []; - $currencies = Cache::get('currencies'); - foreach ($currencies as $currency) { - $currencyMap[strtolower($currency->code)] = $currency->id; - } - - return [ - ENTITY_CLIENT => $clientMap, - ENTITY_INVOICE => $invoiceMap, - ENTITY_INVOICE.'_'.ENTITY_CLIENT => $invoiceClientMap, - ENTITY_PRODUCT => $productMap, - 'countries' => $countryMap, - 'countries2' => $countryMap2, - 'currencies' => $currencyMap, - ]; - } - public function mapCSV($files) { $data = []; @@ -430,7 +451,7 @@ class ImportService $data = Session::get("{$entityType}-data"); $this->checkData($entityType, count($data)); - $maps = $this->createMaps(); + $this->initMaps(); // Convert the data $row_list = array(); @@ -441,7 +462,7 @@ class ImportService } $row = $this->convertToObject($entityType, $row, $map); - $data_index = $this->transformRow($source, $entityType, $row, $maps); + $data_index = $this->transformRow($source, $entityType, $row); if ($data_index !== false) { if ($data_index !== true) { @@ -455,7 +476,7 @@ class ImportService // Save the data foreach ($row_list as $row_data) { - $result = $this->saveData($source, $entityType, $row_data['row'], $row_data['data_index'], $maps); + $result = $this->saveData($source, $entityType, $row_data['row'], $row_data['data_index']); if ($result) { $results[RESULT_SUCCESS][] = $result; @@ -493,4 +514,93 @@ class ImportService return $obj; } + + private function addSuccess($entity) + { + $this->results[$entity->getEntityType()][RESULT_SUCCESS][] = $entity; + } + + private function addFailure($entityType, $data) + { + $this->results[$entityType][RESULT_FAILURE][] = $data; + } + + private function init() + { + EntityModel::$notifySubscriptions = false; + + foreach ([ENTITY_CLIENT, ENTITY_INVOICE, ENTITY_PAYMENT] as $entityType) { + $this->results[$entityType] = [ + RESULT_SUCCESS => [], + RESULT_FAILURE => [], + ]; + } + } + + private function initMaps() + { + $this->init(); + + $this->maps = [ + 'client' => [], + 'invoice' => [], + 'invoice_client' => [], + 'product' => [], + 'countries' => [], + 'countries2' => [], + 'currencies' => [], + 'client_ids' => [], + 'invoice_ids' => [], + ]; + + $clients = $this->clientRepo->all(); + foreach ($clients as $client) { + $this->addClientToMaps($client); + } + + $invoices = $this->invoiceRepo->all(); + foreach ($invoices as $invoice) { + $this->addInvoiceToMaps($invoice); + } + + $products = $this->productRepo->all(); + foreach ($products as $product) { + $this->addProductToMaps($product); + } + + $countries = Cache::get('countries'); + foreach ($countries as $country) { + $this->maps['countries'][strtolower($country->name)] = $country->id; + $this->maps['countries2'][strtolower($country->iso_3166_2)] = $country->id; + } + + $currencies = Cache::get('currencies'); + foreach ($currencies as $currency) { + $this->maps['currencies'][strtolower($currency->code)] = $currency->id; + } + } + + private function addInvoiceToMaps($invoice) + { + if ($number = strtolower(trim($invoice->invoice_number))) { + $this->maps['invoice'][$number] = $invoice->id; + $this->maps['invoice_client'][$number] = $invoice->client_id; + $this->maps['invoice_ids'][$invoice->public_id] = $invoice->id; + } + } + + private function addClientToMaps($client) + { + if ($name = strtolower(trim($client->name))) { + $this->maps['client'][$name] = $client->id; + $this->maps['client_ids'][$client->public_id] = $client->id; + } + } + + private function addProductToMaps($product) + { + if ($key = strtolower(trim($product->product_key))) { + $this->maps['product'][$key] = $product->id; + } + } } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index b96ed265e9d3..6d4fcdba26ec 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1321,6 +1321,8 @@ $LANG = array( 'products_will_create' => 'products will be created.', 'product_key' => 'Product', 'created_products' => 'Successfully created :count product(s)', + 'export_help' => 'Use JSON if you plan to import the data into Invoice Ninja.', + 'JSON_file' => 'JSON File', ); diff --git a/resources/views/accounts/import_export.blade.php b/resources/views/accounts/import_export.blade.php index b0f2fd5d7c26..93acd555e801 100644 --- a/resources/views/accounts/import_export.blade.php +++ b/resources/views/accounts/import_export.blade.php @@ -53,7 +53,8 @@ ->addOption('CSV', 'CSV') ->addOption('XLS', 'XLS') ->addOption('JSON', 'JSON') - ->style('max-width: 200px') !!} + ->style('max-width: 200px') + ->inlineHelp('export_help') !!} {!! Former::checkbox('entity_types') ->label('include') @@ -100,6 +101,11 @@ @endif @endforeach } + @if ($source === IMPORT_JSON) + if (val === '{{ $source }}') { + $('.JSON-file').show(); + } + @endif @endforeach } diff --git a/resources/views/master.blade.php b/resources/views/master.blade.php index 8cd3bbb034e4..463be8be0eb7 100644 --- a/resources/views/master.blade.php +++ b/resources/views/master.blade.php @@ -4,12 +4,12 @@ @if (isset($account) && $account instanceof \App\Models\Account && $account->hasFeature(FEATURE_WHITE_LABEL)) {{ trans('texts.client_portal') }} @else - {{ isset($title) ? ($title . ' | Invoice Ninja') : ('Invoice Ninja | ' . trans('texts.app_title')) }} + {{ isset($title) ? ($title . ' | Invoice Ninja') : ('Invoice Ninja | ' . trans('texts.app_title')) }} @endif - + @@ -22,24 +22,24 @@ - + - + @@ -132,7 +132,7 @@ - @if (isset($_ENV['TAG_MANAGER_KEY']) && $_ENV['TAG_MANAGER_KEY']) + @if (isset($_ENV['TAG_MANAGER_KEY']) && $_ENV['TAG_MANAGER_KEY']) @@ -140,20 +140,20 @@ new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= '//www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); - })(window,document,'script','dataLayer','{{ $_ENV['TAG_MANAGER_KEY'] }}'); + })(window,document,'script','dataLayer','{{ $_ENV['TAG_MANAGER_KEY'] }}'); - @elseif (isset($_ENV['ANALYTICS_KEY']) && $_ENV['ANALYTICS_KEY']) + @elseif (isset($_ENV['ANALYTICS_KEY']) && $_ENV['ANALYTICS_KEY']) @endif - + @yield('body') + From dafb867dc3a7fe3c3351de13f9f9624ef0e9810d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 2 Jun 2016 22:05:29 +0300 Subject: [PATCH 214/386] Updated link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3133c4c1010..d2f06ed9fa65 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

    # Invoice Ninja -### [http://www.invoiceninja.org](http://www.invoiceninja.org) +### [https://invoiceninja.org](https://invoiceninja.org) [![Build Status](https://travis-ci.org/invoiceninja/invoiceninja.svg?branch=develop)](https://travis-ci.org/invoiceninja/invoiceninja) [![Join the chat at https://gitter.im/hillelcoren/invoice-ninja](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hillelcoren/invoice-ninja?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) From d3372680cf773e8369c8b2a314f17b583cb00730 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 3 Jun 2016 11:17:55 +0300 Subject: [PATCH 215/386] Include archived records in JSON export --- app/Http/Controllers/ExportController.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index 114c52a110ab..d38f492486ae 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -42,8 +42,17 @@ class ExportController extends BaseController $manager = new Manager(); $manager->setSerializer(new ArraySerializer()); + // eager load data, include archived but exclude deleted $account = Auth::user()->account; - $account->load(['clients.contacts', 'clients.invoices.payments', 'clients.invoices.invoice_items']); + $account->load(['clients' => function($query) { + $query->withArchived() + ->with(['contacts', 'invoices' => function($query) { + $query->withArchived() + ->with(['invoice_items', 'payments' => function($query) { + $query->withArchived(); + }]); + }]); + }]); $resource = new Item($account, new AccountTransformer); $data = $manager->parseIncludes('clients.invoices.payments') From 5cda4c0dd96beccf2f229656972db5088ae0f513 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Fri, 3 Jun 2016 11:29:03 +0300 Subject: [PATCH 216/386] Fix capitalization of migration file name --- database/migrations/2016_05_24_164847_wepay_ach.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2016_05_24_164847_wepay_ach.php b/database/migrations/2016_05_24_164847_wepay_ach.php index cb1b79433321..088e9225d027 100644 --- a/database/migrations/2016_05_24_164847_wepay_ach.php +++ b/database/migrations/2016_05_24_164847_wepay_ach.php @@ -3,7 +3,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -class WePayAch extends Migration +class WepayAch extends Migration { /** * Run the migrations. From 37a92fc853914b3ab98fcf6557051a19ed8aee77 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sat, 4 Jun 2016 22:11:46 +0300 Subject: [PATCH 217/386] Fix for online payments page --- resources/views/accounts/account_gateway.blade.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index f195d2849ab8..af70957e2d3e 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -147,7 +147,8 @@ ->class('creditcard-types') ->addGroupClass('gateway-option') !!} - @if(isset($accountGateway) && $accountGateway->gateway_id == GATEWAY_WEPAY) + + @if(!isset($accountGateway) && $accountGateway->gateway_id == GATEWAY_WEPAY) @if ($account->getGatewayByType(PAYMENT_TYPE_DIRECT_DEBIT, $accountGateway)) {!! Former::checkbox('enable_ach') ->label(trans('texts.ach')) @@ -161,7 +162,7 @@ ->text(trans('texts.enable_ach')) !!} @endif - @elseif(!isset($accountGateway) || $accountGateway->gateway_id == GATEWAY_STRIPE) + @elseif(!isset($accountGateway) && $accountGateway->gateway_id == GATEWAY_STRIPE)
    @if ($account->getGatewayByType(PAYMENT_TYPE_DIRECT_DEBIT, isset($accountGateway)?$accountGateway:null)) {!! Former::checkbox('enable_ach') From 73a1c6a6e3839472bc088161dfb3f010b45a3078 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sat, 4 Jun 2016 23:02:18 +0300 Subject: [PATCH 218/386] Fix for error in Authenticate.php --- app/Http/Middleware/Authenticate.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 1b38969fe929..e5135eeaa7fd 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -18,7 +18,7 @@ class Authenticate { public function handle($request, Closure $next, $guard = 'user') { $authenticated = Auth::guard($guard)->check(); - + if($guard=='client'){ if(!empty($request->invitation_key)){ $contact_key = session('contact_key'); @@ -33,7 +33,7 @@ class Authenticate { ]); } - if ($contact->id != $invitation->contact_id) { + if ($contact && $contact->id != $invitation->contact_id) { // This is a different client; reauthenticate $authenticated = false; Auth::guard($guard)->logout(); @@ -64,17 +64,17 @@ class Authenticate { // This is an admin; let them pretend to be a client $authenticated = true; } - + // Does this account require portal passwords? if($account && (!$account->enable_portal_password || !$account->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD))){ $authenticated = true; } - + if(!$authenticated && $contact && !$contact->password){ $authenticated = true; } } - + if (!$authenticated) { if ($request->ajax()) @@ -89,7 +89,7 @@ class Authenticate { return $next($request); } - + protected function getInvitation($key){ $invitation = Invitation::withTrashed()->where('invitation_key', '=', $key)->first(); if ($invitation && !$invitation->is_deleted) { From e0d512dab751682b1162c3314a71afcf3b37195c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sat, 4 Jun 2016 23:20:01 +0300 Subject: [PATCH 219/386] Remove individual languages from migration files --- ...2014_03_19_201454_add_language_support.php | 20 +++++++++---------- ...14_10_14_225227_add_danish_translation.php | 6 +++--- .../2015_04_12_093447_add_sv_language.php | 8 ++++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/database/migrations/2014_03_19_201454_add_language_support.php b/database/migrations/2014_03_19_201454_add_language_support.php index cdb092a9a498..97bb13fc5bb8 100644 --- a/database/migrations/2014_03_19_201454_add_language_support.php +++ b/database/migrations/2014_03_19_201454_add_language_support.php @@ -15,18 +15,18 @@ class AddLanguageSupport extends Migration { Schema::create('languages', function($table) { $table->increments('id'); - $table->string('name'); - $table->string('locale'); + $table->string('name'); + $table->string('locale'); }); - DB::table('languages')->insert(['name' => 'English', 'locale' => 'en']); - DB::table('languages')->insert(['name' => 'Italian', 'locale' => 'it']); - DB::table('languages')->insert(['name' => 'German', 'locale' => 'de']); - DB::table('languages')->insert(['name' => 'French', 'locale' => 'fr']); - DB::table('languages')->insert(['name' => 'Brazilian Portuguese', 'locale' => 'pt_BR']); - DB::table('languages')->insert(['name' => 'Dutch', 'locale' => 'nl']); - DB::table('languages')->insert(['name' => 'Spanish', 'locale' => 'es']); - DB::table('languages')->insert(['name' => 'Norwegian', 'locale' => 'nb_NO']); + //DB::table('languages')->insert(['name' => 'English', 'locale' => 'en']); + //DB::table('languages')->insert(['name' => 'Italian', 'locale' => 'it']); + //DB::table('languages')->insert(['name' => 'German', 'locale' => 'de']); + //DB::table('languages')->insert(['name' => 'French', 'locale' => 'fr']); + //DB::table('languages')->insert(['name' => 'Brazilian Portuguese', 'locale' => 'pt_BR']); + //DB::table('languages')->insert(['name' => 'Dutch', 'locale' => 'nl']); + //DB::table('languages')->insert(['name' => 'Spanish', 'locale' => 'es']); + //DB::table('languages')->insert(['name' => 'Norwegian', 'locale' => 'nb_NO']); Schema::table('accounts', function($table) { diff --git a/database/migrations/2014_10_14_225227_add_danish_translation.php b/database/migrations/2014_10_14_225227_add_danish_translation.php index 226ac9b96126..6da361b2a35b 100644 --- a/database/migrations/2014_10_14_225227_add_danish_translation.php +++ b/database/migrations/2014_10_14_225227_add_danish_translation.php @@ -12,7 +12,7 @@ class AddDanishTranslation extends Migration { */ public function up() { - DB::table('languages')->insert(['name' => 'Danish', 'locale' => 'da']); + //DB::table('languages')->insert(['name' => 'Danish', 'locale' => 'da']); } /** @@ -22,8 +22,8 @@ class AddDanishTranslation extends Migration { */ public function down() { - $language = \App\Models\Language::whereLocale('da')->first(); - $language->delete(); + //$language = \App\Models\Language::whereLocale('da')->first(); + //$language->delete(); } } diff --git a/database/migrations/2015_04_12_093447_add_sv_language.php b/database/migrations/2015_04_12_093447_add_sv_language.php index ed11361ac1ca..fc3342f727c8 100644 --- a/database/migrations/2015_04_12_093447_add_sv_language.php +++ b/database/migrations/2015_04_12_093447_add_sv_language.php @@ -12,10 +12,10 @@ class AddSvLanguage extends Migration { */ public function up() { - DB::table('languages')->insert(['name' => 'Swedish', 'locale' => 'sv']); - DB::table('languages')->insert(['name' => 'Spanish - Spain', 'locale' => 'es_ES']); - DB::table('languages')->insert(['name' => 'French - Canada', 'locale' => 'fr_CA']); - DB::table('languages')->insert(['name' => 'Lithuanian', 'locale' => 'lt']); + //DB::table('languages')->insert(['name' => 'Swedish', 'locale' => 'sv']); + //DB::table('languages')->insert(['name' => 'Spanish - Spain', 'locale' => 'es_ES']); + //DB::table('languages')->insert(['name' => 'French - Canada', 'locale' => 'fr_CA']); + //DB::table('languages')->insert(['name' => 'Lithuanian', 'locale' => 'lt']); } /** From 0c16d8954021b36cef2e8058ace38fe735b5d9da Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sat, 4 Jun 2016 23:22:05 +0300 Subject: [PATCH 220/386] Updated grandfathered user date --- app/Http/routes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index 0db6eaaa6d2d..d215755ba940 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -761,7 +761,7 @@ if (!defined('CONTACT_EMAIL')) { define('FEATURE_USER_PERMISSIONS', 'user_permissions'); // Pro users who started paying on or before this date will be able to manage users - define('PRO_USERS_GRANDFATHER_DEADLINE', '2016-05-15'); + define('PRO_USERS_GRANDFATHER_DEADLINE', '2016-06-04'); // WePay define('WEPAY_PRODUCTION', 'production'); From 1617be248518b953c647de20b77d729020a62c35 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 5 Jun 2016 20:00:21 +1000 Subject: [PATCH 221/386] refactoring documentAPI --- app/Http/Controllers/DocumentAPIController.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/DocumentAPIController.php b/app/Http/Controllers/DocumentAPIController.php index a1eef82c703d..e80c0299b03e 100644 --- a/app/Http/Controllers/DocumentAPIController.php +++ b/app/Http/Controllers/DocumentAPIController.php @@ -21,7 +21,10 @@ class DocumentAPIController extends BaseAPIController public function index() { - //stub + $documents = Document::scope()->get(); + + return $this->itemResponse($document); + } public function show(DocumentRequest $request) From 8ff41dfc7ee778b3e9625b1fd917ab727f608fe8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 5 Jun 2016 20:12:21 +1000 Subject: [PATCH 222/386] refactoring documentAPI --- app/Http/Controllers/DocumentAPIController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/DocumentAPIController.php b/app/Http/Controllers/DocumentAPIController.php index e80c0299b03e..ee00bf6e049f 100644 --- a/app/Http/Controllers/DocumentAPIController.php +++ b/app/Http/Controllers/DocumentAPIController.php @@ -23,7 +23,7 @@ class DocumentAPIController extends BaseAPIController { $documents = Document::scope()->get(); - return $this->itemResponse($document); + return $this->itemResponse($documents); } From d5c2fc81bd777f8c51344867a2f919a953f75174 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 5 Jun 2016 20:13:38 +1000 Subject: [PATCH 223/386] refactoring documentAPI --- app/Http/Controllers/DocumentAPIController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/DocumentAPIController.php b/app/Http/Controllers/DocumentAPIController.php index ee00bf6e049f..703209978382 100644 --- a/app/Http/Controllers/DocumentAPIController.php +++ b/app/Http/Controllers/DocumentAPIController.php @@ -23,7 +23,7 @@ class DocumentAPIController extends BaseAPIController { $documents = Document::scope()->get(); - return $this->itemResponse($documents); + return $this->listResponse($documents); } From d27219a6a0b17fa153159ea8f5aca771654c4d58 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 5 Jun 2016 20:15:49 +1000 Subject: [PATCH 224/386] refactoring documentAPI --- app/Http/Controllers/DocumentAPIController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/DocumentAPIController.php b/app/Http/Controllers/DocumentAPIController.php index 703209978382..442b3923ccd2 100644 --- a/app/Http/Controllers/DocumentAPIController.php +++ b/app/Http/Controllers/DocumentAPIController.php @@ -21,7 +21,7 @@ class DocumentAPIController extends BaseAPIController public function index() { - $documents = Document::scope()->get(); + $documents = Document::scope(); return $this->listResponse($documents); From 8b2f7eb39fe182ff2e92f6ac5ad257bae72ddc5b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 5 Jun 2016 18:50:41 +0300 Subject: [PATCH 225/386] Check for blank api secret --- app/Http/Middleware/ApiCheck.php | 5 ++++- app/Http/routes.php | 4 ++-- app/Models/Document.php | 5 +---- resources/views/expenses/edit.blade.php | 4 ++-- resources/views/invoices/edit.blade.php | 3 +-- resources/views/master.blade.php | 13 +++++++++++++ 6 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/Http/Middleware/ApiCheck.php b/app/Http/Middleware/ApiCheck.php index 5200c3264a96..524b718cc44f 100644 --- a/app/Http/Middleware/ApiCheck.php +++ b/app/Http/Middleware/ApiCheck.php @@ -23,7 +23,10 @@ class ApiCheck { { $loggingIn = $request->is('api/v1/login') || $request->is('api/v1/register'); $headers = Utils::getApiHeaders(); - $hasApiSecret = hash_equals($request->api_secret ?: '', env(API_SECRET)); + + if ($secret = env(API_SECRET)) { + $hasApiSecret = hash_equals($request->api_secret ?: '', $secret); + } if ($loggingIn) { // check API secret diff --git a/app/Http/routes.php b/app/Http/routes.php index d215755ba940..d31991e577ad 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -322,8 +322,8 @@ Route::get('/testimonials', function() { Route::get('/compare-online-invoicing{sites?}', function() { return Redirect::to(NINJA_WEB_URL, 301); }); -Route::get('/forgot_password', function() { - return Redirect::to(NINJA_APP_URL.'/forgot', 301); +Route::get('/forgot', function() { + return Redirect::to(NINJA_APP_URL.'/recover_password', 301); }); diff --git a/app/Models/Document.php b/app/Models/Document.php index cc455fed0d79..589c2ccdccd6 100644 --- a/app/Models/Document.php +++ b/app/Models/Document.php @@ -18,7 +18,7 @@ class Document extends EntityModel public static $allowedMimes = array(// Used by Dropzone.js; does not affect what the server accepts 'image/png', 'image/jpeg', 'image/tiff', 'application/pdf', 'image/gif', 'image/vnd.adobe.photoshop', 'text/plain', - 'application/zip', 'application/msword', + 'application/msword', 'application/excel', 'application/vnd.ms-excel', 'application/x-excel', 'application/x-msexcel', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','application/postscript', 'image/svg+xml', @@ -53,9 +53,6 @@ class Document extends EntityModel 'txt' => array( 'mime' => 'text/plain', ), - 'zip' => array( - 'mime' => 'application/zip', - ), 'doc' => array( 'mime' => 'application/msword', ), diff --git a/resources/views/expenses/edit.blade.php b/resources/views/expenses/edit.blade.php index 146005ebd1f5..f49a1c9097b3 100644 --- a/resources/views/expenses/edit.blade.php +++ b/resources/views/expenses/edit.blade.php @@ -371,7 +371,7 @@ } window.countUploadingDocuments = 0; - @if (Auth::user()->account->hasFeature(FEATURE_DOCUMENTS)) + function handleDocumentAdded(file){ // open document when clicked if (file.url) { @@ -412,7 +412,7 @@ function handleDocumentError() { window.countUploadingDocuments--; } - @endif + @stop diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 42aa40d27457..fcef2507ccc7 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1463,7 +1463,7 @@ } window.countUploadingDocuments = 0; - @if ($account->hasFeature(FEATURE_DOCUMENTS)) + function handleDocumentAdded(file){ // open document when clicked if (file.url) { @@ -1508,7 +1508,6 @@ function handleDocumentError() { window.countUploadingDocuments--; } - @endif @if ($account->hasFeature(FEATURE_DOCUMENTS) && $account->invoice_embed_documents) diff --git a/resources/views/master.blade.php b/resources/views/master.blade.php index 463be8be0eb7..c0f95e70323b 100644 --- a/resources/views/master.blade.php +++ b/resources/views/master.blade.php @@ -19,6 +19,15 @@ + + + + + + + + + @@ -38,6 +47,10 @@ return; } + if (errorMsg.indexOf('No unicode cmap for font') > -1) { + alert("Please force refresh the page to update the font cache.\n\n - Windows: Ctrl + F5\n - Mac/Apple: Apple + R or Command + R\n - Linux: F5"); + } + try { // Use StackTraceJS to parse the error context if (error) { From 5c7a15ff838c1bb92681b563a63685fccbceb451 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 5 Jun 2016 19:10:15 +0300 Subject: [PATCH 226/386] Minor fixes --- app/Http/routes.php | 3 +++ app/Ninja/Repositories/ReferralRepository.php | 6 +++--- resources/lang/en/texts.php | 5 ++++- resources/views/master.blade.php | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/Http/routes.php b/app/Http/routes.php index d31991e577ad..c42049ff369b 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -325,6 +325,9 @@ Route::get('/compare-online-invoicing{sites?}', function() { Route::get('/forgot', function() { return Redirect::to(NINJA_APP_URL.'/recover_password', 301); }); +Route::get('/feed', function() { + return Redirect::to(NINJA_WEB_URL.'/feed', 301); +}); if (!defined('CONTACT_EMAIL')) { diff --git a/app/Ninja/Repositories/ReferralRepository.php b/app/Ninja/Repositories/ReferralRepository.php index f96475c72ddc..f6170af296e3 100644 --- a/app/Ninja/Repositories/ReferralRepository.php +++ b/app/Ninja/Repositories/ReferralRepository.php @@ -7,7 +7,7 @@ class ReferralRepository { public function getCounts($userId) { - $accounts = Account::where('referral_user_id', $userId); + $accounts = Account::where('referral_user_id', $userId)->get(); $counts = [ 'free' => 0, @@ -18,7 +18,7 @@ class ReferralRepository foreach ($accounts as $account) { $counts['free']++; $plan = $account->getPlanDetails(false, false); - + if ($plan) { $counts['pro']++; if ($plan['plan'] == PLAN_ENTERPRISE) { @@ -29,4 +29,4 @@ class ReferralRepository return $counts; } -} \ No newline at end of file +} diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 093601aa7660..41d4af2a6021 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1334,7 +1334,7 @@ $LANG = array( '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.', 'payment_settings' => 'Payment Settings', - + 'on_send_date' => 'On send date', 'on_due_date' => 'On due date', 'auto_bill_ach_date_help' => 'ACH auto bill will always happen on the due date', @@ -1348,6 +1348,9 @@ $LANG = array( 'payment_settings_supported_gateways' => 'These options are supported by the WePay, Stripe, and Braintree gateways.', 'ach_email_prompt' => 'Please enter your email address:', 'verification_pending' => 'Verification Pending', + + 'update_font_cache' => 'Please force refresh the page to update the font cache.', + ); return $LANG; diff --git a/resources/views/master.blade.php b/resources/views/master.blade.php index c0f95e70323b..db5e8f366519 100644 --- a/resources/views/master.blade.php +++ b/resources/views/master.blade.php @@ -48,7 +48,7 @@ } if (errorMsg.indexOf('No unicode cmap for font') > -1) { - alert("Please force refresh the page to update the font cache.\n\n - Windows: Ctrl + F5\n - Mac/Apple: Apple + R or Command + R\n - Linux: F5"); + alert("{{ trans('texts.update_font_cache') }}\n\n - Windows: Ctrl + F5\n - Mac/Apple: Apple + R or Command + R\n - Linux: F5"); } try { From 706c26b4d519defd4032e6d0cc64f308695eb8f3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 5 Jun 2016 21:05:11 +0300 Subject: [PATCH 227/386] Minor fixes --- app/Http/Controllers/AccountController.php | 6 ++-- .../Controllers/AccountGatewayController.php | 7 ++++ app/Http/routes.php | 4 ++- resources/views/accounts/management.blade.php | 34 +++++++++++-------- resources/views/header.blade.php | 2 +- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 4fa6e1b90126..72f56e14ee3c 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -230,7 +230,7 @@ class AccountController extends BaseController Session::flash('message', trans('texts.updated_plan')); } - if (!empty($new_plan)) { + if (!empty($new_plan) && $new_plan['plan'] != PLAN_FREE) { $invitation = $this->accountRepo->enablePlan($new_plan['plan'], $new_plan['term'], $credit, !empty($pending_monthly)); return Redirect::to('view/'.$invitation->invitation_key); } @@ -1333,12 +1333,14 @@ class AccountController extends BaseController } $account = Auth::user()->account; + $invitation = $invoice->invitations->first(); // replace the variables with sample data $data = [ 'account' => $account, 'invoice' => $invoice, - 'invitation' => $invoice->invitations->first(), + 'invitation' => $invitation, + 'link' => $invitation->getLink(), 'client' => $invoice->client, 'amount' => $invoice->amount ]; diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 629932c567e8..38d4e7f65c9d 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -40,6 +40,13 @@ class AccountGatewayController extends BaseController return $this->accountGatewayService->getDatatable(Auth::user()->account_id); } + public function show($publicId) + { + Session::reflash(); + + return Redirect::to("gateways/$publicId/edit"); + } + public function edit($publicId) { $accountGateway = AccountGateway::scope($publicId)->firstOrFail(); diff --git a/app/Http/routes.php b/app/Http/routes.php index c42049ff369b..9efd8c77ad43 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -328,7 +328,9 @@ Route::get('/forgot', function() { Route::get('/feed', function() { return Redirect::to(NINJA_WEB_URL.'/feed', 301); }); - +Route::get('/comments/feed', function() { + return Redirect::to(NINJA_WEB_URL.'/comments/feed', 301); +}); if (!defined('CONTACT_EMAIL')) { define('CONTACT_EMAIL', Config::get('mail.from.address')); diff --git a/resources/views/accounts/management.blade.php b/resources/views/accounts/management.blade.php index ac57cf737dd6..a493028591ca 100644 --- a/resources/views/accounts/management.blade.php +++ b/resources/views/accounts/management.blade.php @@ -1,6 +1,6 @@ @extends('header') -@section('content') +@section('content') @parent @include('accounts.nav', ['selected' => ACCOUNT_MANAGEMENT]) @@ -134,10 +134,10 @@
    @@ -165,12 +165,12 @@
     

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

      -  

    {!! Former::textarea('reason')->placeholder(trans('texts.reason_for_canceling'))->raw() !!}

      +  

    {!! Former::textarea('reason')->placeholder(trans('texts.reason_for_canceling'))->raw() !!}

     
    @@ -182,7 +182,7 @@ -@stop \ No newline at end of file +@stop diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index b8fdccf9bd31..3fe91386b251 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -423,7 +423,7 @@ @if (!Auth::user()->registered) {!! Button::success(trans('texts.sign_up'))->withAttributes(array('id' => 'signUpButton', 'data-toggle'=>'modal', 'data-target'=>'#signUpModal', 'style' => 'max-width:100px;;overflow:hidden'))->small() !!}   @elseif (Utils::isNinjaProd() && (!Auth::user()->isPro() || Auth::user()->isTrial())) - {!! Button::success(trans('texts.go_pro'))->withAttributes(array('id' => 'proPlanButton', 'onclick' => 'showProPlan("")', 'style' => 'max-width:100px;overflow:hidden'))->small() !!}   + {!! Button::success(trans('texts.plan_upgrade'))->asLinkTo(url('/settings/account_management?upgrade=true'))->withAttributes(array('style' => 'max-width:100px;overflow:hidden'))->small() !!}   @endif @endif From 5eb76df20ec49eba9a03e28ae25b86f5c47ca85d Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sun, 5 Jun 2016 15:06:34 -0400 Subject: [PATCH 228/386] Fix refunds on Stripe --- app/Services/PaymentService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index efa27d945b7a..00f45a561e7d 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -1005,7 +1005,7 @@ class PaymentService extends BaseService 'transactionReference' => $payment->transaction_reference, ); - if ($amount != ($payment->amount - $payment->refunded)) { + if ($accountGateway->gateway_id != GATEWAY_WEPAY || $amount != ($payment->amount - $payment->refunded)) { $details['amount'] = $amount; } From 0967ca65e8d1441fd6bf681794bf3d3abd84ae1b Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sun, 5 Jun 2016 15:07:02 -0400 Subject: [PATCH 229/386] Show "Credit Card" instead of "Stripe" as a payment option. --- app/Http/Controllers/ClientPortalController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index 8d66e18293d1..ba088df49e91 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -225,6 +225,9 @@ class ClientPortalController extends BaseController foreach(Gateway::$paymentTypes as $type) { + if ($type == PAYMENT_TYPE_STRIPE) { + continue; + } if ($gateway = $account->getGatewayByType($type)) { if ($type == PAYMENT_TYPE_DIRECT_DEBIT) { if ($gateway->gateway_id == GATEWAY_STRIPE) { From d5cb8b3abe8dcc0b12a571d2b2c6e47266b1e7f2 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 5 Jun 2016 22:55:23 +0300 Subject: [PATCH 230/386] Minor fixes --- resources/lang/en/texts.php | 2 +- .../views/accounts/customize_design.blade.php | 46 +++++++-------- .../views/accounts/invoice_design.blade.php | 56 +++++++++---------- resources/views/accounts/management.blade.php | 2 +- resources/views/invoices/edit.blade.php | 4 +- resources/views/invoices/history.blade.php | 18 +++--- resources/views/invoices/view.blade.php | 2 +- 7 files changed, 65 insertions(+), 65 deletions(-) diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 41d4af2a6021..0e38859b66df 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1163,7 +1163,7 @@ $LANG = array( 'preview' => 'Preview', 'list_vendors' => 'List Vendors', 'add_users_not_supported' => 'Upgrade to the Enterprise plan to add additional users to your account.', - 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments.', + 'enterprise_plan_features' => 'The Enterprise plan adds support for multiple users and file attachments, :link to see the full list of features.', 'return_to_app' => 'Return to app', diff --git a/resources/views/accounts/customize_design.blade.php b/resources/views/accounts/customize_design.blade.php index 9c9fff51121c..f4eb56a79a30 100644 --- a/resources/views/accounts/customize_design.blade.php +++ b/resources/views/accounts/customize_design.blade.php @@ -10,16 +10,16 @@ @foreach ($account->getFontFolders() as $font) @endforeach - + + + + + + {!! Former::close() !!} + +@stop diff --git a/resources/views/accounts/partials/account_gateway_wepay.blade.php b/resources/views/accounts/partials/account_gateway_wepay.blade.php deleted file mode 100644 index a030d4c825b6..000000000000 --- a/resources/views/accounts/partials/account_gateway_wepay.blade.php +++ /dev/null @@ -1,97 +0,0 @@ -{!! Former::open($url)->method($method)->rules(array( - 'first_name' => 'required', - 'last_name' => 'required', - 'email' => 'required', - 'description' => 'required', - 'company_name' => 'required', - 'tos_agree' => 'required', - 'country' => 'required', - ))->addClass('warn-on-exit') !!} -{!! Former::populateField('company_name', $account->getDisplayName()) !!} -@if($account->country) - {!! Former::populateField('country', $account->country->iso_3166_2) !!} -@endif -{!! Former::populateField('first_name', $user->first_name) !!} -{!! Former::populateField('last_name', $user->last_name) !!} -{!! Former::populateField('email', $user->email) !!} -{!! Former::populateField('show_address', 1) !!} -{!! Former::populateField('update_address', 1) !!} -
    -
    -

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

    -
    -
    - {!! Former::text('first_name') !!} - {!! Former::text('last_name') !!} - {!! Former::text('email') !!} - {!! Former::text('company_name')->help('wepay_company_name_help')->maxlength(255) !!} - {!! Former::text('description')->help('wepay_description_help') !!} - @if (WEPAY_ENABLE_CANADA) -
    - {!! Former::radios('country') - ->radios([ - trans('texts.united_states') => ['value' => 'US'], - trans('texts.canada') => ['value' => 'CA'], - ]) !!} -
    -
    - {!! Former::checkbox('debit_cards') - ->text(trans('texts.accept_debit_cards')) !!} -
    - @endif - {!! Former::checkbox('show_address') - ->label(trans('texts.billing_address')) - ->text(trans('texts.show_address_help')) !!} - {!! Former::checkbox('update_address') - ->label(' ') - ->text(trans('texts.update_address_help')) !!} - {!! Former::checkboxes('creditCardTypes[]') - ->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') !!} -
    - {!! Button::primary(trans('texts.sign_up_with_wepay')) - ->submit() - ->large() !!} - @if(isset($gateways)) -

    - {{ trans('texts.use_another_provider') }} - @endif -
    -
    -
    - - - -{!! Former::close() !!} \ No newline at end of file diff --git a/resources/views/accounts/payments.blade.php b/resources/views/accounts/payments.blade.php index 603aec4bf6f5..55e7177c683c 100644 --- a/resources/views/accounts/payments.blade.php +++ b/resources/views/accounts/payments.blade.php @@ -1,7 +1,7 @@ @extends('header') -@section('content') - @parent +@section('content') + @parent @include('accounts.nav', ['selected' => ACCOUNT_PAYMENTS]) {!! Former::open()->addClass('warn-on-exit') !!} @@ -31,12 +31,6 @@
    {!! Former::close() !!} - @if ($showSwitchToWepay) - {!! Button::success(trans('texts.switch_to_wepay')) - ->asLinkTo(URL::to('/gateways/switch/wepay')) - ->appendIcon(Icon::create('circle-arrow-up')) !!} -   - @endif
    - {!! Former::select('mail[driver]')->label('Driver')->options(['smtp' => 'SMTP', 'mail' => 'Mail', 'sendmail' => 'Sendmail']) - ->value(isset($_ENV['MAIL_DRIVER']) ? $_ENV['MAIL_DRIVER'] : 'smtp') !!} - {!! Former::text('mail[host]')->label('Host') - ->value(isset($_ENV['MAIL_HOST']) ? $_ENV['MAIL_HOST'] : '') !!} - {!! Former::text('mail[port]')->label('Port') - ->value(isset($_ENV['MAIL_PORT']) ? $_ENV['MAIL_PORT'] : '587') !!} - {!! Former::select('mail[encryption]')->label('Encryption')->options(['tls' => 'TLS', 'ssl' => 'SSL']) - ->value(isset($_ENV['MAIL_ENCRYPTION']) ? $_ENV['MAIL_ENCRYPTION'] : 'tls') !!} + {!! Former::select('mail[driver]')->label('Driver')->options(['smtp' => 'SMTP', 'mail' => 'Mail', 'sendmail' => 'Sendmail', 'mailgun' => 'Mailgun']) + ->value(isset($_ENV['MAIL_DRIVER']) ? $_ENV['MAIL_DRIVER'] : 'smtp')->setAttributes(['onchange' => 'mailDriverChange()']) !!} {!! Former::text('mail[from][name]')->label('From Name') - ->value(isset($_ENV['MAIL_FROM_NAME']) ? $_ENV['MAIL_FROM_NAME'] : '') !!} + ->value(isset($_ENV['MAIL_FROM_NAME']) ? $_ENV['MAIL_FROM_NAME'] : '') !!} {!! Former::text('mail[username]')->label('Email') ->value(isset($_ENV['MAIL_USERNAME']) ? $_ENV['MAIL_USERNAME'] : '') !!} - {!! Former::password('mail[password]')->label('Password') - ->value(isset($_ENV['MAIL_PASSWORD']) ? $_ENV['MAIL_PASSWORD'] : '') !!} +
    + {!! Former::text('mail[host]')->label('Host') + ->value(isset($_ENV['MAIL_HOST']) ? $_ENV['MAIL_HOST'] : '') !!} + {!! Former::text('mail[port]')->label('Port') + ->value(isset($_ENV['MAIL_PORT']) ? $_ENV['MAIL_PORT'] : '587') !!} + {!! Former::select('mail[encryption]')->label('Encryption')->options(['tls' => 'TLS', 'ssl' => 'SSL']) + ->value(isset($_ENV['MAIL_ENCRYPTION']) ? $_ENV['MAIL_ENCRYPTION'] : 'tls') !!} + {!! Former::password('mail[password]')->label('Password') + ->value(isset($_ENV['MAIL_PASSWORD']) ? $_ENV['MAIL_PASSWORD'] : '') !!} +
    +
    + {!! Former::text('mail[mailgun_domain]')->label('Mailgun Domain') + ->value(isset($_ENV['MAILGUN_DOMAIN']) ? $_ENV['MAILGUN_DOMAIN'] : '') !!} + {!! Former::text('mail[mailgun_secret]')->label('Mailgun Private Key') + ->value(isset($_ENV['MAILGUN_SECRET']) ? $_ENV['MAILGUN_SECRET'] : '') !!} +
    {{-- Former::actions( Button::primary('Send test email')->small()->withAttributes(['onclick' => 'testMail()']), '  ' ) --}}
    @@ -57,7 +65,8 @@ var db_valid = false var mail_valid = false - + mailDriverChange(); + function testDatabase() { var data = $("form").serialize() + "&test=db"; @@ -78,6 +87,23 @@ return db_valid; } + function mailDriverChange() { + if ($("select[name='mail[driver]'").val() == 'mailgun') { + $("#standardMailSetup").hide(); + $("#standardMailSetup").children('select,input').prop('disabled',true); + $("#mailgunMailSetup").show(); + $("#mailgunMailSetup").children('select,input').prop('disabled',false); + + } else { + $("#standardMailSetup").show(); + $("#standardMailSetup").children('select,input').prop('disabled',false); + + $("#mailgunMailSetup").hide(); + $("#mailgunMailSetup").children('select,input').prop('disabled',true); + + } + } + function testMail() { var data = $("form").serialize() + "&test=mail"; From 5ff69c6f295a4c05babb50b84823f3cf0ff090d9 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 20 Jun 2016 17:14:43 +0300 Subject: [PATCH 266/386] Payments refactor --- .../Commands/SendRecurringInvoices.php | 31 +- .../Controllers/AccountGatewayController.php | 13 +- app/Http/Controllers/ClientController.php | 6 +- .../Controllers/ClientPortalController.php | 318 ++---- app/Http/Controllers/InvoiceController.php | 2 +- app/Http/Controllers/NinjaController.php | 236 +++++ .../Controllers/OnlinePaymentController.php | 333 +++++++ app/Http/Controllers/PaymentController.php | 864 +--------------- .../Requests/CreateOnlinePaymentRequest.php | 46 + app/Http/routes.php | 47 +- app/Listeners/CreditListener.php | 6 +- app/Listeners/InvoiceListener.php | 6 +- app/Models/Account.php | 59 +- app/Models/AccountGateway.php | 23 +- app/Models/AccountGatewayToken.php | 45 +- app/Models/Client.php | 71 +- app/Models/Gateway.php | 51 +- app/Models/Invoice.php | 8 +- app/Models/Payment.php | 50 +- app/Models/PaymentMethod.php | 46 +- app/Models/User.php | 3 + .../Datatables/AccountGatewayDatatable.php | 6 - app/Ninja/Mailers/ContactMailer.php | 36 +- .../AuthorizeNetAIMPaymentDriver.php | 6 + .../PaymentDrivers/BasePaymentDriver.php | 796 +++++++++++++++ .../PaymentDrivers/BitPayPaymentDriver.php | 12 + .../PaymentDrivers/BraintreePaymentDriver.php | 193 ++++ .../CheckoutComPaymentDriver.php | 35 + .../CybersourcePaymentDriver.php | 15 + .../PaymentDrivers/DwollaPaymentDriver.php | 24 + .../EwayRapidSharedPaymentDriver.php | 6 + .../GoCardlessPaymentDriver.php | 6 + .../PaymentDrivers/MolliePaymentDriver.php | 16 + .../PaymentDrivers/PayFastPaymentDriver.php | 13 + .../PayPalExpressPaymentDriver.php | 29 + .../PaymentDrivers/PayPalProPaymentDriver.php | 20 + .../PaymentDrivers/StripePaymentDriver.php | 304 ++++++ .../TwoCheckoutPaymentDriver.php | 13 + .../PaymentDrivers/WePayPaymentDriver.php | 118 +++ app/Ninja/Repositories/InvoiceRepository.php | 1 + app/Services/DatatableService.php | 6 +- app/Services/InvoiceService.php | 7 +- app/Services/PaymentService.php | 933 +----------------- app/Services/TemplateService.php | 19 +- .../2016_04_23_182223_payments_changes.php | 6 +- database/seeds/PaymentLibrariesSeeder.php | 10 +- database/seeds/UserTableSeeder.php | 4 +- resources/lang/en/texts.php | 8 +- .../views/accounts/account_gateway.blade.php | 16 +- .../accounts/account_gateway_wepay.blade.php | 15 +- .../partials/payment_credentials.blade.php | 47 + resources/views/accounts/payments.blade.php | 9 +- .../templates_and_reminders.blade.php | 44 +- resources/views/clients/show.blade.php | 16 +- resources/views/expenses/edit.blade.php | 2 +- resources/views/invited/dashboard.blade.php | 15 +- resources/views/invoices/view.blade.php | 27 +- .../payments/add_paymentmethod.blade.php | 524 ---------- .../views/payments/bank_transfer.blade.php | 11 + .../payments/braintree/credit_card.blade.php | 73 ++ .../views/payments/braintree/paypal.blade.php | 42 + .../checkoutcom/partial.blade.php} | 12 +- .../views/payments/credit_card.blade.php | 259 +++++ .../views/payments/payment_css.blade.php | 20 +- .../views/payments/payment_method.blade.php | 74 ++ .../payments/paymentmethods_list.blade.php | 120 ++- .../payments/stripe/bank_transfer.blade.php | 285 ++++++ .../payments/stripe/credit_card.blade.php | 89 ++ .../payments/tokenization_braintree.blade.php | 67 -- .../payments/tokenization_stripe.blade.php | 154 --- .../payments/wepay/bank_transfer.blade.php | 40 + resources/views/public/header.blade.php | 16 +- 72 files changed, 3659 insertions(+), 3224 deletions(-) create mode 100644 app/Http/Controllers/NinjaController.php create mode 100644 app/Http/Controllers/OnlinePaymentController.php create mode 100644 app/Http/Requests/CreateOnlinePaymentRequest.php create mode 100644 app/Ninja/PaymentDrivers/AuthorizeNetAIMPaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/BasePaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/BitPayPaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/BraintreePaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/CheckoutComPaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/CybersourcePaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/DwollaPaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/EwayRapidSharedPaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/GoCardlessPaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/MolliePaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/PayFastPaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/PayPalExpressPaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/PayPalProPaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/StripePaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/TwoCheckoutPaymentDriver.php create mode 100644 app/Ninja/PaymentDrivers/WePayPaymentDriver.php create mode 100644 resources/views/accounts/partials/payment_credentials.blade.php delete mode 100644 resources/views/payments/add_paymentmethod.blade.php create mode 100644 resources/views/payments/bank_transfer.blade.php create mode 100644 resources/views/payments/braintree/credit_card.blade.php create mode 100644 resources/views/payments/braintree/paypal.blade.php rename resources/views/{partials/checkout_com_payment.blade.php => payments/checkoutcom/partial.blade.php} (62%) create mode 100644 resources/views/payments/credit_card.blade.php create mode 100644 resources/views/payments/payment_method.blade.php create mode 100644 resources/views/payments/stripe/bank_transfer.blade.php create mode 100644 resources/views/payments/stripe/credit_card.blade.php delete mode 100644 resources/views/payments/tokenization_braintree.blade.php delete mode 100644 resources/views/payments/tokenization_stripe.blade.php create mode 100644 resources/views/payments/wepay/bank_transfer.blade.php diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index 574d324d3842..4934ca417e34 100644 --- a/app/Console/Commands/SendRecurringInvoices.php +++ b/app/Console/Commands/SendRecurringInvoices.php @@ -45,33 +45,19 @@ class SendRecurringInvoices extends Command if (!$recurInvoice->user->confirmed) { continue; } - + $recurInvoice->account->loadLocalizationSettings($recurInvoice->client); $this->info('Processing Invoice '.$recurInvoice->id.' - Should send '.($recurInvoice->shouldSendToday() ? 'YES' : 'NO')); $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 || $this->paymentService->getClientRequiresDelayedAutoBill($invoice->client)) { - $autoBillLater = true; - } - - if($autoBillLater) { - if($paymentMethod = $this->paymentService->getClientDefaultPaymentMethod($invoice->client)) { - $invoice->autoBillPaymentMethod = $paymentMethod; - } - } - - $this->info('Sending Invoice'); $this->mailer->sendInvoice($invoice); } } $delayedAutoBillInvoices = Invoice::with('account.timezone', 'recurring_invoice', 'invoice_items', 'client', 'user') - ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS FALSE + ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS FALSE AND balance > 0 AND due_date = ? AND recurring_invoice_id IS NOT NULL', array($today->format('Y-m-d'))) ->orderBy('invoices.id', 'asc') @@ -79,17 +65,12 @@ class SendRecurringInvoices extends Command $this->info(count($delayedAutoBillInvoices).' due recurring 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 || $this->paymentService->getClientRequiresDelayedAutoBill($invoice->client); + if ($invoice->isPaid()) { + continue; } - $this->info('Processing Invoice '.$invoice->id.' - Should bill '.($billNow ? 'YES' : 'NO')); - - if ($billNow) { - // autoBillInvoice will check for changes to ACH invoices, so we're not checking here + if ($invoice->getAutoBillEnabled() && $invoice->client->autoBillLater()) { + $this->info('Processing Invoice '.$invoice->id.' - Should bill '.($billNow ? 'YES' : 'NO')); $this->paymentService->autoBillInvoice($invoice); } } diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 1d9a05e761a2..e0f6ba9b5754 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -62,7 +62,6 @@ class AccountGatewayController extends BaseController $data['title'] = trans('texts.edit_gateway') . ' - ' . $accountGateway->gateway->name; $data['config'] = $config; $data['hiddenFields'] = Gateway::$hiddenFields; - $data['paymentTypeId'] = $accountGateway->getPaymentType(); $data['selectGateways'] = Gateway::where('id', '=', $accountGateway->gateway_id)->get(); return View::make('accounts.account_gateway', $data); @@ -82,7 +81,7 @@ class AccountGatewayController extends BaseController * Displays the form for account creation * */ - public function create($showWepay = true) + public function create() { if ( ! \Request::secure() && ! Utils::isNinjaDev()) { Session::flash('warning', trans('texts.enable_https')); @@ -90,10 +89,10 @@ class AccountGatewayController extends BaseController $account = Auth::user()->account; $accountGatewaysIds = $account->gatewayIds(); - $showWepay = filter_var($showWepay, FILTER_VALIDATE_BOOLEAN); + $otherProviders = Input::get('other_providers'); if ( ! Utils::isNinja() || Gateway::hasStandardGateway($accountGatewaysIds)) { - $showWepay = false; + $otherProviders = true; } $data = self::getViewModel(); @@ -101,14 +100,14 @@ class AccountGatewayController extends BaseController $data['method'] = 'POST'; $data['title'] = trans('texts.add_gateway'); - if ($showWepay) { - return View::make('accounts.account_gateway_wepay', $data); - } else { + if ($otherProviders) { $data['primaryGateways'] = Gateway::primary($accountGatewaysIds)->orderBy('name', 'desc')->get(); $data['secondaryGateways'] = Gateway::secondary($accountGatewaysIds)->orderBy('name')->get(); $data['hiddenFields'] = Gateway::$hiddenFields; return View::make('accounts.account_gateway', $data); + } else { + return View::make('accounts.account_gateway_wepay', $data); } } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 8a451113115e..66a5e1e25e0d 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -129,6 +129,8 @@ class ClientController extends BaseController $actionLinks[] = ['label' => trans('texts.enter_expense'), 'url' => URL::to('/expenses/create/0/'.$client->public_id)]; } + $token = $client->getGatewayToken(); + $data = array( 'actionLinks' => $actionLinks, 'showBreadcrumbs' => false, @@ -138,8 +140,8 @@ class ClientController extends BaseController 'hasRecurringInvoices' => Invoice::scope()->where('is_recurring', '=', true)->whereClientId($client->id)->count() > 0, 'hasQuotes' => Invoice::scope()->invoiceType(INVOICE_TYPE_QUOTE)->whereClientId($client->id)->count() > 0, 'hasTasks' => Task::scope()->whereClientId($client->id)->count() > 0, - 'gatewayLink' => $client->getGatewayLink($accountGateway), - 'gateway' => $accountGateway + 'gatewayLink' => $token ? $token->gatewayLink() : false, + 'gatewayName' => $token ? $token->gatewayName() : false, ); return View::make('clients.show', $data); diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index ba088df49e91..493ae23c5f0c 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -98,7 +98,7 @@ class ClientPortalController extends BaseController ]); $data = array(); - $paymentTypes = $this->getPaymentTypes($client, $invitation); + $paymentTypes = $this->getPaymentTypes($account, $client, $invitation); $paymentURL = ''; if (count($paymentTypes) == 1) { $paymentURL = $paymentTypes[0]['url']; @@ -107,11 +107,9 @@ class ClientPortalController extends BaseController } } - if ($braintreeGateway = $account->getGatewayConfig(GATEWAY_BRAINTREE)){ - if($braintreeGateway->getPayPalEnabled()) { - $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); - } - } elseif ($wepayGateway = $account->getGatewayConfig(GATEWAY_WEPAY)){ + $paymentDriver = $account->paymentDriver($invitation, GATEWAY_TYPE_CREDIT_CARD); + + if ($wepayGateway = $account->getGatewayConfig(GATEWAY_WEPAY)){ $data['enableWePayACH'] = $wepayGateway->getAchEnabled(); } @@ -123,19 +121,6 @@ class ClientPortalController extends BaseController $showApprove = false; } - // Checkout.com requires first getting a payment token - $checkoutComToken = false; - $checkoutComKey = false; - $checkoutComDebug = false; - if ($accountGateway = $account->getGatewayConfig(GATEWAY_CHECKOUT_COM)) { - $checkoutComDebug = $accountGateway->getConfigField('testMode'); - if ($checkoutComToken = $this->paymentService->getCheckoutComToken($invitation)) { - $checkoutComKey = $accountGateway->getConfigField('publicApiKey'); - $invitation->transaction_reference = $checkoutComToken; - $invitation->save(); - } - } - $data += array( 'account' => $account, 'showApprove' => $showApprove, @@ -147,9 +132,9 @@ class ClientPortalController extends BaseController 'contact' => $contact, 'paymentTypes' => $paymentTypes, 'paymentURL' => $paymentURL, - 'checkoutComToken' => $checkoutComToken, - 'checkoutComKey' => $checkoutComKey, - 'checkoutComDebug' => $checkoutComDebug, + 'transactionToken' => $paymentDriver->createTransactionToken(), + 'partialView' => $paymentDriver->partialView(), + 'accountGateway' => $paymentDriver->accountGateway, 'phantomjs' => Input::has('phantomjs'), ); @@ -164,7 +149,7 @@ 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(); @@ -177,95 +162,17 @@ class ClientPortalController extends BaseController return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/invoices/'); } - private function getPaymentTypes($client, $invitation) + private function getPaymentTypes($account, $client, $invitation) { - $paymentTypes = []; - $account = $client->account; + $links = []; - $paymentMethods = $this->paymentService->getClientPaymentMethods($client); - - if ($paymentMethods) { - foreach ($paymentMethods as $paymentMethod) { - if ($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED) { - $code = htmlentities(str_replace(' ', '', strtolower($paymentMethod->payment_type->name))); - $html = ''; - - if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) { - if ($paymentMethod->bank_name) { - $html = '
    ' . htmlentities($paymentMethod->bank_name) . '
    '; - } else { - $html = ''.trans('; - } - } elseif ($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL) { - $html = ''.trans('; - } else { - $html = ''.trans('; - } - - $url = URL::to("/payment/{$invitation->invitation_key}/token/".$paymentMethod->public_id); - - if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL) { - $html .= '  '.$paymentMethod->email.''; - $url .= '#braintree_paypal'; - } elseif ($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH) { - $html .= '
    '.trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($paymentMethod->expiration, false)->format('m/y'))).'
    '; - $html .= '•••'.$paymentMethod->last4.'
    '; - } else { - $html .= '
    '; - $html .= '•••'.$paymentMethod->last4.'
    '; - } - - $paymentTypes[] = [ - 'url' => $url, - 'label' => $html, - ]; - } - } + foreach ($account->account_gateways as $accountGateway) { + $paymentDriver = $accountGateway->paymentDriver($invitation); + $links = array_merge($links, $paymentDriver->tokenLinks()); + $links = array_merge($links, $paymentDriver->paymentLinks()); } - - foreach(Gateway::$paymentTypes as $type) { - if ($type == PAYMENT_TYPE_STRIPE) { - continue; - } - if ($gateway = $account->getGatewayByType($type)) { - if ($type == PAYMENT_TYPE_DIRECT_DEBIT) { - if ($gateway->gateway_id == GATEWAY_STRIPE) { - $type = PAYMENT_TYPE_STRIPE_ACH; - } elseif ($gateway->gateway_id == GATEWAY_WEPAY) { - $type = PAYMENT_TYPE_WEPAY_ACH; - } - } elseif ($type == PAYMENT_TYPE_PAYPAL && $gateway->gateway_id == GATEWAY_BRAINTREE) { - $type = PAYMENT_TYPE_BRAINTREE_PAYPAL; - } elseif ($type == PAYMENT_TYPE_CREDIT_CARD && $gateway->gateway_id == GATEWAY_STRIPE) { - $type = PAYMENT_TYPE_STRIPE_CREDIT_CARD; - } - - $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 || $type == PAYMENT_TYPE_WEPAY_ACH) { - $label = trans('texts.' . strtolower(PAYMENT_TYPE_DIRECT_DEBIT)); - } elseif ($type == PAYMENT_TYPE_BRAINTREE_PAYPAL) { - $label = trans('texts.' . strtolower(PAYMENT_TYPE_PAYPAL)); - } else { - $label = trans('texts.' . strtolower($type)); - } - - $paymentTypes[] = [ - 'url' => $url, 'label' => $label - ]; - } - } - - return $paymentTypes; + return $links; } public function download($invitationKey) @@ -303,6 +210,9 @@ class ClientPortalController extends BaseController return $this->returnError(); } + $paymentDriver = $account->paymentDriver(false, GATEWAY_TYPE_TOKEN); + $customer = $paymentDriver->customer($client->id); + $data = [ 'color' => $color, 'contact' => $contact, @@ -310,15 +220,10 @@ class ClientPortalController extends BaseController 'client' => $client, 'clientFontUrl' => $account->getFontsUrl(), 'gateway' => $account->getTokenGateway(), - 'paymentMethods' => $this->paymentService->getClientPaymentMethods($client), + 'paymentMethods' => $customer ? $customer->payment_methods : false, + 'transactionToken' => $paymentDriver->createTransactionToken(), ]; - if ($braintreeGateway = $account->getGatewayConfig(GATEWAY_BRAINTREE)){ - if($braintreeGateway->getPayPalEnabled()) { - $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); - } - } - return response()->view('invited.dashboard', $data); } @@ -767,7 +672,9 @@ class ClientPortalController extends BaseController $client = $contact->client; $account = $client->account; - $paymentMethods = $this->paymentService->getClientPaymentMethods($client); + + $paymentDriver = $account->paymentDriver(false, GATEWAY_TYPE_TOKEN); + $customer = $paymentDriver->customer($client->id); $data = array( 'account' => $account, @@ -776,17 +683,12 @@ class ClientPortalController extends BaseController 'client' => $client, 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), - 'paymentMethods' => $paymentMethods, + 'paymentMethods' => $customer ? $customer->payment_methods : false, 'gateway' => $account->getTokenGateway(), - 'title' => trans('texts.payment_methods') + 'title' => trans('texts.payment_methods'), + 'transactionToken' => $paymentDriver->createTransactionToken(), ); - if ($braintreeGateway = $account->getGatewayConfig(GATEWAY_BRAINTREE)){ - if($braintreeGateway->getPayPalEnabled()) { - $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); - } - } - return response()->view('payments.paymentmethods', $data); } @@ -801,7 +703,10 @@ class ClientPortalController extends BaseController } $client = $contact->client; - $result = $this->paymentService->verifyClientPaymentMethod($client, $publicId, $amount1, $amount2); + $account = $client->account; + + $paymentDriver = $account->paymentDriver(null, GATEWAY_TYPE_BANK_TRANSFER); + $result = $paymentDriver->verifyBankAccount($client, $publicId, $amount1, $amount2); if (is_string($result)) { Session::flash('error', $result); @@ -809,7 +714,7 @@ class ClientPortalController extends BaseController Session::flash('message', trans('texts.payment_method_verified')); } - return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/'); + return redirect()->to($account->enable_client_portal?'/client/dashboard':'/client/payment_methods/'); } public function removePaymentMethod($publicId) @@ -819,161 +724,48 @@ class ClientPortalController extends BaseController } $client = $contact->client; - $result = $this->paymentService->removeClientPaymentMethod($client, $publicId); + $account = $contact->account; - if (is_string($result)) { - Session::flash('error', $result); - } else { + $paymentDriver = $account->paymentDriver(false, GATEWAY_TYPE_TOKEN); + $paymentMethod = PaymentMethod::clientId($client->id) + ->wherePublicId($publicId) + ->firstOrFail(); + + try { + $paymentDriver->removePaymentMethod($paymentMethod); Session::flash('message', trans('texts.payment_method_removed')); + } catch (Exception $exception) { + Session::flash('error', $exception->getMessage()); } - return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/'); + return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/payment_methods/'); } - - public function addPaymentMethod($paymentType, $token=false) - { - if (!$contact = $this->getContact()) { - return $this->returnError(); - } - - $client = $contact->client; - $account = $client->account; - - $typeLink = $paymentType; - $paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType); - $accountGateway = $client->account->getTokenGateway(); - $gateway = $accountGateway->gateway; - - if ($token && $paymentType == PAYMENT_TYPE_BRAINTREE_PAYPAL) { - $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); - } else { - Session::flash('message', trans('texts.payment_method_added')); - } - return redirect()->to($account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/'); - } - - $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); - - - $data = [ - 'showBreadcrumbs' => false, - 'client' => $client, - 'contact' => $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, - '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'); - } - - if ($gateway->id == GATEWAY_BRAINTREE) { - $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); - } - - if(!empty($data['braintreeClientToken']) || $accountGateway->getPublishableStripeKey()|| ($accountGateway->gateway_id == GATEWAY_WEPAY && $paymentType != PAYMENT_TYPE_WEPAY_ACH)) { - $data['tokenize'] = true; - } - - return View::make('payments.add_paymentmethod', $data); - } - - public function postAddPaymentMethod($paymentType) - { - if (!$contact = $this->getContact()) { - return $this->returnError(); - } - - $client = $contact->client; - - $typeLink = $paymentType; - $paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType); - $account = $client->account; - - $accountGateway = $account->getGatewayByType($paymentType); - $sourceToken = Input::get('sourceToken'); - - if (($validator = PaymentController::processPaymentClientDetails($client, $accountGateway, $paymentType)) !== true) { - return Redirect::to('client/paymentmethods/add/' . $typeLink.'/'.$sourceToken) - ->withErrors($validator) - ->withInput(Request::except('cvv')); - } - - if ($sourceToken) { - $details = array('token' => $sourceToken); - } elseif (Input::get('plaidPublicToken')) { - $usingPlaid = true; - $details = array('plaidPublicToken' => Input::get('plaidPublicToken'), 'plaidAccountId' => Input::get('plaidAccountId')); - } - - 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.'/'.$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($paymentType, $gateway, $details, $accountGateway, $client, $contact->id); - } else { - 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.'/'.$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')); - return Redirect::to($account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/'); - } else { - Session::flash('message', trans('texts.payment_method_added')); - return redirect()->to($account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/'); - } - } - + public function setDefaultPaymentMethod(){ if (!$contact = $this->getContact()) { return $this->returnError(); } $client = $contact->client; + $account = $client->account; $validator = Validator::make(Input::all(), array('source' => 'required')); if ($validator->fails()) { - return Redirect::to($client->account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/'); + return Redirect::to($client->account->enable_client_portal?'/client/dashboard':'/client/payment_methods/'); } - $result = $this->paymentService->setClientDefaultPaymentMethod($client, Input::get('source')); + $paymentDriver = $account->paymentDriver(false, GATEWAY_TYPE_TOKEN); + $paymentMethod = PaymentMethod::clientId($client->id) + ->wherePublicId(Input::get('source')) + ->firstOrFail(); - if (is_string($result)) { - Session::flash('error', $result); - } else { - Session::flash('message', trans('texts.payment_method_set_as_default')); - } + $customer = $paymentDriver->customer($client->id); + $customer->default_payment_method_id = $paymentMethod->id; + $customer->save(); - return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/paymentmethods/'); + Session::flash('message', trans('texts.payment_method_set_as_default')); + + return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/payment_methods/'); } private function paymentMethodError($type, $error, $accountGateway = false, $exception = false) diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index dcef1b61464e..28c73ba65e3c 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -200,7 +200,7 @@ class InvoiceController extends BaseController $data = array_merge($data, self::getViewModel($invoice)); if ($invoice->isSent() && $invoice->getAutoBillEnabled() && !$invoice->isPaid()) { - $data['autoBillChangeWarning'] = $this->paymentService->getClientRequiresDelayedAutoBill($invoice->client); + $data['autoBillChangeWarning'] = $invoice->client->autoBillLater(); } if ($clone) { diff --git a/app/Http/Controllers/NinjaController.php b/app/Http/Controllers/NinjaController.php new file mode 100644 index 000000000000..11f9238ae6b4 --- /dev/null +++ b/app/Http/Controllers/NinjaController.php @@ -0,0 +1,236 @@ +accountRepo = $accountRepo; + $this->contactMailer = $contactMailer; + } + + private function getLicensePaymentDetails($input, $affiliate) + { + $country = Country::find($input['country_id']); + + $data = [ + 'firstName' => $input['first_name'], + 'lastName' => $input['last_name'], + 'email' => $input['email'], + 'number' => $input['card_number'], + 'expiryMonth' => $input['expiration_month'], + 'expiryYear' => $input['expiration_year'], + 'cvv' => $input['cvv'], + 'billingAddress1' => $input['address1'], + 'billingAddress2' => $input['address2'], + 'billingCity' => $input['city'], + 'billingState' => $input['state'], + 'billingPostcode' => $input['postal_code'], + 'billingCountry' => $country->iso_3166_2, + 'shippingAddress1' => $input['address1'], + 'shippingAddress2' => $input['address2'], + 'shippingCity' => $input['city'], + 'shippingState' => $input['state'], + 'shippingPostcode' => $input['postal_code'], + 'shippingCountry' => $country->iso_3166_2 + ]; + + $card = new CreditCard($data); + + return [ + 'amount' => $affiliate->price, + 'card' => $card, + 'currency' => 'USD', + 'returnUrl' => URL::to('license_complete'), + 'cancelUrl' => URL::to('/') + ]; + } + + public function show_license_payment() + { + if (Input::has('return_url')) { + Session::set('return_url', Input::get('return_url')); + } + + if (Input::has('affiliate_key')) { + if ($affiliate = Affiliate::where('affiliate_key', '=', Input::get('affiliate_key'))->first()) { + Session::set('affiliate_id', $affiliate->id); + } + } + + if (Input::has('product_id')) { + Session::set('product_id', Input::get('product_id')); + } else if (!Session::has('product_id')) { + Session::set('product_id', PRODUCT_ONE_CLICK_INSTALL); + } + + if (!Session::get('affiliate_id')) { + return Utils::fatalError(); + } + + if (Utils::isNinjaDev() && Input::has('test_mode')) { + Session::set('test_mode', Input::get('test_mode')); + } + + $account = $this->accountRepo->getNinjaAccount(); + $account->load('account_gateways.gateway'); + $accountGateway = $account->getGatewayByType(GATEWAY_TYPE_CREDIT_CARD); + $gateway = $accountGateway->gateway; + $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); + + $affiliate = Affiliate::find(Session::get('affiliate_id')); + + $data = [ + 'showBreadcrumbs' => false, + 'hideHeader' => true, + 'url' => 'license', + 'amount' => $affiliate->price, + 'client' => false, + 'contact' => false, + 'gateway' => $gateway, + 'account' => $account, + 'accountGateway' => $accountGateway, + 'acceptedCreditCardTypes' => $acceptedCreditCardTypes, + 'countries' => Cache::get('countries'), + 'currencyId' => 1, + 'currencyCode' => 'USD', + 'paymentTitle' => $affiliate->payment_title, + 'paymentSubtitle' => $affiliate->payment_subtitle, + 'showAddress' => true, + ]; + + return View::make('payments.stripe.credit_card', $data); + } + + public function do_license_payment() + { + $testMode = Session::get('test_mode') === 'true'; + + $rules = array( + 'first_name' => 'required', + 'last_name' => 'required', + 'email' => 'required', + 'card_number' => 'required', + 'expiration_month' => 'required', + 'expiration_year' => 'required', + 'cvv' => 'required', + 'address1' => 'required', + 'city' => 'required', + 'state' => 'required', + 'postal_code' => 'required', + 'country_id' => 'required', + ); + + $validator = Validator::make(Input::all(), $rules); + + if ($validator->fails()) { + return redirect()->to('license') + ->withErrors($validator) + ->withInput(); + } + + $account = $this->accountRepo->getNinjaAccount(); + $account->load('account_gateways.gateway'); + $accountGateway = $account->getGatewayByType(GATEWAY_TYPE_CREDIT_CARD); + + try { + $affiliate = Affiliate::find(Session::get('affiliate_id')); + + if ($testMode) { + $ref = 'TEST_MODE'; + } else { + $details = self::getLicensePaymentDetails(Input::all(), $affiliate); + + $gateway = Omnipay::create($accountGateway->gateway->provider); + $gateway->initialize((array) $accountGateway->getConfig()); + $response = $gateway->purchase($details)->send(); + + $ref = $response->getTransactionReference(); + + if (!$response->isSuccessful() || !$ref) { + $this->error('License', $response->getMessage(), $accountGateway); + return redirect()->to('license')->withInput(); + } + } + + $licenseKey = Utils::generateLicense(); + + $license = new License(); + $license->first_name = Input::get('first_name'); + $license->last_name = Input::get('last_name'); + $license->email = Input::get('email'); + $license->transaction_reference = $ref; + $license->license_key = $licenseKey; + $license->affiliate_id = Session::get('affiliate_id'); + $license->product_id = Session::get('product_id'); + $license->save(); + + $data = [ + 'message' => $affiliate->payment_subtitle, + 'license' => $licenseKey, + 'hideHeader' => true, + 'productId' => $license->product_id, + 'price' => $affiliate->price, + ]; + + $name = "{$license->first_name} {$license->last_name}"; + $this->contactMailer->sendLicensePaymentConfirmation($name, $license->email, $affiliate->price, $license->license_key, $license->product_id); + + if (Session::has('return_url')) { + $data['redirectTo'] = Session::get('return_url')."?license_key={$license->license_key}&product_id=".Session::get('product_id'); + $data['message'] = "Redirecting to " . Session::get('return_url'); + } + + return View::make('public.license', $data); + } catch (\Exception $e) { + $this->error('License-Uncaught', false, $accountGateway, $e); + return redirect()->to('license')->withInput(); + } + } + + public function claim_license() + { + $licenseKey = Input::get('license_key'); + $productId = Input::get('product_id', PRODUCT_ONE_CLICK_INSTALL); + + $license = License::where('license_key', '=', $licenseKey) + ->where('is_claimed', '<', 10) + ->where('product_id', '=', $productId) + ->first(); + + if ($license) { + if ($license->transaction_reference != 'TEST_MODE') { + $license->is_claimed = $license->is_claimed + 1; + $license->save(); + } + + if ($productId == PRODUCT_INVOICE_DESIGNS) { + return file_get_contents(storage_path() . '/invoice_designs.txt'); + } else { + // temporary fix to enable previous version to work + if (Input::get('get_date')) { + return $license->created_at->format('Y-m-d'); + } else { + return 'valid'; + } + } + } else { + return RESULT_FAILURE; + } + } + +} diff --git a/app/Http/Controllers/OnlinePaymentController.php b/app/Http/Controllers/OnlinePaymentController.php new file mode 100644 index 000000000000..5b1229484dd8 --- /dev/null +++ b/app/Http/Controllers/OnlinePaymentController.php @@ -0,0 +1,333 @@ +paymentService = $paymentService; + $this->userMailer = $userMailer; + } + + public function showPayment($invitationKey, $gatewayType = false, $sourceId = false) + { + $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway') + ->where('invitation_key', '=', $invitationKey)->firstOrFail(); + + if ( ! $gatewayType) { + $gatewayType = Session::get($invitation->id . 'gateway_type'); + } + + $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); + + try { + return $paymentDriver->startPurchase(Input::all(), $sourceId); + } catch (Exception $exception) { + return $this->error($paymentDriver, $exception); + } + } + + public function doPayment(CreateOnlinePaymentRequest $request) + { + $invitation = $request->invitation; + $gatewayType = Session::get($invitation->id . 'gateway_type'); + $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); + + try { + $paymentDriver->completeOnsitePurchase($request->all()); + + if ($paymentDriver->isTwoStep()) { + Session::flash('warning', trans('texts.bank_account_verification_next_steps')); + } else { + Session::flash('message', trans('texts.applied_payment')); + } + return redirect()->to('view/' . $invitation->invitation_key); + } catch (Exception $exception) { + return $this->error($paymentDriver, $exception); + } + } + + public function offsitePayment($invitationKey = false, $gatewayType = false) + { + $invitationKey = $invitationKey ?: Session::get('invitation_key'); + $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway') + ->where('invitation_key', '=', $invitationKey)->firstOrFail(); + + $gatewayType = $gatewayType ?: Session::get($invitation->id . 'gateway_type'); + $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); + + if ($error = Input::get('error_description') ?: Input::get('error')) { + return $this->error($paymentDriver, $error); + } + + try { + $paymentDriver->completeOffsitePurchase(Input::all()); + Session::flash('message', trans('texts.applied_payment')); + return redirect()->to('view/' . $invitation->invitation_key); + } catch (Exception $exception) { + return $this->error($paymentDriver, $exception); + } + } + + private function error($paymentDriver, $exception) + { + if (is_string($exception)) { + $displayError = $exception; + $logError = $exception; + } else { + $displayError = $exception->getMessage(); + $logError = Utils::getErrorString($exception); + } + + $message = sprintf('%s: %s', ucwords($paymentDriver->providerName()), $displayError); + Session::flash('error', $message); + + $message = sprintf('Payment Error [%s]: %s', $paymentDriver->providerName(), $logError); + Utils::logError($message, 'PHP', true); + + return redirect()->to('view/' . $paymentDriver->invitation->invitation_key); + } + + public function getBankInfo($routingNumber) { + if (strlen($routingNumber) != 9 || !preg_match('/\d{9}/', $routingNumber)) { + return response()->json([ + 'message' => 'Invalid routing number', + ], 400); + } + + $data = PaymentMethod::lookupBankData($routingNumber); + + if (is_string($data)) { + return response()->json([ + 'message' => $data, + ], 500); + } elseif (!empty($data)) { + return response()->json($data); + } + + return response()->json([ + 'message' => 'Bank not found', + ], 404); + } + + public function handlePaymentWebhook($accountKey, $gatewayId) + { + $gatewayId = intval($gatewayId); + + $account = Account::where('accounts.account_key', '=', $accountKey)->first(); + + if (!$account) { + return response()->json([ + 'message' => 'Unknown account', + ], 404); + } + + $accountGateway = $account->getGatewayConfig(intval($gatewayId)); + + if (!$accountGateway) { + return response()->json([ + 'message' => 'Unknown gateway', + ], 404); + } + + switch($gatewayId) { + case GATEWAY_STRIPE: + return $this->handleStripeWebhook($accountGateway); + case GATEWAY_WEPAY: + return $this->handleWePayWebhook($accountGateway); + default: + return response()->json([ + 'message' => 'Unsupported gateway', + ], 404); + } + } + + protected function handleWePayWebhook($accountGateway) { + $data = Input::all(); + $accountId = $accountGateway->account_id; + + foreach (array_keys($data) as $key) { + if ('_id' == substr($key, -3)) { + $objectType = substr($key, 0, -3); + $objectId = $data[$key]; + break; + } + } + + if (!isset($objectType)) { + return response()->json([ + 'message' => 'Could not find object id parameter', + ], 400); + } + + if ($objectType == 'credit_card') { + $paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $objectId)->first(); + + if (!$paymentMethod) { + return array('message' => 'Unknown payment method'); + } + + $wepay = \Utils::setupWePay($accountGateway); + $source = $wepay->request('credit_card', array( + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => intval($objectId), + )); + + if ($source->state == 'deleted') { + $paymentMethod->delete(); + } else { + $this->paymentService->convertPaymentMethodFromWePay($source, null, $paymentMethod)->save(); + } + + return array('message' => 'Processed successfully'); + } elseif ($objectType == 'account') { + $config = $accountGateway->getConfig(); + if ($config->accountId != $objectId) { + return array('message' => 'Unknown account'); + } + + $wepay = \Utils::setupWePay($accountGateway); + $wepayAccount = $wepay->request('account', array( + 'account_id' => intval($objectId), + )); + + if ($wepayAccount->state == 'deleted') { + $accountGateway->delete(); + } else { + $config->state = $wepayAccount->state; + $accountGateway->setConfig($config); + $accountGateway->save(); + } + + return array('message' => 'Processed successfully'); + } elseif ($objectType == 'checkout') { + $payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $objectId)->first(); + + if (!$payment) { + return array('message' => 'Unknown payment'); + } + + $wepay = \Utils::setupWePay($accountGateway); + $checkout = $wepay->request('checkout', array( + 'checkout_id' => intval($objectId), + )); + + if ($checkout->state == 'refunded') { + $payment->recordRefund(); + } elseif (!empty($checkout->refund) && !empty($checkout->refund->amount_refunded) && ($checkout->refund->amount_refunded - $payment->refunded) > 0) { + $payment->recordRefund($checkout->refund->amount_refunded - $payment->refunded); + } + + if ($checkout->state == 'captured') { + $payment->markComplete(); + } elseif ($checkout->state == 'cancelled') { + $payment->markCancelled(); + } elseif ($checkout->state == 'failed') { + $payment->markFailed(); + } + + return array('message' => 'Processed successfully'); + } else { + return array('message' => 'Ignoring event'); + } + } + + protected function handleStripeWebhook($accountGateway) { + $eventId = Input::get('id'); + $eventType= Input::get('type'); + $accountId = $accountGateway->account_id; + + if (!$eventId) { + return response()->json(['message' => 'Missing event id'], 400); + } + + if (!$eventType) { + return response()->json(['message' => 'Missing event type'], 400); + } + + $supportedEvents = array( + 'charge.failed', + 'charge.succeeded', + 'customer.source.updated', + 'customer.source.deleted', + ); + + if (!in_array($eventType, $supportedEvents)) { + return array('message' => 'Ignoring event'); + } + + // Fetch the event directly from Stripe for security + $eventDetails = $this->paymentService->makeStripeCall($accountGateway, 'GET', 'events/'.$eventId); + + if (is_string($eventDetails) || !$eventDetails) { + return response()->json([ + 'message' => $eventDetails ? $eventDetails : 'Could not get event details.', + ], 500); + } + + if ($eventType != $eventDetails['type']) { + return response()->json(['message' => 'Event type mismatch'], 400); + } + + if (!$eventDetails['pending_webhooks']) { + return response()->json(['message' => 'This is not a pending event'], 400); + } + + + if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded') { + $charge = $eventDetails['data']['object']; + $transactionRef = $charge['id']; + + $payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $transactionRef)->first(); + + if (!$payment) { + return array('message' => 'Unknown payment'); + } + + if ($eventType == 'charge.failed') { + if (!$payment->isFailed()) { + $payment->markFailed($charge['failure_message']); + $this->userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment); + } + } elseif ($eventType == 'charge.succeeded') { + $payment->markComplete(); + } elseif ($eventType == 'charge.refunded') { + $payment->recordRefund($charge['amount_refunded'] / 100 - $payment->refunded); + } + } elseif($eventType == 'customer.source.updated' || $eventType == 'customer.source.deleted') { + $source = $eventDetails['data']['object']; + $sourceRef = $source['id']; + + $paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $sourceRef)->first(); + + if (!$paymentMethod) { + return array('message' => 'Unknown payment method'); + } + + if ($eventType == 'customer.source.deleted') { + $paymentMethod->delete(); + } elseif ($eventType == 'customer.source.updated') { + $this->paymentService->convertPaymentMethodFromStripe($source, null, $paymentMethod)->save(); + } + } + + return array('message' => 'Processed successfully'); + } + +} diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 3dabd2a4c2aa..fe16fcbb9ce4 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -1,33 +1,16 @@ paymentRepo = $paymentRepo; - $this->invoiceRepo = $invoiceRepo; - $this->accountRepo = $accountRepo; $this->contactMailer = $contactMailer; $this->paymentService = $paymentService; - $this->userMailer = $userMailer; } public function index() @@ -121,606 +99,6 @@ class PaymentController extends BaseController return View::make('payments.edit', $data); } - private function getLicensePaymentDetails($input, $affiliate) - { - $data = $this->paymentService->convertInputForOmnipay($input); - $card = new CreditCard($data); - - return [ - 'amount' => $affiliate->price, - 'card' => $card, - 'currency' => 'USD', - 'returnUrl' => URL::to('license_complete'), - 'cancelUrl' => URL::to('/') - ]; - } - - 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; - $account = $client->account; - $useToken = false; - - if ($paymentType) { - $paymentType = 'PAYMENT_TYPE_' . strtoupper($paymentType); - } else { - $paymentType = Session::get($invitation->id . 'payment_type') ?: - $account->account_gateways[0]->getPaymentType(); - } - - $data = array(); - - 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 ($deviceData = Input::get('device_data')) { - Session::put($invitation->id . 'device_data', $deviceData); - } - - Session::put($invitation->id . 'payment_type', PAYMENT_TYPE_BRAINTREE_PAYPAL); - if (!$sourceId || !$details) { - return Redirect::to('view/'.$invitationKey); - } - } elseif ($paymentType == PAYMENT_TYPE_WEPAY_ACH) { - Session::put($invitation->id . 'payment_type', PAYMENT_TYPE_WEPAY_ACH); - - if (!$sourceId) { - return Redirect::to('view/'.$invitationKey); - } - } else { - if ($paymentType == PAYMENT_TYPE_TOKEN) { - $useToken = true; - $accountGateway = $invoice->client->account->getTokenGateway(); - $paymentType = $accountGateway->getPaymentType(); - } else { - $accountGateway = $invoice->client->account->getGatewayByType($paymentType); - } - - Session::put($invitation->id . 'payment_type', $paymentType); - - $gateway = $accountGateway->gateway; - - $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); - - $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; - - // 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, $sourceId); - } - } - - $data += [ - 'accountGateway' => $accountGateway, - 'acceptedCreditCardTypes' => $acceptedCreditCardTypes, - 'gateway' => $gateway, - '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); - } - - if(!empty($data['braintreeClientToken']) || $accountGateway->getPublishableStripeKey()|| $accountGateway->gateway_id == GATEWAY_WEPAY) { - $data['tokenize'] = true; - } - - } - - $data += [ - 'showBreadcrumbs' => false, - 'url' => 'payment/'.$invitationKey, - 'amount' => $invoice->getRequestedAmount(), - 'invoiceNumber' => $invoice->invoice_number, - 'client' => $client, - 'contact' => $invitation->contact, - 'paymentType' => $paymentType, - 'countries' => Cache::get('countries'), - 'currencyId' => $client->getCurrencyId(), - 'currencyCode' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD'), - 'account' => $client->account, - 'sourceId' => $sourceId, - 'clientFontUrl' => $client->account->getFontsUrl(), - ]; - - return View::make('payments.add_paymentmethod', $data); - } - - public function show_license_payment() - { - if (Input::has('return_url')) { - Session::set('return_url', Input::get('return_url')); - } - - if (Input::has('affiliate_key')) { - if ($affiliate = Affiliate::where('affiliate_key', '=', Input::get('affiliate_key'))->first()) { - Session::set('affiliate_id', $affiliate->id); - } - } - - if (Input::has('product_id')) { - Session::set('product_id', Input::get('product_id')); - } else if (!Session::has('product_id')) { - Session::set('product_id', PRODUCT_ONE_CLICK_INSTALL); - } - - if (!Session::get('affiliate_id')) { - return Utils::fatalError(); - } - - if (Utils::isNinjaDev() && Input::has('test_mode')) { - Session::set('test_mode', Input::get('test_mode')); - } - - $account = $this->accountRepo->getNinjaAccount(); - $account->load('account_gateways.gateway'); - $accountGateway = $account->getGatewayByType(PAYMENT_TYPE_STRIPE_CREDIT_CARD); - $gateway = $accountGateway->gateway; - $acceptedCreditCardTypes = $accountGateway->getCreditcardTypes(); - - $affiliate = Affiliate::find(Session::get('affiliate_id')); - - $data = [ - 'showBreadcrumbs' => false, - 'hideHeader' => true, - 'url' => 'license', - 'amount' => $affiliate->price, - 'client' => false, - 'contact' => false, - 'gateway' => $gateway, - 'account' => $account, - 'accountGateway' => $accountGateway, - 'acceptedCreditCardTypes' => $acceptedCreditCardTypes, - 'countries' => Cache::get('countries'), - 'currencyId' => 1, - 'currencyCode' => 'USD', - 'paymentTitle' => $affiliate->payment_title, - 'paymentSubtitle' => $affiliate->payment_subtitle, - 'showAddress' => true, - ]; - - return View::make('payments.add_paymentmethod', $data); - } - - public function do_license_payment() - { - $testMode = Session::get('test_mode') === 'true'; - - $rules = array( - 'first_name' => 'required', - 'last_name' => 'required', - 'card_number' => 'required', - 'expiration_month' => 'required', - 'expiration_year' => 'required', - 'cvv' => 'required', - 'address1' => 'required', - 'city' => 'required', - 'state' => 'required', - 'postal_code' => 'required', - 'country_id' => 'required', - ); - - $validator = Validator::make(Input::all(), $rules); - - if ($validator->fails()) { - return Redirect::to('license') - ->withErrors($validator) - ->withInput(); - } - - $account = $this->accountRepo->getNinjaAccount(); - $account->load('account_gateways.gateway'); - $accountGateway = $account->getGatewayByType(PAYMENT_TYPE_STRIPE_CREDIT_CARD); - - try { - $affiliate = Affiliate::find(Session::get('affiliate_id')); - - if ($testMode) { - $ref = 'TEST_MODE'; - } else { - $details = self::getLicensePaymentDetails(Input::all(), $affiliate); - $response = $this->paymentService->purchase($accountGateway, $details); - $ref = $response->getTransactionReference(); - - if (!$response->isSuccessful() || !$ref) { - $this->error('License', $response->getMessage(), $accountGateway); - return Redirect::to('license')->withInput(); - } - } - - $licenseKey = Utils::generateLicense(); - - $license = new License(); - $license->first_name = Input::get('first_name'); - $license->last_name = Input::get('last_name'); - $license->email = Input::get('email'); - $license->transaction_reference = $ref; - $license->license_key = $licenseKey; - $license->affiliate_id = Session::get('affiliate_id'); - $license->product_id = Session::get('product_id'); - $license->save(); - - $data = [ - 'message' => $affiliate->payment_subtitle, - 'license' => $licenseKey, - 'hideHeader' => true, - 'productId' => $license->product_id, - 'price' => $affiliate->price, - ]; - - $name = "{$license->first_name} {$license->last_name}"; - $this->contactMailer->sendLicensePaymentConfirmation($name, $license->email, $affiliate->price, $license->license_key, $license->product_id); - - if (Session::has('return_url')) { - $data['redirectTo'] = Session::get('return_url')."?license_key={$license->license_key}&product_id=".Session::get('product_id'); - $data['message'] = "Redirecting to " . Session::get('return_url'); - } - - return View::make('public.license', $data); - } catch (\Exception $e) { - $this->error('License-Uncaught', false, $accountGateway, $e); - return Redirect::to('license')->withInput(); - } - } - - public function claim_license() - { - $licenseKey = Input::get('license_key'); - $productId = Input::get('product_id', PRODUCT_ONE_CLICK_INSTALL); - - $license = License::where('license_key', '=', $licenseKey) - ->where('is_claimed', '<', 5) - ->where('product_id', '=', $productId) - ->first(); - - if ($license) { - if ($license->transaction_reference != 'TEST_MODE') { - $license->is_claimed = $license->is_claimed + 1; - $license->save(); - } - - if ($productId == PRODUCT_INVOICE_DESIGNS) { - return file_get_contents(storage_path() . '/invoice_designs.txt'); - } else { - // temporary fix to enable previous version to work - if (Input::get('get_date')) { - return $license->created_at->format('Y-m-d'); - } else { - return 'valid'; - } - } - } else { - return RESULT_FAILURE; - } - } - - public static function processPaymentClientDetails($client, $accountGateway, $paymentType, $onSite = true){ - $rules = ($paymentType == PAYMENT_TYPE_STRIPE_ACH || $paymentType == PAYMENT_TYPE_WEPAY_ACH)? [] : [ - 'first_name' => 'required', - 'last_name' => 'required', - ]; - - if ( !Input::get('sourceToken') && !(Input::get('plaidPublicToken') && Input::get('plaidAccountId'))) { - $rules = array_merge( - $rules, - [ - 'card_number' => 'required', - 'expiration_month' => 'required', - 'expiration_year' => 'required', - 'cvv' => 'required', - ] - ); - } - - $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, [ - 'address1' => 'required', - 'city' => 'required', - 'state' => 'required', - 'postal_code' => 'required', - 'country_id' => 'required', - ]); - } - - if ($onSite) { - $validator = Validator::make(Input::all(), $rules); - - if ($validator->fails()) { - return $validator; - } - - if ($requireAddress && $accountGateway->update_address) { - $client->address1 = trim(Input::get('address1')); - $client->address2 = trim(Input::get('address2')); - $client->city = trim(Input::get('city')); - $client->state = trim(Input::get('state')); - $client->postal_code = trim(Input::get('postal_code')); - $client->country_id = Input::get('country_id'); - $client->save(); - } - } - - return true; - } - - 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; - $paymentType = Session::get($invitation->id . 'payment_type'); - $accountGateway = $account->getGatewayByType($paymentType); - $paymentMethod = null; - - if ($useToken) { - if(!$sourceId) { - Session::flash('error', trans('texts.no_payment_method_specified')); - return Redirect::to('payment/' . $invitationKey)->withInput(Request::except('cvv')); - } else { - $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; - } - } - } - - if (($validator = static::processPaymentClientDetails($client, $accountGateway, $paymentType, $onSite)) !== true) { - return Redirect::to('payment/'.$invitationKey) - ->withErrors($validator) - ->withInput(Request::except('cvv')); - } - - try { - // For offsite payments send the client's details on file - // If we're using a token then we don't need to send any other data - if (!$onSite || $useToken) { - $data = false; - } else { - $data = Input::all(); - } - - $gateway = $this->paymentService->createGateway($accountGateway); - $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway, $data); - $details['paymentType'] = $paymentType; - - // Check for authorization - 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.'/'.$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')); - } - - // check if we're creating/using a billing token - $tokenBillingSupported = false; - $sourceReferenceParam = 'token'; - if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - $tokenBillingSupported = true; - $customerReferenceParam = 'customerReference'; - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $tokenBillingSupported = true; - $sourceReferenceParam = 'paymentMethodToken'; - $customerReferenceParam = 'customerId'; - - $deviceData = Input::get('device_data'); - if (!$deviceData) { - $deviceData = Session::get($invitation->id . 'device_data'); - } - - if($deviceData) { - $details['device_data'] = $deviceData; - } - } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { - $tokenBillingSupported = true; - $customerReferenceParam = false; - } - - if ($tokenBillingSupported) { - if ($useToken) { - if ($customerReferenceParam) { - $details[$customerReferenceParam] = $customerReference; - } - $details[$sourceReferenceParam] = $sourceReference; - unset($details['card']); - } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing') || $paymentType == PAYMENT_TYPE_STRIPE_ACH || $paymentType == PAYMENT_TYPE_WEPAY_ACH) { - $token = $this->paymentService->createToken($paymentType, $gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */); - if ($token) { - $details[$sourceReferenceParam] = $token; - if ($customerReferenceParam) { - $details[$customerReferenceParam] = $customerReference; - } - - if ($paymentType == PAYMENT_TYPE_STRIPE_ACH && empty(Input::get('plaidPublicToken')) ) { - // 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')); - } - } - } - - $response = $this->paymentService->purchase($accountGateway, $details); - - if ($accountGateway->gateway_id == GATEWAY_EWAY) { - $ref = $response->getData()['AccessCode']; - } elseif ($accountGateway->gateway_id == GATEWAY_TWO_CHECKOUT) { - $ref = $response->getData()['cart_order_id']; - } elseif ($accountGateway->gateway_id == GATEWAY_PAYFAST) { - $ref = $response->getData()['m_payment_id']; - } elseif ($accountGateway->gateway_id == GATEWAY_GOCARDLESS) { - $ref = $response->getData()['signature']; - } elseif ($accountGateway->gateway_id == GATEWAY_CYBERSOURCE) { - $ref = $response->getData()['transaction_uuid']; - } else { - $ref = $response->getTransactionReference(); - } - - if (!$ref) { - $this->error('No-Ref', $response->getMessage(), $accountGateway); - - if ($onSite && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL && $paymentType != PAYMENT_TYPE_WEPAY_ACH) { - return Redirect::to('payment/'.$invitationKey) - ->withInput(Request::except('cvv')); - } else { - return Redirect::to('view/'.$invitationKey); - } - } - - if ($response->isSuccessful()) { - $payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, null, $details, $paymentMethod, $response); - Session::flash('message', trans('texts.applied_payment')); - - if ($account->account_key == NINJA_ACCOUNT_KEY) { - Session::flash('trackEventCategory', '/account'); - Session::flash('trackEventAction', '/buy_pro_plan'); - Session::flash('trackEventAmount', $payment->amount); - } - - return Redirect::to('view/'.$payment->invitation->invitation_key); - } elseif ($response->isRedirect()) { - - $invitation->transaction_reference = $ref; - $invitation->save(); - Session::put('transaction_reference', $ref); - Session::save(); - $response->redirect(); - } else { - $this->error('Unknown', $response->getMessage(), $accountGateway); - if ($onSite && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL && $paymentType != PAYMENT_TYPE_WEPAY_ACH) { - return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); - } else { - return Redirect::to('view/'.$invitationKey); - } - } - } catch (\Exception $e) { - $this->error('Uncaught', false, $accountGateway, $e); - if ($onSite && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL && $paymentType != PAYMENT_TYPE_WEPAY_ACH) { - return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv')); - } else { - return Redirect::to('view/'.$invitationKey); - } - } - } - - public function offsite_payment() - { - $payerId = Request::query('PayerID'); - $token = Request::query('token'); - - if (!$token) { - $token = Session::pull('transaction_reference'); - } - if (!$token) { - return redirect(NINJA_WEB_URL); - } - - $invitation = Invitation::with('invoice.client.currency', 'invoice.client.account.account_gateways.gateway')->where('transaction_reference', '=', $token)->firstOrFail(); - $invoice = $invitation->invoice; - $client = $invoice->client; - $account = $client->account; - - if ($payerId) { - $paymentType = PAYMENT_TYPE_PAYPAL; - } else { - $paymentType = Session::get($invitation->id . 'payment_type'); - } - if (!$paymentType) { - $this->error('No-Payment-Type', false, false); - return Redirect::to($invitation->getLink()); - } - $accountGateway = $account->getGatewayByType($paymentType); - $gateway = $this->paymentService->createGateway($accountGateway); - - // Check for Dwolla payment error - if ($accountGateway->isGateway(GATEWAY_DWOLLA) && Input::get('error')) { - $this->error('Dwolla', Input::get('error_description'), $accountGateway); - return Redirect::to($invitation->getLink()); - } - - // PayFast transaction referencce - if ($accountGateway->isGateway(GATEWAY_PAYFAST) && Request::has('pt')) { - $token = Request::query('pt'); - } - - try { - if ($accountGateway->isGateway(GATEWAY_CYBERSOURCE)) { - if (Input::get('decision') == 'ACCEPT') { - $payment = $this->paymentService->createPayment($invitation, $accountGateway, $token, $payerId); - Session::flash('message', trans('texts.applied_payment')); - } else { - $message = Input::get('message') . ': ' . Input::get('invalid_fields'); - Session::flash('error', $message); - } - return Redirect::to($invitation->getLink()); - } elseif (method_exists($gateway, 'completePurchase') - && !$accountGateway->isGateway(GATEWAY_TWO_CHECKOUT) - && !$accountGateway->isGateway(GATEWAY_CHECKOUT_COM)) { - $details = $this->paymentService->getPaymentDetails($invitation, $accountGateway, array()); - - $response = $this->paymentService->completePurchase($gateway, $accountGateway, $details, $token); - - $ref = $response->getTransactionReference() ?: $token; - - if ($response->isCancelled()) { - // do nothing - } elseif ($response->isSuccessful()) { - $payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, $payerId, $details, null, $purchaseResponse); - Session::flash('message', trans('texts.applied_payment')); - } else { - $this->error('offsite', $response->getMessage(), $accountGateway); - } - return Redirect::to($invitation->getLink()); - } else { - $payment = $this->paymentService->createPayment($invitation, $accountGateway, $token, $payerId); - Session::flash('message', trans('texts.applied_payment')); - return Redirect::to($invitation->getLink()); - } - } catch (\Exception $e) { - $this->error('Offsite-uncaught', false, $accountGateway, $e); - return Redirect::to($invitation->getLink()); - } - } - public function store(CreatePaymentRequest $request) { $input = $request->input(); @@ -760,245 +138,7 @@ class PaymentController extends BaseController Session::flash('message', $message); } - return Redirect::to('payments'); + return redirect()->to('payments'); } - private function error($type, $error, $accountGateway = false, $exception = false) - { - $message = ''; - if ($accountGateway && $accountGateway->gateway) { - $message = $accountGateway->gateway->name . ': '; - } - $message .= $error ?: trans('texts.payment_error'); - - 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 = PaymentMethod::lookupBankData($routingNumber); - - if (is_string($data)) { - return response()->json([ - 'message' => $data, - ], 500); - } elseif (!empty($data)) { - return response()->json($data); - } - - return response()->json([ - 'message' => 'Bank not found', - ], 404); - } - - public function handlePaymentWebhook($accountKey, $gatewayId) - { - $gatewayId = intval($gatewayId); - - $account = Account::where('accounts.account_key', '=', $accountKey)->first(); - - if (!$account) { - return response()->json([ - 'message' => 'Unknown account', - ], 404); - } - - $accountGateway = $account->getGatewayConfig(intval($gatewayId)); - - if (!$accountGateway) { - return response()->json([ - 'message' => 'Unknown gateway', - ], 404); - } - - switch($gatewayId) { - case GATEWAY_STRIPE: - return $this->handleStripeWebhook($accountGateway); - case GATEWAY_WEPAY: - return $this->handleWePayWebhook($accountGateway); - default: - return response()->json([ - 'message' => 'Unsupported gateway', - ], 404); - } - } - - protected function handleWePayWebhook($accountGateway) { - $data = Input::all(); - $accountId = $accountGateway->account_id; - - foreach (array_keys($data) as $key) { - if ('_id' == substr($key, -3)) { - $objectType = substr($key, 0, -3); - $objectId = $data[$key]; - break; - } - } - - if (!isset($objectType)) { - return response()->json([ - 'message' => 'Could not find object id parameter', - ], 400); - } - - if ($objectType == 'credit_card') { - $paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $objectId)->first(); - - if (!$paymentMethod) { - return array('message' => 'Unknown payment method'); - } - - $wepay = \Utils::setupWePay($accountGateway); - $source = $wepay->request('credit_card', array( - 'client_id' => WEPAY_CLIENT_ID, - 'client_secret' => WEPAY_CLIENT_SECRET, - 'credit_card_id' => intval($objectId), - )); - - if ($source->state == 'deleted') { - $paymentMethod->delete(); - } else { - $this->paymentService->convertPaymentMethodFromWePay($source, null, $paymentMethod)->save(); - } - - return array('message' => 'Processed successfully'); - } elseif ($objectType == 'account') { - $config = $accountGateway->getConfig(); - if ($config->accountId != $objectId) { - return array('message' => 'Unknown account'); - } - - $wepay = \Utils::setupWePay($accountGateway); - $wepayAccount = $wepay->request('account', array( - 'account_id' => intval($objectId), - )); - - if ($wepayAccount->state == 'deleted') { - $accountGateway->delete(); - } else { - $config->state = $wepayAccount->state; - $accountGateway->setConfig($config); - $accountGateway->save(); - } - - return array('message' => 'Processed successfully'); - } elseif ($objectType == 'checkout') { - $payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $objectId)->first(); - - if (!$payment) { - return array('message' => 'Unknown payment'); - } - - $wepay = \Utils::setupWePay($accountGateway); - $checkout = $wepay->request('checkout', array( - 'checkout_id' => intval($objectId), - )); - - if ($checkout->state == 'refunded') { - $payment->recordRefund(); - } elseif (!empty($checkout->refund) && !empty($checkout->refund->amount_refunded) && ($checkout->refund->amount_refunded - $payment->refunded) > 0) { - $payment->recordRefund($checkout->refund->amount_refunded - $payment->refunded); - } - - if ($checkout->state == 'captured') { - $payment->markComplete(); - } elseif ($checkout->state == 'cancelled') { - $payment->markCancelled(); - } elseif ($checkout->state == 'failed') { - $payment->markFailed(); - } - - return array('message' => 'Processed successfully'); - } else { - return array('message' => 'Ignoring event'); - } - } - - protected function handleStripeWebhook($accountGateway) { - $eventId = Input::get('id'); - $eventType= Input::get('type'); - $accountId = $accountGateway->account_id; - - if (!$eventId) { - return response()->json(['message' => 'Missing event id'], 400); - } - - if (!$eventType) { - return response()->json(['message' => 'Missing event type'], 400); - } - - $supportedEvents = array( - 'charge.failed', - 'charge.succeeded', - 'customer.source.updated', - 'customer.source.deleted', - ); - - if (!in_array($eventType, $supportedEvents)) { - return array('message' => 'Ignoring event'); - } - - // Fetch the event directly from Stripe for security - $eventDetails = $this->paymentService->makeStripeCall($accountGateway, 'GET', 'events/'.$eventId); - - if (is_string($eventDetails) || !$eventDetails) { - return response()->json([ - 'message' => $eventDetails ? $eventDetails : 'Could not get event details.', - ], 500); - } - - if ($eventType != $eventDetails['type']) { - return response()->json(['message' => 'Event type mismatch'], 400); - } - - if (!$eventDetails['pending_webhooks']) { - return response()->json(['message' => 'This is not a pending event'], 400); - } - - - if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded') { - $charge = $eventDetails['data']['object']; - $transactionRef = $charge['id']; - - $payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $transactionRef)->first(); - - if (!$payment) { - return array('message' => 'Unknown payment'); - } - - if ($eventType == 'charge.failed') { - if (!$payment->isFailed()) { - $payment->markFailed($charge['failure_message']); - $this->userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment); - } - } elseif ($eventType == 'charge.succeeded') { - $payment->markComplete(); - } elseif ($eventType == 'charge.refunded') { - $payment->recordRefund($charge['amount_refunded'] / 100 - $payment->refunded); - } - } elseif($eventType == 'customer.source.updated' || $eventType == 'customer.source.deleted') { - $source = $eventDetails['data']['object']; - $sourceRef = $source['id']; - - $paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $sourceRef)->first(); - - if (!$paymentMethod) { - return array('message' => 'Unknown payment method'); - } - - if ($eventType == 'customer.source.deleted') { - $paymentMethod->delete(); - } elseif ($eventType == 'customer.source.updated') { - $this->paymentService->convertPaymentMethodFromStripe($source, null, $paymentMethod)->save(); - } - } - - return array('message' => 'Processed successfully'); - } } diff --git a/app/Http/Requests/CreateOnlinePaymentRequest.php b/app/Http/Requests/CreateOnlinePaymentRequest.php new file mode 100644 index 000000000000..815c8f5b217d --- /dev/null +++ b/app/Http/Requests/CreateOnlinePaymentRequest.php @@ -0,0 +1,46 @@ +invitation->account; + + $paymentDriver = $account->paymentDriver($this->invitation, $this->gateway_type); + + return $paymentDriver->rules(); + } + + public function sanitize() + { + $input = $this->all(); + + $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.currency', 'invoice.client.account.account_gateways.gateway') + ->where('invitation_key', '=', $this->invitation_key) + ->firstOrFail(); + + $input['invitation'] = $invitation; + $input['gateway_type'] = session($invitation->id . 'gateway_type'); + + $this->replace($input); + + return $this->all(); + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index ab1bd91d3273..4413174f5ec3 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -41,15 +41,16 @@ Route::group(['middleware' => 'auth:client'], function() { Route::get('download/{invitation_key}', 'ClientPortalController@download'); Route::get('view', 'HomeController@viewLogo'); Route::get('approve/{invitation_key}', 'QuoteController@approve'); - 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', 'ClientPortalController@paymentMethods'); - Route::post('client/paymentmethods/verify', 'ClientPortalController@verifyPaymentMethod'); - Route::get('client/paymentmethods/add/{payment_type}/{source_id?}', 'ClientPortalController@addPaymentMethod'); - Route::post('client/paymentmethods/add/{payment_type}', 'ClientPortalController@postAddPaymentMethod'); - Route::post('client/paymentmethods/default', 'ClientPortalController@setDefaultPaymentMethod'); - Route::post('client/paymentmethods/{source_id}/remove', 'ClientPortalController@removePaymentMethod'); + Route::get('payment/{invitation_key}/{gateway_type?}/{source_id?}', 'OnlinePaymentController@showPayment'); + Route::post('payment/{invitation_key}', 'OnlinePaymentController@doPayment'); + Route::match(['GET', 'POST'], 'complete/{invitation_key?}/{gateway_type?}', 'OnlinePaymentController@offsitePayment'); + Route::get('bank/{routing_number}', 'OnlinePaymentController@getBankInfo'); + Route::get('client/payment_methods', 'ClientPortalController@paymentMethods'); + Route::post('client/payment_methods/verify', 'ClientPortalController@verifyPaymentMethod'); + //Route::get('client/payment_methods/add/{gateway_type}/{source_id?}', 'ClientPortalController@addPaymentMethod'); + //Route::post('client/payment_methods/add/{gateway_type}', 'ClientPortalController@postAddPaymentMethod'); + Route::post('client/payment_methods/default', 'ClientPortalController@setDefaultPaymentMethod'); + Route::post('client/payment_methods/{source_id}/remove', 'ClientPortalController@removePaymentMethod'); Route::get('client/quotes', 'ClientPortalController@quoteIndex'); Route::get('client/invoices', 'ClientPortalController@invoiceIndex'); Route::get('client/invoices/recurring', 'ClientPortalController@recurringInvoiceIndex'); @@ -71,11 +72,10 @@ Route::group(['middleware' => 'auth:client'], function() { }); -Route::get('bank/{routing_number}', 'PaymentController@getBankInfo'); Route::post('paymenthook/{accountKey}/{gatewayId}', 'PaymentController@handlePaymentWebhook'); -Route::get('license', 'PaymentController@show_license_payment'); -Route::post('license', 'PaymentController@do_license_payment'); -Route::get('claim_license', 'PaymentController@claim_license'); +Route::get('license', 'NinjaController@show_license_payment'); +Route::post('license', 'NinjaController@do_license_payment'); +Route::get('claim_license', 'NinjaController@claim_license'); Route::post('signup/validate', 'AccountController@checkEmail'); Route::post('signup/submit', 'AccountController@submitSignup'); @@ -557,7 +557,6 @@ if (!defined('CONTACT_EMAIL')) { define('PAYMENT_LIBRARY_PHP_PAYMENTS', 2); define('GATEWAY_AUTHORIZE_NET', 1); - define('GATEWAY_AUTHORIZE_NET_SIM', 2); define('GATEWAY_EWAY', 4); define('GATEWAY_MOLLIE', 9); define('GATEWAY_PAYFAST', 13); @@ -661,7 +660,7 @@ if (!defined('CONTACT_EMAIL')) { define('PAYMENT_TYPE_EUROCARD', 11); define('PAYMENT_TYPE_NOVA', 12); define('PAYMENT_TYPE_CREDIT_CARD_OTHER', 13); - define('PAYMENT_TYPE_ID_PAYPAL', 14); + define('PAYMENT_TYPE_PAYPAL', 14); define('PAYMENT_TYPE_CARTE_BLANCHE', 17); define('PAYMENT_TYPE_UNIONPAY', 18); define('PAYMENT_TYPE_JCB', 19); @@ -674,18 +673,12 @@ if (!defined('CONTACT_EMAIL')) { define('PAYMENT_METHOD_STATUS_VERIFICATION_FAILED', 'verification_failed'); define('PAYMENT_METHOD_STATUS_VERIFIED', 'verified'); - 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_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'); - define('PAYMENT_TYPE_DWOLLA', 'PAYMENT_TYPE_DWOLLA'); - define('PAYMENT_TYPE_TOKEN', 'PAYMENT_TYPE_TOKEN'); - define('PAYMENT_TYPE_ANY', 'PAYMENT_TYPE_ANY'); + define('GATEWAY_TYPE_CREDIT_CARD', 'credit_card'); + define('GATEWAY_TYPE_BANK_TRANSFER', 'bank_transfer'); + define('GATEWAY_TYPE_PAYPAL', 'paypal'); + define('GATEWAY_TYPE_BITCOIN', 'bitcoin'); + define('GATEWAY_TYPE_DWOLLA', 'dwolla'); + define('GATEWAY_TYPE_TOKEN', 'token'); define('REMINDER1', 'reminder1'); define('REMINDER2', 'reminder2'); diff --git a/app/Listeners/CreditListener.php b/app/Listeners/CreditListener.php index aad2dd4d65b8..fe8e174ab3ed 100644 --- a/app/Listeners/CreditListener.php +++ b/app/Listeners/CreditListener.php @@ -19,7 +19,7 @@ class CreditListener public function deletedPayment(PaymentWasDeleted $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; @@ -28,7 +28,7 @@ class CreditListener $credit = Credit::createNew(); $credit->client_id = $payment->client_id; $credit->credit_date = Carbon::now()->toDateTimeString(); - $credit->balance = $credit->amount = $payment->amount - $payment->refunded; + $credit->balance = $credit->amount = $payment->getCompletedAmount(); $credit->private_notes = $payment->transaction_reference; $credit->save(); } @@ -36,7 +36,7 @@ class CreditListener 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; diff --git a/app/Listeners/InvoiceListener.php b/app/Listeners/InvoiceListener.php index a797c418ef25..d5d20ae991c6 100644 --- a/app/Listeners/InvoiceListener.php +++ b/app/Listeners/InvoiceListener.php @@ -61,7 +61,7 @@ class InvoiceListener { $payment = $event->payment; $invoice = $payment->invoice; - $adjustment = $payment->amount - $payment->refunded; + $adjustment = $payment->getCompletedAmount(); $invoice->updateBalances($adjustment); $invoice->updatePaidStatus(); @@ -91,7 +91,7 @@ class InvoiceListener { $payment = $event->payment; $invoice = $payment->invoice; - $adjustment = $payment->amount - $payment->refunded; + $adjustment = $payment->getCompletedAmount(); $invoice->updateBalances($adjustment); $invoice->updatePaidStatus(); @@ -105,7 +105,7 @@ class InvoiceListener $payment = $event->payment; $invoice = $payment->invoice; - $adjustment = ($payment->amount - $payment->refunded) * -1; + $adjustment = $payment->getCompletedAmount() * -1; $invoice->updateBalances($adjustment); $invoice->updatePaidStatus(); diff --git a/app/Models/Account.php b/app/Models/Account.php index 7df9218438bf..e1c849ed80aa 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -384,37 +384,44 @@ class Account extends Eloquent return $format; } - public function getGatewayByType($type = PAYMENT_TYPE_ANY, $exceptFor = null) + /* + public function defaultGatewayType() { - if ($type == PAYMENT_TYPE_STRIPE_ACH || $type == PAYMENT_TYPE_STRIPE_CREDIT_CARD) { - $type = PAYMENT_TYPE_STRIPE; + $accountGateway = $this->account_gateways[0]; + $paymentDriver = $accountGateway->paymentDriver(); + + return $paymentDriver->gatewayTypes()[0]; + } + */ + + public function getGatewayByType($type = false) + { + if ( ! $this->relationLoaded('account_gateways')) { + $this->load('account_gateways'); } - if ($type == PAYMENT_TYPE_WEPAY_ACH) { - return $this->getGatewayConfig(GATEWAY_WEPAY); - } - - foreach ($this->account_gateways as $gateway) { - if ($exceptFor && ($gateway->id == $exceptFor->id)) { - continue; + foreach ($this->account_gateways as $accountGateway) { + if ( ! $type) { + return $accountGateway; } - 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; + $paymentDriver = $accountGateway->paymentDriver(); + + if ($paymentDriver->handles($type)) { + return $accountGateway; } } return false; } + public function paymentDriver($invitation = false, $gatewayType = false) + { + $accountGateway = $this->getGatewayByType($gatewayType); + + return $accountGateway->paymentDriver($invitation, $gatewayType); + } + public function gatewayIds() { return $this->account_gateways()->pluck('gateway_id')->toArray(); @@ -1437,18 +1444,6 @@ class Account extends Eloquent public function getFontFolders(){ return array_map(function($item){return $item['folder'];}, $this->getFontsData()); } - - public function canAddGateway($type){ - if ($type == PAYMENT_TYPE_STRIPE) { - $type == PAYMENT_TYPE_CREDIT_CARD; - } - - if($this->getGatewayByType($type)) { - return false; - } - - return true; - } } Account::updated(function ($account) diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php index 563fe16720fb..fec6cc722c9c 100644 --- a/app/Models/AccountGateway.php +++ b/app/Models/AccountGateway.php @@ -3,10 +3,14 @@ use Crypt; use App\Models\Gateway; use Illuminate\Database\Eloquent\SoftDeletes; +use Laracasts\Presenter\PresentableTrait; class AccountGateway extends EntityModel { use SoftDeletes; + use PresentableTrait; + + protected $presenter = 'App\Ninja\Presenters\AccountGatewayPresenter'; protected $dates = ['deleted_at']; public function getEntityType() @@ -33,14 +37,18 @@ class AccountGateway extends EntityModel return $arrayOfImages; } - public function getPaymentType() + public function paymentDriver($invitation = false, $gatewayType = false) { - return Gateway::getPaymentType($this->gateway_id); - } - - public function isPaymentType($type) - { - return $this->getPaymentType() == $type; + $folder = "App\\Ninja\\PaymentDrivers\\"; + $class = $folder . $this->gateway->provider . 'PaymentDriver'; + $class = str_replace('_', '', $class); + + if (class_exists($class)) { + return new $class($this, $invitation, $gatewayType); + } else { + $baseClass = $folder . "BasePaymentDriver"; + return new $baseClass($this, $invitation, $gatewayType); + } } public function isGateway($gatewayId) @@ -131,4 +139,3 @@ class AccountGateway extends EntityModel return \URL::to(env('WEBHOOK_PREFIX','').'paymenthook/'.$account->account_key.'/'.$this->gateway_id.env('WEBHOOK_SUFFIX','')); } } - diff --git a/app/Models/AccountGatewayToken.php b/app/Models/AccountGatewayToken.php index 06494325662e..363bd26e86a2 100644 --- a/app/Models/AccountGatewayToken.php +++ b/app/Models/AccountGatewayToken.php @@ -9,17 +9,54 @@ class AccountGatewayToken extends Eloquent protected $dates = ['deleted_at']; public $timestamps = true; - protected $casts = [ - 'uses_local_payment_methods' => 'boolean', - ]; + protected $casts = []; public function payment_methods() { return $this->hasMany('App\Models\PaymentMethod'); } + public function account_gateway() + { + return $this->belongsTo('App\Models\AccountGateway'); + } + public function default_payment_method() { return $this->hasOne('App\Models\PaymentMethod', 'id', 'default_payment_method_id'); } -} \ No newline at end of file + + public function autoBillLater() + { + return $this->default_payment_method->requiresDelayedAutoBill(); + } + + public function scopeClientAndGateway($query, $clientId, $accountGatewayId) + { + $query->where('client_id', '=', $clientId) + ->where('account_gateway_id', '=', $accountGatewayId); + + return $query; + } + + public function gatewayName() + { + return $this->account_gateway->gateway->name; + } + + public function gatewayLink() + { + $accountGateway = $this->account_gateway; + + if ($accountGateway->gateway_id == GATEWAY_STRIPE) { + return "https://dashboard.stripe.com/customers/{$this->token}"; + } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { + $merchantId = $accountGateway->getConfig()->merchantId; + $testMode = $accountGateway->getConfig()->testMode; + return $testMode ? "https://sandbox.braintreegateway.com/merchants/{$merchantId}/customers/{$this->token}" : "https://www.braintreegateway.com/merchants/{$merchantId}/customers/{$this->token}"; + } else { + return false; + } + } + +} diff --git a/app/Models/Client.php b/app/Models/Client.php index 0198d46c200d..e573b5feba2c 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -5,6 +5,7 @@ use DB; use Carbon; use Laracasts\Presenter\PresentableTrait; use Illuminate\Database\Eloquent\SoftDeletes; +use App\Models\AccountGatewayToken; class Client extends EntityModel { @@ -154,7 +155,7 @@ class Client extends EntityModel $contact = Contact::createNew(); $contact->send_invoice = true; } - + if (Utils::hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $this->account->enable_portal_password){ if(!empty($data['password']) && $data['password']!='-%unchanged%-'){ $contact->password = bcrypt($data['password']); @@ -162,7 +163,7 @@ class Client extends EntityModel $contact->password = null; } } - + $contact->fill($data); $contact->is_primary = $isPrimary; @@ -177,7 +178,7 @@ class Client extends EntityModel $this->balance = $this->balance + $balanceAdjustment; $this->paid_to_date = $this->paid_to_date + $paidToDateAdjustment; - + $this->save(); } @@ -198,20 +199,20 @@ class Client extends EntityModel { return $this->name; } - + public function getPrimaryContact() { return $this->contacts() ->whereIsPrimary(true) ->first(); } - + public function getDisplayName() { if ($this->name) { return $this->name; } - + if ( ! count($this->contacts)) { return ''; } @@ -260,49 +261,28 @@ class Client extends EntityModel } } - - public function getGatewayToken(&$accountGateway = null, &$token = null) + public function getGatewayToken() { - $account = $this->account; - - if ( ! $account->relationLoaded('account_gateways')) { - $account->load('account_gateways'); - } + $accountGateway = $this->account->getGatewayByType(GATEWAY_TYPE_TOKEN); - if (!count($account->account_gateways)) { - return false; - } - - if (!$accountGateway){ - $accountGateway = $account->getTokenGateway(); - } - - if (!$accountGateway) { + if ( ! $accountGateway) { return false; } - $token = AccountGatewayToken::where('client_id', '=', $this->id) - ->where('account_gateway_id', '=', $accountGateway->id)->first(); - - return $token ? $token->token : false; + return AccountGatewayToken::clientAndGateway($this->id, $accountGateway->id)->first(); } - public function getGatewayLink(&$accountGateway = null) + public function autoBillLater() { - $token = $this->getGatewayToken($accountGateway); - if (!$token) { - return false; + if ($token = $this->getGatewayToken()) { + if ($this->account->auto_bill_on_due_date) { + return true; + } + + return $token->autoBillLater(); } - if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - return "https://dashboard.stripe.com/customers/{$token}"; - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $merchantId = $accountGateway->getConfig()->merchantId; - $testMode = $accountGateway->getConfig()->testMode; - return $testMode ? "https://sandbox.braintreegateway.com/merchants/{$merchantId}/customers/{$token}" : "https://www.braintreegateway.com/merchants/{$merchantId}/customers/{$token}"; - } else { - return false; - } + return false; } public function getAmount() @@ -323,6 +303,19 @@ class Client extends EntityModel return $this->account->currency_id ?: DEFAULT_CURRENCY; } + public function getCurrencyCode() + { + if ($this->currency) { + return $this->currency->code; + } + + if (!$this->account) { + $this->load('account'); + } + + return $this->account->currency ? $this->account->currency->code : 'USD'; + } + public function getCounter($isQuote) { return $isQuote ? $this->quote_number_counter : $this->invoice_number_counter; diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 6df8af5e5f33..57064bb73f91 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -8,6 +8,15 @@ class Gateway extends Eloquent { public $timestamps = true; + public static $gatewayTypes = [ + GATEWAY_TYPE_CREDIT_CARD, + GATEWAY_TYPE_BANK_TRANSFER, + GATEWAY_TYPE_PAYPAL, + GATEWAY_TYPE_BITCOIN, + GATEWAY_TYPE_DWOLLA, + GATEWAY_TYPE_TOKEN, + ]; + // these will appear in the primary gateway select // the rest are shown when selecting 'more options' public static $preferred = [ @@ -26,16 +35,6 @@ class Gateway extends Eloquent GATEWAY_DWOLLA, ]; - // TODO remove this - public static $paymentTypes = [ - PAYMENT_TYPE_STRIPE, - PAYMENT_TYPE_CREDIT_CARD, - PAYMENT_TYPE_PAYPAL, - PAYMENT_TYPE_BITCOIN, - PAYMENT_TYPE_DIRECT_DEBIT, - PAYMENT_TYPE_DWOLLA, - ]; - public static $hiddenFields = [ // PayPal 'headerImageUrl', @@ -103,21 +102,11 @@ class Gateway extends Eloquent } } - /* - public static function getPaymentTypeLinks() { - $data = []; - foreach (self::$paymentTypes as $type) { - $data[] = Utils::toCamelCase(strtolower(str_replace('PAYMENT_TYPE_', '', $type))); - } - return $data; - } - */ - public function getHelp() { $link = ''; - if ($this->id == GATEWAY_AUTHORIZE_NET || $this->id == GATEWAY_AUTHORIZE_NET_SIM) { + if ($this->id == GATEWAY_AUTHORIZE_NET) { $link = 'http://reseller.authorize.net/application/?id=5560364'; } elseif ($this->id == GATEWAY_PAYPAL_EXPRESS) { $link = 'https://www.paypal.com/us/cgi-bin/webscr?cmd=_login-api-run'; @@ -141,24 +130,4 @@ class Gateway extends Eloquent { return Omnipay::create($this->provider)->getDefaultParameters(); } - - public static function getPaymentType($gatewayId) { - if ($gatewayId == GATEWAY_PAYPAL_EXPRESS) { - return PAYMENT_TYPE_PAYPAL; - } else if ($gatewayId == GATEWAY_BITPAY) { - return PAYMENT_TYPE_BITCOIN; - } else if ($gatewayId == GATEWAY_DWOLLA) { - return PAYMENT_TYPE_DWOLLA; - } else if ($gatewayId == GATEWAY_GOCARDLESS) { - return PAYMENT_TYPE_DIRECT_DEBIT; - } else if ($gatewayId == GATEWAY_STRIPE) { - return PAYMENT_TYPE_STRIPE; - } else { - return PAYMENT_TYPE_CREDIT_CARD; - } - } - - public static function getPrettyPaymentType($gatewayId) { - return trans('texts.' . strtolower(Gateway::getPaymentType($gatewayId))); - } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index fad5832da867..5a645b333067 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -890,13 +890,13 @@ class Invoice extends EntityModel implements BalanceAffecting if ($this->tax_name1) { $invoiceTaxAmount = round($taxable * ($this->tax_rate1 / 100), 2); - $invoicePaidAmount = $this->amount && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0; + $invoicePaidAmount = floatVal($this->amount) && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0; $this->calculateTax($taxes, $this->tax_name1, $this->tax_rate1, $invoiceTaxAmount, $invoicePaidAmount); } if ($this->tax_name2) { $invoiceTaxAmount = round($taxable * ($this->tax_rate2 / 100), 2); - $invoicePaidAmount = $this->amount && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0; + $invoicePaidAmount = floatVal($this->amount) && $invoiceTaxAmount ? ($paidAmount / $this->amount * $invoiceTaxAmount) : 0; $this->calculateTax($taxes, $this->tax_name2, $this->tax_rate2, $invoiceTaxAmount, $invoicePaidAmount); } @@ -905,13 +905,13 @@ class Invoice extends EntityModel implements BalanceAffecting if ($invoiceItem->tax_name1) { $itemTaxAmount = round($taxable * ($invoiceItem->tax_rate1 / 100), 2); - $itemPaidAmount = $this->amount && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0; + $itemPaidAmount = floatVal($this->amount) && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0; $this->calculateTax($taxes, $invoiceItem->tax_name1, $invoiceItem->tax_rate1, $itemTaxAmount, $itemPaidAmount); } if ($invoiceItem->tax_name2) { $itemTaxAmount = round($taxable * ($invoiceItem->tax_rate2 / 100), 2); - $itemPaidAmount = $this->amount && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0; + $itemPaidAmount = floatVal($this->amount) && $itemTaxAmount ? ($paidAmount / $this->amount * $itemTaxAmount) : 0; $this->calculateTax($taxes, $invoiceItem->tax_name2, $invoiceItem->tax_rate2, $itemTaxAmount, $itemPaidAmount); } } diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 288bc566a8f1..a3f094315782 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -118,33 +118,41 @@ class Payment extends EntityModel public function recordRefund($amount = null) { - if (!$this->isRefunded() && !$this->isVoided()) { - 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)); - } + if ($this->isRefunded() || $this->isVoided()) { + return false; } + + 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)); + } + + return true; } public function markVoided() { - if (!$this->isVoided() && !$this->isPartiallyRefunded() && !$this->isRefunded()) { - $this->refunded = $this->amount; - $this->payment_status_id = PAYMENT_STATUS_VOIDED; - $this->save(); - - Event::fire(new PaymentWasVoided($this)); + if ($this->isVoided() || $this->isPartiallyRefunded() || $this->isRefunded()) { + return false; } + + $this->refunded = $this->amount; + $this->payment_status_id = PAYMENT_STATUS_VOIDED; + $this->save(); + + Event::fire(new PaymentWasVoided($this)); + + return true; } public function markComplete() diff --git a/app/Models/PaymentMethod.php b/app/Models/PaymentMethod.php index 8c3ea8b2dfd8..51b919946ff1 100644 --- a/app/Models/PaymentMethod.php +++ b/app/Models/PaymentMethod.php @@ -8,31 +8,11 @@ class PaymentMethod extends EntityModel { use SoftDeletes; - protected $dates = ['deleted_at']; public $timestamps = true; + + protected $dates = ['deleted_at']; protected $hidden = ['id']; - public static function createNew($accountGatewayToken = null) - { - $entity = new PaymentMethod(); - - $entity->account_id = $accountGatewayToken->account_id; - $entity->account_gateway_token_id = $accountGatewayToken->id; - - $lastEntity = static::scope(false, $entity->account_id); - - $lastEntity = $lastEntity->orderBy('public_id', 'DESC') - ->first(); - - if ($lastEntity) { - $entity->public_id = $lastEntity->public_id + 1; - } else { - $entity->public_id = 1; - } - - return $entity; - } - public function account() { return $this->belongsTo('App\Models\Account'); @@ -86,15 +66,25 @@ class PaymentMethod extends EntityModel return $value ? str_pad($value, 4, '0', STR_PAD_LEFT) : null; } - public function scopeScope($query, $publicId = false, $accountId = false, $accountGatewayTokenId = false) + public function scopeClientId($query, $clientId) { - $query = parent::scopeScope($query, $publicId, $accountId); + return $query->with(['contact' => function($query) use ($clientId) { + return $query->whereClientId($clientId); + }]); + } - if ($accountGatewayTokenId) { - $query->where($this->getTable() . '.account_gateway_token_id', '=', $accountGatewayTokenId); + public function scopeIsBankAccount($query, $isBank) + { + if ($isBank) { + $query->where('payment_type_id', '=', PAYMENT_TYPE_ACH); + } else { + $query->where('payment_type_id', '!=', PAYMENT_TYPE_ACH); } + } - return $query; + public function imageUrl() + { + return url(sprintf('/images/credit_cards/%s.png', str_replace(' ', '', strtolower($this->payment_type->name)))); } public static function lookupBankData($routingNumber) { @@ -173,4 +163,4 @@ PaymentMethod::deleting(function($paymentMethod) { $accountGatewayToken->default_payment_method_id = $newDefault ? $newDefault->id : null; $accountGatewayToken->save(); } -}); \ No newline at end of file +}); diff --git a/app/Models/User.php b/app/Models/User.php index 9dab5e759a15..96cb7bc1191e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -280,6 +280,9 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac } else { $bitmask = 0; foreach($value as $permission){ + if ( ! $permission) { + continue; + } $bitmask = $bitmask | static::$all_permissions[$permission]; } diff --git a/app/Ninja/Datatables/AccountGatewayDatatable.php b/app/Ninja/Datatables/AccountGatewayDatatable.php index fe6a8df0df7a..5d873b91de6b 100644 --- a/app/Ninja/Datatables/AccountGatewayDatatable.php +++ b/app/Ninja/Datatables/AccountGatewayDatatable.php @@ -49,12 +49,6 @@ class AccountGatewayDatatable extends EntityDatatable } } ], - [ - 'payment_type', - function ($model) { - return Gateway::getPrettyPaymentType($model->gateway_id); - } - ], ]; } diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index 9fa2009a7b72..6ed657d15747 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -107,20 +107,6 @@ 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; @@ -152,12 +138,14 @@ class ContactMailer extends Mailer 'amount' => $invoice->getRequestedAmount() ]; - if ($invoice->autoBillPaymentMethod) { + /* + if ($client->autoBillLater()) { // 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) { + 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(); $invitation->contact->password = bcrypt($password); @@ -293,4 +281,20 @@ class ContactMailer extends Mailer $this->sendTo($email, CONTACT_EMAIL, CONTACT_NAME, $subject, $view, $data); } + + /* + 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_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]); + } + */ } diff --git a/app/Ninja/PaymentDrivers/AuthorizeNetAIMPaymentDriver.php b/app/Ninja/PaymentDrivers/AuthorizeNetAIMPaymentDriver.php new file mode 100644 index 000000000000..00ba9421ce03 --- /dev/null +++ b/app/Ninja/PaymentDrivers/AuthorizeNetAIMPaymentDriver.php @@ -0,0 +1,6 @@ +accountGateway = $accountGateway; + $this->invitation = $invitation; + $this->gatewayType = $gatewayType ?: $this->gatewayTypes()[0]; + } + + public function isGateway($gatewayId) + { + return $this->accountGateway->gateway_id == $gatewayId; + } + + protected function isGatewayType($gatewayType) + { + return $this->gatewayType === $gatewayType; + } + + protected function gatewayTypes() + { + return [ + GATEWAY_TYPE_CREDIT_CARD + ]; + } + + public function handles($type) + { + return in_array($type, $this->gatewayTypes()); + } + + // when set to true we won't pass the card details with the form + public function tokenize() + { + return false; + } + + // set payment method as pending until confirmed + public function isTwoStep() + { + return false; + } + + public function providerName() + { + return strtolower($this->accountGateway->gateway->provider); + } + + protected function invoice() + { + return $this->invitation->invoice; + } + + protected function contact() + { + return $this->invitation->contact; + } + + protected function client() + { + return $this->invoice()->client; + } + + protected function account() + { + return $this->client()->account; + } + + public function startPurchase($input = false, $sourceId = false) + { + $this->input = $input; + $this->sourceId = $sourceId; + + Session::put('invitation_key', $this->invitation->invitation_key); + Session::put($this->invitation->id . 'gateway_type', $this->gatewayType); + Session::put($this->invitation->id . 'payment_ref', $this->invoice()->id . '_' . uniqid()); + + $gateway = $this->accountGateway->gateway; + + if ($this->isGatewayType(GATEWAY_TYPE_TOKEN) || $gateway->is_offsite) { + if (Session::has('error')) { + Session::reflash(); + } else { + $this->completeOnsitePurchase(); + Session::flash('message', trans('texts.applied_payment')); + } + + return redirect()->to('view/' . $this->invitation->invitation_key); + } + + $data = [ + 'accountGateway' => $this->accountGateway, + 'acceptedCreditCardTypes' => $this->accountGateway->getCreditcardTypes(), + 'gateway' => $gateway, + 'showAddress' => $this->accountGateway->show_address, + 'showBreadcrumbs' => false, + 'url' => 'payment/' . $this->invitation->invitation_key, + 'amount' => $this->invoice()->getRequestedAmount(), + 'invoiceNumber' => $this->invoice()->invoice_number, + 'client' => $this->client(), + 'contact' => $this->invitation->contact, + 'gatewayType' => $this->gatewayType, + 'currencyId' => $this->client()->getCurrencyId(), + 'currencyCode' => $this->client()->getCurrencyCode(), + 'account' => $this->account(), + 'sourceId' => $sourceId, + 'clientFontUrl' => $this->account()->getFontsUrl(), + 'tokenize' => $this->tokenize(), + 'transactionToken' => $this->createTransactionToken(), + ]; + + return view($this->paymentView(), $data); + } + + // check if a custom view exists for this provider + protected function paymentView() + { + $file = sprintf('%s/views/payments/%s/%s.blade.php', resource_path(), $this->providerName(), $this->gatewayType); + + if (file_exists($file)) { + return sprintf('payments.%s/%s', $this->providerName(), $this->gatewayType); + } else { + return sprintf('payments.%s', $this->gatewayType); + } + } + + // check if a custom partial exists for this provider + public function partialView() + { + $file = sprintf('%s/views/payments/%s/partial.blade.php', resource_path(), $this->providerName()); + + if (file_exists($file)) { + return sprintf('payments.%s.partial', $this->providerName()); + } else { + return false; + } + } + + public function rules() + { + $rules = []; + + if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)) { + + $rules = array_merge($rules, [ + 'first_name' => 'required', + 'last_name' => 'required', + ]); + + // TODO check this is always true + if ( ! $this->tokenize()) { + $rules = array_merge($rules, [ + 'card_number' => 'required', + 'expiration_month' => 'required', + 'expiration_year' => 'required', + 'cvv' => 'required', + ]); + } + + if ($this->accountGateway->show_address) { + $rules = array_merge($rules, [ + 'address1' => 'required', + 'city' => 'required', + 'state' => 'required', + 'postal_code' => 'required', + 'country_id' => 'required', + ]); + } + } + + return $rules; + } + + protected function gateway() + { + if ($this->gateway) { + return $this->gateway; + } + + $this->gateway = Omnipay::create($this->accountGateway->gateway->provider); + $this->gateway->initialize((array) $this->accountGateway->getConfig()); + + return $this->gateway; + } + + public function completeOnsitePurchase($input = false, $paymentMethod = false) + { + $this->input = count($input) ? $input : false; + $gateway = $this->gateway(); + + if ($input) { + $this->updateAddress(); + } + + // load or create token + if ($this->isGatewayType(GATEWAY_TYPE_TOKEN)) { + if ( ! $paymentMethod) { + $paymentMethod = PaymentMethod::clientId($this->client()->id) + ->wherePublicId($this->sourceId) + ->firstOrFail(); + } + } elseif ($this->shouldCreateToken()) { + $paymentMethod = $this->createToken(); + } + + if ($this->isTwoStep()) { + return; + } + + // prepare and process payment + $data = $this->paymentDetails($paymentMethod); + $response = $gateway->purchase($data)->send(); + $this->purchaseResponse = (array) $response->getData(); + + // parse the transaction reference + if ($this->transactionReferenceParam) { + $ref = $this->purchaseResponse[$this->transactionReferenceParam]; + } else { + $ref = $response->getTransactionReference(); + } + + // wrap up + if ($response->isSuccessful() && $ref) { + $payment = $this->createPayment($ref, $paymentMethod); + + // TODO move this to stripe driver + if ($this->invitation->invoice->account->account_key == NINJA_ACCOUNT_KEY) { + Session::flash('trackEventCategory', '/account'); + Session::flash('trackEventAction', '/buy_pro_plan'); + Session::flash('trackEventAmount', $payment->amount); + } + + return $payment; + } elseif ($response->isRedirect()) { + $this->invitation->transaction_reference = $ref; + $this->invitation->save(); + //Session::put('transaction_reference', $ref); + Session::save(); + $response->redirect(); + } else { + throw new Exception($response->getMessage() ?: trans('texts.payment_error')); + } + } + + private function updateAddress() + { + if ( ! $this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)) { + return; + } + + if ( ! $this->accountGateway->show_address || ! $this->accountGateway->update_address) { + return; + } + + $client = $this->client(); + $client->address1 = trim($this->input['address1']); + $client->address2 = trim($this->input['address2']); + $client->city = trim($this->input['city']); + $client->state = trim($this->input['state']); + $client->postal_code = trim($this->input['postal_code']); + $client->country_id = trim($this->input['country_id']); + $client->save(); + } + + protected function paymentDetails($paymentMethod = false) + { + $invoice = $this->invoice(); + $completeUrl = url('complete/' . $this->invitation->invitation_key . '/' . $this->gatewayType); + + $data = [ + 'amount' => $invoice->getRequestedAmount(), + 'currency' => $invoice->getCurrencyCode(), + 'returnUrl' => $completeUrl, + 'cancelUrl' => $this->invitation->getLink(), + 'description' => trans('texts.' . $invoice->getEntityType()) . " {$invoice->invoice_number}", + 'transactionId' => $invoice->invoice_number, + 'transactionType' => 'Purchase', + 'ip' => Request::ip() + ]; + + if ($paymentMethod) { + if ($this->customerReferenceParam) { + $data[$this->customerReferenceParam] = $paymentMethod->account_gateway_token->token; + } + $data[$this->sourceReferenceParam] = $paymentMethod->source_reference; + } elseif ($this->input) { + $data['card'] = new CreditCard($this->paymentDetailsFromInput($this->input)); + } else { + $data['card'] = new CreditCard($this->paymentDetailsFromClient()); + } + + return $data; + } + + private function paymentDetailsFromInput($input) + { + $invoice = $this->invoice(); + $client = $this->client(); + + $data = [ + 'company' => $client->getDisplayName(), + 'firstName' => isset($input['first_name']) ? $input['first_name'] : null, + 'lastName' => isset($input['last_name']) ? $input['last_name'] : null, + 'email' => isset($input['email']) ? $input['email'] : null, + 'number' => isset($input['card_number']) ? $input['card_number'] : null, + 'expiryMonth' => isset($input['expiration_month']) ? $input['expiration_month'] : null, + 'expiryYear' => isset($input['expiration_year']) ? $input['expiration_year'] : null, + ]; + + // allow space until there's a setting to disable + if (isset($input['cvv']) && $input['cvv'] != ' ') { + $data['cvv'] = $input['cvv']; + } + + if (isset($input['address1'])) { + // TODO use cache instead + $country = Country::find($input['country_id']); + + $data = array_merge($data, [ + 'billingAddress1' => $input['address1'], + 'billingAddress2' => $input['address2'], + 'billingCity' => $input['city'], + 'billingState' => $input['state'], + 'billingPostcode' => $input['postal_code'], + 'billingCountry' => $country->iso_3166_2, + 'shippingAddress1' => $input['address1'], + 'shippingAddress2' => $input['address2'], + 'shippingCity' => $input['city'], + 'shippingState' => $input['state'], + 'shippingPostcode' => $input['postal_code'], + 'shippingCountry' => $country->iso_3166_2 + ]); + } + + return $data; + } + + public function paymentDetailsFromClient() + { + $invoice = $this->invoice(); + $client = $this->client(); + $contact = $this->invitation->contact ?: $client->contacts()->first(); + + return [ + 'email' => $contact->email, + 'company' => $client->getDisplayName(), + 'firstName' => $contact->first_name, + 'lastName' => $contact->last_name, + 'billingAddress1' => $client->address1, + 'billingAddress2' => $client->address2, + 'billingCity' => $client->city, + 'billingPostcode' => $client->postal_code, + 'billingState' => $client->state, + 'billingCountry' => $client->country ? $client->country->iso_3166_2 : '', + 'billingPhone' => $contact->phone, + 'shippingAddress1' => $client->address1, + 'shippingAddress2' => $client->address2, + 'shippingCity' => $client->city, + 'shippingPostcode' => $client->postal_code, + 'shippingState' => $client->state, + 'shippingCountry' => $client->country ? $client->country->iso_3166_2 : '', + 'shippingPhone' => $contact->phone, + ]; + } + + protected function shouldCreateToken() + { + if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) { + return true; + } + + if ( ! $this->handles(GATEWAY_TYPE_TOKEN)) { + return false; + } + + if ($this->account()->token_billing_type_id == TOKEN_BILLING_ALWAYS) { + return true; + } + + return boolval(array_get($this->input, 'token_billing')); + } + + /* + protected function tokenDetails() + { + $details = []; + + if ($customer = $this->customer()) { + $details['customerReference'] = $customer->token; + } + + return $details; + } + */ + + public function customer($clientId = false) + { + if ($this->customer) { + return $this->customer; + } + + if ( ! $clientId) { + $clientId = $this->client()->id; + } + + $this->customer = AccountGatewayToken::clientAndGateway($clientId, $this->accountGateway->id) + ->with('payment_methods') + ->first(); + + if ($this->customer) { + $this->customer = $this->checkCustomerExists($this->customer) ? $this->customer : null; + } + + return $this->customer; + } + + protected function checkCustomerExists($customer) + { + return true; + } + + public function verifyBankAccount($client, $publicId, $amount1, $amount2) + { + throw new Exception('verifyBankAccount not implemented'); + } + + public function removePaymentMethod($paymentMethod) + { + $paymentMethod->delete(); + } + + // Some gateways (ie, Checkout.com and Braintree) require generating a token before paying for the invoice + public function createTransactionToken() + { + return null; + } + + public function createToken() + { + $account = $this->account(); + + if ( ! $customer = $this->customer()) { + $customer = new AccountGatewayToken(); + $customer->account_id = $account->id; + $customer->contact_id = $this->invitation->contact_id; + $customer->account_gateway_id = $this->accountGateway->id; + $customer->client_id = $this->client()->id; + $customer = $this->creatingCustomer($customer); + $customer->save(); + } + + /* + // archive the old payment method + $paymentMethod = PaymentMethod::clientId($this->client()->id) + ->isBankAccount($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) + ->first(); + + if ($paymentMethod) { + $paymentMethod->delete(); + } + */ + + $paymentMethod = $this->createPaymentMethod($customer); + + if ($paymentMethod && ! $customer->default_payment_method_id) { + $customer->default_payment_method_id = $paymentMethod->id; + $customer->save(); + } + + return $paymentMethod; + } + + protected function creatingCustomer($customer) + { + return $customer; + } + + public function createPaymentMethod($customer) + { + $paymentMethod = PaymentMethod::createNew($this->invitation); + $paymentMethod->ip = Request::ip(); + $paymentMethod->account_gateway_token_id = $customer->id; + $paymentMethod->setRelation('account_gateway_token', $customer); + $paymentMethod = $this->creatingPaymentMethod($paymentMethod); + + if ($paymentMethod) { + $paymentMethod->save(); + } + + return $paymentMethod; + } + + protected function creatingPaymentMethod($paymentMethod) + { + return $paymentMethod; + } + + public function deleteToken() + { + + } + + public function createPayment($ref = false, $paymentMethod = null) + { + $invitation = $this->invitation; + $invoice = $this->invoice(); + + $payment = Payment::createNew($invitation); + $payment->invitation_id = $invitation->id; + $payment->account_gateway_id = $this->accountGateway->id; + $payment->invoice_id = $invoice->id; + $payment->amount = $invoice->getRequestedAmount(); + $payment->client_id = $invoice->client_id; + $payment->contact_id = $invitation->contact_id; + $payment->transaction_reference = $ref; + $payment->payment_date = date_create()->format('Y-m-d'); + $payment->ip = Request::ip(); + + $payment = $this->creatingPayment($payment); + + if ($paymentMethod) { + $payment->last4 = $paymentMethod->last4; + $payment->expiration = $paymentMethod->expiration; + $payment->routing_number = $paymentMethod->routing_number; + $payment->payment_type_id = $paymentMethod->payment_type_id; + $payment->email = $paymentMethod->email; + $payment->bank_name = $paymentMethod->bank_name; + $payment->payment_method_id = $paymentMethod->id; + } + + $payment->save(); + + // TODO move this code + // enable pro plan for hosted users + if ($invoice->account->account_key == NINJA_ACCOUNT_KEY) { + foreach ($invoice->invoice_items as $invoice_item) { + // Hacky, but invoices don't have meta fields to allow us to store this easily + if (1 == preg_match('/^Plan - (.+) \((.+)\)$/', $invoice_item->product_key, $matches)) { + $plan = strtolower($matches[1]); + $term = strtolower($matches[2]); + } elseif ($invoice_item->product_key == 'Pending Monthly') { + $pending_monthly = true; + } + } + + if (!empty($plan)) { + $account = Account::with('users')->find($invoice->client->public_id); + + if( + $account->company->plan != $plan + || DateTime::createFromFormat('Y-m-d', $account->company->plan_expires) >= date_create('-7 days') + ) { + // Either this is a different plan, or the subscription expired more than a week ago + // Reset any grandfathering + $account->company->plan_started = date_create()->format('Y-m-d'); + } + + if ( + $account->company->plan == $plan + && $account->company->plan_term == $term + && DateTime::createFromFormat('Y-m-d', $account->company->plan_expires) >= date_create() + ) { + // This is a renewal; mark it paid as of when this term expires + $account->company->plan_paid = $account->company->plan_expires; + } else { + $account->company->plan_paid = date_create()->format('Y-m-d'); + } + + $account->company->payment_id = $payment->id; + $account->company->plan = $plan; + $account->company->plan_term = $term; + $account->company->plan_expires = DateTime::createFromFormat('Y-m-d', $account->company->plan_paid) + ->modify($term == PLAN_TERM_MONTHLY ? '+1 month' : '+1 year')->format('Y-m-d'); + + if (!empty($pending_monthly)) { + $account->company->pending_plan = $plan; + $account->company->pending_term = PLAN_TERM_MONTHLY; + } else { + $account->company->pending_plan = null; + $account->company->pending_term = null; + } + + $account->company->save(); + } + } + + return $payment; + } + + protected function creatingPayment($payment) + { + return $payment; + } + + public function refundPayment($payment, $amount) + { + $amount = min($amount, $payment->getCompletedAmount()); + + if ( ! $amount) { + return false; + } + + if ($payment->payment_type_id == PAYMENT_TYPE_CREDIT) { + return $payment->recordRefund($amount); + } + + $details = $this->refundDetails($payment, $amount); + $response = $this->gateway()->refund($details)->send(); + + if ($response->isSuccessful()) { + return $payment->recordRefund($amount); + } elseif ($this->attemptVoidPayment($response, $payment, $amount)) { + $details = ['transactionReference' => $payment->transaction_reference]; + $response = $this->gateway->void($details)->send(); + if ($response->isSuccessful()) { + return $payment->markVoided(); + } + } + + return false; + } + + protected function refundDetails($payment, $amount) + { + return [ + 'amount' => $amount, + 'transactionReference' => $payment->transaction_reference, + ]; + } + + protected function attemptVoidPayment($response, $payment, $amount) + { + // Partial refund not allowed for unsettled transactions + return $amount == $payment->amount; + } + + protected function createLocalPayment($payment) + { + return $payment; + } + + public function completeOffsitePurchase($input) + { + $this->input = $input; + $ref = array_get($this->input, 'token') ?: $this->invitation->transaction_reference; + + if (method_exists($this->gateway(), 'completePurchase')) { + + $details = $this->paymentDetails(); + $response = $this->gateway()->completePurchase($details)->send(); + $ref = $response->getTransactionReference() ?: $ref; + + if ($response->isCancelled()) { + return false; + } elseif ( ! $response->isSuccessful()) { + throw new Exception($response->getMessage()); + } + } + + return $this->createPayment($ref); + } + + public function tokenLinks() + { + if ( ! $this->customer()) { + return []; + } + + $paymentMethods = $this->customer()->payment_methods; + $links = []; + + foreach ($paymentMethods as $paymentMethod) { + if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH && $paymentMethod->status != PAYMENT_METHOD_STATUS_VERIFIED) { + continue; + } + + $url = URL::to("/payment/{$this->invitation->invitation_key}/token/".$paymentMethod->public_id); + + if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) { + if ($paymentMethod->bank_name) { + $label = $paymentMethod->bank_name; + } else { + $label = trans('texts.use_bank_on_file'); + } + } elseif ($paymentMethod->payment_type_id == PAYMENT_TYPE_PAYPAL) { + $label = 'PayPal: ' . $paymentMethod->email; + } else { + $label = trans('texts.use_card_on_file'); + } + + $links[] = [ + 'url' => $url, + 'label' => $label, + ]; + } + + return $links; + } + + public function paymentLinks() + { + $links = []; + + foreach ($this->gatewayTypes() as $gatewayType) { + if ($gatewayType === GATEWAY_TYPE_TOKEN) { + continue; + } + + $links[] = [ + 'url' => $this->paymentUrl($gatewayType), + 'label' => trans("texts.{$gatewayType}") + ]; + } + + return $links; + } + + protected function paymentUrl($gatewayType) + { + $account = $this->account(); + $url = URL::to("/payment/{$this->invitation->invitation_key}/{$gatewayType}"); + + // PayPal doesn't allow being run in an iframe so we need to open in new tab + if ($gatewayType === GATEWAY_TYPE_PAYPAL) { + $url .= "#braintree_paypal"; + + if ($account->iframe_url) { + return 'javascript:window.open("' . $url . '", "_blank")'; + } + } + + return $url; + } + + protected function parseCardType($cardName) { + $cardTypes = array( + 'visa' => PAYMENT_TYPE_VISA, + 'americanexpress' => PAYMENT_TYPE_AMERICAN_EXPRESS, + 'amex' => PAYMENT_TYPE_AMERICAN_EXPRESS, + 'mastercard' => PAYMENT_TYPE_MASTERCARD, + 'discover' => PAYMENT_TYPE_DISCOVER, + 'jcb' => PAYMENT_TYPE_JCB, + 'dinersclub' => PAYMENT_TYPE_DINERS, + 'carteblanche' => PAYMENT_TYPE_CARTE_BLANCHE, + 'chinaunionpay' => PAYMENT_TYPE_UNIONPAY, + 'unionpay' => PAYMENT_TYPE_UNIONPAY, + 'laser' => PAYMENT_TYPE_LASER, + 'maestro' => PAYMENT_TYPE_MAESTRO, + 'solo' => PAYMENT_TYPE_SOLO, + 'switch' => PAYMENT_TYPE_SWITCH + ); + + $cardName = strtolower(str_replace(array(' ', '-', '_'), '', $cardName)); + + if (empty($cardTypes[$cardName]) && 1 == preg_match('/^('.implode('|', array_keys($cardTypes)).')/', $cardName, $matches)) { + // Some gateways return extra stuff after the card name + $cardName = $matches[1]; + } + + if (!empty($cardTypes[$cardName])) { + return $cardTypes[$cardName]; + } else { + return PAYMENT_TYPE_CREDIT_CARD_OTHER; + } + } +} diff --git a/app/Ninja/PaymentDrivers/BitPayPaymentDriver.php b/app/Ninja/PaymentDrivers/BitPayPaymentDriver.php new file mode 100644 index 000000000000..420c876a7090 --- /dev/null +++ b/app/Ninja/PaymentDrivers/BitPayPaymentDriver.php @@ -0,0 +1,12 @@ +accountGateway->getPayPalEnabled()) { + $types[] = GATEWAY_TYPE_PAYPAL; + } + + return $types; + } + + public function tokenize() + { + return true; + } + + public function startPurchase($input = false, $sourceId = false) + { + $data = parent::startPurchase($input, $sourceId); + + if ($this->isGatewayType(GATEWAY_TYPE_PAYPAL)) { + /* + if ( ! $sourceId || empty($input['device_data'])) { + throw new Exception(); + } + + Session::put($this->invitation->id . 'device_data', $input['device_data']); + */ + + $data['details'] = ! empty($input['device_data']) ? json_decode($input['device_data']) : false; + } + + return $data; + } + + protected function checkCustomerExists($customer) + { + if ( ! parent::checkCustomerExists($customer)) { + return false; + } + + $customer = $this->gateway()->findCustomer($customer->token) + ->send() + ->getData(); + + return ($customer instanceof Customer); + } + + protected function paymentDetails($paymentMethod = false) + { + $data = parent::paymentDetails($paymentMethod); + + $deviceData = array_get($this->input, 'device_data') ?: Session::get($this->invitation->id . 'device_data'); + + if ($deviceData) { + $data['device_data'] = $deviceData; + } + + if ($this->isGatewayType(GATEWAY_TYPE_PAYPAL)) { + $data['ButtonSource'] = 'InvoiceNinja_SP'; + } + + if ( ! empty($this->input['sourceToken'])) { + $data['token'] = $this->input['sourceToken']; + } + + return $data; + } + + public function createToken() + { + if ($customer = $this->customer()) { + $customerReference = $customer->token; + } else { + $data = $this->paymentDetails(); + $tokenResponse = $this->gateway()->createCustomer(['customerData' => $this->customerData()])->send(); + if ($tokenResponse->isSuccessful()) { + $customerReference = $tokenResponse->getCustomerData()->id; + } else { + return false; + } + } + + if ($customerReference) { + $data['customerId'] = $customerReference; + + if ($this->isGatewayType(GATEWAY_TYPE_PAYPAL)) { + $data['paymentMethodNonce'] = $this->input['sourceToken']; + } + + $tokenResponse = $this->gateway->createPaymentMethod($data)->send(); + if ($tokenResponse->isSuccessful()) { + $this->tokenResponse = $tokenResponse->getData()->paymentMethod; + } else { + return false; + } + } + + return parent::createToken(); + } + + private function customerData() + { + return [ + 'firstName' => array_get($this->input, 'first_name') ?: $this->contact()->first_name, + 'lastName' => array_get($this->input, 'last_name') ?: $this->contact()->last_name, + 'company' => $this->client()->name, + 'email' => $this->contact()->email, + 'phone' => $this->contact()->phone, + 'website' => $this->client()->website + ]; + } + + public function creatingCustomer($customer) + { + $customer->token = $this->tokenResponse->customerId; + + return $customer; + } + + protected function creatingPaymentMethod($paymentMethod) + { + $response = $this->tokenResponse; + + $paymentMethod->source_reference = $response->token; + + if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)) { + $paymentMethod->payment_type_id = $this->parseCardType($response->cardType); + $paymentMethod->last4 = $response->last4; + $paymentMethod->expiration = $response->expirationYear . '-' . $response->expirationMonth . '-01'; + } elseif ($this->isGatewayType(GATEWAY_TYPE_PAYPAL)) { + $paymentMethod->email = $response->email; + $paymentMethod->payment_type_id = PAYMENT_TYPE_PAYPAL; + } else { + return null; + } + + return $paymentMethod; + } + + public function removePaymentMethod($paymentMethod) + { + $response = $this->gateway()->deletePaymentMethod([ + 'token' => $paymentMethod->source_reference + ])->send(); + + if ($response->isSuccessful()) { + return parent::removePaymentMethod($paymentMethod); + } else { + throw new Exception($response->getMessage()); + } + } + + protected function attemptVoidPayment($response, $payment, $amount) + { + if ( ! parent::attemptVoidPayment($response, $payment, $amount)) { + return false; + } + + $data = $response->getData(); + + if ($data instanceof \Braintree\Result\Error) { + $error = $data->errors->deepAll()[0]; + if ($error && $error->code == 91506) { + return true; + } + } + + return false; + } + + public function createTransactionToken() + { + return $this->gateway() + ->clientToken() + ->send() + ->getToken(); + } +} diff --git a/app/Ninja/PaymentDrivers/CheckoutComPaymentDriver.php b/app/Ninja/PaymentDrivers/CheckoutComPaymentDriver.php new file mode 100644 index 000000000000..ede0b678234b --- /dev/null +++ b/app/Ninja/PaymentDrivers/CheckoutComPaymentDriver.php @@ -0,0 +1,35 @@ +gateway()->purchase([ + 'amount' => $this->invoice()->getRequestedAmount(), + 'currency' => $this->client()->getCurrencyCode() + ])->send(); + + if ($response->isRedirect()) { + $token = $response->getTransactionReference(); + + $this->invitation->transaction_reference = $token; + $this->invitation->save(); + + return $token; + } + + return false; + } + + protected function paymentDetails($paymentMethod = false) + { + $data = parent::paymentDetails(); + + if ($ref = array_get($this->input, 'token')) { + $data['transactionReference'] = $ref; + } + + return $data; + } + +} diff --git a/app/Ninja/PaymentDrivers/CybersourcePaymentDriver.php b/app/Ninja/PaymentDrivers/CybersourcePaymentDriver.php new file mode 100644 index 000000000000..29299eb94fb2 --- /dev/null +++ b/app/Ninja/PaymentDrivers/CybersourcePaymentDriver.php @@ -0,0 +1,15 @@ +createPayment($input['bill_trans_ref_no']); + } else { + throw new Exception($input['message'] . ': ' . $input['invalid_fields']); + } + } +} diff --git a/app/Ninja/PaymentDrivers/DwollaPaymentDriver.php b/app/Ninja/PaymentDrivers/DwollaPaymentDriver.php new file mode 100644 index 000000000000..bfec26068a3f --- /dev/null +++ b/app/Ninja/PaymentDrivers/DwollaPaymentDriver.php @@ -0,0 +1,24 @@ +getSandbox() && isset($_ENV['DWOLLA_SANDBOX_KEY']) && isset($_ENV['DWOLLA_SANSBOX_SECRET'])) { + $gateway->setKey($_ENV['DWOLLA_SANDBOX_KEY']); + $gateway->setSecret($_ENV['DWOLLA_SANSBOX_SECRET']); + } elseif (isset($_ENV['DWOLLA_KEY']) && isset($_ENV['DWOLLA_SECRET'])) { + $gateway->setKey($_ENV['DWOLLA_KEY']); + $gateway->setSecret($_ENV['DWOLLA_SECRET']); + } + + return $gateway; + } +} diff --git a/app/Ninja/PaymentDrivers/EwayRapidSharedPaymentDriver.php b/app/Ninja/PaymentDrivers/EwayRapidSharedPaymentDriver.php new file mode 100644 index 000000000000..fc842919e888 --- /dev/null +++ b/app/Ninja/PaymentDrivers/EwayRapidSharedPaymentDriver.php @@ -0,0 +1,6 @@ +paymentDetails(); + + $details['transactionReference'] = $this->invitation->transaction_reference; + + $response = $this->gateway()->fetchTransaction($details)->send(); + + return $this->createPayment($response->getTransactionReference()); + } + +} diff --git a/app/Ninja/PaymentDrivers/PayFastPaymentDriver.php b/app/Ninja/PaymentDrivers/PayFastPaymentDriver.php new file mode 100644 index 000000000000..79b49b23499e --- /dev/null +++ b/app/Ninja/PaymentDrivers/PayFastPaymentDriver.php @@ -0,0 +1,13 @@ +isGateway(GATEWAY_PAYFAST) && Request::has('pt')) { + $token = Request::query('pt'); + } + } +} diff --git a/app/Ninja/PaymentDrivers/PayPalExpressPaymentDriver.php b/app/Ninja/PaymentDrivers/PayPalExpressPaymentDriver.php new file mode 100644 index 000000000000..88e3639efa4d --- /dev/null +++ b/app/Ninja/PaymentDrivers/PayPalExpressPaymentDriver.php @@ -0,0 +1,29 @@ +payer_id = $this->input['PayerID']; + + return $payment; + } +} diff --git a/app/Ninja/PaymentDrivers/PayPalProPaymentDriver.php b/app/Ninja/PaymentDrivers/PayPalProPaymentDriver.php new file mode 100644 index 000000000000..9acf75e7cdfe --- /dev/null +++ b/app/Ninja/PaymentDrivers/PayPalProPaymentDriver.php @@ -0,0 +1,20 @@ +accountGateway->getAchEnabled()) { + $types[] = GATEWAY_TYPE_BANK_TRANSFER; + } + + return $types; + } + + public function tokenize() + { + return $this->accountGateway->getPublishableStripeKey(); + } + + public function rules() + { + $rules = parent::rules(); + + if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) { + $rules['authorize_ach'] = 'required'; + } + + return $rules; + } + + protected function checkCustomerExists($customer) + { + $response = $this->gateway() + ->fetchCustomer(['customerReference' => $customer->token]) + ->send(); + + if ( ! $response->isSuccessful()) { + return false; + } + + $this->tokenResponse = $response->getData(); + + // import Stripe tokens created before payment methods table was added + if ( ! count($customer->payment_methods)) { + if ($paymentMethod = $this->createPaymentMethod($customer)) { + $customer->default_payment_method_id = $paymentMethod->id; + $customer->save(); + $customer->load('payment_methods'); + } + } + + return true; + } + + public function isTwoStep() + { + return $this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER) && empty($this->input['plaidPublicToken']); + } + + protected function paymentDetails($paymentMethod = false) + { + $data = parent::paymentDetails($paymentMethod); + + if ( ! empty($this->input['sourceToken'])) { + $data['token'] = $this->input['sourceToken']; + unset($data['card']); + } + + if ( ! empty($this->input['plaidPublicToken'])) { + $data['plaidPublicToken'] = $this->input['plaidPublicToken']; + $data['plaidAccountId'] = $this->input['plaidAccountId']; + unset($data['card']); + } + + return $data; + } + + public function createToken() + { + $invoice = $this->invitation->invoice; + $client = $invoice->client; + + $data = $this->paymentDetails(); + $data['description'] = $client->getDisplayName(); + + if ( ! empty($data['plaidPublicToken'])) { + $plaidResult = $this->getPlaidToken($data['plaidPublicToken'], $data['plaidAccountId']); + unset($data['plaidPublicToken']); + unset($data['plaidAccountId']); + $data['token'] = $plaidResult['stripe_bank_account_token']; + } + + // if a customer already exists link the token to it + if ($customer = $this->customer()) { + $data['customerReference'] = $customer->token; + } + + $tokenResponse = $this->gateway() + ->createCard($data) + ->send(); + + if ($tokenResponse->isSuccessful()) { + $this->tokenResponse = $tokenResponse->getData(); + + return parent::createToken(); + } else { + throw new Exception($tokenResponse->getMessage()); + } + } + + public function creatingCustomer($customer) + { + $customer->token = $this->tokenResponse['id']; + + return $customer; + } + + protected function creatingPaymentMethod($paymentMethod) + { + $data = $this->tokenResponse; + + if (!empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) { + $source = $data; + } elseif (!empty($data['object']) && $data['object'] == 'customer') { + $sources = !empty($data['sources']) ? $data['sources'] : $data['cards']; + $source = reset($sources['data']); + } else { + $source = !empty($data['source']) ? $data['source'] : $data['card']; + } + + if ( ! $source) { + return false; + } + + $paymentMethod->source_reference = $source['id']; + $paymentMethod->last4 = $source['last4']; + + if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)) { + + $paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-01'; + $paymentMethod->payment_type_id = $this->parseCardType($source['brand']); + + } elseif ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) { + + $paymentMethod->routing_number = $source['routing_number']; + $paymentMethod->payment_type_id = PAYMENT_TYPE_ACH; + $paymentMethod->status = $source['status']; + $currency = Cache::get('currencies')->where('code', strtoupper($source['currency']))->first(); + + if ($currency) { + $paymentMethod->currency_id = $currency->id; + $paymentMethod->setRelation('currency', $currency); + } + + } + + return $paymentMethod; + } + + protected function creatingPayment($payment) + { + if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) { + $payment->payment_status_id = $this->purchaseResponse['status'] == 'succeeded' ? PAYMENT_STATUS_COMPLETED : PAYMENT_STATUS_PENDING; + } + + return $payment; + } + + public function removePaymentMethod($paymentMethod) + { + if ( ! $paymentMethod->relationLoaded('account_gateway_token')) { + $paymentMethod->load('account_gateway_token'); + } + + $response = $this->gateway()->deleteCard([ + 'customerReference' => $paymentMethod->account_gateway_token->token, + 'cardReference' => $paymentMethod->source_reference + ])->send(); + + if ($response->isSuccessful()) { + return parent::removePaymentMethod($paymentMethod); + } else { + throw new Exception($response->getMessage()); + } + } + + private function getPlaidToken($publicToken, $accountId) + { + $clientId = $this->accountGateway->getPlaidClientId(); + $secret = $this->accountGateway->getPlaidSecret(); + + if (!$clientId) { + throw new Exception('plaid client id not set'); // TODO use text strings + } + + if (!$secret) { + throw new Exception('plaid secret not set'); + } + + try { + $subdomain = $this->accountGateway->getPlaidEnvironment() == 'production' ? 'api' : 'tartan'; + $response = (new \GuzzleHttp\Client(['base_uri'=>"https://{$subdomain}.plaid.com"]))->request( + 'POST', + 'exchange_token', + [ + 'allow_redirects' => false, + 'headers' => ['content-type' => 'application/x-www-form-urlencoded'], + 'body' => http_build_query(array( + 'client_id' => $clientId, + 'secret' => $secret, + 'public_token' => $publicToken, + 'account_id' => $accountId, + )) + ] + ); + return json_decode($response->getBody(), true); + } catch (\GuzzleHttp\Exception\BadResponseException $e) { + $response = $e->getResponse(); + $body = json_decode($response->getBody(), true); + + if ($body && !empty($body['message'])) { + throw new Exception($body['message']); + } else { + throw new Exception($e->getMessage()); + } + } + } + + public function verifyBankAccount($client, $publicId, $amount1, $amount2) + { + $customer = $this->customer($client->id); + $paymentMethod = PaymentMethod::clientId($client->id) + ->wherePublicId($publicId) + ->firstOrFail(); + + // Omnipay doesn't support verifying payment methods + // Also, it doesn't want to urlencode without putting numbers inside the brackets + $result = $this->makeStripeCall( + 'POST', + 'customers/' . $customer->token . '/sources/' . $paymentMethod->source_reference . '/verify', + 'amounts[]=' . intval($amount1) . '&amounts[]=' . intval($amount2) + ); + + if (is_string($result)) { + return $result; + } + + $paymentMethod->status = PAYMENT_METHOD_STATUS_VERIFIED; + $paymentMethod->save(); + + if ( ! $customer->default_payment_method_id) { + $customer->default_payment_method_id = $paymentMethod->id; + $customer->save(); + } + + return true; + } + + public function makeStripeCall($method, $url, $body = null) + { + $apiKey = $this->accountGateway->getConfig()->apiKey; + + if (!$apiKey) { + return 'No API key set'; + } + + try{ + $options = [ + 'headers' => ['content-type' => 'application/x-www-form-urlencoded'], + 'auth' => [$apiKey, ''], + ]; + + if ($body) { + $options['body'] = $body; + } + + $response = (new \GuzzleHttp\Client(['base_uri'=>'https://api.stripe.com/v1/']))->request( + $method, + $url, + $options + ); + 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(); + } + } +} diff --git a/app/Ninja/PaymentDrivers/TwoCheckoutPaymentDriver.php b/app/Ninja/PaymentDrivers/TwoCheckoutPaymentDriver.php new file mode 100644 index 000000000000..005611d4a034 --- /dev/null +++ b/app/Ninja/PaymentDrivers/TwoCheckoutPaymentDriver.php @@ -0,0 +1,13 @@ +createPayment($input['order_number']); + } + +} diff --git a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php new file mode 100644 index 000000000000..05561fb69d18 --- /dev/null +++ b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php @@ -0,0 +1,118 @@ +isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) { + if ( ! $sourceId) { + throw new Exception(); + } + } + + return $data; + } + + public function tokenize() + { + return true; + } + + protected function checkCustomerExists($customer) + { + return true; + } + + public function rules() + { + $rules = parent::rules(); + + if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) { + $rules = array_merge($rules, [ + 'authorize_ach' => 'required', + 'tos_agree' => 'required', + ]); + } + + return $rules; + } + + protected function paymentDetails($paymentMethod = false) + { + $data = parent::paymentDetails($paymentMethod); + + if ($transactionId = Session::get($invitation->id . 'payment_ref')) { + $data['transaction_id'] = $transactionId; + } + + $data['applicationFee'] = $this->calculateApplicationFee($data['amount']); + $data['feePayer'] = WEPAY_FEE_PAYER; + $data['callbackUri'] = $this->accountGateway->getWebhookUrl(); + + if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) { + $data['paymentMethodType'] = 'payment_bank'; + } + + return $data; + } + + public function removePaymentMethod($paymentMethod) + { + $wepay = Utils::setupWePay($this->accountGateway); + $wepay->request('/credit_card/delete', [ + 'client_id' => WEPAY_CLIENT_ID, + 'client_secret' => WEPAY_CLIENT_SECRET, + 'credit_card_id' => intval($paymentMethod->source_reference), + ]); + + if ($response->isSuccessful()) { + return parent::removePaymentMethod($paymentMethod); + } else { + throw new Exception($response->getMessage()); + } + } + + protected function refundDetails($payment, $amount) + { + $data = parent::refundDetails($parent); + + $data['refund_reason'] = 'Refund issued by merchant.'; + + // WePay issues a full refund when no amount is set. If an amount is set, it will try + // to issue a partial refund without refunding any fees. However, the Stripe driver + // (but not the API) requires the amount parameter to be set no matter what. + if ($data['amount'] == $payment->getCompletedAmount()) { + unset($data['amount']); + } + + return $data; + } + + protected function attemptVoidPayment($response, $payment, $amount) + { + if ( ! parent::attemptVoidPayment($response, $payment, $amount)) { + return false; + } + + return $response->getCode() == 4004; + } + + private function calculateApplicationFee($amount) + { + $fee = WEPAY_APP_FEE_MULTIPLIER * $amount + WEPAY_APP_FEE_FIXED; + + return floor(min($fee, $amount * 0.2));// Maximum fee is 20% of the amount. + } + +} diff --git a/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index 91ac711390bc..d76c1902a75a 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -741,6 +741,7 @@ class InvoiceRepository extends BaseRepository } $invoice = Invoice::createNew($recurInvoice); + $invoice->invoice_type_id = INVOICE_TYPE_STANDARD; $invoice->client_id = $recurInvoice->client_id; $invoice->recurring_invoice_id = $recurInvoice->id; $invoice->invoice_number = $recurInvoice->account->getNextInvoiceNumber($invoice); diff --git a/app/Services/DatatableService.php b/app/Services/DatatableService.php index 8fa49df5f7df..5126bd48d6a9 100644 --- a/app/Services/DatatableService.php +++ b/app/Services/DatatableService.php @@ -114,10 +114,8 @@ class DatatableService . trans("texts.archive_{$datatable->entityType}") . ""; } } else if($can_edit) { - if ($datatable->entityType != ENTITY_ACCOUNT_GATEWAY || Auth::user()->account->canAddGateway(\App\Models\Gateway::getPaymentType($model->gateway_id))) { - $dropdown_contents .= "
  • public_id})\">" - . trans("texts.restore_{$datatable->entityType}") . "
  • "; - } + $dropdown_contents .= "
  • public_id})\">" + . trans("texts.restore_{$datatable->entityType}") . "
  • "; } if (property_exists($model, 'is_deleted') && !$model->is_deleted && $can_edit) { diff --git a/app/Services/InvoiceService.php b/app/Services/InvoiceService.php index a6dc8d873d39..d5a2ab7f1218 100644 --- a/app/Services/InvoiceService.php +++ b/app/Services/InvoiceService.php @@ -35,14 +35,19 @@ class InvoiceService extends BaseService { if (isset($data['client'])) { $canSaveClient = false; + $canViewClient = false; $clientPublicId = array_get($data, 'client.public_id') ?: array_get($data, 'client.id'); if (empty($clientPublicId) || $clientPublicId == '-1') { $canSaveClient = Auth::user()->can('create', ENTITY_CLIENT); } else { - $canSaveClient = Auth::user()->can('edit', Client::scope($clientPublicId)->first()); + $client = Client::scope($clientPublicId)->first(); + $canSaveClient = Auth::user()->can('edit', $client); + $canViewClient = Auth::user()->can('view', $client); } if ($canSaveClient) { $client = $this->clientRepo->save($data['client']); + } + if ($canSaveClient || $canViewClient) { $data['client_id'] = $client->id; } } diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 3070671b3595..1f0b594fae3a 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -43,360 +43,9 @@ class PaymentService extends BaseService return $this->paymentRepo; } - public function createGateway($accountGateway) - { - $gateway = Omnipay::create($accountGateway->gateway->provider); - $gateway->initialize((array)$accountGateway->getConfig()); - - if ($accountGateway->isGateway(GATEWAY_DWOLLA)) { - if ($gateway->getSandbox() && isset($_ENV['DWOLLA_SANDBOX_KEY']) && isset($_ENV['DWOLLA_SANSBOX_SECRET'])) { - $gateway->setKey($_ENV['DWOLLA_SANDBOX_KEY']); - $gateway->setSecret($_ENV['DWOLLA_SANSBOX_SECRET']); - } elseif (isset($_ENV['DWOLLA_KEY']) && isset($_ENV['DWOLLA_SECRET'])) { - $gateway->setKey($_ENV['DWOLLA_KEY']); - $gateway->setSecret($_ENV['DWOLLA_SECRET']); - } - } - - return $gateway; - } - - public function getPaymentDetails($invitation, $accountGateway, $input = null) - { - $invoice = $invitation->invoice; - $account = $invoice->account; - $key = $invoice->account_id . '-' . $invoice->invoice_number; - $currencyCode = $invoice->client->currency ? $invoice->client->currency->code : ($invoice->account->currency ? $invoice->account->currency->code : 'USD'); - - if ($input) { - $data = self::convertInputForOmnipay($input); - Session::put($key, $data); - } elseif (Session::get($key)) { - $data = Session::get($key); - } else { - $data = $this->createDataForClient($invitation); - } - - $card = !empty($data['number']) ? new CreditCard($data) : null; - $data = [ - 'amount' => $invoice->getRequestedAmount(), - 'card' => $card, - 'currency' => $currencyCode, - 'returnUrl' => URL::to('complete'), - 'cancelUrl' => $invitation->getLink(), - 'description' => trans('texts.' . $invoice->getEntityType()) . " {$invoice->invoice_number}", - 'transactionId' => $invoice->invoice_number, - 'transactionType' => 'Purchase', - ]; - - if ($input !== null) { - $data['ip'] = \Request::ip(); - } - - if ($accountGateway->isGateway(GATEWAY_PAYPAL_EXPRESS) || $accountGateway->isGateway(GATEWAY_PAYPAL_PRO)) { - $data['ButtonSource'] = 'InvoiceNinja_SP'; - }; - - if ($input) { - if (!empty($input['sourceToken'])) { - $data['token'] = $input['sourceToken']; - unset($data['card']); - } elseif (!empty($input['plaidPublicToken'])) { - $data['plaidPublicToken'] = $input['plaidPublicToken']; - $data['plaidAccountId'] = $input['plaidAccountId']; - unset($data['card']); - } - } - - if ($accountGateway->isGateway(GATEWAY_WEPAY) && $transactionId = Session::get($invitation->id.'payment_ref')) { - $data['transaction_id'] = $transactionId; - } - - return $data; - } - - public function convertInputForOmnipay($input) - { - $data = [ - 'firstName' => isset($input['first_name']) ? $input['first_name'] : null, - 'lastName' =>isset($input['last_name']) ? $input['last_name'] : null, - 'email' => isset($input['email']) ? $input['email'] : null, - 'number' => isset($input['card_number']) ? $input['card_number'] : null, - 'expiryMonth' => isset($input['expiration_month']) ? $input['expiration_month'] : null, - 'expiryYear' => isset($input['expiration_year']) ? $input['expiration_year'] : null, - ]; - - // allow space until there's a setting to disable - if (isset($input['cvv']) && $input['cvv'] != ' ') { - $data['cvv'] = $input['cvv']; - } - - if (isset($input['address1'])) { - $country = Country::find($input['country_id']); - - $data = array_merge($data, [ - 'billingAddress1' => $input['address1'], - 'billingAddress2' => $input['address2'], - 'billingCity' => $input['city'], - 'billingState' => $input['state'], - 'billingPostcode' => $input['postal_code'], - 'billingCountry' => $country->iso_3166_2, - 'shippingAddress1' => $input['address1'], - 'shippingAddress2' => $input['address2'], - 'shippingCity' => $input['city'], - 'shippingState' => $input['state'], - 'shippingPostcode' => $input['postal_code'], - 'shippingCountry' => $country->iso_3166_2 - ]); - } - - return $data; - } - - public function createDataForClient($invitation) - { - $invoice = $invitation->invoice; - $client = $invoice->client; - $contact = $invitation->contact ?: $client->contacts()->first(); - - return [ - 'email' => $contact->email, - 'company' => $client->getDisplayName(), - 'firstName' => $contact->first_name, - 'lastName' => $contact->last_name, - 'billingAddress1' => $client->address1, - 'billingAddress2' => $client->address2, - 'billingCity' => $client->city, - 'billingPostcode' => $client->postal_code, - 'billingState' => $client->state, - 'billingCountry' => $client->country ? $client->country->iso_3166_2 : '', - 'billingPhone' => $contact->phone, - 'shippingAddress1' => $client->address1, - 'shippingAddress2' => $client->address2, - 'shippingCity' => $client->city, - 'shippingPostcode' => $client->postal_code, - 'shippingState' => $client->state, - 'shippingCountry' => $client->country ? $client->country->iso_3166_2 : '', - 'shippingPhone' => $contact->phone, - ]; - } - - public function getClientPaymentMethods($client) - { - $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); - if (!$token) { - return null; - } - - if (!$accountGatewayToken->uses_local_payment_methods && $accountGateway->gateway_id == GATEWAY_STRIPE) { - // Migrate Stripe data - $gateway = $this->createGateway($accountGateway); - $response = $gateway->fetchCustomer(array('customerReference' => $token))->send(); - if (!$response->isSuccessful()) { - return null; - } - - $data = $response->getData(); - $sources_list = isset($data['sources']) ? $data['sources'] : $data['cards']; - $sources = isset($sources_list['data'])?$sources_list['data']:$sources_list; - - // Load - $accountGatewayToken->payment_methods(); - foreach ($sources as $source) { - $paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken); - if ($paymentMethod) { - $paymentMethod->save(); - } - - if ($data['default_source'] == $source['id']) { - $accountGatewayToken->default_payment_method_id = $paymentMethod->id; - } - } - - $accountGatewayToken->uses_local_payment_methods = true; - $accountGatewayToken->save(); - } - - return $accountGatewayToken->payment_methods; - } - - public function verifyClientPaymentMethod($client, $publicId, $amount1, $amount2) - { - $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); - if ($accountGateway->gateway_id != GATEWAY_STRIPE) { - return 'Unsupported gateway'; - } - - $paymentMethod = PaymentMethod::scope($publicId, $client->account_id, $accountGatewayToken->id)->firstOrFail(); - - // Omnipay doesn't support verifying payment methods - // Also, it doesn't want to urlencode without putting numbers inside the brackets - $result = $this->makeStripeCall( - $accountGateway, - 'POST', - 'customers/' . $token . '/sources/' . $paymentMethod->source_reference . '/verify', - 'amounts[]=' . intval($amount1) . '&amounts[]=' . intval($amount2) - ); - - if (is_string($result)) { - return $result; - } - - $paymentMethod->status = PAYMENT_METHOD_STATUS_VERIFIED; - $paymentMethod->save(); - - if (!$paymentMethod->account_gateway_token->default_payment_method_id) { - $paymentMethod->account_gateway_token->default_payment_method_id = $paymentMethod->id; - $paymentMethod->account_gateway_token->save(); - } - - return true; - } - - public function removeClientPaymentMethod($client, $publicId) - { - $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); - if (!$token) { - return null; - } - - $paymentMethod = PaymentMethod::scope($publicId, $client->account_id, $accountGatewayToken->id)->firstOrFail(); - - $gateway = $this->createGateway($accountGateway); - - if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - $response = $gateway->deleteCard(array('customerReference' => $token, 'cardReference' => $paymentMethod->source_reference))->send(); - if (!$response->isSuccessful()) { - return $response->getMessage(); - } - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $response = $gateway->deletePaymentMethod(array('token' => $paymentMethod->source_reference))->send(); - - if (!$response->isSuccessful()) { - return $response->getMessage(); - } - } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { - try { - $wepay = Utils::setupWePay($accountGateway); - $wepay->request('/credit_card/delete', [ - 'client_id' => WEPAY_CLIENT_ID, - 'client_secret' => WEPAY_CLIENT_SECRET, - 'credit_card_id' => intval($paymentMethod->source_reference), - ]); - } catch (\WePayException $ex){ - return $ex->getMessage(); - } - } - - $paymentMethod->delete(); - - return true; - } - - public function setClientDefaultPaymentMethod($client, $publicId) - { - $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); - if (!$token) { - return null; - } - - $paymentMethod = PaymentMethod::scope($publicId, $client->account_id, $accountGatewayToken->id)->firstOrFail(); - $paymentMethod->account_gateway_token->default_payment_method_id = $paymentMethod->id; - $paymentMethod->account_gateway_token->save(); - - return true; - } - public function createToken($paymentType, $gateway, $details, $accountGateway, $client, $contactId, &$customerReference = null, &$paymentMethod = null) { - $customerReference = $client->getGatewayToken($accountGateway, $accountGatewayToken/* return paramenter */); - - if ($customerReference && $customerReference != CUSTOMER_REFERENCE_LOCAL) { - $details['customerReference'] = $customerReference; - - if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - $customerResponse = $gateway->fetchCustomer(array('customerReference' => $customerReference))->send(); - - if (!$customerResponse->isSuccessful()) { - $customerReference = null; // The customer might not exist anymore - } - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $customer = $gateway->findCustomer($customerReference)->send()->getData(); - - if (!($customer instanceof \Braintree\Customer)) { - $customerReference = null; // The customer might not exist anymore - } - } - } - - if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - if (!empty($details['plaidPublicToken'])) { - $plaidResult = $this->getPlaidToken($accountGateway, $details['plaidPublicToken'], $details['plaidAccountId']); - - if (is_string($plaidResult)) { - $this->lastError = $plaidResult; - return; - } elseif (!$plaidResult) { - $this->lastError = 'No token received from Plaid'; - return; - } - - unset($details['plaidPublicToken']); - unset($details['plaidAccountId']); - $details['token'] = $plaidResult['stripe_bank_account_token']; - } - - $tokenResponse = $gateway->createCard($details)->send(); - - if ($tokenResponse->isSuccessful()) { - $sourceReference = $tokenResponse->getCardReference(); - if (!$customerReference) { - $customerReference = $tokenResponse->getCustomerReference(); - } - - if (!$sourceReference) { - $responseData = $tokenResponse->getData(); - if (!empty($responseData['object']) && ($responseData['object'] == 'bank_account' || $responseData['object'] == 'card')) { - $sourceReference = $responseData['id']; - } - } - - if ($customerReference == $sourceReference) { - // This customer was just created; find the card - $data = $tokenResponse->getData(); - if (!empty($data['default_source'])) { - $sourceReference = $data['default_source']; - } - } - } else { - $data = $tokenResponse->getData(); - if ($data && $data['error'] && $data['error']['type'] == 'invalid_request_error') { - $this->lastError = $data['error']['message']; - return; - } - } - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - if (!$customerReference) { - $tokenResponse = $gateway->createCustomer(array('customerData' => array()))->send(); - if ($tokenResponse->isSuccessful()) { - $customerReference = $tokenResponse->getCustomerData()->id; - } else { - $this->lastError = $tokenResponse->getData()->message; - return; - } - } - - if ($customerReference) { - $details['customerId'] = $customerReference; - - $tokenResponse = $gateway->createPaymentMethod($details)->send(); - if ($tokenResponse->isSuccessful()) { - $sourceReference = $tokenResponse->getData()->paymentMethod->token; - } else { - $this->lastError = $tokenResponse->getData()->message; - return; - } - } + if ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { $wepay = Utils::setupWePay($accountGateway); try { @@ -466,60 +115,6 @@ class PaymentService extends BaseService return $sourceReference; } - public function convertPaymentMethodFromStripe($source, $accountGatewayToken = null, $paymentMethod = null) { - // Creating a new one or updating an existing one - if (!$paymentMethod) { - $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); - } - - $paymentMethod->last4 = $source['last4']; - $paymentMethod->source_reference = $source['id']; - - if ($source['object'] == 'bank_account') { - $paymentMethod->routing_number = $source['routing_number']; - $paymentMethod->payment_type_id = PAYMENT_TYPE_ACH; - $paymentMethod->status = $source['status']; - $currency = Cache::get('currencies')->where('code', strtoupper($source['currency']))->first(); - if ($currency) { - $paymentMethod->currency_id = $currency->id; - $paymentMethod->setRelation('currency', $currency); - } - } elseif ($source['object'] == 'card') { - $paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-01'; - $paymentMethod->payment_type_id = $this->parseCardType($source['brand']); - } else { - return null; - } - - $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); - - return $paymentMethod; - } - - public function convertPaymentMethodFromBraintree($source, $accountGatewayToken = null, $paymentMethod = null) { - // Creating a new one or updating an existing one - if (!$paymentMethod) { - $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); - } - - if ($source instanceof \Braintree\CreditCard) { - $paymentMethod->payment_type_id = $this->parseCardType($source->cardType); - $paymentMethod->last4 = $source->last4; - $paymentMethod->expiration = $source->expirationYear . '-' . $source->expirationMonth . '-01'; - } elseif ($source instanceof \Braintree\PayPalAccount) { - $paymentMethod->email = $source->email; - $paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; - } else { - return null; - } - - $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); - - $paymentMethod->source_reference = $source->token; - - return $paymentMethod; - } - public function convertPaymentMethodFromWePay($source, $accountGatewayToken = null, $paymentMethod = null) { // Creating a new one or updating an existing one if (!$paymentMethod) { @@ -554,47 +149,7 @@ class PaymentService extends BaseService } public function convertPaymentMethodFromGatewayResponse($gatewayResponse, $accountGateway, $accountGatewayToken = null, $contactId = null, $existingPaymentMethod = null) { - if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - $data = $gatewayResponse->getData(); - if (!empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) { - $source = $data; - } elseif (!empty($data['object']) && $data['object'] == 'customer') { - $sources = !empty($data['sources']) ? $data['sources'] : $data['cards']; - $source = reset($sources['data']); - } else { - $source = !empty($data['source']) ? $data['source'] : $data['card']; - } - - if ($source) { - $paymentMethod = $this->convertPaymentMethodFromStripe($source, $accountGatewayToken, $existingPaymentMethod); - } - } elseif ($accountGateway->gateway_id == GATEWAY_BRAINTREE) { - $data = $gatewayResponse->getData(); - - if (!empty($data->transaction)) { - $transaction = $data->transaction; - - if ($existingPaymentMethod) { - $paymentMethod = $existingPaymentMethod; - } else { - $paymentMethod = $accountGatewayToken ? PaymentMethod::createNew($accountGatewayToken) : new PaymentMethod(); - } - - if ($transaction->paymentInstrumentType == 'credit_card') { - $card = $transaction->creditCardDetails; - $paymentMethod->last4 = $card->last4; - $paymentMethod->expiration = $card->expirationYear . '-' . $card->expirationMonth . '-01'; - $paymentMethod->payment_type_id = $this->parseCardType($card->cardType); - } elseif ($transaction->paymentInstrumentType == 'paypal_account') { - $paymentMethod->payment_type_id = PAYMENT_TYPE_ID_PAYPAL; - $paymentMethod->email = $transaction->paypalDetails->payerEmail; - } - $paymentMethod->setRelation('payment_type', Cache::get('paymentTypes')->find($paymentMethod->payment_type_id)); - } elseif (!empty($data->paymentMethod)) { - $paymentMethod = $this->convertPaymentMethodFromBraintree($data->paymentMethod, $accountGatewayToken, $existingPaymentMethod); - } - - } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY) { + if ($accountGateway->gateway_id == GATEWAY_WEPAY) { if ($gatewayResponse instanceof \Omnipay\WePay\Message\CustomCheckoutResponse) { $wepay = \Utils::setupWePay($accountGateway); $paymentMethodType = $gatewayResponse->getData()['payment_method']['type']; @@ -624,255 +179,27 @@ class PaymentService extends BaseService return $paymentMethod; } - public function getCheckoutComToken($invitation) - { - $token = false; - $invoice = $invitation->invoice; - $client = $invoice->client; - $account = $invoice->account; - - $accountGateway = $account->getGatewayConfig(GATEWAY_CHECKOUT_COM); - - $response = $this->purchase($accountGateway, [ - 'amount' => $invoice->getRequestedAmount(), - 'currency' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD') - ])->send(); - - if ($response->isRedirect()) { - $token = $response->getTransactionReference(); - } - - Session::set($invitation->id . 'payment_type', PAYMENT_TYPE_CREDIT_CARD); - - return $token; - } - - public function getBraintreeClientToken($account) - { - $token = false; - - $accountGateway = $account->getGatewayConfig(GATEWAY_BRAINTREE); - $gateway = $this->createGateway($accountGateway); - - $token = $gateway->clientToken()->send()->getToken(); - - return $token; - } - - public function createPayment($invitation, $accountGateway, $ref, $payerId = null, $paymentDetails = null, $paymentMethod = null, $purchaseResponse = null) - { - $invoice = $invitation->invoice; - - $payment = Payment::createNew($invitation); - $payment->invitation_id = $invitation->id; - $payment->account_gateway_id = $accountGateway->id; - $payment->invoice_id = $invoice->id; - $payment->amount = $invoice->getRequestedAmount(); - $payment->client_id = $invoice->client_id; - $payment->contact_id = $invitation->contact_id; - $payment->transaction_reference = $ref; - $payment->payment_date = date_create()->format('Y-m-d'); - - if (!empty($paymentDetails['card'])) { - $card = $paymentDetails['card']; - $payment->last4 = $card->getNumberLastFour(); - $payment->payment_type_id = $this->detectCardType($card->getNumber()); - } - - if (!empty($paymentDetails['ip'])) { - $payment->ip = $paymentDetails['ip']; - } - - $savePaymentMethod = !empty($paymentMethod); - - // This will convert various gateway's formats to a known format - $paymentMethod = $this->convertPaymentMethodFromGatewayResponse($purchaseResponse, $accountGateway, null, null, $paymentMethod); - - // If this is a stored payment method, we'll update it with the latest info - if ($savePaymentMethod) { - $paymentMethod->save(); - } - - if ($accountGateway->gateway_id == GATEWAY_STRIPE) { - $data = $purchaseResponse->getData(); - $payment->payment_status_id = $data['status'] == 'succeeded' ? PAYMENT_STATUS_COMPLETED : PAYMENT_STATUS_PENDING; - } - - if ($paymentMethod) { - if ($paymentMethod->last4) { - $payment->last4 = $paymentMethod->last4; - } - - if ($paymentMethod->expiration) { - $payment->expiration = $paymentMethod->expiration; - } - - if ($paymentMethod->routing_number) { - $payment->routing_number = $paymentMethod->routing_number; - } - - if ($paymentMethod->payment_type_id) { - $payment->payment_type_id = $paymentMethod->payment_type_id; - } - - if ($paymentMethod->email) { - $payment->email = $paymentMethod->email; - } - - if ($paymentMethod->bank_name) { - $payment->bank_name = $paymentMethod->bank_name; - } - - if ($payerId) { - $payment->payer_id = $payerId; - } - - if ($savePaymentMethod) { - $payment->payment_method_id = $paymentMethod->id; - } - } - - $payment->save(); - - // enable pro plan for hosted users - if ($invoice->account->account_key == NINJA_ACCOUNT_KEY) { - foreach ($invoice->invoice_items as $invoice_item) { - // Hacky, but invoices don't have meta fields to allow us to store this easily - if (1 == preg_match('/^Plan - (.+) \((.+)\)$/', $invoice_item->product_key, $matches)) { - $plan = strtolower($matches[1]); - $term = strtolower($matches[2]); - } elseif ($invoice_item->product_key == 'Pending Monthly') { - $pending_monthly = true; - } - } - - if (!empty($plan)) { - $account = Account::with('users')->find($invoice->client->public_id); - - if( - $account->company->plan != $plan - || DateTime::createFromFormat('Y-m-d', $account->company->plan_expires) >= date_create('-7 days') - ) { - // Either this is a different plan, or the subscription expired more than a week ago - // Reset any grandfathering - $account->company->plan_started = date_create()->format('Y-m-d'); - } - - if ( - $account->company->plan == $plan - && $account->company->plan_term == $term - && DateTime::createFromFormat('Y-m-d', $account->company->plan_expires) >= date_create() - ) { - // This is a renewal; mark it paid as of when this term expires - $account->company->plan_paid = $account->company->plan_expires; - } else { - $account->company->plan_paid = date_create()->format('Y-m-d'); - } - - $account->company->payment_id = $payment->id; - $account->company->plan = $plan; - $account->company->plan_term = $term; - $account->company->plan_expires = DateTime::createFromFormat('Y-m-d', $account->company->plan_paid) - ->modify($term == PLAN_TERM_MONTHLY ? '+1 month' : '+1 year')->format('Y-m-d'); - - if (!empty($pending_monthly)) { - $account->company->pending_plan = $plan; - $account->company->pending_term = PLAN_TERM_MONTHLY; - } else { - $account->company->pending_plan = null; - $account->company->pending_term = null; - } - - $account->company->save(); - } - } - - return $payment; - } - - private function parseCardType($cardName) { - $cardTypes = array( - 'visa' => PAYMENT_TYPE_VISA, - 'americanexpress' => PAYMENT_TYPE_AMERICAN_EXPRESS, - 'amex' => PAYMENT_TYPE_AMERICAN_EXPRESS, - 'mastercard' => PAYMENT_TYPE_MASTERCARD, - 'discover' => PAYMENT_TYPE_DISCOVER, - 'jcb' => PAYMENT_TYPE_JCB, - 'dinersclub' => PAYMENT_TYPE_DINERS, - 'carteblanche' => PAYMENT_TYPE_CARTE_BLANCHE, - 'chinaunionpay' => PAYMENT_TYPE_UNIONPAY, - 'unionpay' => PAYMENT_TYPE_UNIONPAY, - 'laser' => PAYMENT_TYPE_LASER, - 'maestro' => PAYMENT_TYPE_MAESTRO, - 'solo' => PAYMENT_TYPE_SOLO, - 'switch' => PAYMENT_TYPE_SWITCH - ); - - $cardName = strtolower(str_replace(array(' ', '-', '_'), '', $cardName)); - - if (empty($cardTypes[$cardName]) && 1 == preg_match('/^('.implode('|', array_keys($cardTypes)).')/', $cardName, $matches)) { - // Some gateways return extra stuff after the card name - $cardName = $matches[1]; - } - - if (!empty($cardTypes[$cardName])) { - return $cardTypes[$cardName]; - } else { - return PAYMENT_TYPE_CREDIT_CARD_OTHER; - } - } - - private function detectCardType($number) - { - if (preg_match('/^3[47][0-9]{13}$/',$number)) { - return PAYMENT_TYPE_AMERICAN_EXPRESS; - } elseif (preg_match('/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/',$number)) { - return PAYMENT_TYPE_DINERS; - } elseif (preg_match('/^6(?:011|5[0-9][0-9])[0-9]{12}$/',$number)) { - return PAYMENT_TYPE_DISCOVER; - } elseif (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/',$number)) { - return PAYMENT_TYPE_JCB; - } elseif (preg_match('/^5[1-5][0-9]{14}$/',$number)) { - return PAYMENT_TYPE_MASTERCARD; - } elseif (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/',$number)) { - return PAYMENT_TYPE_VISA; - } - return PAYMENT_TYPE_CREDIT_CARD_OTHER; - } - - public function completePurchase($gateway, $accountGateway, $details, $token) - { - if ($accountGateway->isGateway(GATEWAY_MOLLIE)) { - $details['transactionReference'] = $token; - $response = $gateway->fetchTransaction($details)->send(); - return $gateway->fetchTransaction($details)->send(); - } else { - - return $gateway->completePurchase($details)->send(); - } - } public function autoBillInvoice($invoice) { $client = $invoice->client; - - // Make sure we've migrated in data from Stripe - $this->getClientPaymentMethods($client); - + $account = $client->account; $invitation = $invoice->invitations->first(); - $token = $client->getGatewayToken($accountGateway/* return parameter */, $accountGatewayToken/* return parameter */); - if (!$accountGatewayToken) { + if ( ! $invitation) { return false; } - $defaultPaymentMethod = $accountGatewayToken->default_payment_method; + $paymentDriver = $account->paymentDriver($invitation, GATEWAY_TYPE_TOKEN); + $customer = $paymentDriver->customer(); - if (!$invitation || !$token || !$defaultPaymentMethod) { + if ( ! $customer) { return false; } - if ($defaultPaymentMethod->requiresDelayedAutoBill()) { + $paymentMethod = $customer->default_payment_method; + + if ($paymentMethod->requiresDelayedAutoBill()) { $invoiceDate = \DateTime::createFromFormat('Y-m-d', $invoice->invoice_date); $minDueDate = clone $invoiceDate; $minDueDate->modify('+10 days'); @@ -906,43 +233,14 @@ class PaymentService extends BaseService } } - // setup the gateway/payment info - $details = $this->getPaymentDetails($invitation, $accountGateway); - $details['customerReference'] = $token; - $details['token'] = $defaultPaymentMethod->source_reference; - $details['paymentType'] = $defaultPaymentMethod->payment_type_id; + return $paymentDriver->completeOnsitePurchase(false, $paymentMethod); + + /* if ($accountGateway->gateway_id == GATEWAY_WEPAY) { $details['transaction_id'] = 'autobill_'.$invoice->id; } - - // submit purchase/get response - $response = $this->purchase($accountGateway, $details); - - if ($response->isSuccessful()) { - $ref = $response->getTransactionReference(); - return $this->createPayment($invitation, $accountGateway, $ref, null, $details, $defaultPaymentMethod, $response); - } else { - return false; - } - } - - 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 getClientRequiresDelayedAutoBill($client) { - $defaultPaymentMethod = $this->getClientDefaultPaymentMethod($client); - - return $defaultPaymentMethod?$defaultPaymentMethod->requiresDelayedAutoBill():null; + */ } public function getDatatable($clientPublicId, $search) @@ -969,9 +267,11 @@ class PaymentService extends BaseService $successful = 0; foreach ($payments as $payment) { - if(Auth::user()->can('edit', $payment)){ + if (Auth::user()->can('edit', $payment)) { $amount = !empty($params['amount']) ? floatval($params['amount']) : null; - if ($this->refund($payment, $amount)) { + $accountGateway = $payment->account_gateway; + $paymentDriver = $accountGateway->paymentDriver(); + if ($paymentDriver->refundPayment($payment, $amount)) { $successful++; } } @@ -983,201 +283,4 @@ class PaymentService extends BaseService } } - public function refund($payment, $amount = null) { - if ($amount) { - $amount = min($amount, $payment->amount - $payment->refunded); - } - - $accountGateway = $payment->account_gateway; - - if (!$accountGateway) { - $accountGateway = AccountGateway::withTrashed()->find($payment->account_gateway_id); - } - - if (!$amount || !$accountGateway) { - return; - } - - if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) { - $gateway = $this->createGateway($accountGateway); - - $details = array( - 'transactionReference' => $payment->transaction_reference, - ); - - if ($accountGateway->gateway_id == GATEWAY_WEPAY && $amount == $payment->getCompletedAmount()) { - // WePay issues a full refund when no amount is set. If an amount is set, it will try - // to issue a partial refund without refunding any fees. However, the Stripe driver - // (but not the API) requires the amount parameter to be set no matter what. - } else { - $details['amount'] = $amount; - } - - if ($accountGateway->gateway_id == GATEWAY_WEPAY) { - $details['refund_reason'] = 'Refund issued by merchant.'; - } - - $refund = $gateway->refund($details); - $response = $refund->send(); - - if ($response->isSuccessful()) { - $payment->recordRefund($amount); - } else { - $data = $response->getData(); - - if ($data instanceof \Braintree\Result\Error) { - $error = $data->errors->deepAll()[0]; - if ($error && $error->code == 91506) { - $tryVoid = true; - } - } elseif ($accountGateway->gateway_id == GATEWAY_WEPAY && $response->getCode() == 4004) { - $tryVoid = true; - } - - if (!empty($tryVoid)) { - if ($amount == $payment->amount) { - // This is an unsettled transaction; try to void it - $void = $gateway->void(array( - 'transactionReference' => $payment->transaction_reference, - )); - $response = $void->send(); - - if ($response->isSuccessful()) { - $payment->markVoided(); - } - } else { - $this->error('Unknown', 'Partial refund not allowed for unsettled transactions.', $accountGateway); - return false; - } - } - - if (!$response->isSuccessful()) { - $this->error('Unknown', $response->getMessage(), $accountGateway); - return false; - } - } - } else { - $payment->recordRefund($amount); - } - return true; - } - - private function error($type, $error, $accountGateway = false, $exception = false) - { - $message = ''; - if ($accountGateway && $accountGateway->gateway) { - $message = $accountGateway->gateway->name . ': '; - } - $message .= $error ?: trans('texts.payment_error'); - - Session::flash('error', $message); - Utils::logError("Payment Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message), 'PHP', true); - } - - public function makeStripeCall($accountGateway, $method, $url, $body = null) { - $apiKey = $accountGateway->getConfig()->apiKey; - - if (!$apiKey) { - return 'No API key set'; - } - - try{ - $options = [ - 'headers' => ['content-type' => 'application/x-www-form-urlencoded'], - 'auth' => [$accountGateway->getConfig()->apiKey,''], - ]; - - if ($body) { - $options['body'] = $body; - } - - $response = (new \GuzzleHttp\Client(['base_uri'=>'https://api.stripe.com/v1/']))->request( - $method, - $url, - $options - ); - 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(); - } - } - - private function getPlaidToken($accountGateway, $publicToken, $accountId) { - $clientId = $accountGateway->getPlaidClientId(); - $secret = $accountGateway->getPlaidSecret(); - - if (!$clientId) { - return 'No client ID set'; - } - - if (!$secret) { - return 'No secret set'; - } - - try{ - $subdomain = $accountGateway->getPlaidEnvironment() == 'production' ? 'api' : 'tartan'; - $response = (new \GuzzleHttp\Client(['base_uri'=>"https://{$subdomain}.plaid.com"]))->request( - 'POST', - 'exchange_token', - [ - 'allow_redirects' => false, - 'headers' => ['content-type' => 'application/x-www-form-urlencoded'], - 'body' => http_build_query(array( - 'client_id' => $clientId, - 'secret' => $secret, - 'public_token' => $publicToken, - 'account_id' => $accountId, - )) - ] - ); - return json_decode($response->getBody(), true); - } catch (\GuzzleHttp\Exception\BadResponseException $e) { - $response = $e->getResponse(); - $body = json_decode($response->getBody(), true); - - if ($body && !empty($body['message'])) { - return $body['message']; - } - - return $e->getMessage(); - } - } - - public function purchase($accountGateway, $details) { - $gateway = $this->createGateway($accountGateway); - - if ($accountGateway->gateway_id == GATEWAY_WEPAY) { - $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(); - - return $response; - } - - private function calculateApplicationFee($accountGateway, $amount) { - if ($accountGateway->gateway_id = GATEWAY_WEPAY) { - $fee = WEPAY_APP_FEE_MULTIPLIER * $amount + WEPAY_APP_FEE_FIXED; - - return floor(min($fee, $amount * 0.2));// Maximum fee is 20% of the amount. - } - - return 0; - } } diff --git a/app/Services/TemplateService.php b/app/Services/TemplateService.php index cc3e1becc031..564eb76da6dd 100644 --- a/app/Services/TemplateService.php +++ b/app/Services/TemplateService.php @@ -28,7 +28,7 @@ class TemplateService } $documentsHTML .= ''; } - + $variables = [ '$footer' => $account->getEmailFooter(), '$client' => $client->getDisplayName(), @@ -55,15 +55,14 @@ class TemplateService ]; // Add variables for available payment types - foreach (Gateway::$paymentTypes as $type) { - $camelType = Gateway::getPaymentTypeName($type); - $type = Utils::toSnakeCase($camelType); + foreach (Gateway::$gatewayTypes as $type) { + $camelType = Utils::toCamelCase($type); $variables["\${$camelType}Link"] = $invitation->getLink('payment') . "/{$type}"; $variables["\${$camelType}Button"] = Form::emailPaymentButton($invitation->getLink('payment') . "/{$type}"); } - + $includesPasswordPlaceholder = strpos($template, '$password') !== false; - + $str = str_replace(array_keys($variables), array_values($variables), $template); if (!$includesPasswordPlaceholder && $passwordHTML) { @@ -72,10 +71,10 @@ class TemplateService { $str = substr_replace($str, $passwordHTML, $pos, 9/* length of "$password" */); } - } + } $str = str_replace('$password', '', $str); $str = autolink($str, 100); - + return $str; - } -} \ No newline at end of file + } +} diff --git a/database/migrations/2016_04_23_182223_payments_changes.php b/database/migrations/2016_04_23_182223_payments_changes.php index 35a1b6134e9b..b565b1dc752a 100644 --- a/database/migrations/2016_04_23_182223_payments_changes.php +++ b/database/migrations/2016_04_23_182223_payments_changes.php @@ -28,6 +28,7 @@ class PaymentsChanges extends Migration { $table->increments('id'); $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); $table->unsignedInteger('contact_id')->nullable(); $table->unsignedInteger('account_gateway_token_id'); $table->unsignedInteger('payment_type_id'); @@ -44,6 +45,7 @@ class PaymentsChanges extends Migration $table->softDeletes(); $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); $table->foreign('account_gateway_token_id')->references('id')->on('account_gateway_tokens'); $table->foreign('payment_type_id')->references('id')->on('payment_types'); @@ -107,7 +109,7 @@ class PaymentsChanges extends Migration $table->dropColumn('refunded'); $table->dropForeign('payments_payment_status_id_foreign'); $table->dropColumn('payment_status_id'); - + $table->dropColumn('routing_number'); $table->dropColumn('last4'); $table->dropColumn('expiration'); @@ -139,7 +141,7 @@ class PaymentsChanges extends Migration Schema::table('invoices', function ($table) { $table->dropColumn('client_enable_auto_bill'); }); - + Schema::dropIfExists('payment_statuses'); Schema::table('account_gateway_tokens', function($table) diff --git a/database/seeds/PaymentLibrariesSeeder.php b/database/seeds/PaymentLibrariesSeeder.php index 88a270930fd4..280db905d3bb 100644 --- a/database/seeds/PaymentLibrariesSeeder.php +++ b/database/seeds/PaymentLibrariesSeeder.php @@ -16,18 +16,18 @@ class PaymentLibrariesSeeder extends Seeder $gateways = [ ['name' => 'Authorize.Net AIM', 'provider' => 'AuthorizeNet_AIM'], - ['name' => 'Authorize.Net SIM', 'provider' => 'AuthorizeNet_SIM'], + ['name' => 'Authorize.Net SIM', 'provider' => 'AuthorizeNet_SIM', 'payment_library_id' => 2], ['name' => 'CardSave', 'provider' => 'CardSave'], - ['name' => 'Eway Rapid', 'provider' => 'Eway_Rapid'], + ['name' => 'Eway Rapid', 'provider' => 'Eway_Rapid', 'is_offsite' => true], ['name' => 'FirstData Connect', 'provider' => 'FirstData_Connect'], ['name' => 'GoCardless', 'provider' => 'GoCardless', 'is_offsite' => true], ['name' => 'Migs ThreeParty', 'provider' => 'Migs_ThreeParty'], ['name' => 'Migs TwoParty', 'provider' => 'Migs_TwoParty'], - ['name' => 'Mollie', 'provider' => 'Mollie'], + ['name' => 'Mollie', 'provider' => 'Mollie', 'is_offsite' => true], ['name' => 'MultiSafepay', 'provider' => 'MultiSafepay'], ['name' => 'Netaxept', 'provider' => 'Netaxept'], ['name' => 'NetBanx', 'provider' => 'NetBanx'], - ['name' => 'PayFast', 'provider' => 'PayFast'], + ['name' => 'PayFast', 'provider' => 'PayFast', 'is_offsite' => true], ['name' => 'Payflow Pro', 'provider' => 'Payflow_Pro'], ['name' => 'PaymentExpress PxPay', 'provider' => 'PaymentExpress_PxPay'], ['name' => 'PaymentExpress PxPost', 'provider' => 'PaymentExpress_PxPost'], @@ -41,7 +41,7 @@ class PaymentLibrariesSeeder extends Seeder ['name' => 'TargetPay Direct eBanking', 'provider' => 'TargetPay_Directebanking'], ['name' => 'TargetPay Ideal', 'provider' => 'TargetPay_Ideal'], ['name' => 'TargetPay Mr Cash', 'provider' => 'TargetPay_Mrcash'], - ['name' => 'TwoCheckout', 'provider' => 'TwoCheckout'], + ['name' => 'TwoCheckout', 'provider' => 'TwoCheckout', 'is_offsite' => true], ['name' => 'WorldPay', 'provider' => 'WorldPay'], ['name' => 'BeanStream', 'provider' => 'BeanStream', 'payment_library_id' => 2], ['name' => 'Psigate', 'provider' => 'Psigate', 'payment_library_id' => 2], diff --git a/database/seeds/UserTableSeeder.php b/database/seeds/UserTableSeeder.php index 1d0759dbc609..990c7c53ae6f 100644 --- a/database/seeds/UserTableSeeder.php +++ b/database/seeds/UserTableSeeder.php @@ -68,7 +68,8 @@ class UserTableSeeder extends Seeder 'city' => $faker->city, 'state' => $faker->state, 'postal_code' => $faker->postcode, - 'country_id' => Country::all()->random()->id, + 'country_id' => DEFAULT_COUNTRY, + 'currency_id' => DEFAULT_CURRENCY, ]); Contact::create([ @@ -77,6 +78,7 @@ class UserTableSeeder extends Seeder 'client_id' => $client->id, 'public_id' => 1, 'email' => TEST_USERNAME, + 'is_primary' => true, ]); Product::create([ diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 92552e8878d0..58efc4ecb53d 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -445,7 +445,7 @@ $LANG = array( 'token_billing_4' => 'Always', 'token_billing_checkbox' => 'Store credit card details', 'view_in_gateway' => 'View in :gateway', - 'use_card_on_file' => 'Use card on file', + 'use_card_on_file' => 'Use Card on File', 'edit_payment_details' => 'Edit payment details', 'token_billing' => 'Save card details', 'token_billing_secure' => 'The data is stored securely by :link', @@ -1351,6 +1351,12 @@ $LANG = array( 'update_font_cache' => 'Please force refresh the page to update the font cache.', 'more_options' => 'More options', + 'credit_card' => 'Credit Card', + 'bank_transfer' => 'Bank Transfer', + 'no_transaction_reference' => 'We did not recieve a payment transaction reference from the gateway.', + 'use_bank_on_file' => 'Use Bank on File', + 'auto_bill_email_message' => 'This invoice will automatically be billed to the payment method on file on the due date.', + 'bitcoin' => 'Bitcoin', ); diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index c14255737fc7..093fdef59ddb 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -13,8 +13,7 @@ {!! Former::open($url)->method($method)->rule()->addClass('warn-on-exit') !!} @if ($accountGateway) - {!! Former::populateField('gateway_id', $accountGateway->gateway_id) !!} - {!! Former::populateField('payment_type_id', $paymentTypeId) !!} + {!! Former::populateField('primary_gateway_id', $accountGateway->gateway_id) !!} {!! Former::populateField('recommendedGateway_id', $accountGateway->gateway_id) !!} {!! Former::populateField('show_address', intval($accountGateway->show_address)) !!} {!! Former::populateField('update_address', intval($accountGateway->update_address)) !!} @@ -39,14 +38,13 @@ {!! Former::populateField('update_address', 1) !!} @if (Utils::isNinjaDev()) - {!! Former::populateField('23_apiKey', env('STRIPE_TEST_SECRET_KEY')) !!} - {!! Former::populateField('publishable_key', env('STRIPE_TEST_PUBLISHABLE_KEY')) !!} + @include('accounts.partials.payment_credentials') @endif @endif @if ($accountGateway)
    - {!! Former::text('primary_gateway_id')->value($accountGateway->gateway_id) !!} + {!! Former::text('primary_gateway_id') !!}
    @else {!! Former::select('primary_gateway_id') @@ -168,7 +166,7 @@
    @endif - +
    @@ -203,6 +201,12 @@ } else { $('.onsite-fields').show(); } + + if (gateway.id == {{ GATEWAY_STRIPE }}) { + $('.stripe-ach').show(); + } else { + $('.stripe-ach').hide(); + } } function gatewayLink(url) { diff --git a/resources/views/accounts/account_gateway_wepay.blade.php b/resources/views/accounts/account_gateway_wepay.blade.php index 9adcdb5be2f2..00367af2de8a 100644 --- a/resources/views/accounts/account_gateway_wepay.blade.php +++ b/resources/views/accounts/account_gateway_wepay.blade.php @@ -62,18 +62,9 @@ ->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') + {!! 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').''] @@ -83,7 +74,7 @@
    - {!! Button::normal(trans('texts.use_another_provider'))->large()->asLinkTo(URL::to('/gateways/create/0')) !!} + {!! Button::normal(trans('texts.use_another_provider'))->large()->asLinkTo(URL::to('/gateways/create?other_providers=true')) !!} {!! Button::success(trans('texts.sign_up_with_wepay'))->submit()->large() !!}
    @@ -108,7 +99,7 @@ }) - + {!! Former::close() !!} @stop diff --git a/resources/views/accounts/partials/payment_credentials.blade.php b/resources/views/accounts/partials/payment_credentials.blade.php new file mode 100644 index 000000000000..b2e641f4e0a1 --- /dev/null +++ b/resources/views/accounts/partials/payment_credentials.blade.php @@ -0,0 +1,47 @@ +{!! Former::populateField(GATEWAY_STRIPE . '_apiKey', env('STRIPE_TEST_SECRET_KEY')) !!} +{!! Former::populateField('publishable_key', env('STRIPE_TEST_PUBLISHABLE_KEY')) !!} +{!! Former::populateField('enable_ach', 1) !!} +{!! Former::populateField('plaid_client_id', env('PLAID_TEST_CLIENT_ID')) !!} +{!! Former::populateField('plaid_secret', env('PLAID_TEST_SECRET')) !!} +{!! Former::populateField('plaid_public_key', env('PLAID_TEST_PUBLIC_KEY')) !!} + +{!! Former::populateField(GATEWAY_PAYPAL_EXPRESS . '_username', env('PAYPAL_TEST_USERNAME')) !!} +{!! Former::populateField(GATEWAY_PAYPAL_EXPRESS . '_password', env('PAYPAL_TEST_PASSWORD')) !!} +{!! Former::populateField(GATEWAY_PAYPAL_EXPRESS . '_signature', env('PAYPAL_TEST_SIGNATURE')) !!} +{!! Former::populateField(GATEWAY_PAYPAL_EXPRESS . '_testMode', 1) !!} + +{!! Former::populateField(GATEWAY_DWOLLA . '_destinationId', env('DWOLLA_TEST_DESTINATION_ID')) !!} + +{!! Former::populateField(GATEWAY_BITPAY . '_testMode', 1) !!} +{!! Former::populateField(GATEWAY_BITPAY . '_apiKey', env('BITPAY_TEST_API_KEY')) !!} + +{!! Former::populateField(GATEWAY_TWO_CHECKOUT . '_secretWord', env('TWO_CHECKOUT_SECRET_WORD')) !!} +{!! Former::populateField(GATEWAY_TWO_CHECKOUT . '_accountNumber', env('TWO_CHECKOUT_ACCOUNT_NUMBER')) !!} +{!! Former::populateField(GATEWAY_TWO_CHECKOUT . '_testMode', 1) !!} + +{!! Former::populateField(GATEWAY_MOLLIE . '_apiKey', env('MOLLIE_TEST_API_KEY')) !!} + +{!! Former::populateField(GATEWAY_AUTHORIZE_NET . '_apiLoginId', env('AUTHORIZE_NET_TEST_API_LOGIN_ID')) !!} +{!! Former::populateField(GATEWAY_AUTHORIZE_NET . '_transactionKey', env('AUTHORIZE_NET_TEST_TRANSACTION_KEY')) !!} +{!! Former::populateField(GATEWAY_AUTHORIZE_NET . '_secretKey', env('AUTHORIZE_NET_TEST_SECRET_KEY')) !!} +{!! Former::populateField(GATEWAY_AUTHORIZE_NET . '_testMode', 1) !!} +{!! Former::populateField(GATEWAY_AUTHORIZE_NET . '_developerMode', 1) !!} + +{!! Former::populateField(GATEWAY_CHECKOUT_COM . '_publicApiKey', env('CHECKOUT_COM_TEST_PUBLIC_API_KEY')) !!} +{!! Former::populateField(GATEWAY_CHECKOUT_COM . '_secretApiKey', env('CHECKOUT_COM_TEST_SECRET_API_KEY')) !!} +{!! Former::populateField(GATEWAY_CHECKOUT_COM . '_testMode', 1) !!} + +{!! Former::populateField(GATEWAY_CYBERSOURCE . '_accessKey', env('CYBERSOURCE_TEST_ACCESS_KEY')) !!} +{!! Former::populateField(GATEWAY_CYBERSOURCE . '_secretKey', env('CYBERSOURCE_TEST_SECRET_KEY')) !!} +{!! Former::populateField(GATEWAY_CYBERSOURCE . '_profileId', env('CYBERSOURCE_TEST_PROFILE_ID')) !!} +{!! Former::populateField(GATEWAY_CYBERSOURCE . '_testMode', 1) !!} + +{!! Former::populateField(GATEWAY_EWAY . '_apiKey', env('EWAY_TEST_API_KEY')) !!} +{!! Former::populateField(GATEWAY_EWAY . '_password', env('EWAY_TEST_PASSWORD')) !!} +{!! Former::populateField(GATEWAY_EWAY . '_testMode', 1) !!} + +{!! Former::populateField(GATEWAY_BRAINTREE . '_privateKey', env('BRAINTREE_TEST_PRIVATE_KEY')) !!} +{!! Former::populateField(GATEWAY_BRAINTREE . '_publicKey', env('BRAINTREE_TEST_PUBLIC_KEY')) !!} +{!! Former::populateField(GATEWAY_BRAINTREE . '_merchantId', env('BRAINTREE_TEST_MERCHANT_ID')) !!} +{!! Former::populateField(GATEWAY_BRAINTREE . '_testMode', 1) !!} +{!! Former::populateField('enable_paypal', 1) !!} diff --git a/resources/views/accounts/payments.blade.php b/resources/views/accounts/payments.blade.php index 55e7177c683c..5bb401c274cb 100644 --- a/resources/views/accounts/payments.blade.php +++ b/resources/views/accounts/payments.blade.php @@ -31,11 +31,13 @@
    {!! Former::close() !!} + + @if ($showAdd) {!! Button::primary(trans('texts.add_gateway')) ->asLinkTo(URL::to('/gateways/create')) @@ -48,14 +50,13 @@ {!! Datatable::table() ->addColumn( trans('texts.name'), - trans('texts.payment_type_id'), trans('texts.action')) ->setUrl(url('api/gateways/')) ->setOptions('sPaginationType', 'bootstrap') ->setOptions('bFilter', false) ->setOptions('bAutoWidth', false) - ->setOptions('aoColumns', [[ "sWidth"=> "50%" ], [ "sWidth"=> "30%" ], ["sWidth"=> "20%"]]) - ->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[2]]]) + ->setOptions('aoColumns', [[ "sWidth"=> "80%" ], ["sWidth"=> "20%"]]) + ->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[1]]]) ->render('datatable') !!} - @endif -@stop - -@section('content') - - @include('payments.payment_css') - - - @if($paymentType == PAYMENT_TYPE_STRIPE_ACH) - {!! Former::open($url) - ->autocomplete('on') - ->addClass('payment-form') - ->id('payment-form') - ->rules(array( - 'first_name' => 'required', - 'last_name' => 'required', - 'account_number' => 'required', - 'routing_number' => 'required', - 'account_holder_name' => 'required', - 'account_holder_type' => 'required', - 'authorize_ach' => 'required', - )) !!} - @else - {!! Former::vertical_open($url) - ->autocomplete('on') - ->addClass('payment-form') - ->id('payment-form') - ->rules(array( - 'first_name' => 'required', - 'last_name' => 'required', - 'card_number' => 'required', - 'expiration_month' => 'required', - 'expiration_year' => 'required', - 'cvv' => 'required', - 'address1' => 'required', - 'city' => 'required', - 'state' => 'required', - 'postal_code' => 'required', - 'country_id' => 'required', - 'phone' => 'required', - 'email' => 'required|email', - 'authorize_ach' => 'required', - 'tos_agree' => 'required', - )) !!} - @endif - - @if ($client) - {{ Former::populate($client) }} - {{ Former::populateField('first_name', $contact->first_name) }} - {{ Former::populateField('last_name', $contact->last_name) }} - {{ Former::populateField('email', $contact->email) }} - @if (!$client->country_id && $client->account->country_id) - {{ Former::populateField('country_id', $client->account->country_id) }} - @endif - @if (!$client->currency_id && $client->account->currency_id) - {{ Former::populateField('currency_id', $client->account->currency_id) }} - {{ Former::populateField('currency', $client->account->currency->code) }} - @endif - @endif - - @if (Utils::isNinjaDev()) - {{ Former::populateField('first_name', 'Test') }} - {{ Former::populateField('last_name', 'Test') }} - {{ Former::populateField('address1', '350 5th Ave') }} - {{ Former::populateField('city', 'New York') }} - {{ Former::populateField('state', 'NY') }} - {{ Former::populateField('postal_code', '10118') }} - {{ Former::populateField('country_id', 840) }} - - - @endif - - -
    -

     

    - -
    -
    - -
    -
    -
    - @if ($client && isset($invoiceNumber)) -

    {{ $client->getDisplayName() }}

    -

    {{ trans('texts.invoice') . ' ' . $invoiceNumber }}|  {{ trans('texts.amount_due') }}: {{ $account->formatMoney($amount, $client, true) }}

    - @elseif ($paymentTitle) -

    {{ $paymentTitle }} - @if(isset($paymentSubtitle)) -
    {{ $paymentSubtitle }} - @endif -

    - @endif -
    -
    -
    - @if (Request::secure() || Utils::isNinjaDev()) -
    -

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

    -
    {{ trans('texts.256_encryption') }}
    -
    - @endif -
    -
    - -

     
     

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

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

    -
    -
    - {!! Former::text('first_name') - ->placeholder(trans('texts.first_name')) - ->label('') !!} -
    -
    - {!! Former::text('last_name') - ->placeholder(trans('texts.last_name')) - ->autocomplete('family-name') - ->label('') !!} -
    -
    - @if (isset($paymentTitle) || ! empty($contact->email)) -
    -
    - {!! Former::text('email') - ->placeholder(trans('texts.email')) - ->autocomplete('email') - ->label('') !!} -
    -
    - @endif - -

     
     

    - - @if (!empty($showAddress)) -

    {{ trans('texts.billing_address') }} {{ trans('texts.payment_footer1') }}

    -
    -
    - {!! Former::text('address1') - ->autocomplete('address-line1') - ->placeholder(trans('texts.address1')) - ->label('') !!} -
    -
    - {!! Former::text('address2') - ->autocomplete('address-line2') - ->placeholder(trans('texts.address2')) - ->label('') !!} -
    -
    -
    -
    - {!! Former::text('city') - ->autocomplete('address-level2') - ->placeholder(trans('texts.city')) - ->label('') !!} -
    -
    - {!! Former::text('state') - ->autocomplete('address-level1') - ->placeholder(trans('texts.state')) - ->label('') !!} -
    -
    -
    -
    - {!! Former::text('postal_code') - ->autocomplete('postal-code') - ->placeholder(trans('texts.postal_code')) - ->label('') !!} -
    -
    - {!! Former::select('country_id') - ->placeholder(trans('texts.country_id')) - ->fromQuery($countries, 'name', 'id') - ->addGroupClass('country-select') - ->label('') !!} -
    -
    - -

     
     

    - @endif - -

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

    - @endif - - - @if($paymentType == PAYMENT_TYPE_STRIPE_ACH) - @if($accountGateway->getPlaidEnabled()) - - @endif -
    - @if($accountGateway->getPlaidEnabled()) -
    {{ trans('texts.or') }}
    -

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

    - @endif -

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

    - {!! Former::radios('account_holder_type')->radios(array( - trans('texts.individual_account') => array('value' => 'individual'), - trans('texts.company_account') => array('value' => 'company'), - ))->inline()->label(trans('texts.account_holder_type')); !!} - {!! Former::text('account_holder_name') - ->label(trans('texts.account_holder_name')) !!} - {!! Former::select('country_id') - ->label(trans('texts.country_id')) - ->fromQuery($countries, 'name', 'id') - ->addGroupClass('country-select') !!} - {!! Former::select('currency') - ->label(trans('texts.currency_id')) - ->fromQuery($currencies, 'name', 'code') - ->addGroupClass('currency-select') !!} - {!! Former::text('') - ->id('routing_number') - ->label(trans('texts.routing_number')) !!} -
    -
    -
    -
    -
    - {!! Former::text('') - ->id('account_number') - ->label(trans('texts.account_number')) !!} - {!! Former::text('') - ->id('confirm_account_number') - ->label(trans('texts.confirm_account_number')) !!} -
    - {!! Former::checkbox('authorize_ach') - ->text(trans('texts.ach_authorization', ['company'=>$account->getDisplayName(), 'email' => $account->work_email])) - ->label(' ') !!} -
    - {!! Button::success(strtoupper(trans('texts.add_account'))) - ->submit() - ->withAttributes(['id'=>'add_account_button']) - ->large() !!} - @if($accountGateway->getPlaidEnabled() && !empty($amount)) - {!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) )) - ->submit() - ->withAttributes(['style'=>'display:none', 'id'=>'pay_now_button']) - ->large() !!} - @endif -
    - @elseif($paymentType == PAYMENT_TYPE_BRAINTREE_PAYPAL) -

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

    -
    {{$details->firstName}} {{$details->lastName}}
    -
    {{$details->email}}
    - - - - -

     

    - @if (isset($amount) && $client && $account->showTokenCheckbox()) - selectTokenCheckbox() ? 'CHECKED' : '' }} value="1" style="margin-left:0px; vertical-align:top"> - - - {!! trans('texts.token_billing_secure', ['link' => link_to('https://www.braintreepayments.com/', 'Braintree', ['target' => '_blank'])]) !!} - - @endif -

     

    -
    - @if(isset($amount)) - {!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) )) - ->submit() - ->large() !!} - @else - {!! Button::success(strtoupper(trans('texts.add_paypal_account') )) - ->submit() - ->large() !!} - @endif -
    - @elseif($paymentType == PAYMENT_TYPE_WEPAY_ACH) -

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

    - @if (!empty($details)) -
    {{$details->bank_account_name}}
    -
    •••••{{$details->bank_account_last_four}}
    - @endif - {!! Former::checkbox('authorize_ach') - ->text(trans('texts.ach_authorization', ['company'=>$account->getDisplayName(), 'email' => $account->work_email])) - ->label(' ') !!} - {!! Former::checkbox('tos_agree') - ->text(trans('texts.wepay_payment_tos_agree', [ - 'terms' => ''.trans('texts.terms_of_service').'', - 'privacy_policy' => ''.trans('texts.privacy_policy').'', - ])) - ->help(trans('texts.payment_processed_through_wepay')) - ->label(' ') !!} - -

     

    -
    - @if(isset($amount) && empty($paymentMethodPending)) - {!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) )) - ->submit() - ->large() !!} - @else - {!! Button::success(strtoupper(trans('texts.add_bank_account') )) - ->submit() - ->large() !!} - @endif -
    - @else -
    -
    - @if (!empty($braintreeClientToken)) -
    - @else - {!! Former::text(!empty($tokenize) ? '' : 'card_number') - ->id('card_number') - ->placeholder(trans('texts.card_number')) - ->autocomplete('cc-number') - ->label('') !!} - @endif -
    -
    - @if (!empty($braintreeClientToken)) -
    - @else - {!! Former::text(!empty($tokenize) ? '' : 'cvv') - ->id('cvv') - ->placeholder(trans('texts.cvv')) - ->autocomplete('off') - ->label('') !!} - @endif -
    -
    -
    -
    - @if (!empty($braintreeClientToken)) -
    - @else - {!! Former::select(!empty($tokenize) ? '' : 'expiration_month') - ->id('expiration_month') - ->autocomplete('cc-exp-month') - ->placeholder(trans('texts.expiration_month')) - ->addOption('01 - January', '1') - ->addOption('02 - February', '2') - ->addOption('03 - March', '3') - ->addOption('04 - April', '4') - ->addOption('05 - May', '5') - ->addOption('06 - June', '6') - ->addOption('07 - July', '7') - ->addOption('08 - August', '8') - ->addOption('09 - September', '9') - ->addOption('10 - October', '10') - ->addOption('11 - November', '11') - ->addOption('12 - December', '12')->label('') - !!} - @endif -
    -
    - @if (!empty($braintreeClientToken)) -
    - @else - {!! Former::select(!empty($tokenize) ? '' : 'expiration_year') - ->id('expiration_year') - ->autocomplete('cc-exp-year') - ->placeholder(trans('texts.expiration_year')) - ->addOption('2016', '2016') - ->addOption('2017', '2017') - ->addOption('2018', '2018') - ->addOption('2019', '2019') - ->addOption('2020', '2020') - ->addOption('2021', '2021') - ->addOption('2022', '2022') - ->addOption('2023', '2023') - ->addOption('2024', '2024') - ->addOption('2025', '2025') - ->addOption('2026', '2026')->label('') - !!} - @endif -
    -
    -
    -
    - @if (isset($amount) && $client && $account->showTokenCheckbox($storageGateway/* will contain gateway id */)) - selectTokenCheckbox() ? 'CHECKED' : '' }} value="1" style="margin-left:0px; vertical-align:top"> - - - @if ($storageGateway == GATEWAY_STRIPE) - {!! trans('texts.token_billing_secure', ['link' => link_to('https://stripe.com/', 'Stripe.com', ['target' => '_blank'])]) !!} - @elseif ($storageGateway == GATEWAY_BRAINTREE) - {!! trans('texts.token_billing_secure', ['link' => link_to('https://www.braintreepayments.com/', 'Braintree', ['target' => '_blank'])]) !!} - @endif - - @endif -
    - -
    - @if (isset($acceptedCreditCardTypes)) -
    - @foreach ($acceptedCreditCardTypes as $card) - {{ $card['alt'] }} - @endforeach -
    - @endif -
    -
    - -

     

    -
    - @if(isset($amount)) - {!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) )) - ->submit() - ->large() !!} - @else - {!! Button::success(strtoupper(trans('texts.add_credit_card') )) - ->submit() - ->large() !!} - @endif -
    -

     

    - @endif - - -
    - -
    -
    - - -

     

    -

     

    - -
    - - {!! Former::close() !!} - - - @if (isset($accountGateway) && $accountGateway->getPlaidEnabled()) - - - @endif -@stop diff --git a/resources/views/payments/bank_transfer.blade.php b/resources/views/payments/bank_transfer.blade.php new file mode 100644 index 000000000000..73a26e010876 --- /dev/null +++ b/resources/views/payments/bank_transfer.blade.php @@ -0,0 +1,11 @@ +@extends('payments.payment_method') + +@section('head') + @parent + + @if (isset($accountGateway) && $accountGateway->getPlaidEnabled()) + + + @endif + +@stop diff --git a/resources/views/payments/braintree/credit_card.blade.php b/resources/views/payments/braintree/credit_card.blade.php new file mode 100644 index 000000000000..9373b72fb1a2 --- /dev/null +++ b/resources/views/payments/braintree/credit_card.blade.php @@ -0,0 +1,73 @@ +@extends('payments.credit_card') + +@section('head') + @parent + + + +@stop diff --git a/resources/views/payments/braintree/paypal.blade.php b/resources/views/payments/braintree/paypal.blade.php new file mode 100644 index 000000000000..711124f6ecf9 --- /dev/null +++ b/resources/views/payments/braintree/paypal.blade.php @@ -0,0 +1,42 @@ +@extends('payments.payment_method') + +@section('payment_details') + @parent + + {!! Former::open($url) !!} + +

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

    + +
    {{$details->firstName}} {{$details->lastName}}
    +
    {{$details->email}}
    + + + + + + +

     

    + + @if (isset($amount) && $client && $account->showTokenCheckbox()) + selectTokenCheckbox() ? 'CHECKED' : '' }} value="1" style="margin-left:0px; vertical-align:top"> + + + {!! trans('texts.token_billing_secure', ['link' => link_to('https://www.braintreepayments.com/', 'Braintree', ['target' => '_blank'])]) !!} + + @endif + +

     

    + +
    + @if(isset($amount)) + {!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) )) + ->submit() + ->large() !!} + @else + {!! Button::success(strtoupper(trans('texts.add_paypal_account') )) + ->submit() + ->large() !!} + @endif +
    + +@stop diff --git a/resources/views/partials/checkout_com_payment.blade.php b/resources/views/payments/checkoutcom/partial.blade.php similarity index 62% rename from resources/views/partials/checkout_com_payment.blade.php rename to resources/views/payments/checkoutcom/partial.blade.php index b214e2a79209..dc326c2ed9f5 100644 --- a/resources/views/partials/checkout_com_payment.blade.php +++ b/resources/views/payments/checkoutcom/partial.blade.php @@ -1,4 +1,4 @@ -@if ($checkoutComDebug) +@if ($accountGateway->getConfigField('testMode')) @else @@ -7,9 +7,9 @@
    -
    \ No newline at end of file + diff --git a/resources/views/payments/credit_card.blade.php b/resources/views/payments/credit_card.blade.php new file mode 100644 index 000000000000..6e8b3bff2dc2 --- /dev/null +++ b/resources/views/payments/credit_card.blade.php @@ -0,0 +1,259 @@ +@extends('payments.payment_method') + +@section('payment_details') + + {!! Former::vertical_open($url) + ->autocomplete('on') + ->addClass('payment-form') + ->id('payment-form') + ->rules(array( + 'first_name' => 'required', + 'last_name' => 'required', + 'card_number' => 'required', + 'expiration_month' => 'required', + 'expiration_year' => 'required', + 'cvv' => 'required', + 'address1' => 'required', + 'city' => 'required', + 'state' => 'required', + 'postal_code' => 'required', + 'country_id' => 'required', + 'phone' => 'required', + 'email' => 'required|email', + 'authorize_ach' => 'required', + 'tos_agree' => 'required', + 'account_number' => 'required', + 'routing_number' => 'required', + 'account_holder_name' => 'required', + 'account_holder_type' => 'required', + )) !!} + + + @if ($client) + {{ Former::populate($client) }} + {{ Former::populateField('first_name', $contact->first_name) }} + {{ Former::populateField('last_name', $contact->last_name) }} + {{ Former::populateField('email', $contact->email) }} + @if (!$client->country_id && $client->account->country_id) + {{ Former::populateField('country_id', $client->account->country_id) }} + @endif + @if (!$client->currency_id && $client->account->currency_id) + {{ Former::populateField('currency_id', $client->account->currency_id) }} + {{ Former::populateField('currency', $client->account->currency->code) }} + @endif + @endif + + @if (Utils::isNinjaDev()) + {{ Former::populateField('first_name', 'Test') }} + {{ Former::populateField('last_name', 'Test') }} + {{ Former::populateField('address1', '350 5th Ave') }} + {{ Former::populateField('city', 'New York') }} + {{ Former::populateField('state', 'NY') }} + {{ Former::populateField('postal_code', '10118') }} + {{ Former::populateField('country_id', 840) }} + + + @endif + +

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

    +
    +
    + {!! Former::text('first_name') + ->placeholder(trans('texts.first_name')) + ->label('') !!} +
    +
    + {!! Former::text('last_name') + ->placeholder(trans('texts.last_name')) + ->autocomplete('family-name') + ->label('') !!} +
    +
    + + @if (isset($paymentTitle) || ! empty($contact->email)) +
    +
    + {!! Former::text('email') + ->placeholder(trans('texts.email')) + ->autocomplete('email') + ->label('') !!} +
    +
    + @endif + +

     
     

    + + @if (!empty($showAddress)) +

    {{ trans('texts.billing_address') }} {{ trans('texts.payment_footer1') }}

    +
    +
    + {!! Former::text('address1') + ->autocomplete('address-line1') + ->placeholder(trans('texts.address1')) + ->label('') !!} +
    +
    + {!! Former::text('address2') + ->autocomplete('address-line2') + ->placeholder(trans('texts.address2')) + ->label('') !!} +
    +
    +
    +
    + {!! Former::text('city') + ->autocomplete('address-level2') + ->placeholder(trans('texts.city')) + ->label('') !!} +
    +
    + {!! Former::text('state') + ->autocomplete('address-level1') + ->placeholder(trans('texts.state')) + ->label('') !!} +
    +
    +
    +
    + {!! Former::text('postal_code') + ->autocomplete('postal-code') + ->placeholder(trans('texts.postal_code')) + ->label('') !!} +
    +
    + {!! Former::select('country_id') + ->placeholder(trans('texts.country_id')) + ->fromQuery(Cache::get('countries'), 'name', 'id') + ->addGroupClass('country-select') + ->label('') !!} +
    +
    + +

     
     

    + @endif + +

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

    + +
    +
    + @if ($accountGateway->gateway_id == GATEWAY_BRAINTREE) +
    + @else + {!! Former::text(!empty($tokenize) ? '' : 'card_number') + ->id('card_number') + ->placeholder(trans('texts.card_number')) + ->autocomplete('cc-number') + ->label('') !!} + @endif +
    +
    + @if ($accountGateway->gateway_id == GATEWAY_BRAINTREE) +
    + @else + {!! Former::text(!empty($tokenize) ? '' : 'cvv') + ->id('cvv') + ->placeholder(trans('texts.cvv')) + ->autocomplete('off') + ->label('') !!} + @endif +
    +
    +
    +
    + @if ($accountGateway->gateway_id == GATEWAY_BRAINTREE) +
    + @else + {!! Former::select(!empty($tokenize) ? '' : 'expiration_month') + ->id('expiration_month') + ->autocomplete('cc-exp-month') + ->placeholder(trans('texts.expiration_month')) + ->addOption('01 - January', '1') + ->addOption('02 - February', '2') + ->addOption('03 - March', '3') + ->addOption('04 - April', '4') + ->addOption('05 - May', '5') + ->addOption('06 - June', '6') + ->addOption('07 - July', '7') + ->addOption('08 - August', '8') + ->addOption('09 - September', '9') + ->addOption('10 - October', '10') + ->addOption('11 - November', '11') + ->addOption('12 - December', '12')->label('') + !!} + @endif +
    +
    + @if ($accountGateway->gateway_id == GATEWAY_BRAINTREE) +
    + @else + {!! Former::select(!empty($tokenize) ? '' : 'expiration_year') + ->id('expiration_year') + ->autocomplete('cc-exp-year') + ->placeholder(trans('texts.expiration_year')) + ->addOption('2016', '2016') + ->addOption('2017', '2017') + ->addOption('2018', '2018') + ->addOption('2019', '2019') + ->addOption('2020', '2020') + ->addOption('2021', '2021') + ->addOption('2022', '2022') + ->addOption('2023', '2023') + ->addOption('2024', '2024') + ->addOption('2025', '2025') + ->addOption('2026', '2026')->label('') + !!} + @endif +
    +
    +
    +
    + @if (isset($amount) && $client && $account->showTokenCheckbox($storageGateway/* will contain gateway id */)) + selectTokenCheckbox() ? 'CHECKED' : '' }} value="1" style="margin-left:0px; vertical-align:top"> + + + @if ($storageGateway == GATEWAY_STRIPE) + {!! trans('texts.token_billing_secure', ['link' => link_to('https://stripe.com/', 'Stripe.com', ['target' => '_blank'])]) !!} + @elseif ($storageGateway == GATEWAY_BRAINTREE) + {!! trans('texts.token_billing_secure', ['link' => link_to('https://www.braintreepayments.com/', 'Braintree', ['target' => '_blank'])]) !!} + @endif + + @endif +
    + +
    + @if (isset($acceptedCreditCardTypes)) +
    + @foreach ($acceptedCreditCardTypes as $card) + {{ $card['alt'] }} + @endforeach +
    + @endif +
    +
    + +
    + +
    + +

     

    +
    + @if(isset($amount)) + {!! Button::success(strtoupper(trans('texts.pay_now') . ' - ' . $account->formatMoney($amount, $client, true) )) + ->submit() + ->large() !!} + @else + {!! Button::success(strtoupper(trans('texts.add_credit_card') )) + ->submit() + ->large() !!} + @endif +
    +

     

    + +@stop diff --git a/resources/views/payments/payment_css.blade.php b/resources/views/payments/payment_css.blade.php index 58b39461b462..eda0d427c66b 100644 --- a/resources/views/payments/payment_css.blade.php +++ b/resources/views/payments/payment_css.blade.php @@ -15,14 +15,14 @@ body { .container select, .braintree-hosted { @if(!empty($account)) - {!! $account->getBodyFontCss() !!} + {!! $account->getBodyFontCss() !!} @else - font-weight: 300; - font-family: 'Roboto', sans-serif; + font-weight: 300; + font-family: 'Roboto', sans-serif; @endif width: 100%; padding: 11px; - color: #8c8c8c; + color: #444444; background: #f9f9f9; border: 1px solid #ebe7e7; border-radius: 3px; @@ -66,7 +66,7 @@ div.row { header { margin: 0px !important } - + @media screen and (min-width: 700px) { header { margin: 20px 0 75px; @@ -100,14 +100,14 @@ h3 .help { } header h3 { - text-transform: uppercase; + text-transform: uppercase; } - + header h3 span { display: inline-block; margin-left: 8px; } - + header h3 em { font-style: normal; color: #eb8039; @@ -119,14 +119,14 @@ header h3 em { background: url({{ asset('/images/icon-shield.png') }}) right 22px no-repeat; padding: 17px 55px 10px 0; } - + .secure h3 { color: #36b855; font-size: 30px; margin-bottom: 8px; margin-top: 0px; } - + .secure div { color: #acacac; font-size: 15px; diff --git a/resources/views/payments/payment_method.blade.php b/resources/views/payments/payment_method.blade.php new file mode 100644 index 000000000000..27d58d9b6aba --- /dev/null +++ b/resources/views/payments/payment_method.blade.php @@ -0,0 +1,74 @@ +@extends('public.header') + +@section('content') + + @include('payments.payment_css') + +
    +

     

    + +
    +
    + +
    +
    +
    + @if ($client && isset($invoiceNumber)) +

    {{ $client->getDisplayName() }}

    +

    {{ trans('texts.invoice') . ' ' . $invoiceNumber }}|  {{ trans('texts.amount_due') }}: {{ $account->formatMoney($amount, $client, true) }}

    + @elseif ($paymentTitle) +

    {{ $paymentTitle }} + @if(isset($paymentSubtitle)) +
    {{ $paymentSubtitle }} + @endif +

    + @endif +
    +
    +
    + @if (Request::secure() || Utils::isNinjaDev()) +
    +

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

    +
    {{ trans('texts.256_encryption') }}
    +
    + @endif +
    +
    + +

     
     

    + +
    + + @yield('payment_details') + +
    + +
    + +
    +
    + + +

     

    +

     

    + +
    + + {!! Former::close() !!} + + + + +@stop diff --git a/resources/views/payments/paymentmethods_list.blade.php b/resources/views/payments/paymentmethods_list.blade.php index 153a9847bffa..5208bcd64a15 100644 --- a/resources/views/payments/paymentmethods_list.blade.php +++ b/resources/views/payments/paymentmethods_list.blade.php @@ -16,6 +16,7 @@ display:inline-block; } + @if (!empty($braintreeClientToken)) @endif -@if(!empty($paymentMethods)) -@foreach ($paymentMethods as $paymentMethod) -
    - - {{trans(payment_type->name)))}}"> - - @if(!empty($paymentMethod->last4)) - •••••{{$paymentMethod->last4}} - @endif - @if($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) - @if($paymentMethod->bank_name) - {{ $paymentMethod->bank_name }} - @endif - @if($paymentMethod->status == PAYMENT_METHOD_STATUS_NEW) - @if($gateway->gateway_id == GATEWAY_STRIPE) - ({{trans('texts.complete_verification')}}) + +@if(!empty($paymentMethods) && count($paymentMethods)) +

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

    + + @foreach ($paymentMethods as $paymentMethod) +
    + + {{ trans(payment_type->name))) }}"> + + + @if($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) + @if($paymentMethod->bank_name) + {{ $paymentMethod->bank_name }} @else - ({{ trans('texts.verification_pending') }}) + {{ trans('texts.bank_account') }} @endif - @elseif($paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFICATION_FAILED) - ({{trans('texts.verification_failed')}}) + @if($paymentMethod->status == PAYMENT_METHOD_STATUS_NEW) + @if($gateway->gateway_id == GATEWAY_STRIPE) + ({{trans('texts.complete_verification')}}) + @else + ({{ trans('texts.verification_pending') }}) + @endif + @elseif($paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFICATION_FAILED) + ({{trans('texts.verification_failed')}}) + @endif + @elseif($paymentMethod->payment_type_id == PAYMENT_TYPE_PAYPAL) + {{ trans('texts.paypal') . ': ' . $paymentMethod->email }} + @else + {{ trans('texts.credit_card') }} @endif - @elseif($paymentMethod->payment_type_id == PAYMENT_TYPE_ID_PAYPAL) - {{ $paymentMethod->email }} - @elseif($paymentMethod->expiration) - {!! trans('texts.card_expiration', array('expires'=>Utils::fromSqlDate($paymentMethod->expiration, false)->format('m/y'))) !!} - @endif - @if($paymentMethod->id == $paymentMethod->account_gateway_token->default_payment_method_id) - ({{trans('texts.used_for_auto_bill')}}) - @elseif($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED) - ({{trans('texts.use_for_auto_bill')}}) - @endif - × -
    -@endforeach + + @if($paymentMethod->id == $paymentMethod->account_gateway_token->default_payment_method_id) + ({{trans('texts.used_for_auto_bill')}}) + @elseif($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED) + ({{trans('texts.use_for_auto_bill')}}) + @endif + × + +
    + @endforeach @endif -@if($gateway->gateway_id != GATEWAY_STRIPE || $gateway->getPublishableStripeKey()) +
    - {!! Button::success(strtoupper(trans('texts.add_credit_card'))) - ->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()) + @if (false && $account->getGatewayByType(GATEWAY_TYPE_CREDIT_CARD) && $account->getGatewayByType(GATEWAY_TYPE_TOKEN)) + {!! Button::success(strtoupper(trans('texts.add_credit_card'))) + ->asLinkTo(URL::to('/client/add/credit_card')) !!}   + @endif + @if (false && $account->getGatewayByType(GATEWAY_TYPE_BANK_TRANSFER) && $account->getGatewayByType(GATEWAY_TYPE_TOKEN)) + {!! Button::success(strtoupper(trans('texts.add_bank_account'))) + ->withAttributes(['id'=>'add-ach']) + ->asLinkTo(URL::to('/client/add/bank_transfer')) !!} +   + @endif + @if (false && $account->getGatewayByType(GATEWAY_TYPE_PAYPAL) && $account->getGatewayByType(GATEWAY_TYPE_TOKEN)) {!! Button::success(strtoupper(trans('texts.add_paypal_account'))) ->withAttributes(['id'=>'add-paypal']) - ->asLinkTo(URL::to('/client/paymentmethods/add/braintree_paypal')) !!} + ->asLinkTo(URL::to('/client/add/paypal')) !!}
    @endif
    -@endif @@ -137,16 +137,16 @@ + -
    - @endif +
    + @endif
    - +
    @if (!isset($account) || !$account->hasFeature(FEATURE_WHITE_LABEL)) From b06b437e277739c6f9fa33e8ecf17173d2c6aba9 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 20 Jun 2016 22:26:48 +0300 Subject: [PATCH 267/386] Fix for tests --- app/Ninja/PaymentDrivers/WePayPaymentDriver.php | 2 +- tests/acceptance/OnlinePaymentCest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php index 05561fb69d18..4c5d06478b4f 100644 --- a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php +++ b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php @@ -11,7 +11,7 @@ class WePayPaymentDriver extends BasePaymentDriver ]; } - public function startPurchase($input, $sourceId) + public function startPurchase($input = false, $sourceId = false) { $data = parent::startPurchase($input, $sourceId); diff --git a/tests/acceptance/OnlinePaymentCest.php b/tests/acceptance/OnlinePaymentCest.php index 1d7d563094eb..5a2d1380ad50 100644 --- a/tests/acceptance/OnlinePaymentCest.php +++ b/tests/acceptance/OnlinePaymentCest.php @@ -23,7 +23,7 @@ class OnlinePaymentCest // set gateway info $I->wantTo('create a gateway'); - $I->amOnPage('/gateways/create/0'); + $I->amOnPage('/gateways/create?other_providers=true'); $I->fillField(['name' =>'23_apiKey'], env('stripe_secret_key') ?: Fixtures::get('stripe_secret_key')); // Fails to load StripeJS causing "ReferenceError: Can't find variable: Stripe" From a2c7a0c12f6814fa5d54732a9ec7609342a1ab5f Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 21 Jun 2016 12:07:16 +0300 Subject: [PATCH 268/386] Improvement to Eway payment process --- app/Ninja/PaymentDrivers/WePayPaymentDriver.php | 2 ++ database/seeds/PaymentLibrariesSeeder.php | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php index 4c5d06478b4f..71e97cc9f329 100644 --- a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php +++ b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php @@ -1,5 +1,7 @@ 'Authorize.Net AIM', 'provider' => 'AuthorizeNet_AIM'], ['name' => 'Authorize.Net SIM', 'provider' => 'AuthorizeNet_SIM', 'payment_library_id' => 2], ['name' => 'CardSave', 'provider' => 'CardSave'], - ['name' => 'Eway Rapid', 'provider' => 'Eway_Rapid', 'is_offsite' => true], + ['name' => 'Eway Rapid', 'provider' => 'Eway_RapidShared', 'is_offsite' => true], ['name' => 'FirstData Connect', 'provider' => 'FirstData_Connect'], ['name' => 'GoCardless', 'provider' => 'GoCardless', 'is_offsite' => true], ['name' => 'Migs ThreeParty', 'provider' => 'Migs_ThreeParty'], @@ -58,7 +58,6 @@ class PaymentLibrariesSeeder extends Seeder ['name' => 'Skrill', 'provider' => 'Skrill'], ['name' => 'BitPay', 'provider' => 'BitPay', 'is_offsite' => true], ['name' => 'Dwolla', 'provider' => 'Dwolla', 'is_offsite' => true], - ['name' => 'Eway Rapid', 'provider' => 'Eway_RapidShared'], ['name' => 'AGMS', 'provider' => 'Agms'], ['name' => 'Barclays', 'provider' => 'BarclaysEpdq\Essential'], ['name' => 'Cardgate', 'provider' => 'Cardgate'], From 851b3e963cd110b1e1e1f985f656366ff5d19f07 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 21 Jun 2016 19:21:50 +1000 Subject: [PATCH 269/386] Add company logo to UserAccount Transformer --- app/Ninja/Transformers/UserAccountTransformer.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Ninja/Transformers/UserAccountTransformer.php b/app/Ninja/Transformers/UserAccountTransformer.php index e914a25c663d..0d64579e2c19 100644 --- a/app/Ninja/Transformers/UserAccountTransformer.php +++ b/app/Ninja/Transformers/UserAccountTransformer.php @@ -33,7 +33,8 @@ class UserAccountTransformer extends EntityTransformer 'account_key' => $user->account->account_key, 'name' => $user->account->present()->name, 'token' => $user->account->getToken($user->id, $this->tokenName), - 'default_url' => SITE_URL + 'default_url' => SITE_URL, + 'logo' => $user->account->logo, ]; } } \ No newline at end of file From 6836ed849bcca353fe096319dcdd5b1dafbf6d78 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 21 Jun 2016 12:31:38 +0300 Subject: [PATCH 270/386] Fix for test --- tests/acceptance/OnlinePaymentCest.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/acceptance/OnlinePaymentCest.php b/tests/acceptance/OnlinePaymentCest.php index 5a2d1380ad50..0a69a6942f01 100644 --- a/tests/acceptance/OnlinePaymentCest.php +++ b/tests/acceptance/OnlinePaymentCest.php @@ -22,14 +22,16 @@ class OnlinePaymentCest $productKey = $this->faker->text(10); // set gateway info - $I->wantTo('create a gateway'); - $I->amOnPage('/gateways/create?other_providers=true'); + if ( ! $I->grabFromDatabase('account_gateways', 'id', ['id' => 1])) { + $I->wantTo('create a gateway'); + $I->amOnPage('/gateways/create?other_providers=true'); - $I->fillField(['name' =>'23_apiKey'], env('stripe_secret_key') ?: Fixtures::get('stripe_secret_key')); - // Fails to load StripeJS causing "ReferenceError: Can't find variable: Stripe" - //$I->fillField(['name' =>'stripe_publishable_key'], env('stripe_secret_key') ?: Fixtures::get('stripe_publishable_key')); - $I->click('Save'); - $I->see('Successfully created gateway'); + $I->fillField(['name' =>'23_apiKey'], env('stripe_secret_key') ?: Fixtures::get('stripe_secret_key')); + // Fails to load StripeJS causing "ReferenceError: Can't find variable: Stripe" + //$I->fillField(['name' =>'stripe_publishable_key'], env('stripe_secret_key') ?: Fixtures::get('stripe_publishable_key')); + $I->click('Save'); + $I->see('Successfully created gateway'); + } // create client $I->amOnPage('/clients/create'); @@ -63,6 +65,7 @@ class OnlinePaymentCest $clientSession->does(function(AcceptanceTester $I) use ($invitationKey) { $I->amOnPage('/view/' . $invitationKey); $I->click('Pay Now'); + $I->click('Credit Card'); /* $I->fillField(['name' => 'first_name'], $this->faker->firstName); From 8a6227bf89edd582848a39fd28ce067d94232de4 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 21 Jun 2016 12:40:10 +0300 Subject: [PATCH 271/386] Fix for tests --- app/Http/Controllers/ClientPortalController.php | 17 +++++++++++------ resources/views/invoices/view.blade.php | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index 493ae23c5f0c..7ab5256cb349 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -107,8 +107,6 @@ class ClientPortalController extends BaseController } } - $paymentDriver = $account->paymentDriver($invitation, GATEWAY_TYPE_CREDIT_CARD); - if ($wepayGateway = $account->getGatewayConfig(GATEWAY_WEPAY)){ $data['enableWePayACH'] = $wepayGateway->getAchEnabled(); } @@ -132,12 +130,19 @@ class ClientPortalController extends BaseController 'contact' => $contact, 'paymentTypes' => $paymentTypes, 'paymentURL' => $paymentURL, - 'transactionToken' => $paymentDriver->createTransactionToken(), - 'partialView' => $paymentDriver->partialView(), - 'accountGateway' => $paymentDriver->accountGateway, 'phantomjs' => Input::has('phantomjs'), ); + if ($paymentDriver = $account->paymentDriver($invitation, GATEWAY_TYPE_CREDIT_CARD)) { + $data += [ + 'transactionToken' => $paymentDriver->createTransactionToken(), + 'partialView' => $paymentDriver->partialView(), + 'accountGateway' => $paymentDriver->accountGateway, + ]; + } + + + if($account->hasFeature(FEATURE_DOCUMENTS) && $this->canCreateZip()){ $zipDocs = $this->getInvoiceZipDocuments($invoice, $size); @@ -740,7 +745,7 @@ class ClientPortalController extends BaseController return redirect()->to($client->account->enable_client_portal?'/client/dashboard':'/client/payment_methods/'); } - + public function setDefaultPaymentMethod(){ if (!$contact = $this->getContact()) { return $this->returnError(); diff --git a/resources/views/invoices/view.blade.php b/resources/views/invoices/view.blade.php index cb8a40b64510..72aff941cf50 100644 --- a/resources/views/invoices/view.blade.php +++ b/resources/views/invoices/view.blade.php @@ -22,7 +22,7 @@ } - @if ($accountGateway->gateway_id == GATEWAY_BRAINTREE && !empty($transactionToken)) + @if (!empty($transactionToken) && $accountGateway->gateway_id == GATEWAY_BRAINTREE)
    - \ No newline at end of file diff --git a/resources/views/payments/wepay/bank_transfer.blade.php b/resources/views/payments/wepay/bank_transfer.blade.php index e273d002673b..cfca52b94a88 100644 --- a/resources/views/payments/wepay/bank_transfer.blade.php +++ b/resources/views/payments/wepay/bank_transfer.blade.php @@ -2,6 +2,8 @@ @section('payment_details') + {!! Former::vertical_open($url) !!} +

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

    @if (!empty($details)) diff --git a/resources/views/payments/wepay/credit_card.blade.php b/resources/views/payments/wepay/credit_card.blade.php new file mode 100644 index 000000000000..1f45bd151ca7 --- /dev/null +++ b/resources/views/payments/wepay/credit_card.blade.php @@ -0,0 +1,68 @@ +@extends('payments.credit_card') + +@section('head') + @parent + + + +@stop From e2ddabe95d1a70cf93a99dfbca62a01f574c6ada Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 22 Jun 2016 12:22:38 +0300 Subject: [PATCH 273/386] Minor fixes for payments --- .../Controllers/ClientPortalController.php | 30 +---- .../Controllers/OnlinePaymentController.php | 7 +- app/Http/Controllers/PaymentController.php | 2 +- app/Ninja/Datatables/PaymentDatatable.php | 2 +- .../PaymentDrivers/WePayPaymentDriver.php | 4 +- public/css/built.public.css | 10 +- public/css/public.style.css | 10 +- resources/lang/en/texts.php | 2 + resources/views/error.blade.php | 11 +- resources/views/invoices/pdf.blade.php | 3 +- .../payments/paymentmethods_list.blade.php | 27 +++-- resources/views/public/header.blade.php | 107 ++++++++++-------- 12 files changed, 102 insertions(+), 113 deletions(-) diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index 7ab5256cb349..094db34c11e6 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -137,7 +137,7 @@ class ClientPortalController extends BaseController $data += [ 'transactionToken' => $paymentDriver->createTransactionToken(), 'partialView' => $paymentDriver->partialView(), - 'accountGateway' => $paymentDriver->accountGateway, + 'accountGateway' => $paymentDriver->accountGateway, ]; } @@ -356,7 +356,7 @@ class ClientPortalController extends BaseController 'clientFontUrl' => $account->getFontsUrl(), 'entityType' => ENTITY_PAYMENT, 'title' => trans('texts.payments'), - 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'source', 'payment_amount', 'payment_date', 'status']) + 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date', 'status']) ]; return response()->view('public_list', $data); @@ -373,32 +373,6 @@ class ClientPortalController extends BaseController ->addColumn('invoice_number', function ($model) { return $model->invitation_key ? link_to('/view/'.$model->invitation_key, $model->invoice_number)->toHtml() : $model->invoice_number; }) ->addColumn('transaction_reference', function ($model) { return $model->transaction_reference ? $model->transaction_reference : 'Manual entry'; }) ->addColumn('payment_type', function ($model) { return ($model->payment_type && !$model->last4) ? $model->payment_type : ($model->account_gateway_id ? 'Online payment' : ''); }) - ->addColumn('payment_source', function ($model) { - $code = str_replace(' ', '', strtolower($model->payment_type)); - $card_type = trans("texts.card_" . $code); - if ($model->payment_type_id != PAYMENT_TYPE_ACH) { - if($model->last4) { - $expiration = trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($model->expiration, false)->format('m/y'))); - return '' . htmlentities($card_type) . '  •••' . $model->last4 . ' ' . $expiration; - } elseif ($model->email) { - return $model->email; - } - } elseif ($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; - } - } - }) ->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); }) diff --git a/app/Http/Controllers/OnlinePaymentController.php b/app/Http/Controllers/OnlinePaymentController.php index 5b1229484dd8..b6b558624e2d 100644 --- a/app/Http/Controllers/OnlinePaymentController.php +++ b/app/Http/Controllers/OnlinePaymentController.php @@ -58,7 +58,7 @@ class OnlinePaymentController extends BaseController } return redirect()->to('view/' . $invitation->invitation_key); } catch (Exception $exception) { - return $this->error($paymentDriver, $exception); + return $this->error($paymentDriver, $exception, true); } } @@ -84,7 +84,7 @@ class OnlinePaymentController extends BaseController } } - private function error($paymentDriver, $exception) + private function error($paymentDriver, $exception, $showPayment) { if (is_string($exception)) { $displayError = $exception; @@ -100,7 +100,8 @@ class OnlinePaymentController extends BaseController $message = sprintf('Payment Error [%s]: %s', $paymentDriver->providerName(), $logError); Utils::logError($message, 'PHP', true); - return redirect()->to('view/' . $paymentDriver->invitation->invitation_key); + $route = $showPayment ? 'payment/' : 'view/'; + return redirect()->to($route . $paymentDriver->invitation->invitation_key); } public function getBankInfo($routingNumber) { diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index fe16fcbb9ce4..4412c765236b 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -31,7 +31,7 @@ class PaymentController extends BaseController return View::make('list', array( 'entityType' => ENTITY_PAYMENT, 'title' => trans('texts.payments'), - 'sortCol' => '6', + 'sortCol' => '7', 'columns' => Utils::trans([ 'checkbox', 'invoice', diff --git a/app/Ninja/Datatables/PaymentDatatable.php b/app/Ninja/Datatables/PaymentDatatable.php index 92473c7fd257..857147ec38f9 100644 --- a/app/Ninja/Datatables/PaymentDatatable.php +++ b/app/Ninja/Datatables/PaymentDatatable.php @@ -59,7 +59,7 @@ class PaymentDatatable extends EntityDatatable $card_type = trans("texts.card_" . $code); if ($model->payment_type_id != PAYMENT_TYPE_ACH) { if($model->last4) { - $expiration = trans('texts.card_expiration', array('expires' => Utils::fromSqlDate($model->expiration, false)->format('m/y'))); + $expiration = Utils::fromSqlDate($model->expiration, false)->format('m/y'); return '' . htmlentities($card_type) . '  •••' . $model->last4 . ' ' . $expiration; } elseif ($model->email) { return $model->email; diff --git a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php index 1f47227ec6fd..c79b216db58e 100644 --- a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php +++ b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php @@ -15,6 +15,7 @@ class WePayPaymentDriver extends BasePaymentDriver ]; } + /* public function startPurchase($input = false, $sourceId = false) { $data = parent::startPurchase($input, $sourceId); @@ -27,6 +28,7 @@ class WePayPaymentDriver extends BasePaymentDriver return $data; } + */ public function tokenize() { @@ -167,7 +169,7 @@ class WePayPaymentDriver extends BasePaymentDriver if ($response->state == 'deleted') { return parent::removePaymentMethod($paymentMethod); } else { - throw new Exception(); + throw new Exception(trans('texts.failed_remove_payment_method')); } } diff --git a/public/css/built.public.css b/public/css/built.public.css index 2a02c2db94b6..58d020337711 100644 --- a/public/css/built.public.css +++ b/public/css/built.public.css @@ -793,7 +793,7 @@ html { .navbar-header { padding-top: 4px; - padding-bottom: 4px; + padding-bottom: 4px; } .navbar li a { padding-top: 18px; @@ -806,7 +806,7 @@ html { .navbar { x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); - x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); + x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); box-shadow: 0 0 10px 2px rgba(0,0,0,.05); } @@ -894,7 +894,7 @@ table.table thead .sorting_desc_disabled:after { content: '' !important } font-weight: 500; } -@media screen and (min-width: 700px) { +@media screen and (min-width: 700px) { #footer .top { padding: 27px 0; } @@ -935,7 +935,7 @@ th {border-left: 1px solid #FFFFFF; } table.dataTable.no-footer { border-bottom: none; } -.table-striped>tbody>tr:nth-child(odd)>td, +.table-striped>tbody>tr:nth-child(odd)>td, .table-striped>tbody>tr:nth-child(odd)>th { background-color: #FDFDFD; } @@ -969,4 +969,4 @@ table td { } /* hide table sorting indicators */ -table.data-table thead .sorting { background: url('') no-repeat center right; } \ No newline at end of file +table.data-table thead .sorting { background: url('') no-repeat center right; } diff --git a/public/css/public.style.css b/public/css/public.style.css index b1f91c25739f..a5cdbcc8210e 100644 --- a/public/css/public.style.css +++ b/public/css/public.style.css @@ -10,7 +10,7 @@ html { .navbar-header { padding-top: 4px; - padding-bottom: 4px; + padding-bottom: 4px; } .navbar li a { padding-top: 18px; @@ -23,7 +23,7 @@ html { .navbar { x-moz-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); - x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); + x-webkit-box-shadow: 0 0 10px 2px rgba(0,0,0,.05); box-shadow: 0 0 10px 2px rgba(0,0,0,.05); } @@ -111,7 +111,7 @@ table.table thead .sorting_desc_disabled:after { content: '' !important } font-weight: 500; } -@media screen and (min-width: 700px) { +@media screen and (min-width: 700px) { #footer .top { padding: 27px 0; } @@ -152,7 +152,7 @@ th {border-left: 1px solid #FFFFFF; } table.dataTable.no-footer { border-bottom: none; } -.table-striped>tbody>tr:nth-child(odd)>td, +.table-striped>tbody>tr:nth-child(odd)>td, .table-striped>tbody>tr:nth-child(odd)>th { background-color: #FDFDFD; } @@ -186,4 +186,4 @@ table td { } /* hide table sorting indicators */ -table.data-table thead .sorting { background: url('') no-repeat center right; } \ No newline at end of file +table.data-table thead .sorting { background: url('') no-repeat center right; } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 58efc4ecb53d..918fc4b0f3c1 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1357,6 +1357,8 @@ $LANG = array( 'use_bank_on_file' => 'Use Bank on File', 'auto_bill_email_message' => 'This invoice will automatically be billed to the payment method on file on the due date.', 'bitcoin' => 'Bitcoin', + 'added_on' => 'Added :date', + 'failed_remove_payment_method' => 'Failed to remove the payment method', ); diff --git a/resources/views/error.blade.php b/resources/views/error.blade.php index 149472fd5011..ec409da311c4 100644 --- a/resources/views/error.blade.php +++ b/resources/views/error.blade.php @@ -16,13 +16,4 @@

     

     

    - - -@stop \ No newline at end of file +@stop diff --git a/resources/views/invoices/pdf.blade.php b/resources/views/invoices/pdf.blade.php index 0e7b2db1ff34..9d777fd587e4 100644 --- a/resources/views/invoices/pdf.blade.php +++ b/resources/views/invoices/pdf.blade.php @@ -1,4 +1,4 @@ - + @if (!Utils::isNinja() || !Utils::isPro()) @@ -140,6 +140,7 @@ canvas.width = viewport.width; page.render({canvasContext: context, viewport: viewport}); + $('#theFrame').hide(); $('#theCanvas').show(); isRefreshing = false; if (needsRefresh) { diff --git a/resources/views/payments/paymentmethods_list.blade.php b/resources/views/payments/paymentmethods_list.blade.php index 5208bcd64a15..4aceca432932 100644 --- a/resources/views/payments/paymentmethods_list.blade.php +++ b/resources/views/payments/paymentmethods_list.blade.php @@ -93,25 +93,30 @@ @else {{ trans('texts.bank_account') }} @endif - @if($paymentMethod->status == PAYMENT_METHOD_STATUS_NEW) - @if($gateway->gateway_id == GATEWAY_STRIPE) - ({{trans('texts.complete_verification')}}) - @else - ({{ trans('texts.verification_pending') }}) - @endif - @elseif($paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFICATION_FAILED) - ({{trans('texts.verification_failed')}}) - @endif @elseif($paymentMethod->payment_type_id == PAYMENT_TYPE_PAYPAL) {{ trans('texts.paypal') . ': ' . $paymentMethod->email }} @else {{ trans('texts.credit_card') }} @endif + - {{ trans('texts.added_on', ['date' => Utils::dateToString($paymentMethod->created_at)]) }} + + @if($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) + @if($paymentMethod->status == PAYMENT_METHOD_STATUS_NEW) + @if($gateway->gateway_id == GATEWAY_STRIPE) + ({{trans('texts.complete_verification')}}) + @else + [{{ trans('texts.verification_pending') }}] + @endif + @elseif($paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFICATION_FAILED) + [{{trans('texts.verification_failed')}}] + @endif + @endif + @if($paymentMethod->id == $paymentMethod->account_gateway_token->default_payment_method_id) - ({{trans('texts.used_for_auto_bill')}}) + [{{trans('texts.used_for_auto_bill')}}] @elseif($paymentMethod->payment_type_id != PAYMENT_TYPE_ACH || $paymentMethod->status == PAYMENT_METHOD_STATUS_VERIFIED) - ({{trans('texts.use_for_auto_bill')}}) + [{{trans('texts.use_for_auto_bill')}}] @endif × diff --git a/resources/views/public/header.blade.php b/resources/views/public/header.blade.php index 257196c59884..24d652b6b628 100644 --- a/resources/views/public/header.blade.php +++ b/resources/views/public/header.blade.php @@ -53,56 +53,66 @@ function getStarted() { $('#startForm').submit(); return false; -} + } + + $(function() { + // check that the footer appears at the bottom of the screen + var height = $(window).height() - ($('#header').height() + $('#footer').height()); + if ($('#mainContent').height() < height) { + $('#mainContent').height(height); + } + }) + -

    +
    @@ -120,8 +130,11 @@
    {!! Session::get('error') !!}
    @endif
    +
    -@yield('content') +
    + @yield('content') +